2023-03-02 15:37:05 -05:00
// Copyright (c) HashiCorp, Inc.
2023-08-10 15:53:29 -07:00
// SPDX-License-Identifier: BUSL-1.1
2023-03-02 15:37:05 -05:00
2021-02-02 18:05:04 +01:00
package plugingetter
import (
"archive/zip"
2024-05-06 22:10:45 -04:00
"bytes"
2021-02-02 18:05:04 +01:00
"encoding/hex"
"encoding/json"
2024-05-06 22:24:51 -04:00
"errors"
2021-02-02 18:05:04 +01:00
"fmt"
"io"
2024-04-04 16:23:28 -04:00
"io/fs"
2021-02-02 18:05:04 +01:00
"log"
"os"
2024-01-17 15:52:59 -05:00
"os/exec"
"path"
2021-02-02 18:05:04 +01:00
"path/filepath"
2024-01-17 15:52:59 -05:00
"regexp"
2021-02-02 18:05:04 +01:00
"sort"
"strconv"
"strings"
2021-10-14 13:57:59 +02:00
"time"
2021-02-02 18:05:04 +01:00
2021-10-14 13:57:59 +02:00
"github.com/hashicorp/go-multierror"
2024-04-10 17:32:12 -04:00
goversion "github.com/hashicorp/go-version"
2024-01-17 15:52:59 -05:00
pluginsdk "github.com/hashicorp/packer-plugin-sdk/plugin"
2021-02-02 18:05:04 +01:00
"github.com/hashicorp/packer-plugin-sdk/tmp"
"github.com/hashicorp/packer/hcl2template/addrs"
2024-01-17 15:52:59 -05:00
"golang.org/x/mod/semver"
2021-02-02 18:05:04 +01:00
)
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 :
2021-02-15 13:58:58 +01:00
// required_plugins { amazon = {...} }
2021-02-02 18:05:04 +01:00
// 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.
2024-04-10 17:32:12 -04:00
VersionConstraints goversion . Constraints
2021-02-02 18:05:04 +01:00
}
type BinaryInstallationOptions struct {
2024-03-21 11:54:02 -04:00
// The API version with which to check remote compatibility
2021-02-02 18:05:04 +01:00
//
2024-03-21 11:54:02 -04:00
// They're generally extracted from the SDK since it's what Packer Core
// supports as far as the protocol goes
2021-02-02 18:05:04 +01:00
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
2024-02-02 10:34:52 -05:00
// ReleasesOnly may be set by commands like validate or build, and
// forces Packer to not consider plugin pre-releases.
ReleasesOnly bool
2021-02-02 18:05:04 +01:00
}
type ListInstallationsOptions struct {
2024-01-15 14:41:25 -05:00
// The directory in which to look for when installing plugins
PluginDirectory string
2021-02-02 18:05:04 +01:00
BinaryInstallationOptions
}
2021-10-14 13:57:59 +02:00
// 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
}
2024-05-03 21:37:39 -04:00
// PrereleaseInstallError is returned when a getter encounters the install of a pre-release version.
type PrereleaseInstallError struct {
2024-05-07 10:51:50 -04:00
PluginSrc string
Err error
2024-05-03 21:37:39 -04:00
}
2024-05-06 22:24:51 -04:00
func ( e * PrereleaseInstallError ) Error () string {
2024-05-07 10:51:50 -04:00
var s strings . Builder
s . WriteString ( e . Err . Error () + "\n" )
s . WriteString ( "Remote installation of pre-release plugin versions is unsupported.\n" )
s . WriteString ( "This is likely an upstream issue, which should be reported.\n" )
2024-05-03 21:37:39 -04:00
s . WriteString ( "If you require this specific version of the plugin, download the binary and install it manually.\n" )
2024-05-07 10:51:50 -04:00
s . WriteString ( "\npacker plugins install --path '<plugin_binary>' " + e . PluginSrc )
2024-05-03 21:37:39 -04:00
return s . String ()
}
2024-05-06 22:24:51 -04:00
// ContinuableInstallError describe a failed getter install that is
// capable of falling back to next available version.
type ContinuableInstallError struct {
Err error
}
func ( e * ContinuableInstallError ) Error () string {
return fmt . Sprintf ( "Continuing to next available version: %s" , e . Err )
}
2021-02-02 18:05:04 +01:00
func ( pr Requirement ) FilenamePrefix () string {
2022-02-10 22:53:50 +01:00
if pr . Identifier == nil {
return "packer-plugin-"
}
2024-04-04 16:23:28 -04:00
return "packer-plugin-" + pr . Identifier . Name () + "_"
2021-02-02 18:05:04 +01:00
}
2022-04-21 12:14:47 -07:00
func ( opts BinaryInstallationOptions ) FilenameSuffix () string {
2021-02-02 18:05:04 +01:00
return "_" + opts . OS + "_" + opts . ARCH + opts . Ext
}
2024-04-04 16:23:28 -04:00
// getPluginBinaries lists the plugin binaries installed locally.
//
// Each plugin binary must be in the right hierarchy (not root) and has to be
// conforming to the packer-plugin-<name>_<version>_<API>_<os>_<arch> convention.
func ( pr Requirement ) getPluginBinaries ( opts ListInstallationsOptions ) ([] string , error ) {
var matches [] string
rootdir := opts . PluginDirectory
if pr . Identifier != nil {
rootdir = filepath . Join ( rootdir , path . Dir ( pr . Identifier . Source ))
}
if _ , err := os . Lstat ( rootdir ); err != nil {
log . Printf ( "Directory %q does not exist, the plugin likely isn't installed locally yet." , rootdir )
return matches , nil
}
err := filepath . WalkDir ( rootdir , func ( path string , d fs . DirEntry , err error ) error {
if err != nil {
return err
}
// No need to inspect directory entries, we can continue walking
if d . IsDir () {
return nil
}
// Skip plugins installed at root, only those in a hierarchy should be considered valid
if filepath . Dir ( path ) == opts . PluginDirectory {
return nil
}
// If the binary's name doesn't start with packer-plugin-, we skip it.
if ! strings . HasPrefix ( filepath . Base ( path ), pr . FilenamePrefix ()) {
return nil
}
// If the binary's name doesn't match the expected convention, we skip it
if ! strings . HasSuffix ( filepath . Base ( path ), opts . FilenameSuffix ()) {
return nil
}
matches = append ( matches , path )
return nil
})
if err != nil {
return nil , err
}
2024-05-09 11:30:06 -04:00
retMatches := make ([] string , 0 , len ( matches ))
// Don't keep plugins that are nested too deep in the hierarchy
for _ , match := range matches {
dir := strings . Replace ( filepath . Dir ( match ), opts . PluginDirectory , "" , 1 )
parts := strings . FieldsFunc ( dir , func ( r rune ) bool {
return r == '/'
})
if len ( parts ) > 16 {
log . Printf ( "[WARN] plugin %q ignored, too many levels of depth: %d (max 16)" , match , len ( parts ))
continue
}
retMatches = append ( retMatches , match )
}
return retMatches , err
2024-04-04 16:23:28 -04:00
}
2021-02-02 18:05:04 +01:00
// 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 {}
2021-02-15 15:32:42 +01:00
log . Printf ( "[TRACE] listing potential installations for %q that match %q. %#v" , pr . Identifier , pr . VersionConstraints , opts )
2021-02-02 18:05:04 +01:00
2024-04-04 16:23:28 -04:00
matches , err := pr . getPluginBinaries ( opts )
if err != nil {
return nil , fmt . Errorf ( "ListInstallations: failed to list installed plugins: %s" , err )
2021-02-02 18:05:04 +01:00
}
2024-01-15 14:41:25 -05:00
for _ , path := range matches {
fname := filepath . Base ( path )
if fname == "." {
continue
}
2024-03-14 15:48:13 -04:00
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
}
2024-01-15 14:41:25 -05:00
// base name could look like packer-plugin-amazon_v1.2.3_x5.1_darwin_amd64.exe
2024-04-04 16:23:28 -04:00
versionsStr := strings . TrimPrefix ( fname , pr . FilenamePrefix ())
versionsStr = strings . TrimSuffix ( versionsStr , opts . FilenameSuffix ())
2024-01-15 14:41:25 -05:00
if pr . Identifier == nil {
if idx := strings . Index ( versionsStr , "_" ); idx > 0 {
versionsStr = versionsStr [ idx + 1 :]
}
}
2024-04-10 17:26:05 -04:00
describeInfo , err := GetPluginDescription ( path )
2024-01-17 15:55:44 -05:00
if err != nil {
2024-04-10 17:26:05 -04:00
log . Printf ( "failed to call describe on %q: %s" , path , err )
2024-01-17 15:55:44 -05:00
continue
}
2024-01-15 14:41:25 -05:00
// versionsStr now looks like v1.2.3_x5.1 or amazon_v1.2.3_x5.1
parts := strings . SplitN ( versionsStr , "_" , 2 )
2024-03-25 17:25:36 -04:00
pluginVersionStr , protocolVersionStr := parts [ 0 ], parts [ 1 ]
2024-04-10 17:32:12 -04:00
ver , err := goversion . NewVersion ( pluginVersionStr )
2024-01-15 14:41:25 -05:00
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
}
2024-03-22 12:13:12 -04:00
if fmt . Sprintf ( "v%s" , ver . String ()) != pluginVersionStr {
log . Printf ( "version %q in path is non canonical, this could introduce ambiguity and is not supported, ignoring it." , pluginVersionStr )
continue
}
2024-02-02 10:34:52 -05:00
if ver . Prerelease () != "" && opts . ReleasesOnly {
log . Printf ( "ignoring pre-release plugin %q" , path )
continue
}
2024-05-15 16:42:35 -04:00
if ver . Metadata () != "" {
log . Printf ( "found version %q with metadata in the name, this could introduce ambiguity and is not supported, ignoring it." , pluginVersionStr )
continue
}
2024-04-10 17:32:12 -04:00
descVersion , err := goversion . NewVersion ( describeInfo . Version )
2024-03-14 15:42:38 -04:00
if err != nil {
log . Printf ( "malformed reported version string %q: %s, ignoring" , describeInfo . Version , err )
2024-02-01 15:13:18 -05:00
continue
}
2024-04-23 15:17:04 -04:00
if ver . Compare ( descVersion ) != 0 {
2024-03-14 15:42:38 -04:00
log . Printf ( "plugin %q reported version %q while its name implies version %q, ignoring" , path , describeInfo . Version , pluginVersionStr )
continue
}
preRel := descVersion . Prerelease ()
if preRel != "" && preRel != "dev" {
log . Printf ( "invalid plugin pre-release version %q, only development or release binaries are accepted" , pluginVersionStr )
}
2024-03-21 11:52:18 -04:00
// Check the API version matches between path and describe
2024-03-25 17:25:36 -04:00
if describeInfo . APIVersion != protocolVersionStr {
log . Printf ( "plugin %q reported API version %q while its name implies version %q, ignoring" , path , describeInfo . APIVersion , protocolVersionStr )
2024-03-21 11:52:18 -04:00
continue
}
2024-01-15 14:41:25 -05:00
// no constraint means always pass, this will happen for implicit
// plugin requirements and when we list all plugins.
2024-02-01 15:13:18 -05:00
//
// 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.
2024-04-23 15:17:04 -04:00
if ! pr . VersionConstraints . Check ( ver . Core ()) {
2024-01-15 14:41:25 -05:00
log . Printf ( "[TRACE] version %q of file %q does not match constraint %q" , pluginVersionStr , path , pr . VersionConstraints . String ())
continue
}
2024-03-25 17:25:36 -04:00
if err := opts . CheckProtocolVersion ( protocolVersionStr ); err != nil {
2024-01-15 14:41:25 -05:00
log . Printf ( "[NOTICE] binary %s requires protocol version %s that is incompatible " +
2024-03-25 17:25:36 -04:00
"with this version of Packer. %s" , path , protocolVersionStr , err )
2024-01-15 14:41:25 -05:00
continue
}
res = append ( res , & Installation {
BinaryPath : path ,
Version : pluginVersionStr ,
2024-03-21 11:54:02 -04:00
APIVersion : describeInfo . APIVersion ,
2024-01-15 14:41:25 -05:00
})
}
2024-01-17 15:52:59 -05:00
sort . Sort ( res )
2021-02-02 18:05:04 +01:00
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 ()
}
2024-01-17 15:49:51 -05:00
// 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
}
2024-03-21 11:54:02 -04:00
verCmp := semver . Compare ( lowPluginPath . Version , hiPluginPath . Version )
if verCmp != 0 {
return verCmp < 0
}
// Ignore errors here, they are already validated when populating the InstallList
loAPIVer , _ := NewAPIVersion ( lowPluginPath . APIVersion )
hiAPIVer , _ := NewAPIVersion ( hiPluginPath . APIVersion )
if loAPIVer . Major != hiAPIVer . Major {
return loAPIVer . Major < hiAPIVer . Major
}
return loAPIVer . Minor < hiAPIVer . Minor
2024-01-17 15:49:51 -05:00
}
// 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
}
2021-02-02 18:05:04 +01:00
// Installation describes a plugin installation
type Installation struct {
2024-01-17 13:39:36 -05:00
// Path to where binary is installed.
// Ex: /usr/azr/.packer.d/plugins/github.com/hashicorp/amazon/packer-plugin-amazon_v1.2.3_darwin_amd64
2021-02-02 18:05:04 +01:00
BinaryPath string
2024-01-17 13:39:36 -05:00
// Version of this plugin. Ex:
2021-02-02 18:05:04 +01:00
// * v1.2.3 for packer-plugin-amazon_v1.2.3_darwin_x5
Version string
2024-03-21 11:54:02 -04:00
// API version for the plugin. Ex:
// * 5.0 for packer-plugin-amazon_v1.2.3_darwin_x5.0
// * 5.1 for packer-plugin-amazon_v1.2.3_darwin_x5.1
APIVersion string
2021-02-02 18:05:04 +01:00
}
// 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
2024-01-15 14:41:25 -05:00
// The directory in which the plugins should be installed
PluginDirectory string
2021-02-02 18:05:04 +01:00
2023-10-25 11:56:30 -04:00
// Forces installation of the plugin, even if already installed.
Force bool
2021-02-02 18:05:04 +01:00
BinaryInstallationOptions
}
type GetOptions struct {
PluginRequirement * Requirement
BinaryInstallationOptions
2024-04-10 17:32:12 -04:00
version * goversion . Version
2021-02-02 18:05:04 +01:00
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
}
2024-03-21 11:54:02 -04:00
type APIVersion struct {
Major int
Minor int
}
2021-02-02 18:05:04 +01:00
2024-03-21 11:54:02 -04:00
func NewAPIVersion ( apiVersion string ) ( APIVersion , error ) {
ver := APIVersion {}
apiVersion = strings . TrimPrefix ( strings . TrimSpace ( apiVersion ), "x" )
parts := strings . Split ( apiVersion , "." )
if len ( parts ) < 2 {
return ver , fmt . Errorf (
"Invalid remote protocol: %q, expected something like '%s.%s'" ,
apiVersion , pluginsdk . APIVersionMajor , pluginsdk . APIVersionMinor ,
)
}
vMajor , err := strconv . Atoi ( parts [ 0 ])
if err != nil {
return ver , err
}
ver . Major = vMajor
vMinor , err := strconv . Atoi ( parts [ 1 ])
if err != nil {
return ver , err
}
ver . Minor = vMinor
return ver , nil
}
var localAPIVersion APIVersion
func ( binOpts * BinaryInstallationOptions ) CheckProtocolVersion ( remoteProt string ) error {
2022-02-10 22:53:50 +01:00
// no protocol version check
if binOpts . APIVersionMajor == "" && binOpts . APIVersionMinor == "" {
return nil
}
2024-03-21 11:54:02 -04:00
localVersion := localAPIVersion
if binOpts . APIVersionMajor != pluginsdk . APIVersionMajor ||
binOpts . APIVersionMinor != pluginsdk . APIVersionMinor {
var err error
localVersion , err = NewAPIVersion ( fmt . Sprintf ( "x%s.%s" , binOpts . APIVersionMajor , binOpts . APIVersionMinor ))
if err != nil {
return fmt . Errorf ( "Failed to parse API Version from constraints: %s" , err )
}
2021-02-02 18:05:04 +01:00
}
2024-03-21 11:54:02 -04:00
remoteVersion , err := NewAPIVersion ( remoteProt )
2021-02-02 18:05:04 +01:00
if err != nil {
return err
}
2024-03-21 11:54:02 -04:00
if localVersion . Major != remoteVersion . Major {
return fmt . Errorf ( "Unsupported remote protocol MAJOR version %d. The current MAJOR protocol version is %d." +
" This version of Packer can only communicate with plugins using that version." , remoteVersion . Major , localVersion . Major )
2021-02-02 18:05:04 +01:00
}
2024-03-21 11:54:02 -04:00
if remoteVersion . Minor > localVersion . Minor {
return fmt . Errorf ( "Unsupported remote protocol MINOR version %d. The supported MINOR protocol versions are version %d and below. " +
"Please upgrade Packer or use an older version of the plugin if possible." , remoteVersion . Minor , localVersion . Minor )
2021-02-02 18:05:04 +01:00
}
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 {
2021-08-11 16:32:38 +02:00
// 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.
2021-02-02 18:05:04 +01:00
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:
//
2023-04-27 15:17:31 -04:00
// packer-plugin-comment_v0.2.12_x5.0_freebsd_amd64.zip
2021-02-02 18:05:04 +01:00
func ( e * ChecksumFileEntry ) init ( req * Requirement ) ( err error ) {
filename := e . Filename
2021-03-15 03:31:21 -07:00
res := strings . TrimPrefix ( filename , req . FilenamePrefix ())
2021-02-02 18:05:04 +01:00
// res now looks like v0.2.12_x5.0_freebsd_amd64.zip
e . ext = filepath . Ext ( res )
2021-03-15 03:31:21 -07:00
res = strings . TrimSuffix ( res , e . ext )
2021-02-02 18:05:04 +01:00
// 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 {
2021-03-15 03:31:21 -07:00
return fmt . Errorf ( "wrong version: '%s' does not match expected %s " , e . binVersion , expectedVersion )
2021-02-02 18:05:04 +01:00
}
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
2021-02-16 13:31:18 -05:00
log . Printf ( "[TRACE] getting available versions for the %s plugin" , pr . Identifier )
2024-04-10 17:32:12 -04:00
versions := goversion . Collection {}
2021-10-14 13:57:59 +02:00
var errs * multierror . Error
2021-02-02 18:05:04 +01:00
for _ , getter := range getters {
releasesFile , err := getter . Get ( "releases" , GetOptions {
PluginRequirement : pr ,
BinaryInstallationOptions : opts . BinaryInstallationOptions ,
})
if err != nil {
2021-10-14 13:57:59 +02:00
errs = multierror . Append ( errs , err )
2021-02-02 18:05:04 +01:00
log . Printf ( "[TRACE] %s" , err . Error ())
continue
}
releases , err := ParseReleases ( releasesFile )
if err != nil {
err := fmt . Errorf ( "could not parse release: %w" , err )
2021-10-14 13:57:59 +02:00
errs = multierror . Append ( errs , err )
2021-02-02 18:05:04 +01:00
log . Printf ( "[TRACE] %s" , err . Error ())
continue
}
if len ( releases ) == 0 {
err := fmt . Errorf ( "no release found" )
2021-10-14 13:57:59 +02:00
errs = multierror . Append ( errs , err )
2021-02-02 18:05:04 +01:00
log . Printf ( "[TRACE] %s" , err . Error ())
continue
}
for _ , release := range releases {
2024-04-10 17:32:12 -04:00
v , err := goversion . NewVersion ( release . Version )
2021-02-02 18:05:04 +01:00
if err != nil {
2021-10-14 13:57:59 +02:00
err := fmt . Errorf ( "could not parse release version %s. %w" , release . Version , err )
errs = multierror . Append ( errs , err )
2021-02-02 18:05:04 +01:00
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 )
2021-10-14 13:57:59 +02:00
errs = multierror . Append ( errs , err )
2021-02-02 18:05:04 +01:00
log . Printf ( "[TRACE] %s" , err . Error ())
continue
}
break
}
2021-10-14 13:57:59 +02:00
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.
2021-02-02 18:05:04 +01:00
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
2024-01-15 14:41:25 -05:00
opts . PluginDirectory ,
2021-02-02 18:05:04 +01:00
// add expected full path
filepath . Join ( pr . Identifier . Parts () ... ),
)
2021-02-15 15:32:42 +01:00
log . Printf ( "[TRACE] fetching checksums file for the %q version of the %s plugin in %q..." , version , pr . Identifier , outputFolder )
2021-02-02 18:05:04 +01:00
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 {
2021-10-14 13:57:59 +02:00
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 )
2022-11-09 17:17:52 -05:00
continue
2021-02-02 18:05:04 +01:00
}
entries , err := ParseChecksumFileEntries ( checksumFile )
_ = checksumFile . Close ()
if err != nil {
2021-10-14 13:57:59 +02:00
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 )
2021-02-02 18:05:04 +01:00
continue
}
for _ , entry := range entries {
if err := entry . init ( pr ); err != nil {
2021-10-14 13:57:59 +02:00
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 )
2021-02-02 18:05:04 +01:00
continue
}
if err := entry . validate ( "v" + version . String (), opts . BinaryInstallationOptions ); err != nil {
continue
}
log . Printf ( "[TRACE] About to get: %s" , entry . Filename )
cs , err := checksummer . ParseChecksum ( strings . NewReader ( entry . Checksum ))
if err != nil {
2021-10-14 13:57:59 +02:00
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 )
2021-02-02 18:05:04 +01:00
continue
}
checksum = & FileChecksum {
Filename : entry . Filename ,
Expected : cs ,
Checksummer : checksummer ,
}
expectedZipFilename := checksum . Filename
expectedBinaryFilename := strings . TrimSuffix ( expectedZipFilename , filepath . Ext ( expectedZipFilename )) + opts . BinaryInstallationOptions . Ext
2024-05-03 14:20:30 -04:00
outputFileName := filepath . Join ( outputFolder , expectedBinaryFilename )
2021-02-02 18:05:04 +01:00
2024-01-15 14:41:25 -05:00
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 ,
}
2021-02-02 18:05:04 +01:00
2024-05-03 14:20:30 -04:00
log . Printf ( "[TRACE] found a pre-existing %q checksum file" , potentialChecksumer . Type )
2024-01-15 14:41:25 -05:00
// 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
2021-02-02 18:05:04 +01:00
}
}
}
for _ , getter := range getters {
// start fetching binary
remoteZipFile , err := getter . Get ( "zip" , GetOptions {
PluginRequirement : pr ,
BinaryInstallationOptions : opts . BinaryInstallationOptions ,
version : version ,
expectedZipFilename : expectedZipFilename ,
})
if err != nil {
2024-05-03 14:20:30 -04:00
errs = multierror . Append ( errs ,
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 ))
2021-02-02 18:05:04 +01:00
continue
}
2024-05-03 14:20:30 -04:00
// 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 download plugin: %w" , err )
errs = multierror . Append ( errs , err )
return nil , errs
}
defer func () {
tmpFilePath := tmpFile . Name ()
tmpFile . Close ()
os . Remove ( tmpFilePath )
}()
2021-02-02 18:05:04 +01:00
// write binary to tmp file
_ , err = io . Copy ( tmpFile , remoteZipFile )
_ = remoteZipFile . Close ()
if err != nil {
2021-10-14 13:57:59 +02:00
err := fmt . Errorf ( "Error getting plugin, trying another getter: %w" , err )
errs = multierror . Append ( errs , err )
2021-02-02 18:05:04 +01:00
continue
}
if _ , err := tmpFile . Seek ( 0 , 0 ); err != nil {
2024-05-03 14:20:30 -04:00
err := fmt . Errorf ( "Error seeking beginning of temporary file for checksumming, continuing: %w" , err )
2021-10-14 13:57:59 +02:00
errs = multierror . Append ( errs , err )
2021-02-02 18:05:04 +01:00
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 )
2021-10-14 13:57:59 +02:00
errs = multierror . Append ( errs , err )
2021-02-02 18:05:04 +01:00
continue
}
2024-05-03 14:20:30 -04:00
zr , err := zip . OpenReader ( tmpFile . Name ())
2021-02-02 18:05:04 +01:00
if err != nil {
2024-05-03 14:20:30 -04:00
errs = multierror . Append ( errs , fmt . Errorf ( "zip : %v" , err ))
2021-10-14 13:57:59 +02:00
return nil , errs
2021-02-02 18:05:04 +01:00
}
var copyFrom io . ReadCloser
for _ , f := range zr . File {
if f . Name != expectedBinaryFilename {
continue
}
copyFrom , err = f . Open ()
if err != nil {
2024-06-19 16:06:15 -04:00
errs = multierror . Append ( errs , fmt . Errorf ( "failed to open temp file: %w" , err ))
2021-10-14 13:57:59 +02:00
return nil , errs
2021-02-02 18:05:04 +01:00
}
break
}
if copyFrom == nil {
2024-04-10 17:43:48 -04:00
err := fmt . Errorf ( "could not find a %q file in zipfile" , expectedBinaryFilename )
2021-10-14 13:57:59 +02:00
errs = multierror . Append ( errs , err )
return nil , errs
2021-02-02 18:05:04 +01:00
}
2024-05-06 22:10:45 -04:00
var outputFileData bytes . Buffer
if _ , err := io . Copy ( & outputFileData , copyFrom ); err != nil {
err := fmt . Errorf ( "extract file: %w" , err )
errs = multierror . Append ( errs , err )
return nil , errs
}
2024-05-03 14:20:30 -04:00
tmpBinFileName := filepath . Join ( os . TempDir (), expectedBinaryFilename )
2024-05-06 22:10:45 -04:00
tmpOutputFile , err := os . OpenFile ( tmpBinFileName , os . O_RDWR | os . O_CREATE | os . O_TRUNC , 0755 )
2021-02-02 18:05:04 +01:00
if err != nil {
2024-05-03 14:20:30 -04:00
err = fmt . Errorf ( "could not create temporary file to download plugin: %w" , err )
errs = multierror . Append ( errs , err )
return nil , errs
2024-04-10 17:37:36 -04:00
}
2024-05-03 14:20:30 -04:00
defer func () {
2024-05-06 22:10:45 -04:00
os . Remove ( tmpBinFileName )
2024-05-03 14:20:30 -04:00
}()
2024-04-10 17:37:36 -04:00
2024-05-06 22:10:45 -04:00
if _ , err := tmpOutputFile . Write ( outputFileData . Bytes ()); err != nil {
2024-05-03 14:20:30 -04:00
err := fmt . Errorf ( "extract file: %w" , err )
errs = multierror . Append ( errs , err )
return nil , errs
2024-04-10 17:37:36 -04:00
}
2024-05-03 14:20:30 -04:00
tmpOutputFile . Close ()
2024-05-06 22:24:51 -04:00
if err := checkVersion ( tmpBinFileName , pr . Identifier . String (), version ); err != nil {
2024-04-10 17:37:36 -04:00
errs = multierror . Append ( errs , err )
2024-05-06 22:24:51 -04:00
var continuableError * ContinuableInstallError
if errors . As ( err , & continuableError ) {
continue
2024-05-03 21:37:39 -04:00
}
2024-05-06 22:24:51 -04:00
return nil , errs
2021-02-02 18:05:04 +01:00
}
2024-05-06 22:10:45 -04:00
// 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
}
outputFile , err := os . OpenFile ( outputFileName , os . O_RDWR | os . O_CREATE | os . O_TRUNC , 0755 )
if err != nil {
err = fmt . Errorf ( "could not create final plugin binary file: %w" , err )
2021-10-14 13:57:59 +02:00
errs = multierror . Append ( errs , err )
2024-05-03 14:20:30 -04:00
return nil , errs
2021-02-02 18:05:04 +01:00
}
2024-05-06 22:10:45 -04:00
if _ , err := outputFile . Write ( outputFileData . Bytes ()); err != nil {
err = fmt . Errorf ( "could not write final plugin binary file: %w" , err )
errs = multierror . Append ( errs , err )
return nil , errs
}
outputFile . Close ()
cs , err := checksum . Checksummer . Sum ( & outputFileData )
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 {
2021-02-02 18:05:04 +01:00
err := fmt . Errorf ( "failed to write local binary checksum file: %s" , err )
2021-10-14 13:57:59 +02:00
errs = multierror . Append ( errs , err )
2021-02-02 18:05:04 +01:00
log . Printf ( "[WARNING] %v, ignoring" , err )
2024-05-03 21:37:39 -04:00
os . Remove ( outputFileName )
continue
2021-02-02 18:05:04 +01:00
}
// Success !!
return & Installation {
2024-05-03 14:20:30 -04:00
BinaryPath : strings . ReplaceAll ( outputFileName , "\\" , "/" ),
2021-02-02 18:05:04 +01:00
Version : "v" + version . String (),
}, nil
}
}
}
}
}
2024-05-03 14:20:30 -04:00
if errs . ErrorOrNil () == nil {
2021-10-14 13:57:59 +02:00
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 )
}
2022-11-09 17:17:52 -05:00
errs = multierror . Append ( errs , fmt . Errorf ( "could not install any compatible version of plugin %q" , pr . Identifier ))
2021-10-14 13:57:59 +02:00
return nil , errs
2021-02-02 18:05:04 +01:00
}
2024-03-21 11:54:02 -04:00
2024-04-10 17:26:05 -04:00
func GetPluginDescription ( pluginPath string ) ( pluginsdk . SetDescription , error ) {
out , err := exec . Command ( pluginPath , "describe" ). Output ()
if err != nil {
return pluginsdk . SetDescription {}, err
}
desc := pluginsdk . SetDescription {}
err = json . Unmarshal ( out , & desc )
return desc , err
}
2024-05-06 22:24:51 -04:00
// checkVersion checks the described version of a plugin binary against the requested version constriant.
// A ContinuableInstallError is returned upon a version mismatch to indicate that the caller should try the next
// available version. A PrereleaseInstallError is returned to indicate an unsupported version install.
func checkVersion ( binPath string , identifier string , version * goversion . Version ) error {
desc , err := GetPluginDescription ( binPath )
if err != nil {
err := fmt . Errorf ( "failed to describe plugin binary %q: %s" , binPath , err )
return & ContinuableInstallError { Err : err }
}
descVersion , err := goversion . NewSemver ( desc . Version )
if err != nil {
err := fmt . Errorf ( "invalid self-reported version %q: %s" , desc . Version , err )
return & ContinuableInstallError { Err : err }
}
if descVersion . Core (). Compare ( version . Core ()) != 0 {
err := fmt . Errorf ( "binary reported version (%q) is different from the expected %q, skipping" , desc . Version , version . String ())
return & ContinuableInstallError { Err : err }
}
2024-05-07 10:51:50 -04:00
if version . Prerelease () != "" {
return & PrereleaseInstallError {
PluginSrc : identifier ,
Err : errors . New ( "binary reported a pre-release version of " + version . String ()),
}
}
2024-05-06 22:24:51 -04:00
// Since only final releases can be installed remotely, a non-empty prerelease version
// means something's not right on the release, as it should report a final version.
//
// Therefore to avoid surprises (and avoid being able to install a version that
// cannot be loaded), we error here, and advise users to manually install the plugin if they
// need it.
if descVersion . Prerelease () != "" {
return & PrereleaseInstallError {
2024-05-07 10:51:50 -04:00
PluginSrc : identifier ,
Err : errors . New ( "binary reported a pre-release version of " + descVersion . String ()),
2024-05-06 22:24:51 -04:00
}
}
return nil
}
2024-03-21 11:54:02 -04:00
func init () {
var err error
// Should never error if both components are set
localAPIVersion , err = NewAPIVersion ( fmt . Sprintf ( "x%s.%s" , pluginsdk . APIVersionMajor , pluginsdk . APIVersionMinor ))
if err != nil {
2024-07-18 09:13:29 -04:00
panic ( "malformed API version in Packer. This is a programming error, please open an error to report it." )
2024-03-21 11:54:02 -04:00
}
}