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"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"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"
2021-02-02 18:05:04 +01:00
"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.
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
}
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
}
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-"
}
2021-02-02 18:05:04 +01:00
return "packer-plugin-" + pr . Identifier . Type + "_"
}
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-02-01 15:13:18 -05:00
var pluginVersionRegex = regexp . MustCompile ( "^v([0-9]+\\.[0-9]+\\.[0-9]+)(-dev)?$" )
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 {}
FilenamePrefix := pr . FilenamePrefix ()
2022-04-21 12:14:47 -07:00
filenameSuffix := opts . FilenameSuffix ()
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-01-15 14:41:25 -05:00
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 )
2021-02-02 18:05:04 +01:00
}
2024-01-15 14:41:25 -05:00
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 :]
}
}
2024-01-17 15:55:44 -05:00
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 )
}
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 )
pluginVersionStr , protocolVerionStr := parts [ 0 ], parts [ 1 ]
2024-02-01 15:13:18 -05:00
_ , err = version . 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-02-01 15:13:18 -05:00
matches := pluginVersionRegex . FindStringSubmatch ( pluginVersionStr )
if matches == nil {
log . Printf ( "invalid version found: %q, ignoring" , pluginVersionStr )
2024-01-17 15:55:44 -05:00
continue
}
2024-02-01 15:13:18 -05:00
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 ])
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.
if ! pr . VersionConstraints . Check ( rawVersion ) {
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
}
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 ,
})
}
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-02-01 15:01:37 -05:00
return semver . Compare ( lowPluginPath . Version , hiPluginPath . Version ) < 0
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
}
// 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
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 ]
2022-02-10 22:53:50 +01:00
// no protocol version check
if binOpts . APIVersionMajor == "" && binOpts . APIVersionMinor == "" {
return nil
}
2021-02-02 18:05:04 +01:00
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 bellow." +
"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 {
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 )
2021-02-02 18:05:04 +01:00
versions := version . 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 {
v , err := version . NewVersion ( release . Version )
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 {
2021-10-14 13:57:59 +02:00
err := fmt . Errorf ( "ignoring invalid remote binary %s: %s" , entry . Filename , err )
errs = multierror . Append ( errs , err )
log . Printf ( "[TRACE] %s" , err )
2021-02-02 18:05:04 +01:00
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-01-15 14:41:25 -05:00
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 ,
}
2021-02-02 18:05:04 +01:00
2024-01-15 14:41:25 -05:00
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
2021-02-02 18:05:04 +01:00
}
}
}
// The last folder from the installation list is where we will install.
2024-01-15 14:41:25 -05:00
outputFileName = filepath . Join ( outputFolder , expectedBinaryFilename )
2021-02-02 18:05:04 +01: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 )
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 ())
2021-10-14 13:57:59 +02:00
return nil , errs
2021-02-02 18:05:04 +01:00
}
for _ , getter := range getters {
// create temporary file that will receive a temporary binary.zip
tmpFile , err := tmp . File ( "packer-plugin-*.zip" )
if err != nil {
2021-10-14 13:57:59 +02:00
err = fmt . Errorf ( "could not create temporary file to dowload plugin: %w" , err )
errs = multierror . Append ( errs , err )
return nil , errs
2021-02-02 18:05:04 +01:00
}
defer tmpFile . Close ()
// start fetching binary
remoteZipFile , err := getter . Get ( "zip" , GetOptions {
PluginRequirement : pr ,
BinaryInstallationOptions : opts . BinaryInstallationOptions ,
version : version ,
expectedZipFilename : expectedZipFilename ,
})
if err != nil {
2021-02-15 15:32:42 +01:00
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 )
2021-10-14 13:57:59 +02:00
errs = multierror . Append ( errs , err )
2021-02-02 18:05:04 +01:00
log . Printf ( "[TRACE] %v" , err )
continue
}
// 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 )
log . Printf ( "[TRACE] %s" , err )
2021-02-02 18:05:04 +01:00
continue
}
if _ , err := tmpFile . Seek ( 0 , 0 ); err != nil {
2021-10-14 13:57:59 +02:00
err := fmt . Errorf ( "Error seeking begining of temporary file for checksumming, continuing: %w" , err )
errs = multierror . Append ( errs , err )
log . Printf ( "[TRACE] %s" , 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
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 {
2021-10-14 13:57:59 +02:00
err := fmt . Errorf ( "failed to stat: %w" , err )
errs = multierror . Append ( errs , err )
return nil , errs
2021-02-02 18:05:04 +01:00
}
zr , err := zip . NewReader ( tmpFile , tmpFileStat . Size ())
if err != nil {
err := fmt . Errorf ( "zip : %v" , err )
2021-10-14 13:57:59 +02:00
errs = multierror . Append ( errs , err )
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 {
2021-10-14 13:57:59 +02:00
err := fmt . Errorf ( "failed to open temp file: %w" , err )
errs = multierror . Append ( errs , err )
return nil , errs
2021-02-02 18:05:04 +01:00
}
break
}
if copyFrom == nil {
err := fmt . Errorf ( "could not find a %s file in zipfile" , checksum . Filename )
2021-10-14 13:57:59 +02:00
errs = multierror . Append ( errs , err )
return nil , errs
2021-02-02 18:05:04 +01:00
}
outputFile , err := os . OpenFile ( outputFileName , os . O_RDWR | os . O_CREATE | os . O_TRUNC , 0755 )
if err != nil {
2021-10-14 13:57:59 +02:00
err := fmt . Errorf ( "failed to create %s: %w" , outputFileName , err )
errs = multierror . Append ( errs , err )
return nil , errs
2021-02-02 18:05:04 +01:00
}
defer outputFile . Close ()
if _ , err := io . Copy ( outputFile , copyFrom ); err != nil {
2021-10-14 13:57:59 +02:00
err := fmt . Errorf ( "extract file: %w" , err )
errs = multierror . Append ( errs , err )
return nil , errs
2021-02-02 18:05:04 +01:00
}
if _ , err := outputFile . Seek ( 0 , 0 ); err != nil {
err := fmt . Errorf ( "Error seeking begining of binary file for checksumming: %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 ( "[WARNING] %v, ignoring" , err )
}
cs , err := checksum . Checksummer . Sum ( outputFile )
if err != nil {
err := fmt . Errorf ( "failed to checksum binary 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 )
}
2023-10-25 16:45:32 -04:00
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 )
}
// Success !!
return & Installation {
BinaryPath : strings . ReplaceAll ( outputFileName , "\\" , "/" ),
Version : "v" + version . String (),
}, nil
}
}
}
}
}
2021-10-14 13:57:59 +02:00
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 )
}
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
}