// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package plugingetter import ( "archive/zip" "encoding/hex" "encoding/json" "fmt" "io" "log" "os" "os/exec" "path" "path/filepath" "regexp" "sort" "strconv" "strings" "time" "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-version" pluginsdk "github.com/hashicorp/packer-plugin-sdk/plugin" "github.com/hashicorp/packer-plugin-sdk/tmp" "github.com/hashicorp/packer/hcl2template/addrs" "golang.org/x/mod/semver" ) type Requirements []*Requirement // Requirement describes a required plugin and how it is installed. Usually a list // of required plugins is generated from a config file. From it we check what // is actually installed and what needs to happen to get in the desired state. type Requirement struct { // Plugin accessor as defined in the config file. // For Packer, using : // required_plugins { amazon = {...} } // Will set Accessor to `amazon`. Accessor string // Something like github.com/hashicorp/packer-plugin-amazon, from the // previous example. Identifier *addrs.Plugin // VersionConstraints as defined by user. Empty ( to be avoided ) means // highest found version. VersionConstraints version.Constraints } type BinaryInstallationOptions struct { // APIVersionMajor, APIVersionMinor string // OS and ARCH usually should be runtime.GOOS and runtime.ARCH, they allow // to pick the correct binary. OS, ARCH string // Ext is ".exe" on windows Ext string Checksummers []Checksummer // ReleasesOnly may be set by commands like validate or build, and // forces Packer to not consider plugin pre-releases. ReleasesOnly bool } type ListInstallationsOptions struct { // The directory in which to look for when installing plugins PluginDirectory string BinaryInstallationOptions } // RateLimitError is returned when a getter is being rate limited. type RateLimitError struct { SetableEnvVar string ResetTime time.Time Err error } func (rlerr *RateLimitError) Error() string { s := fmt.Sprintf("Plugin host rate limited the plugin getter. Try again in %s.\n", time.Until(rlerr.ResetTime)) if rlerr.SetableEnvVar != "" { s += fmt.Sprintf("HINT: Set the %s env var with a token to get more requests.\n", rlerr.SetableEnvVar) } s += rlerr.Err.Error() return s } func (pr Requirement) FilenamePrefix() string { if pr.Identifier == nil { return "packer-plugin-" } return "packer-plugin-" + pr.Identifier.Type + "_" } func (opts BinaryInstallationOptions) FilenameSuffix() string { return "_" + opts.OS + "_" + opts.ARCH + opts.Ext } var pluginVersionRegex = regexp.MustCompile(`^v([0-9]+\.[0-9]+\.[0-9]+)(-dev)?$`) // ListInstallations lists unique installed versions of plugin Requirement pr // with opts as a filter. // // Installations are sorted by version and one binary per version is returned. // Last binary detected takes precedence: in the order 'FromFolders' option. // // At least one opts.Checksumers must be given for a binary to be even // considered. func (pr Requirement) ListInstallations(opts ListInstallationsOptions) (InstallList, error) { res := InstallList{} FilenamePrefix := pr.FilenamePrefix() filenameSuffix := opts.FilenameSuffix() log.Printf("[TRACE] listing potential installations for %q that match %q. %#v", pr.Identifier, pr.VersionConstraints, opts) glob := "" if pr.Identifier == nil { glob = filepath.Join(opts.PluginDirectory, "*", "*", "*", FilenamePrefix+"*"+filenameSuffix) } else { glob = filepath.Join(opts.PluginDirectory, pr.Identifier.Hostname, pr.Identifier.Namespace, pr.Identifier.Type, FilenamePrefix+"*"+filenameSuffix) } matches, err := filepath.Glob(glob) if err != nil { return nil, fmt.Errorf("ListInstallations: %q failed to list binaries in folder: %v", pr.Identifier.String(), err) } for _, path := range matches { fname := filepath.Base(path) if fname == "." { continue } // base name could look like packer-plugin-amazon_v1.2.3_x5.1_darwin_amd64.exe versionsStr := strings.TrimPrefix(fname, FilenamePrefix) versionsStr = strings.TrimSuffix(versionsStr, filenameSuffix) if pr.Identifier == nil { if idx := strings.Index(versionsStr, "_"); idx > 0 { versionsStr = versionsStr[idx+1:] } } descOut, err := exec.Command(path, "describe").Output() if err != nil { log.Printf("couldn't call describe on %q, ignoring", path) continue } var describeInfo pluginsdk.SetDescription err = json.Unmarshal(descOut, &describeInfo) if err != nil { log.Printf("%q: describe output deserialization error %q, ignoring", path, err) } // versionsStr now looks like v1.2.3_x5.1 or amazon_v1.2.3_x5.1 parts := strings.SplitN(versionsStr, "_", 2) pluginVersionStr, protocolVerionStr := parts[0], parts[1] ver, err := version.NewVersion(pluginVersionStr) if err != nil { // could not be parsed, ignoring the file log.Printf("found %q with an incorrect %q version, ignoring it. %v", path, pluginVersionStr, err) continue } if ver.Prerelease() != "" && opts.ReleasesOnly { log.Printf("ignoring pre-release plugin %q", path) continue } matches := pluginVersionRegex.FindStringSubmatch(pluginVersionStr) if matches == nil { log.Printf("invalid version found: %q, ignoring", pluginVersionStr) continue } absVersion := matches[1] if len(matches) == 3 { absVersion = fmt.Sprintf("%s%s", absVersion, matches[2]) } if absVersion != describeInfo.Version { log.Printf("plugin %q reported version %s while its name implies version %s, ignoring", path, describeInfo.Version, absVersion) continue } rawVersion, _ := version.NewVersion(matches[1]) // no constraint means always pass, this will happen for implicit // plugin requirements and when we list all plugins. // // Note: we use the raw version name here, without the pre-release // suffix, as otherwise constraints reject them, which is not // what we want by default. if !pr.VersionConstraints.Check(rawVersion) { log.Printf("[TRACE] version %q of file %q does not match constraint %q", pluginVersionStr, path, pr.VersionConstraints.String()) continue } if err := opts.CheckProtocolVersion(protocolVerionStr); err != nil { log.Printf("[NOTICE] binary %s requires protocol version %s that is incompatible "+ "with this version of Packer. %s", path, protocolVerionStr, err) continue } checksumOk := false for _, checksummer := range opts.Checksummers { cs, err := checksummer.GetCacheChecksumOfFile(path) if err != nil { log.Printf("[TRACE] GetChecksumOfFile(%q) failed: %v", path, err) continue } if err := checksummer.ChecksumFile(cs, path); err != nil { log.Printf("[TRACE] ChecksumFile(%q) failed: %v", path, err) continue } checksumOk = true break } if !checksumOk { log.Printf("[TRACE] No checksum found for %q ignoring possibly unsafe binary", path) continue } res = append(res, &Installation{ BinaryPath: path, Version: pluginVersionStr, }) } sort.Sort(res) return res, nil } // InstallList is a list of installed plugins (binaries) with their versions, // ListInstallations should be used to get an InstallList. // // ListInstallations sorts binaries by version and one binary per version is // returned. type InstallList []*Installation func (l InstallList) String() string { v := &strings.Builder{} v.Write([]byte("[")) for i, inst := range l { if i > 0 { v.Write([]byte(",")) } fmt.Fprintf(v, "%v", *inst) } v.Write([]byte("]")) return v.String() } // Len is the number of elements in the collection. func (l InstallList) Len() int { return len(l) } var rawPluginName = regexp.MustCompile("packer-plugin-[^_]+") // Less reports whether the element with index i // must sort before the element with index j. // // If both Less(i, j) and Less(j, i) are false, // then the elements at index i and j are considered equal. // Sort may place equal elements in any order in the final result, // while Stable preserves the original input order of equal elements. // // Less must describe a transitive ordering: // - if both Less(i, j) and Less(j, k) are true, then Less(i, k) must be true as well. // - if both Less(i, j) and Less(j, k) are false, then Less(i, k) must be false as well. // // Note that floating-point comparison (the < operator on float32 or float64 values) // is not a transitive ordering when not-a-number (NaN) values are involved. // See Float64Slice.Less for a correct implementation for floating-point values. func (l InstallList) Less(i, j int) bool { lowPluginPath := l[i] hiPluginPath := l[j] lowRawPluginName := rawPluginName.FindString(path.Base(lowPluginPath.BinaryPath)) hiRawPluginName := rawPluginName.FindString(path.Base(hiPluginPath.BinaryPath)) // We group by path, then by descending order for the versions // // i.e. if the path are not the same, we can return the plain // lexicographic order, otherwise, we'll do a semver-conscious // version comparison for sorting. if lowRawPluginName != hiRawPluginName { return lowRawPluginName < hiRawPluginName } return semver.Compare(lowPluginPath.Version, hiPluginPath.Version) < 0 } // Swap swaps the elements with indexes i and j. func (l InstallList) Swap(i, j int) { tmp := l[i] l[i] = l[j] l[j] = tmp } // Installation describes a plugin installation type Installation struct { // Path to where binary is installed. // Ex: /usr/azr/.packer.d/plugins/github.com/hashicorp/amazon/packer-plugin-amazon_v1.2.3_darwin_amd64 BinaryPath string // Version of this plugin. Ex: // * v1.2.3 for packer-plugin-amazon_v1.2.3_darwin_x5 Version string } // InstallOptions describes the possible options for installing the plugin that // fits the plugin Requirement. type InstallOptions struct { // Different means to get releases, sha256 and binary files. Getters []Getter // The directory in which the plugins should be installed PluginDirectory string // Forces installation of the plugin, even if already installed. Force bool BinaryInstallationOptions } type GetOptions struct { PluginRequirement *Requirement BinaryInstallationOptions version *version.Version expectedZipFilename string } // ExpectedZipFilename is the filename of the zip we expect to find, the // value is known only after parsing the checksum file file. func (gp *GetOptions) ExpectedZipFilename() string { return gp.expectedZipFilename } func (binOpts *BinaryInstallationOptions) CheckProtocolVersion(remoteProt string) error { remoteProt = strings.TrimPrefix(remoteProt, "x") parts := strings.Split(remoteProt, ".") if len(parts) < 2 { return fmt.Errorf("Invalid remote protocol: %q, expected something like '%s.%s'", remoteProt, binOpts.APIVersionMajor, binOpts.APIVersionMinor) } vMajor, vMinor := parts[0], parts[1] // no protocol version check if binOpts.APIVersionMajor == "" && binOpts.APIVersionMinor == "" { return nil } if vMajor != binOpts.APIVersionMajor { return fmt.Errorf("Unsupported remote protocol MAJOR version %q. The current MAJOR protocol version is %q."+ " This version of Packer can only communicate with plugins using that version.", vMajor, binOpts.APIVersionMajor) } if vMinor == binOpts.APIVersionMinor { return nil } vMinori, err := strconv.Atoi(vMinor) if err != nil { return err } APIVersoinMinori, err := strconv.Atoi(binOpts.APIVersionMinor) if err != nil { return err } if vMinori > APIVersoinMinori { return fmt.Errorf("Unsupported remote protocol MINOR version %q. The supported MINOR protocol versions are version %q and below. "+ "Please upgrade Packer or use an older version of the plugin if possible.", vMinor, binOpts.APIVersionMinor) } return nil } func (gp *GetOptions) Version() string { return "v" + gp.version.String() } // A Getter helps get the appropriate files to download a binary. type Getter interface { // Get allows Packer to know more information about releases of a plugin in // order to decide which version to install. Get behaves similarly to an // HTTP server. Packer will stream responses from get in order to do what's // needed. In order to minimize the amount of requests done, Packer is // strict on filenames and we highly recommend on automating releases. // In the future, Packer will make it possible to ship plugin getters as // binaries this is why Packer streams from the output of get, which will // then be a command. // // * 'releases', get 'releases' should return the complete list of Releases // in JSON format following the format of the Release struct. It is also // possible to read GetOptions to filter for a smaller response. Some // getters don't. Packer will then decide the highest compatible // version of the plugin to install by using the sha256 function. // // * 'sha256', get 'sha256' should return a SHA256SUMS txt file. It will be // called with the highest possible & user allowed version from get // 'releases'. Packer will check if the release has a binary matching what // Packer can install and use. If so, get 'binary' will be called; // otherwise, lower versions will be checked. // For version 1.0.0 of the 'hashicorp/amazon' builder, the GitHub getter // will fetch the following URL: // https://github.com/hashicorp/packer-plugin-amazon/releases/download/v1.0.0/packer-plugin-amazon_v1.0.0_SHA256SUMS // This URL can be parameterized to the following one: // https://github.com/{plugin.path}/releases/download/{plugin.version}/packer-plugin-{plugin.name}_{plugin.version}_SHA256SUMS // If Packer is running on Linux AMD 64, then Packer will check for the // existence of a packer-plugin-amazon_v1.0.0_x5.0_linux_amd64 checksum in // that file. This filename can be parameterized to the following one: // packer-plugin-{plugin.name}_{plugin.version}_x{proto_ver.major}.{proto_ver._minor}_{os}_{arch} // // See // https://github.com/hashicorp/packer-plugin-scaffolding/blob/main/.goreleaser.yml // and // https://www.packer.io/docs/plugins/creation#plugin-development-basics // to learn how to create and automate your releases and for docs on // plugin development basics. // // * get 'zip' is called once we know what version we want and that it is // compatible with the OS and Packer. Zip expects an io stream of a zip // file containing a binary. For version 1.0.0 of the 'hashicorp/amazon' // builder and on darwin_amd64, the GitHub getter will fetch the // following ZIP: // https://github.com/hashicorp/packer-plugin-amazon/releases/download/v1.0.0/packer-plugin-amazon_v1.0.0_x5.0_darwin_amd64.zip // this zip is expected to contain a // packer-plugin-amazon_v1.0.0_x5.0_linux_amd64 file that will be checksum // verified then copied to the correct plugin location. Get(what string, opts GetOptions) (io.ReadCloser, error) } type Release struct { Version string `json:"version"` } func ParseReleases(f io.ReadCloser) ([]Release, error) { var releases []Release defer f.Close() return releases, json.NewDecoder(f).Decode(&releases) } type ChecksumFileEntry struct { Filename string `json:"filename"` Checksum string `json:"checksum"` ext, binVersion, os, arch string protVersion string } func (e ChecksumFileEntry) Ext() string { return e.ext } func (e ChecksumFileEntry) BinVersion() string { return e.binVersion } func (e ChecksumFileEntry) ProtVersion() string { return e.protVersion } func (e ChecksumFileEntry) Os() string { return e.os } func (e ChecksumFileEntry) Arch() string { return e.arch } // a file inside will look like so: // // packer-plugin-comment_v0.2.12_x5.0_freebsd_amd64.zip func (e *ChecksumFileEntry) init(req *Requirement) (err error) { filename := e.Filename res := strings.TrimPrefix(filename, req.FilenamePrefix()) // res now looks like v0.2.12_x5.0_freebsd_amd64.zip e.ext = filepath.Ext(res) res = strings.TrimSuffix(res, e.ext) // res now looks like v0.2.12_x5.0_freebsd_amd64 parts := strings.Split(res, "_") // ["v0.2.12", "x5.0", "freebsd", "amd64"] if len(parts) < 4 { return fmt.Errorf("malformed filename expected %s{version}_x{protocol-version}_{os}_{arch}", req.FilenamePrefix()) } e.binVersion, e.protVersion, e.os, e.arch = parts[0], parts[1], parts[2], parts[3] return err } func (e *ChecksumFileEntry) validate(expectedVersion string, installOpts BinaryInstallationOptions) error { if e.binVersion != expectedVersion { return fmt.Errorf("wrong version: '%s' does not match expected %s ", e.binVersion, expectedVersion) } if e.os != installOpts.OS || e.arch != installOpts.ARCH { return fmt.Errorf("wrong system, expected %s_%s ", installOpts.OS, installOpts.ARCH) } return installOpts.CheckProtocolVersion(e.protVersion) } func ParseChecksumFileEntries(f io.Reader) ([]ChecksumFileEntry, error) { var entries []ChecksumFileEntry return entries, json.NewDecoder(f).Decode(&entries) } func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) { getters := opts.Getters log.Printf("[TRACE] getting available versions for the %s plugin", pr.Identifier) versions := version.Collection{} var errs *multierror.Error for _, getter := range getters { releasesFile, err := getter.Get("releases", GetOptions{ PluginRequirement: pr, BinaryInstallationOptions: opts.BinaryInstallationOptions, }) if err != nil { errs = multierror.Append(errs, err) log.Printf("[TRACE] %s", err.Error()) continue } releases, err := ParseReleases(releasesFile) if err != nil { err := fmt.Errorf("could not parse release: %w", err) errs = multierror.Append(errs, err) log.Printf("[TRACE] %s", err.Error()) continue } if len(releases) == 0 { err := fmt.Errorf("no release found") errs = multierror.Append(errs, err) log.Printf("[TRACE] %s", err.Error()) continue } for _, release := range releases { v, err := version.NewVersion(release.Version) if err != nil { err := fmt.Errorf("could not parse release version %s. %w", release.Version, err) errs = multierror.Append(errs, err) log.Printf("[TRACE] %s, ignoring it", err.Error()) continue } if pr.VersionConstraints.Check(v) { versions = append(versions, v) } } if len(versions) == 0 { err := fmt.Errorf("no matching version found in releases. In %v", releases) errs = multierror.Append(errs, err) log.Printf("[TRACE] %s", err.Error()) continue } break } if len(versions) == 0 { if errs.Len() == 0 { err := fmt.Errorf("no release version found for constraints: %q", pr.VersionConstraints.String()) errs = multierror.Append(errs, err) } return nil, errs } // Here we want to try every release in order, starting from the highest one // that matches the requirements. The system and protocol version need to // match too. sort.Sort(sort.Reverse(versions)) log.Printf("[DEBUG] will try to install: %s", versions) for _, version := range versions { //TODO(azr): split in its own InstallVersion(version, opts) function outputFolder := filepath.Join( // Pick last folder as it's the one with the highest priority opts.PluginDirectory, // add expected full path filepath.Join(pr.Identifier.Parts()...), ) log.Printf("[TRACE] fetching checksums file for the %q version of the %s plugin in %q...", version, pr.Identifier, outputFolder) var checksum *FileChecksum for _, getter := range getters { if checksum != nil { break } for _, checksummer := range opts.Checksummers { if checksum != nil { break } checksumFile, err := getter.Get(checksummer.Type, GetOptions{ PluginRequirement: pr, BinaryInstallationOptions: opts.BinaryInstallationOptions, version: version, }) if err != nil { err := fmt.Errorf("could not get %s checksum file for %s version %s. Is the file present on the release and correctly named ? %w", checksummer.Type, pr.Identifier, version, err) errs = multierror.Append(errs, err) log.Printf("[TRACE] %s", err) continue } entries, err := ParseChecksumFileEntries(checksumFile) _ = checksumFile.Close() if err != nil { err := fmt.Errorf("could not parse %s checksumfile: %v. Make sure the checksum file contains a checksum and a binary filename per line", checksummer.Type, err) errs = multierror.Append(errs, err) log.Printf("[TRACE] %s", err) continue } for _, entry := range entries { if err := entry.init(pr); err != nil { err := fmt.Errorf("could not parse checksum filename %s. Is it correctly formatted ? %s", entry.Filename, err) errs = multierror.Append(errs, err) log.Printf("[TRACE] %s", err) continue } if err := entry.validate("v"+version.String(), opts.BinaryInstallationOptions); err != nil { err := fmt.Errorf("ignoring invalid remote binary %s: %s", entry.Filename, err) errs = multierror.Append(errs, err) log.Printf("[TRACE] %s", err) continue } log.Printf("[TRACE] About to get: %s", entry.Filename) cs, err := checksummer.ParseChecksum(strings.NewReader(entry.Checksum)) if err != nil { err := fmt.Errorf("could not parse %s checksum: %s. Make sure the checksum file contains the checksum and only the checksum", checksummer.Type, err) errs = multierror.Append(errs, err) log.Printf("[TRACE] %s", err) continue } checksum = &FileChecksum{ Filename: entry.Filename, Expected: cs, Checksummer: checksummer, } expectedZipFilename := checksum.Filename expectedBinaryFilename := strings.TrimSuffix(expectedZipFilename, filepath.Ext(expectedZipFilename)) + opts.BinaryInstallationOptions.Ext outputFileName := filepath.Join( outputFolder, expectedBinaryFilename, ) for _, potentialChecksumer := range opts.Checksummers { // First check if a local checksum file is already here in the expected // download folder. Here we want to download a binary so we only check // for an existing checksum file from the folder we want to download // into. cs, err := potentialChecksumer.GetCacheChecksumOfFile(outputFileName) if err == nil && len(cs) > 0 { localChecksum := &FileChecksum{ Expected: cs, Checksummer: potentialChecksumer, } log.Printf("[TRACE] found a pre-exising %q checksum file", potentialChecksumer.Type) // if outputFile is there and matches the checksum: do nothing more. if err := localChecksum.ChecksumFile(localChecksum.Expected, outputFileName); err == nil && !opts.Force { log.Printf("[INFO] %s v%s plugin is already correctly installed in %q", pr.Identifier, version, outputFileName) return nil, nil // success } } } // The last folder from the installation list is where we will install. outputFileName = filepath.Join(outputFolder, expectedBinaryFilename) // create directories if need be if err := os.MkdirAll(outputFolder, 0755); err != nil { err := fmt.Errorf("could not create plugin folder %q: %w", outputFolder, err) errs = multierror.Append(errs, err) log.Printf("[TRACE] %s", err.Error()) return nil, errs } for _, getter := range getters { // create temporary file that will receive a temporary binary.zip tmpFile, err := tmp.File("packer-plugin-*.zip") if err != nil { err = fmt.Errorf("could not create temporary file to dowload plugin: %w", err) errs = multierror.Append(errs, err) return nil, errs } defer tmpFile.Close() // start fetching binary remoteZipFile, err := getter.Get("zip", GetOptions{ PluginRequirement: pr, BinaryInstallationOptions: opts.BinaryInstallationOptions, version: version, expectedZipFilename: expectedZipFilename, }) if err != nil { err := fmt.Errorf("could not get binary for %s version %s. Is the file present on the release and correctly named ? %s", pr.Identifier, version, err) errs = multierror.Append(errs, err) log.Printf("[TRACE] %v", err) continue } // write binary to tmp file _, err = io.Copy(tmpFile, remoteZipFile) _ = remoteZipFile.Close() if err != nil { err := fmt.Errorf("Error getting plugin, trying another getter: %w", err) errs = multierror.Append(errs, err) log.Printf("[TRACE] %s", err) continue } if _, err := tmpFile.Seek(0, 0); err != nil { err := fmt.Errorf("Error seeking begining of temporary file for checksumming, continuing: %w", err) errs = multierror.Append(errs, err) log.Printf("[TRACE] %s", err) continue } // verify that the checksum for the zip is what we expect. if err := checksum.Checksummer.Checksum(checksum.Expected, tmpFile); err != nil { err := fmt.Errorf("%w. Is the checksum file correct ? Is the binary file correct ?", err) errs = multierror.Append(errs, err) log.Printf("%s, truncating the zipfile", err) if err := tmpFile.Truncate(0); err != nil { log.Printf("[TRACE] %v", err) } continue } tmpFileStat, err := tmpFile.Stat() if err != nil { err := fmt.Errorf("failed to stat: %w", err) errs = multierror.Append(errs, err) return nil, errs } zr, err := zip.NewReader(tmpFile, tmpFileStat.Size()) if err != nil { err := fmt.Errorf("zip : %v", err) errs = multierror.Append(errs, err) return nil, errs } var copyFrom io.ReadCloser for _, f := range zr.File { if f.Name != expectedBinaryFilename { continue } copyFrom, err = f.Open() if err != nil { err := fmt.Errorf("failed to open temp file: %w", err) errs = multierror.Append(errs, err) return nil, errs } break } if copyFrom == nil { err := fmt.Errorf("could not find a %s file in zipfile", checksum.Filename) errs = multierror.Append(errs, err) return nil, errs } outputFile, err := os.OpenFile(outputFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { err := fmt.Errorf("failed to create %s: %w", outputFileName, err) errs = multierror.Append(errs, err) return nil, errs } defer outputFile.Close() if _, err := io.Copy(outputFile, copyFrom); err != nil { err := fmt.Errorf("extract file: %w", err) errs = multierror.Append(errs, err) return nil, errs } if _, err := outputFile.Seek(0, 0); err != nil { err := fmt.Errorf("Error seeking begining of binary file for checksumming: %w", err) errs = multierror.Append(errs, err) log.Printf("[WARNING] %v, ignoring", err) } cs, err := checksum.Checksummer.Sum(outputFile) if err != nil { err := fmt.Errorf("failed to checksum binary file: %s", err) errs = multierror.Append(errs, err) log.Printf("[WARNING] %v, ignoring", err) } if err := os.WriteFile(outputFileName+checksum.Checksummer.FileExt(), []byte(hex.EncodeToString(cs)), 0644); err != nil { err := fmt.Errorf("failed to write local binary checksum file: %s", err) errs = multierror.Append(errs, err) log.Printf("[WARNING] %v, ignoring", err) } // Success !! return &Installation{ BinaryPath: strings.ReplaceAll(outputFileName, "\\", "/"), Version: "v" + version.String(), }, nil } } } } } if errs.Len() == 0 { err := fmt.Errorf("could not find a local nor a remote checksum for plugin %q %q", pr.Identifier, pr.VersionConstraints) errs = multierror.Append(errs, err) } errs = multierror.Append(errs, fmt.Errorf("could not install any compatible version of plugin %q", pr.Identifier)) return nil, errs }