1
0
mirror of synced 2026-05-03 02:03:31 +00:00

Add support for plugin binary artifacts

Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
This commit is contained in:
Stefan Prodan
2026-04-09 00:05:47 +03:00
parent 8be056324a
commit 9a4b93056b
2 changed files with 480 additions and 14 deletions

View File

@@ -77,21 +77,32 @@ func (inst *Installer) Install(pluginDir string, manifest *PluginManifest, pv *P
return fmt.Errorf("checksum verification failed (expected: %s, got: %s)", plat.Checksum, actualChecksum)
}
// manifest.Bin is the single source of truth for the installed binary
// name (e.g. "flux-validate"). On Windows we always append ".exe".
// For archives it's also the entry name we look up; for raw binaries
// it's the rename target regardless of the URL's filename.
binName := manifest.Bin
if runtime.GOOS == "windows" {
binName += ".exe"
}
destPath := filepath.Join(pluginDir, binName)
destName := pluginPrefix + manifest.Name
if runtime.GOOS == "windows" {
destName += ".exe"
format, err := detectArchiveFormat(tmpFile.Name(), plat.URL)
if err != nil {
return fmt.Errorf("failed to detect plugin format: %w", err)
}
destPath := filepath.Join(pluginDir, destName)
if strings.HasSuffix(plat.URL, ".zip") {
switch format {
case formatZip:
err = extractFromZip(tmpFile.Name(), binName, destPath)
} else {
case formatTarGz:
err = extractFromTarGz(tmpFile.Name(), binName, destPath)
case formatTar:
err = extractFromTar(tmpFile.Name(), binName, destPath)
case formatBinary:
err = copyPluginBinary(tmpFile.Name(), destPath)
default:
return fmt.Errorf("unexpected plugin format: %v", format)
}
if err != nil {
return err
@@ -162,6 +173,60 @@ func writeReceipt(pluginDir, name string, receipt *Receipt) error {
return os.WriteFile(receiptPath(pluginDir, name), data, 0o644)
}
// archiveFormat is the detected format of a downloaded plugin artifact.
type archiveFormat int
const (
formatBinary archiveFormat = iota
formatZip
formatTarGz
formatTar
)
// detectArchiveFormat determines the artifact format by first checking the URL
// extension, then falling back to magic-byte inspection. Returns formatBinary
// if neither indicates a known archive, in which case the downloaded file is
// installed as-is.
func detectArchiveFormat(path, url string) (archiveFormat, error) {
switch lower := strings.ToLower(url); {
case strings.HasSuffix(lower, ".zip"):
return formatZip, nil
case strings.HasSuffix(lower, ".tar.gz"), strings.HasSuffix(lower, ".tgz"):
return formatTarGz, nil
case strings.HasSuffix(lower, ".tar"):
return formatTar, nil
}
f, err := os.Open(path)
if err != nil {
return formatBinary, err
}
defer f.Close()
// Read enough bytes to cover the tar "ustar" magic at offset 257.
var hdr [262]byte
n, err := io.ReadFull(f, hdr[:])
if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF {
return formatBinary, err
}
// ZIP: PK\x03\x04 (file), PK\x05\x06 (empty), PK\x07\x08 (spanned).
if n >= 4 && hdr[0] == 'P' && hdr[1] == 'K' &&
(hdr[2] == 0x03 || hdr[2] == 0x05 || hdr[2] == 0x07) {
return formatZip, nil
}
// gzip: \x1f\x8b
if n >= 2 && hdr[0] == 0x1f && hdr[1] == 0x8b {
return formatTarGz, nil
}
// tar: "ustar" at offset 257
if n >= 262 && string(hdr[257:262]) == "ustar" {
return formatTar, nil
}
return formatBinary, nil
}
// extractFromTarGz extracts a named file from a tar.gz archive
// and streams it directly to destPath.
func extractFromTarGz(archivePath, targetName, destPath string) error {
@@ -177,7 +242,26 @@ func extractFromTarGz(archivePath, targetName, destPath string) error {
}
defer gr.Close()
tr := tar.NewReader(gr)
return extractTarStream(gr, targetName, destPath)
}
// extractFromTar extracts a named file from an uncompressed tar archive
// and streams it directly to destPath.
func extractFromTar(archivePath, targetName, destPath string) error {
f, err := os.Open(archivePath)
if err != nil {
return err
}
defer f.Close()
return extractTarStream(f, targetName, destPath)
}
// extractTarStream walks a tar stream and streams the first matching
// regular file to destPath. Non-regular entries (symlinks, devices,
// directories) and entries with unsafe paths are skipped.
func extractTarStream(r io.Reader, targetName, destPath string) error {
tr := tar.NewReader(r)
for {
header, err := tr.Next()
if err == io.EOF {
@@ -187,10 +271,13 @@ func extractFromTarGz(archivePath, targetName, destPath string) error {
return fmt.Errorf("failed to read tar: %w", err)
}
if filepath.IsAbs(header.Name) || strings.Contains(header.Name, "..") {
if !filepath.IsLocal(header.Name) {
continue
}
if filepath.Base(header.Name) == targetName && header.Typeflag == tar.TypeReg {
if !header.FileInfo().Mode().IsRegular() {
continue
}
if filepath.Base(header.Name) == targetName {
return writeStreamToFile(tr, destPath)
}
}
@@ -198,8 +285,21 @@ func extractFromTarGz(archivePath, targetName, destPath string) error {
return fmt.Errorf("binary %q not found in archive", targetName)
}
// extractFromZip extracts a named file from a zip archive
// and streams it directly to destPath.
// copyPluginBinary copies a raw downloaded binary to destPath with 0755 mode.
// Used when the downloaded artifact is not an archive.
func copyPluginBinary(srcPath, destPath string) error {
src, err := os.Open(srcPath)
if err != nil {
return fmt.Errorf("failed to open downloaded binary: %w", err)
}
defer src.Close()
return writeStreamToFile(src, destPath)
}
// extractFromZip extracts a named file from a zip archive and streams it
// directly to destPath. Non-regular entries (symlinks, devices, directories)
// and entries with unsafe paths are skipped.
func extractFromZip(archivePath, targetName, destPath string) error {
r, err := zip.OpenReader(archivePath)
if err != nil {
@@ -208,13 +308,20 @@ func extractFromZip(archivePath, targetName, destPath string) error {
defer r.Close()
for _, f := range r.File {
if filepath.Base(f.Name) == targetName && !f.FileInfo().IsDir() {
if !filepath.IsLocal(f.Name) {
continue
}
if !f.FileInfo().Mode().IsRegular() {
continue
}
if filepath.Base(f.Name) == targetName {
rc, err := f.Open()
if err != nil {
return fmt.Errorf("failed to open %q in zip: %w", targetName, err)
}
defer rc.Close()
return writeStreamToFile(rc, destPath)
err = writeStreamToFile(rc, destPath)
rc.Close()
return err
}
}