github.com/hashicorp/packer@v1.14.3/packer/plugin-getter/plugins.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package plugingetter
     5  
     6  import (
     7  	"archive/zip"
     8  	"bytes"
     9  	"encoding/hex"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"io/fs"
    15  	"log"
    16  	"os"
    17  	"os/exec"
    18  	"path"
    19  	"path/filepath"
    20  	"regexp"
    21  	"sort"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/hashicorp/go-multierror"
    27  	goversion "github.com/hashicorp/go-version"
    28  	pluginsdk "github.com/hashicorp/packer-plugin-sdk/plugin"
    29  	"github.com/hashicorp/packer-plugin-sdk/tmp"
    30  	"github.com/hashicorp/packer/hcl2template/addrs"
    31  	"golang.org/x/mod/semver"
    32  )
    33  
    34  const JSONExtension = ".json"
    35  
    36  var HTTPFailure = errors.New("http call failed to releases.hashicorp.com failed")
    37  
    38  type ManifestMeta struct {
    39  	Metadata Metadata `json:"metadata"`
    40  }
    41  type Metadata struct {
    42  	ProtocolVersion string `json:"protocol_version"`
    43  }
    44  
    45  type Requirements []*Requirement
    46  
    47  // Requirement describes a required plugin and how it is installed. Usually a list
    48  // of required plugins is generated from a config file. From it we check what
    49  // is actually installed and what needs to happen to get in the desired state.
    50  type Requirement struct {
    51  	// Plugin accessor as defined in the config file.
    52  	// For Packer, using :
    53  	//  required_plugins { amazon = {...} }
    54  	// Will set Accessor to `amazon`.
    55  	Accessor string
    56  
    57  	// Something like github.com/hashicorp/packer-plugin-amazon, from the
    58  	// previous example.
    59  	Identifier *addrs.Plugin
    60  
    61  	// VersionConstraints as defined by user. Empty ( to be avoided ) means
    62  	// highest found version.
    63  	VersionConstraints goversion.Constraints
    64  }
    65  
    66  type BinaryInstallationOptions struct {
    67  	// The API version with which to check remote compatibility
    68  	//
    69  	// They're generally extracted from the SDK since it's what Packer Core
    70  	// supports as far as the protocol goes
    71  	APIVersionMajor, APIVersionMinor string
    72  	// OS and ARCH usually should be runtime.GOOS and runtime.ARCH, they allow
    73  	// to pick the correct binary.
    74  	OS, ARCH string
    75  
    76  	// Ext is ".exe" on windows
    77  	Ext string
    78  
    79  	Checksummers []Checksummer
    80  
    81  	// ReleasesOnly may be set by commands like validate or build, and
    82  	// forces Packer to not consider plugin pre-releases.
    83  	ReleasesOnly bool
    84  }
    85  
    86  type ListInstallationsOptions struct {
    87  	// The directory in which to look for when installing plugins
    88  	PluginDirectory string
    89  
    90  	BinaryInstallationOptions
    91  }
    92  
    93  // RateLimitError is returned when a getter is being rate limited.
    94  type RateLimitError struct {
    95  	SetableEnvVar string
    96  	ResetTime     time.Time
    97  	Err           error
    98  }
    99  
   100  func (rlerr *RateLimitError) Error() string {
   101  	s := fmt.Sprintf("Plugin host rate limited the plugin getter. Try again in %s.\n", time.Until(rlerr.ResetTime))
   102  	if rlerr.SetableEnvVar != "" {
   103  		s += fmt.Sprintf("HINT: Set the %s env var with a token to get more requests.\n", rlerr.SetableEnvVar)
   104  	}
   105  	s += rlerr.Err.Error()
   106  	return s
   107  }
   108  
   109  // PrereleaseInstallError is returned when a getter encounters the install of a pre-release version.
   110  type PrereleaseInstallError struct {
   111  	PluginSrc string
   112  	Err       error
   113  }
   114  
   115  func (e *PrereleaseInstallError) Error() string {
   116  	var s strings.Builder
   117  	s.WriteString(e.Err.Error() + "\n")
   118  	s.WriteString("Remote installation of pre-release plugin versions is unsupported.\n")
   119  	s.WriteString("This is likely an upstream issue, which should be reported.\n")
   120  	s.WriteString("If you require this specific version of the plugin, download the binary and install it manually.\n")
   121  	s.WriteString("\npacker plugins install --path '<plugin_binary>' " + e.PluginSrc)
   122  	return s.String()
   123  }
   124  
   125  // ContinuableInstallError describe a failed getter install that is
   126  // capable of falling back to next available version.
   127  type ContinuableInstallError struct {
   128  	Err error
   129  }
   130  
   131  func (e *ContinuableInstallError) Error() string {
   132  	return fmt.Sprintf("Continuing to next available version: %s", e.Err)
   133  }
   134  
   135  func (pr Requirement) FilenamePrefix() string {
   136  	if pr.Identifier == nil {
   137  		return "packer-plugin-"
   138  	}
   139  
   140  	return "packer-plugin-" + pr.Identifier.Name() + "_"
   141  }
   142  
   143  func (opts BinaryInstallationOptions) FilenameSuffix() string {
   144  	return "_" + opts.OS + "_" + opts.ARCH + opts.Ext
   145  }
   146  
   147  // getPluginBinaries lists the plugin binaries installed locally.
   148  //
   149  // Each plugin binary must be in the right hierarchy (not root) and has to be
   150  // conforming to the packer-plugin-<name>_<version>_<API>_<os>_<arch> convention.
   151  func (pr Requirement) getPluginBinaries(opts ListInstallationsOptions) ([]string, error) {
   152  	var matches []string
   153  
   154  	rootdir := opts.PluginDirectory
   155  	if pr.Identifier != nil {
   156  		rootdir = filepath.Join(rootdir, path.Dir(pr.Identifier.Source))
   157  	}
   158  
   159  	if _, err := os.Lstat(rootdir); err != nil {
   160  		log.Printf("Directory %q does not exist, the plugin likely isn't installed locally yet.", rootdir)
   161  		return matches, nil
   162  	}
   163  
   164  	err := filepath.WalkDir(rootdir, func(path string, d fs.DirEntry, err error) error {
   165  		if err != nil {
   166  			return err
   167  		}
   168  
   169  		// No need to inspect directory entries, we can continue walking
   170  		if d.IsDir() {
   171  			return nil
   172  		}
   173  
   174  		// Skip plugins installed at root, only those in a hierarchy should be considered valid
   175  		if filepath.Dir(path) == opts.PluginDirectory {
   176  			return nil
   177  		}
   178  
   179  		// If the binary's name doesn't start with packer-plugin-, we skip it.
   180  		if !strings.HasPrefix(filepath.Base(path), pr.FilenamePrefix()) {
   181  			return nil
   182  		}
   183  		// If the binary's name doesn't match the expected convention, we skip it
   184  		if !strings.HasSuffix(filepath.Base(path), opts.FilenameSuffix()) {
   185  			return nil
   186  		}
   187  
   188  		matches = append(matches, path)
   189  
   190  		return nil
   191  	})
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  
   196  	retMatches := make([]string, 0, len(matches))
   197  	// Don't keep plugins that are nested too deep in the hierarchy
   198  	for _, match := range matches {
   199  		dir := strings.Replace(filepath.Dir(match), opts.PluginDirectory, "", 1)
   200  		parts := strings.FieldsFunc(dir, func(r rune) bool {
   201  			return r == '/'
   202  		})
   203  		if len(parts) > 16 {
   204  			log.Printf("[WARN] plugin %q ignored, too many levels of depth: %d (max 16)", match, len(parts))
   205  			continue
   206  		}
   207  
   208  		retMatches = append(retMatches, match)
   209  	}
   210  
   211  	return retMatches, err
   212  }
   213  
   214  // ListInstallations lists unique installed versions of plugin Requirement pr
   215  // with opts as a filter.
   216  //
   217  // Installations are sorted by version and one binary per version is returned.
   218  // Last binary detected takes precedence: in the order 'FromFolders' option.
   219  //
   220  // At least one opts.Checksumers must be given for a binary to be even
   221  // considered.
   222  func (pr Requirement) ListInstallations(opts ListInstallationsOptions) (InstallList, error) {
   223  	res := InstallList{}
   224  	log.Printf("[TRACE] listing potential installations for %q that match %q. %#v", pr.Identifier, pr.VersionConstraints, opts)
   225  
   226  	matches, err := pr.getPluginBinaries(opts)
   227  	if err != nil {
   228  		return nil, fmt.Errorf("ListInstallations: failed to list installed plugins: %s", err)
   229  	}
   230  
   231  	for _, path := range matches {
   232  		fname := filepath.Base(path)
   233  		if fname == "." {
   234  			continue
   235  		}
   236  
   237  		checksumOk := false
   238  		for _, checksummer := range opts.Checksummers {
   239  
   240  			cs, err := checksummer.GetCacheChecksumOfFile(path)
   241  			if err != nil {
   242  				log.Printf("[TRACE] GetChecksumOfFile(%q) failed: %v", path, err)
   243  				continue
   244  			}
   245  
   246  			if err := checksummer.ChecksumFile(cs, path); err != nil {
   247  				log.Printf("[TRACE] ChecksumFile(%q) failed: %v", path, err)
   248  				continue
   249  			}
   250  			checksumOk = true
   251  			break
   252  		}
   253  		if !checksumOk {
   254  			log.Printf("[TRACE] No checksum found for %q ignoring possibly unsafe binary", path)
   255  			continue
   256  		}
   257  
   258  		// base name could look like packer-plugin-amazon_v1.2.3_x5.1_darwin_amd64.exe
   259  		versionsStr := strings.TrimPrefix(fname, pr.FilenamePrefix())
   260  		versionsStr = strings.TrimSuffix(versionsStr, opts.FilenameSuffix())
   261  
   262  		if pr.Identifier == nil {
   263  			if idx := strings.Index(versionsStr, "_"); idx > 0 {
   264  				versionsStr = versionsStr[idx+1:]
   265  			}
   266  		}
   267  
   268  		describeInfo, err := GetPluginDescription(path)
   269  		if err != nil {
   270  			log.Printf("failed to call describe on %q: %s", path, err)
   271  			continue
   272  		}
   273  
   274  		// versionsStr now looks like v1.2.3_x5.1 or amazon_v1.2.3_x5.1
   275  		parts := strings.SplitN(versionsStr, "_", 2)
   276  		pluginVersionStr, protocolVersionStr := parts[0], parts[1]
   277  		ver, err := goversion.NewVersion(pluginVersionStr)
   278  		if err != nil {
   279  			// could not be parsed, ignoring the file
   280  			log.Printf("found %q with an incorrect %q version, ignoring it. %v", path, pluginVersionStr, err)
   281  			continue
   282  		}
   283  
   284  		if fmt.Sprintf("v%s", ver.String()) != pluginVersionStr {
   285  			log.Printf("version %q in path is non canonical, this could introduce ambiguity and is not supported, ignoring it.", pluginVersionStr)
   286  			continue
   287  		}
   288  
   289  		if ver.Prerelease() != "" && opts.ReleasesOnly {
   290  			log.Printf("ignoring pre-release plugin %q", path)
   291  			continue
   292  		}
   293  
   294  		if ver.Metadata() != "" {
   295  			log.Printf("found version %q with metadata in the name, this could introduce ambiguity and is not supported, ignoring it.", pluginVersionStr)
   296  			continue
   297  		}
   298  
   299  		descVersion, err := goversion.NewVersion(describeInfo.Version)
   300  		if err != nil {
   301  			log.Printf("malformed reported version string %q: %s, ignoring", describeInfo.Version, err)
   302  			continue
   303  		}
   304  
   305  		if ver.Compare(descVersion) != 0 {
   306  			log.Printf("plugin %q reported version %q while its name implies version %q, ignoring", path, describeInfo.Version, pluginVersionStr)
   307  			continue
   308  		}
   309  
   310  		preRel := descVersion.Prerelease()
   311  		if preRel != "" && preRel != "dev" {
   312  			log.Printf("invalid plugin pre-release version %q, only development or release binaries are accepted", pluginVersionStr)
   313  		}
   314  
   315  		// Check the API version matches between path and describe
   316  		if describeInfo.APIVersion != protocolVersionStr {
   317  			log.Printf("plugin %q reported API version %q while its name implies version %q, ignoring", path, describeInfo.APIVersion, protocolVersionStr)
   318  			continue
   319  		}
   320  
   321  		// no constraint means always pass, this will happen for implicit
   322  		// plugin requirements and when we list all plugins.
   323  		//
   324  		// Note: we use the raw version name here, without the pre-release
   325  		// suffix, as otherwise constraints reject them, which is not
   326  		// what we want by default.
   327  		if !pr.VersionConstraints.Check(ver.Core()) {
   328  			log.Printf("[TRACE] version %q of file %q does not match constraint %q", pluginVersionStr, path, pr.VersionConstraints.String())
   329  			continue
   330  		}
   331  
   332  		if err := opts.CheckProtocolVersion(protocolVersionStr); err != nil {
   333  			log.Printf("[NOTICE] binary %s requires protocol version %s that is incompatible "+
   334  				"with this version of Packer. %s", path, protocolVersionStr, err)
   335  			continue
   336  		}
   337  
   338  		res = append(res, &Installation{
   339  			BinaryPath: path,
   340  			Version:    pluginVersionStr,
   341  			APIVersion: describeInfo.APIVersion,
   342  		})
   343  	}
   344  
   345  	sort.Sort(res)
   346  
   347  	return res, nil
   348  }
   349  
   350  // InstallList is a list of installed plugins (binaries) with their versions,
   351  // ListInstallations should be used to get an InstallList.
   352  //
   353  // ListInstallations sorts binaries by version and one binary per version is
   354  // returned.
   355  type InstallList []*Installation
   356  
   357  func (l InstallList) String() string {
   358  	v := &strings.Builder{}
   359  	v.Write([]byte("["))
   360  	for i, inst := range l {
   361  		if i > 0 {
   362  			v.Write([]byte(","))
   363  		}
   364  		fmt.Fprintf(v, "%v", *inst)
   365  	}
   366  	v.Write([]byte("]"))
   367  	return v.String()
   368  }
   369  
   370  // Len is the number of elements in the collection.
   371  func (l InstallList) Len() int {
   372  	return len(l)
   373  }
   374  
   375  var rawPluginName = regexp.MustCompile("packer-plugin-[^_]+")
   376  
   377  // Less reports whether the element with index i
   378  // must sort before the element with index j.
   379  //
   380  // If both Less(i, j) and Less(j, i) are false,
   381  // then the elements at index i and j are considered equal.
   382  // Sort may place equal elements in any order in the final result,
   383  // while Stable preserves the original input order of equal elements.
   384  //
   385  // Less must describe a transitive ordering:
   386  //   - if both Less(i, j) and Less(j, k) are true, then Less(i, k) must be true as well.
   387  //   - if both Less(i, j) and Less(j, k) are false, then Less(i, k) must be false as well.
   388  //
   389  // Note that floating-point comparison (the < operator on float32 or float64 values)
   390  // is not a transitive ordering when not-a-number (NaN) values are involved.
   391  // See Float64Slice.Less for a correct implementation for floating-point values.
   392  func (l InstallList) Less(i, j int) bool {
   393  	lowPluginPath := l[i]
   394  	hiPluginPath := l[j]
   395  
   396  	lowRawPluginName := rawPluginName.FindString(path.Base(lowPluginPath.BinaryPath))
   397  	hiRawPluginName := rawPluginName.FindString(path.Base(hiPluginPath.BinaryPath))
   398  
   399  	// We group by path, then by descending order for the versions
   400  	//
   401  	// i.e. if the path are not the same, we can return the plain
   402  	// lexicographic order, otherwise, we'll do a semver-conscious
   403  	// version comparison for sorting.
   404  	if lowRawPluginName != hiRawPluginName {
   405  		return lowRawPluginName < hiRawPluginName
   406  	}
   407  
   408  	verCmp := semver.Compare(lowPluginPath.Version, hiPluginPath.Version)
   409  	if verCmp != 0 {
   410  		return verCmp < 0
   411  	}
   412  
   413  	// Ignore errors here, they are already validated when populating the InstallList
   414  	loAPIVer, _ := NewAPIVersion(lowPluginPath.APIVersion)
   415  	hiAPIVer, _ := NewAPIVersion(hiPluginPath.APIVersion)
   416  
   417  	if loAPIVer.Major != hiAPIVer.Major {
   418  		return loAPIVer.Major < hiAPIVer.Major
   419  	}
   420  
   421  	return loAPIVer.Minor < hiAPIVer.Minor
   422  }
   423  
   424  // Swap swaps the elements with indexes i and j.
   425  func (l InstallList) Swap(i, j int) {
   426  	tmp := l[i]
   427  	l[i] = l[j]
   428  	l[j] = tmp
   429  }
   430  
   431  // Installation describes a plugin installation
   432  type Installation struct {
   433  	// Path to where binary is installed.
   434  	// Ex: /usr/azr/.packer.d/plugins/github.com/hashicorp/amazon/packer-plugin-amazon_v1.2.3_darwin_amd64
   435  	BinaryPath string
   436  
   437  	// Version of this plugin. Ex:
   438  	//  * v1.2.3 for packer-plugin-amazon_v1.2.3_darwin_x5
   439  	Version string
   440  
   441  	// API version for the plugin. Ex:
   442  	//  * 5.0 for packer-plugin-amazon_v1.2.3_darwin_x5.0
   443  	//  * 5.1 for packer-plugin-amazon_v1.2.3_darwin_x5.1
   444  	APIVersion string
   445  }
   446  
   447  // InstallOptions describes the possible options for installing the plugin that
   448  // fits the plugin Requirement.
   449  type InstallOptions struct {
   450  	// Different means to get releases, sha256 and binary files.
   451  	Getters []Getter
   452  
   453  	// The directory in which the plugins should be installed
   454  	PluginDirectory string
   455  
   456  	// Forces installation of the plugin, even if already installed.
   457  	Force bool
   458  
   459  	BinaryInstallationOptions
   460  }
   461  
   462  type GetOptions struct {
   463  	PluginRequirement *Requirement
   464  
   465  	BinaryInstallationOptions
   466  
   467  	version *goversion.Version
   468  
   469  	expectedZipFilename string
   470  }
   471  
   472  // ExpectedZipFilename is the filename of the zip we expect to find, the
   473  // value is known only after parsing the checksum file file.
   474  func (gp *GetOptions) ExpectedZipFilename() string {
   475  	return gp.expectedZipFilename
   476  }
   477  
   478  type APIVersion struct {
   479  	Major int
   480  	Minor int
   481  }
   482  
   483  func NewAPIVersion(apiVersion string) (APIVersion, error) {
   484  	ver := APIVersion{}
   485  
   486  	apiVersion = strings.TrimPrefix(strings.TrimSpace(apiVersion), "x")
   487  	parts := strings.Split(apiVersion, ".")
   488  	if len(parts) < 2 {
   489  		return ver, fmt.Errorf(
   490  			"Invalid remote protocol: %q, expected something like '%s.%s'",
   491  			apiVersion, pluginsdk.APIVersionMajor, pluginsdk.APIVersionMinor,
   492  		)
   493  	}
   494  
   495  	vMajor, err := strconv.Atoi(parts[0])
   496  	if err != nil {
   497  		return ver, err
   498  	}
   499  	ver.Major = vMajor
   500  
   501  	vMinor, err := strconv.Atoi(parts[1])
   502  	if err != nil {
   503  		return ver, err
   504  	}
   505  	ver.Minor = vMinor
   506  
   507  	return ver, nil
   508  }
   509  
   510  var localAPIVersion APIVersion
   511  
   512  func (binOpts *BinaryInstallationOptions) CheckProtocolVersion(remoteProt string) error {
   513  	// no protocol version check
   514  	if binOpts.APIVersionMajor == "" && binOpts.APIVersionMinor == "" {
   515  		return nil
   516  	}
   517  
   518  	localVersion := localAPIVersion
   519  	if binOpts.APIVersionMajor != pluginsdk.APIVersionMajor ||
   520  		binOpts.APIVersionMinor != pluginsdk.APIVersionMinor {
   521  		var err error
   522  
   523  		localVersion, err = NewAPIVersion(fmt.Sprintf("x%s.%s", binOpts.APIVersionMajor, binOpts.APIVersionMinor))
   524  		if err != nil {
   525  			return fmt.Errorf("Failed to parse API Version from constraints: %s", err)
   526  		}
   527  	}
   528  
   529  	remoteVersion, err := NewAPIVersion(remoteProt)
   530  	if err != nil {
   531  		return err
   532  	}
   533  
   534  	if localVersion.Major != remoteVersion.Major {
   535  		return fmt.Errorf("unsupported remote protocol MAJOR version %d. The current MAJOR protocol version is %d."+
   536  			" This version of Packer can only communicate with plugins using that version", remoteVersion.Major, localVersion.Major)
   537  	}
   538  
   539  	if remoteVersion.Minor > localVersion.Minor {
   540  		return fmt.Errorf("unsupported remote protocol MINOR version %d. The supported MINOR protocol versions are version %d and below. "+
   541  			"Please upgrade Packer or use an older version of the plugin if possible", remoteVersion.Minor, localVersion.Minor)
   542  	}
   543  
   544  	return nil
   545  }
   546  
   547  func (gp *GetOptions) Version() string {
   548  	return "v" + gp.version.String()
   549  }
   550  
   551  func (gp *GetOptions) VersionString() string {
   552  	return gp.version.String()
   553  }
   554  
   555  // A Getter helps get the appropriate files to download a binary.
   556  type Getter interface {
   557  	// Get allows Packer to know more information about releases of a plugin in
   558  	// order to decide which version to install. Get behaves similarly to an
   559  	// HTTP server. Packer will stream responses from get in order to do what's
   560  	// needed. In order to minimize the amount of requests done, Packer is
   561  	// strict on filenames and we highly recommend on automating releases.
   562  	// In the future, Packer will make it possible to ship plugin getters as
   563  	// binaries this is why Packer streams from the output of get, which will
   564  	// then be a command.
   565  	//
   566  	//  * 'releases', get 'releases' should return the complete list of Releases
   567  	//    in JSON format following the format of the Release struct. It is also
   568  	//    possible to read GetOptions to filter for a smaller response. Some
   569  	//    getters don't. Packer will then decide the highest compatible
   570  	//    version of the plugin to install by using the sha256 function.
   571  	//
   572  	//  * 'sha256', get 'sha256' should return a SHA256SUMS txt file. It will be
   573  	//    called with the highest possible & user allowed version from get
   574  	//   'releases'. Packer will check if the release has a binary matching what
   575  	//    Packer can install and use. If so, get 'binary' will be called;
   576  	//    otherwise, lower versions will be checked.
   577  	//    For version 1.0.0 of the 'hashicorp/amazon' builder, the GitHub getter
   578  	//    will fetch the following URL:
   579  	//    https://github.com/hashicorp/packer-plugin-amazon/releases/download/v1.0.0/packer-plugin-amazon_v1.0.0_SHA256SUMS
   580  	//    This URL can be parameterized to the following one:
   581  	//    https://github.com/{plugin.path}/releases/download/{plugin.version}/packer-plugin-{plugin.name}_{plugin.version}_SHA256SUMS
   582  	//    If Packer is running on Linux AMD 64, then Packer will check for the
   583  	//    existence of a packer-plugin-amazon_v1.0.0_x5.0_linux_amd64 checksum in
   584  	//    that file. This filename can be parameterized to the following one:
   585  	//    packer-plugin-{plugin.name}_{plugin.version}_x{proto_ver.major}.{proto_ver._minor}_{os}_{arch}
   586  	//
   587  	//    See
   588  	//    https://github.com/hashicorp/packer-plugin-scaffolding/blob/main/.goreleaser.yml
   589  	//    and
   590  	//    https://www.packer.io/docs/plugins/creation#plugin-development-basics
   591  	//    to learn how to create and automate your releases and for docs on
   592  	//    plugin development basics.
   593  	//
   594  	//  * get 'zip' is called once we know what version we want and that it is
   595  	//    compatible with the OS and Packer. Zip expects an io stream of a zip
   596  	//    file containing a binary. For version 1.0.0 of the 'hashicorp/amazon'
   597  	//    builder and on darwin_amd64, the GitHub getter will fetch the
   598  	//    following ZIP:
   599  	//    https://github.com/hashicorp/packer-plugin-amazon/releases/download/v1.0.0/packer-plugin-amazon_v1.0.0_x5.0_darwin_amd64.zip
   600  	//    this zip is expected to contain a
   601  	//    packer-plugin-amazon_v1.0.0_x5.0_linux_amd64 file that will be checksum
   602  	//    verified then copied to the correct plugin location.
   603  	Get(what string, opts GetOptions) (io.ReadCloser, error)
   604  
   605  	// Init this method parses the checksum file and initializes the entry
   606  	Init(req *Requirement, entry *ChecksumFileEntry) error
   607  
   608  	// Validate checks if OS, ARCH, and protocol version matches with the system and local version
   609  	Validate(opt GetOptions, expectedVersion string, installOpts BinaryInstallationOptions, entry *ChecksumFileEntry) error
   610  
   611  	// ExpectedFileName returns the expected file name for the binary, which needs to be installed
   612  	ExpectedFileName(pr *Requirement, version string, entry *ChecksumFileEntry, zipFileName string) string
   613  }
   614  
   615  type Release struct {
   616  	Version string `json:"version"`
   617  }
   618  
   619  func ParseReleases(f io.ReadCloser) ([]Release, error) {
   620  	var releases []Release
   621  	defer f.Close()
   622  	return releases, json.NewDecoder(f).Decode(&releases)
   623  }
   624  
   625  type ChecksumFileEntry struct {
   626  	Filename                  string `json:"filename"`
   627  	Checksum                  string `json:"checksum"`
   628  	Ext, BinVersion, Os, Arch string
   629  	ProtVersion               string
   630  }
   631  
   632  func ParseChecksumFileEntries(f io.Reader) ([]ChecksumFileEntry, error) {
   633  	var entries []ChecksumFileEntry
   634  	return entries, json.NewDecoder(f).Decode(&entries)
   635  }
   636  
   637  func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) {
   638  
   639  	getters := opts.Getters
   640  
   641  	versions := goversion.Collection{}
   642  	var errs *multierror.Error
   643  	for _, getter := range getters {
   644  
   645  		releasesFile, err := getter.Get("releases", GetOptions{
   646  			PluginRequirement:         pr,
   647  			BinaryInstallationOptions: opts.BinaryInstallationOptions,
   648  		})
   649  		if err != nil {
   650  			if errors.Is(err, HTTPFailure) {
   651  				continue
   652  			}
   653  			errs = multierror.Append(errs, err)
   654  			log.Printf("[TRACE] %s", err.Error())
   655  			return nil, errs
   656  		}
   657  
   658  		releases, err := ParseReleases(releasesFile)
   659  		if err != nil {
   660  			err := fmt.Errorf("could not parse release: %w", err)
   661  			errs = multierror.Append(errs, err)
   662  			log.Printf("[TRACE] %s", err.Error())
   663  			continue
   664  		}
   665  		if len(releases) == 0 {
   666  			err := fmt.Errorf("no release found")
   667  			errs = multierror.Append(errs, err)
   668  			log.Printf("[TRACE] %s", err.Error())
   669  			continue
   670  		}
   671  		for _, release := range releases {
   672  			v, err := goversion.NewVersion(release.Version)
   673  			if err != nil {
   674  				err := fmt.Errorf("could not parse release version %s. %w", release.Version, err)
   675  				errs = multierror.Append(errs, err)
   676  				log.Printf("[TRACE] %s, ignoring it", err.Error())
   677  				continue
   678  			}
   679  			if pr.VersionConstraints.Check(v) {
   680  				versions = append(versions, v)
   681  			}
   682  		}
   683  		if len(versions) == 0 {
   684  			err := fmt.Errorf("no matching version found in releases. In %v", releases)
   685  			errs = multierror.Append(errs, err)
   686  			log.Printf("[TRACE] %s", err.Error())
   687  			continue
   688  		}
   689  
   690  		// Here we want to try every release in order, starting from the highest one
   691  		// that matches the requirements. The system and protocol version need to
   692  		// match too.
   693  		sort.Sort(sort.Reverse(versions))
   694  		log.Printf("[DEBUG] will try to install: %s", versions)
   695  
   696  		for _, version := range versions {
   697  			//TODO(azr): split in its own InstallVersion(version, opts) function
   698  
   699  			outputFolder := filepath.Join(
   700  				// Pick last folder as it's the one with the highest priority
   701  				opts.PluginDirectory,
   702  				// add expected full path
   703  				filepath.Join(pr.Identifier.Parts()...),
   704  			)
   705  
   706  			log.Printf("[TRACE] fetching checksums file for the %q version of the %s plugin in %q...", version, pr.Identifier, outputFolder)
   707  
   708  			var checksum *FileChecksum
   709  			for _, checksummer := range opts.Checksummers {
   710  				if checksum != nil {
   711  					break
   712  				}
   713  				checksumFile, err := getter.Get(checksummer.Type, GetOptions{
   714  					PluginRequirement:         pr,
   715  					BinaryInstallationOptions: opts.BinaryInstallationOptions,
   716  					version:                   version,
   717  				})
   718  				if err != nil {
   719  					if errors.Is(err, HTTPFailure) {
   720  						return nil, err
   721  					}
   722  					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)
   723  					errs = multierror.Append(errs, err)
   724  					log.Printf("[TRACE] %s", err)
   725  					continue
   726  				}
   727  
   728  				entries, err := ParseChecksumFileEntries(checksumFile)
   729  				_ = checksumFile.Close()
   730  				if err != nil {
   731  					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)
   732  					errs = multierror.Append(errs, err)
   733  					log.Printf("[TRACE] %s", err)
   734  					continue
   735  				}
   736  
   737  				for _, entry := range entries {
   738  					if filepath.Ext(entry.Filename) == JSONExtension {
   739  						continue
   740  					}
   741  					if err := getter.Init(pr, &entry); err != nil {
   742  						err := fmt.Errorf("could not parse checksum filename %s. Is it correctly formatted ? %s", entry.Filename, err)
   743  						errs = multierror.Append(errs, err)
   744  						log.Printf("[TRACE] %s", err)
   745  						continue
   746  					}
   747  
   748  					metaOpts := GetOptions{
   749  						PluginRequirement:         pr,
   750  						BinaryInstallationOptions: opts.BinaryInstallationOptions,
   751  						version:                   version,
   752  					}
   753  					if err := getter.Validate(metaOpts, version.String(), opts.BinaryInstallationOptions, &entry); err != nil {
   754  						continue
   755  					}
   756  
   757  					log.Printf("[TRACE] About to get: %s", entry.Filename)
   758  
   759  					cs, err := checksummer.ParseChecksum(strings.NewReader(entry.Checksum))
   760  					if err != nil {
   761  						err := fmt.Errorf("could not parse %s checksum: %s. Make sure the checksum file contains the checksum and only the checksum", checksummer.Type, err)
   762  						errs = multierror.Append(errs, err)
   763  						log.Printf("[TRACE] %s", err)
   764  						continue
   765  					}
   766  
   767  					checksum = &FileChecksum{
   768  						Filename:    entry.Filename,
   769  						Expected:    cs,
   770  						Checksummer: checksummer,
   771  					}
   772  
   773  					expectedZipFilename := checksum.Filename
   774  					expectedBinFilename := getter.ExpectedFileName(pr, version.String(), &entry, expectedZipFilename)
   775  					expectedBinaryFilename := strings.TrimSuffix(expectedBinFilename, filepath.Ext(expectedBinFilename)) + opts.BinaryInstallationOptions.Ext
   776  					outputFileName := filepath.Join(outputFolder, expectedBinaryFilename)
   777  
   778  					for _, potentialChecksumer := range opts.Checksummers {
   779  						// First check if a local checksum file is already here in the expected
   780  						// download folder. Here we want to download a binary so we only check
   781  						// for an existing checksum file from the folder we want to download
   782  						// into.
   783  						cs, err := potentialChecksumer.GetCacheChecksumOfFile(outputFileName)
   784  						if err == nil && len(cs) > 0 {
   785  							localChecksum := &FileChecksum{
   786  								Expected:    cs,
   787  								Checksummer: potentialChecksumer,
   788  							}
   789  
   790  							log.Printf("[TRACE] found a pre-existing %q checksum file", potentialChecksumer.Type)
   791  							// if outputFile is there and matches the checksum: do nothing more.
   792  							if err := localChecksum.ChecksumFile(localChecksum.Expected, outputFileName); err == nil && !opts.Force {
   793  								log.Printf("[INFO] %s v%s plugin is already correctly installed in %q", pr.Identifier, version, outputFileName)
   794  								return nil, nil // success
   795  							}
   796  						}
   797  					}
   798  
   799  					// start fetching binary
   800  					remoteZipFile, err := getter.Get("zip", GetOptions{
   801  						PluginRequirement:         pr,
   802  						BinaryInstallationOptions: opts.BinaryInstallationOptions,
   803  						version:                   version,
   804  						expectedZipFilename:       expectedZipFilename,
   805  					})
   806  					if err != nil {
   807  						if errors.Is(err, HTTPFailure) {
   808  							return nil, err
   809  						}
   810  						errs = multierror.Append(errs,
   811  							fmt.Errorf("could not get binary for %s version %s. Is the file present on the release and correctly named ? %s",
   812  								pr.Identifier, version, err))
   813  						continue
   814  					}
   815  					// create temporary file that will receive a temporary binary.zip
   816  					tmpFile, err := tmp.File("packer-plugin-*.zip")
   817  					if err != nil {
   818  						err = fmt.Errorf("could not create temporary file to download plugin: %w", err)
   819  						errs = multierror.Append(errs, err)
   820  						return nil, errs
   821  					}
   822  					defer func() {
   823  						tmpFilePath := tmpFile.Name()
   824  						tmpFile.Close()
   825  						os.Remove(tmpFilePath)
   826  					}()
   827  
   828  					// write binary to tmp file
   829  					_, err = io.Copy(tmpFile, remoteZipFile)
   830  					_ = remoteZipFile.Close()
   831  					if err != nil {
   832  						err := fmt.Errorf("Error getting plugin, trying another getter: %w", err)
   833  						errs = multierror.Append(errs, err)
   834  						continue
   835  					}
   836  					if _, err := tmpFile.Seek(0, 0); err != nil {
   837  						err := fmt.Errorf("Error seeking beginning of temporary file for checksumming, continuing: %w", err)
   838  						errs = multierror.Append(errs, err)
   839  						continue
   840  					}
   841  
   842  					// verify that the checksum for the zip is what we expect.
   843  					if err := checksum.Checksummer.Checksum(checksum.Expected, tmpFile); err != nil {
   844  						err := fmt.Errorf("%w. Is the checksum file correct ? Is the binary file correct ?", err)
   845  						errs = multierror.Append(errs, err)
   846  						continue
   847  					}
   848  					zr, err := zip.OpenReader(tmpFile.Name())
   849  					if err != nil {
   850  						errs = multierror.Append(errs, fmt.Errorf("zip : %v", err))
   851  						return nil, errs
   852  					}
   853  
   854  					var copyFrom io.ReadCloser
   855  					for _, f := range zr.File {
   856  						if f.Name != expectedBinaryFilename {
   857  							continue
   858  						}
   859  						copyFrom, err = f.Open()
   860  						if err != nil {
   861  							errs = multierror.Append(errs, fmt.Errorf("failed to open temp file: %w", err))
   862  							return nil, errs
   863  						}
   864  						break
   865  					}
   866  					if copyFrom == nil {
   867  						err := fmt.Errorf("could not find a %q file in zipfile", expectedBinaryFilename)
   868  						errs = multierror.Append(errs, err)
   869  						return nil, errs
   870  					}
   871  
   872  					var outputFileData bytes.Buffer
   873  					if _, err := io.Copy(&outputFileData, copyFrom); err != nil {
   874  						err := fmt.Errorf("extract file: %w", err)
   875  						errs = multierror.Append(errs, err)
   876  						return nil, errs
   877  					}
   878  					tmpBinFileName := filepath.Join(os.TempDir(), expectedBinaryFilename)
   879  					tmpOutputFile, err := os.OpenFile(tmpBinFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
   880  					if err != nil {
   881  						err = fmt.Errorf("could not create temporary file to download plugin: %w", err)
   882  						errs = multierror.Append(errs, err)
   883  						return nil, errs
   884  					}
   885  					defer func() {
   886  						os.Remove(tmpBinFileName)
   887  					}()
   888  
   889  					if _, err := tmpOutputFile.Write(outputFileData.Bytes()); err != nil {
   890  						err := fmt.Errorf("extract file: %w", err)
   891  						errs = multierror.Append(errs, err)
   892  						return nil, errs
   893  					}
   894  					tmpOutputFile.Close()
   895  
   896  					if err := checkVersion(tmpBinFileName, pr.Identifier.String(), version); err != nil {
   897  						errs = multierror.Append(errs, err)
   898  						var continuableError *ContinuableInstallError
   899  						if errors.As(err, &continuableError) {
   900  							continue
   901  						}
   902  						return nil, errs
   903  					}
   904  
   905  					// create directories if need be
   906  					if err := os.MkdirAll(outputFolder, 0755); err != nil {
   907  						err := fmt.Errorf("could not create plugin folder %q: %w", outputFolder, err)
   908  						errs = multierror.Append(errs, err)
   909  						log.Printf("[TRACE] %s", err.Error())
   910  						return nil, errs
   911  					}
   912  					outputFile, err := os.OpenFile(outputFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
   913  					if err != nil {
   914  						err = fmt.Errorf("could not create final plugin binary file: %w", err)
   915  						errs = multierror.Append(errs, err)
   916  						return nil, errs
   917  					}
   918  					if _, err := outputFile.Write(outputFileData.Bytes()); err != nil {
   919  						err = fmt.Errorf("could not write final plugin binary file: %w", err)
   920  						errs = multierror.Append(errs, err)
   921  						return nil, errs
   922  					}
   923  					outputFile.Close()
   924  
   925  					cs, err = checksum.Checksummer.Sum(&outputFileData)
   926  					if err != nil {
   927  						err := fmt.Errorf("failed to checksum binary file: %s", err)
   928  						errs = multierror.Append(errs, err)
   929  						log.Printf("[WARNING] %v, ignoring", err)
   930  					}
   931  					if err := os.WriteFile(outputFileName+checksum.Checksummer.FileExt(), []byte(hex.EncodeToString(cs)), 0644); err != nil {
   932  						err := fmt.Errorf("failed to write local binary checksum file: %s", err)
   933  						errs = multierror.Append(errs, err)
   934  						log.Printf("[WARNING] %v, ignoring", err)
   935  						os.Remove(outputFileName)
   936  						continue
   937  					}
   938  
   939  					// Success !!
   940  					return &Installation{
   941  						BinaryPath: strings.ReplaceAll(outputFileName, "\\", "/"),
   942  						Version:    "v" + version.String(),
   943  					}, nil
   944  				}
   945  
   946  			}
   947  		}
   948  	}
   949  
   950  	if len(versions) == 0 {
   951  		if errs.Len() == 0 {
   952  			err := fmt.Errorf("no release version found for constraints: %q", pr.VersionConstraints.String())
   953  			errs = multierror.Append(errs, err)
   954  		}
   955  		return nil, errs
   956  	}
   957  
   958  	if errs.ErrorOrNil() == nil {
   959  		err := fmt.Errorf("could not find a local nor a remote checksum for plugin %q %q", pr.Identifier, pr.VersionConstraints)
   960  		errs = multierror.Append(errs, err)
   961  	}
   962  	errs = multierror.Append(errs, fmt.Errorf("could not install any compatible version of plugin %q", pr.Identifier))
   963  	return nil, errs
   964  }
   965  
   966  func GetPluginDescription(pluginPath string) (pluginsdk.SetDescription, error) {
   967  	out, err := exec.Command(pluginPath, "describe").Output()
   968  	if err != nil {
   969  		return pluginsdk.SetDescription{}, err
   970  	}
   971  
   972  	desc := pluginsdk.SetDescription{}
   973  	err = json.Unmarshal(out, &desc)
   974  
   975  	return desc, err
   976  }
   977  
   978  // checkVersion checks the described version of a plugin binary against the requested version constriant.
   979  // A ContinuableInstallError is returned upon a version mismatch to indicate that the caller should try the next
   980  // available version. A PrereleaseInstallError is returned to indicate an unsupported version install.
   981  func checkVersion(binPath string, identifier string, version *goversion.Version) error {
   982  	desc, err := GetPluginDescription(binPath)
   983  	if err != nil {
   984  		err := fmt.Errorf("failed to describe plugin binary %q: %s", binPath, err)
   985  		return &ContinuableInstallError{Err: err}
   986  	}
   987  	descVersion, err := goversion.NewSemver(desc.Version)
   988  	if err != nil {
   989  		err := fmt.Errorf("invalid self-reported version %q: %s", desc.Version, err)
   990  		return &ContinuableInstallError{Err: err}
   991  	}
   992  	if descVersion.Core().Compare(version.Core()) != 0 {
   993  		err := fmt.Errorf("binary reported version (%q) is different from the expected %q, skipping", desc.Version, version.String())
   994  		return &ContinuableInstallError{Err: err}
   995  	}
   996  	if version.Prerelease() != "" {
   997  		return &PrereleaseInstallError{
   998  			PluginSrc: identifier,
   999  			Err:       errors.New("binary reported a pre-release version of " + version.String()),
  1000  		}
  1001  	}
  1002  	// Since only final releases can be installed remotely, a non-empty prerelease version
  1003  	// means something's not right on the release, as it should report a final version.
  1004  	//
  1005  	// Therefore to avoid surprises (and avoid being able to install a version that
  1006  	// cannot be loaded), we error here, and advise users to manually install the plugin if they
  1007  	// need it.
  1008  	if descVersion.Prerelease() != "" {
  1009  		return &PrereleaseInstallError{
  1010  			PluginSrc: identifier,
  1011  			Err:       errors.New("binary reported a pre-release version of " + descVersion.String()),
  1012  		}
  1013  	}
  1014  	return nil
  1015  }
  1016  
  1017  func init() {
  1018  	var err error
  1019  	// Should never error if both components are set
  1020  	localAPIVersion, err = NewAPIVersion(fmt.Sprintf("x%s.%s", pluginsdk.APIVersionMajor, pluginsdk.APIVersionMinor))
  1021  	if err != nil {
  1022  		panic("malformed API version in Packer. This is a programming error, please open an error to report it.")
  1023  	}
  1024  }