packer: relax constraints on sources

The source parsing logic was heavily directed towards Github compatible
source URIs, however if we want to support more cases, we need to make
sure we are able to specify those URIs, and to load plugins installed
from those sources.

Right now, since the getters available are only github.com, we will not
support remotely instlling plugins from sources other than github.com,
with the same set of constraints as before. However, we do support now
installing from a local plugin binary to any kind of source, and we
support loading them, including if a template wants this plugin
installed locally with version constraints.
This commit is contained in:
Lucas Bajolet 2024-04-04 16:23:28 -04:00 committed by Lucas Bajolet
parent 1bf3e86ee1
commit 6fc1d154bd
10 changed files with 296 additions and 165 deletions

View file

@ -22,9 +22,7 @@ var basicAmazonAmiDatasourceHCL2Template string
func TestAccInitAndBuildBasicAmazonAmiDatasource(t *testing.T) {
plugin := addrs.Plugin{
Hostname: "github.com",
Namespace: "hashicorp",
Type: "amazon",
Source: "github.com/hashicorp/amazon",
}
testCase := &acctest.PluginTestCase{
Name: "amazon-ami_basic_datasource_test",

View file

@ -18,7 +18,7 @@ import (
"github.com/hashicorp/packer-plugin-sdk/acctest"
"github.com/hashicorp/packer-plugin-sdk/acctest/testutils"
"github.com/hashicorp/packer/hcl2template/addrs"
"github.com/mitchellh/go-homedir"
"github.com/hashicorp/packer/packer"
)
//go:embed test-fixtures/basic-amazon-ebs.pkr.hcl
@ -26,9 +26,7 @@ var basicAmazonEbsHCL2Template string
func TestAccInitAndBuildBasicAmazonEbs(t *testing.T) {
plugin := addrs.Plugin{
Hostname: "github.com",
Namespace: "hashicorp",
Type: "amazon",
Source: "github.com/hashicorp/amazon",
}
testCase := &acctest.PluginTestCase{
Name: "amazon-ebs_basic_plugin_init_and_build_test",
@ -69,27 +67,22 @@ func TestAccInitAndBuildBasicAmazonEbs(t *testing.T) {
acctest.TestPlugin(t, testCase)
}
func pluginDirectory(plugin addrs.Plugin) (string, error) {
pluginDir, err := packer.PluginFolder()
if err != nil {
return "", err
}
pluginParts := []string{pluginDir}
pluginParts = append(pluginParts, plugin.Parts()...)
return filepath.Join(pluginParts...), nil
}
func cleanupPluginInstallation(plugin addrs.Plugin) error {
home, err := homedir.Dir()
pluginPath, err := pluginDirectory(plugin)
if err != nil {
return err
}
pluginPath := filepath.Join(home,
".packer.d",
"plugins",
plugin.Hostname,
plugin.Namespace,
plugin.Type)
if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
pluginPath = filepath.Join(xdgConfigHome,
"packer",
"plugins",
plugin.Hostname,
plugin.Namespace,
plugin.Type)
}
testutils.CleanupFiles(pluginPath)
return nil
}
@ -100,27 +93,11 @@ func checkPluginInstallation(initOutput string, plugin addrs.Plugin) error {
return fmt.Errorf("logs doesn't contain expected foo value %q", initOutput)
}
home, err := homedir.Dir()
pluginPath, err := pluginDirectory(plugin)
if err != nil {
return err
}
pluginPath := filepath.Join(home,
".packer.d",
"plugins",
plugin.Hostname,
plugin.Namespace,
plugin.Type)
if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
pluginPath = filepath.Join(xdgConfigHome,
"packer",
"plugins",
plugin.Hostname,
plugin.Namespace,
plugin.Type)
}
if !testutils.FileExists(pluginPath) {
return fmt.Errorf("%s plugin installation not found", plugin.String())
}

View file

@ -308,7 +308,7 @@ func (c *PluginsInstallCommand) InstallFromBinary(opts plugingetter.ListInstalla
outputPrefix := fmt.Sprintf(
"packer-plugin-%s_v%s_%s",
pluginIdentifier.Type,
pluginIdentifier.Name(),
noMetaVersion,
desc.APIVersion,
)

View file

@ -5,6 +5,8 @@ package addrs
import (
"fmt"
"net/url"
"path"
"strings"
"github.com/hashicorp/hcl/v2"
@ -13,21 +15,31 @@ import (
// Plugin encapsulates a single plugin type.
type Plugin struct {
Hostname string
Namespace string
Type string
}
func (p Plugin) RealRelativePath() string {
return p.Namespace + "/packer-plugin-" + p.Type
Source string
}
// Parts returns the list of components of the source URL, starting with the
// host, and ending with the name of the plugin.
//
// This will correspond more or less to the filesystem hierarchy where
// the plugin is installed.
func (p Plugin) Parts() []string {
return []string{p.Hostname, p.Namespace, p.Type}
return strings.FieldsFunc(p.Source, func(r rune) bool {
return r == '/'
})
}
// Name returns the raw name of the plugin from its source
//
// Exemples:
// - "github.com/hashicorp/amazon" -> "amazon"
func (p Plugin) Name() string {
parts := p.Parts()
return parts[len(parts)-1]
}
func (p Plugin) String() string {
return strings.Join(p.Parts(), "/")
return p.Source
}
// ParsePluginPart processes an addrs.Plugin namespace or type string
@ -98,42 +110,58 @@ func IsPluginPartNormalized(str string) (bool, error) {
//
// The following are valid source string formats:
//
// name
// namespace/name
// hostname/namespace/name
func ParsePluginSourceString(str string) (*Plugin, hcl.Diagnostics) {
ret := &Plugin{
Hostname: "",
Namespace: "",
}
var diags hcl.Diagnostics
// split the source string into individual components
parts := strings.Split(str, "/")
if len(parts) != 3 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid plugin source string",
Detail: `The "source" attribute must be in the format "hostname/namespace/name"`,
})
return nil, diags
var errs []string
if strings.HasPrefix(str, "/") {
errs = append(errs, "A source URL must not start with a '/' character.")
}
// check for an invalid empty string in any part
for i := range parts {
if parts[i] == "" {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid plugin source string",
Detail: `The "source" attribute must be in the format "hostname/namespace/name"`,
})
return nil, diags
if strings.HasSuffix(str, "/") {
errs = append(errs, "A source URL must not end with a '/' character.")
}
if !strings.Contains(str, "/") {
errs = append(errs, "A source URL must at least contain a host and a path.")
}
url, err := url.Parse(str)
if err != nil {
errs = append(errs, fmt.Sprintf("Failed to parse source URL: %s", err))
}
if url != nil && url.Scheme != "" {
errs = append(errs, "A source URL must not contain a scheme (e.g. https://).")
}
if url != nil && url.RawQuery != "" {
errs = append(errs, "A source URL must not contain a query (e.g. ?var=val)")
}
if url != nil && url.Fragment != "" {
errs = append(errs, "A source URL must not contain a fragment (e.g. #anchor).")
}
if errs != nil {
errsMsg := &strings.Builder{}
for _, err := range errs {
fmt.Fprintf(errsMsg, "* %s\n", err)
}
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Malformed source URL",
Detail: fmt.Sprintf("The provided source URL %q is invalid. The following errors have been discovered:\n%s\nA valid source looks like \"github.com/hashicorp/happycloud\"", str, errsMsg),
})
}
// check the 'name' portion, which is always the last part
givenName := parts[len(parts)-1]
name, err := ParsePluginPart(givenName)
_, givenName := path.Split(str)
_, err = ParsePluginPart(givenName)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
@ -142,23 +170,6 @@ func ParsePluginSourceString(str string) (*Plugin, hcl.Diagnostics) {
})
return nil, diags
}
ret.Type = name
// the namespace is always the second-to-last part
givenNamespace := parts[len(parts)-2]
namespace, err := ParsePluginPart(givenNamespace)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid plugin namespace",
Detail: fmt.Sprintf(`Invalid plugin namespace %q in source %q: %s"`, namespace, str, err),
})
return nil, diags
}
ret.Namespace = namespace
// the hostname is always the first part in a three-part source string
ret.Hostname = parts[0]
// Due to how plugin executables are named and plugin git repositories
// are conventionally named, it's a reasonable and
@ -171,8 +182,8 @@ func ParsePluginSourceString(str string) (*Plugin, hcl.Diagnostics) {
// packer-plugin- prefix to help them self-correct.
const redundantPrefix = "packer-"
const userErrorPrefix = "packer-plugin-"
if strings.HasPrefix(ret.Type, redundantPrefix) {
if strings.HasPrefix(ret.Type, userErrorPrefix) {
if strings.HasPrefix(givenName, redundantPrefix) {
if strings.HasPrefix(givenName, userErrorPrefix) {
// Likely user error. We only return this specialized error if
// whatever is after the prefix would otherwise be a
// syntactically-valid plugin type, so we don't end up advising
@ -181,32 +192,33 @@ func ParsePluginSourceString(str string) (*Plugin, hcl.Diagnostics) {
// (This is mainly just for robustness, because the validation
// we already did above should've rejected most/all ways for
// the suggestedType to end up invalid here.)
suggestedType := ret.Type[len(userErrorPrefix):]
suggestedType := strings.Replace(givenName, userErrorPrefix, "", -1)
if _, err := ParsePluginPart(suggestedType); err == nil {
suggestedAddr := ret
suggestedAddr.Type = suggestedType
diags = diags.Append(&hcl.Diagnostic{
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid plugin type",
Detail: fmt.Sprintf("Plugin source %q has a type with the prefix %q, which isn't valid. "+
"Although that prefix is often used in the names of version control repositories for Packer plugins, "+
"plugin source strings should not include it.\n"+
"\nDid you mean %q?", ret, userErrorPrefix, suggestedAddr),
"\nDid you mean %q?", str, userErrorPrefix, suggestedType),
})
return nil, diags
}
}
// Otherwise, probably instead an incorrectly-named plugin, perhaps
// arising from a similar instinct to what causes there to be
// thousands of Python packages on PyPI with "python-"-prefixed
// names.
diags = diags.Append(&hcl.Diagnostic{
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid plugin type",
Detail: fmt.Sprintf("Plugin source %q has a type with the prefix %q, which isn't allowed because it would be redundant to name a Packer plugin with that prefix. If you are the author of this plugin, rename it to not include the prefix.", ret, redundantPrefix),
Detail: fmt.Sprintf("Plugin source %q has a type with the prefix %q, which isn't allowed "+
"because it would be redundant to name a Packer plugin with that prefix. "+
"If you are the author of this plugin, rename it to not include the prefix.",
str, redundantPrefix),
})
return nil, diags
}
return ret, diags
return &Plugin{
Source: str,
}, diags
}

View file

@ -6,29 +6,109 @@ package addrs
import (
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestParsePluginSourceString(t *testing.T) {
type args struct {
str string
}
func TestPluginParseSourceString(t *testing.T) {
tests := []struct {
args args
name string
source string
want *Plugin
wantDiags bool
}{
{args{"potato"}, nil, true},
{args{"hashicorp/azr"}, nil, true},
{args{"github.com/hashicorp/azr"}, &Plugin{"github.com", "hashicorp", "azr"}, false},
{"invalid: only one component, rejected", "potato", nil, true},
{"valid: two components in name", "hashicorp/azr", &Plugin{"hashicorp/azr"}, false},
{"valid: three components, nothing superfluous", "github.com/hashicorp/azr", &Plugin{"github.com/hashicorp/azr"}, false},
{"invalid: trailing slash", "github.com/hashicorp/azr/", nil, true},
{"invalid: reject because scheme specified", "https://github.com/hashicorp/azr", nil, true},
{"invalid: reject because query non nil", "github.com/hashicorp/azr?arg=1", nil, true},
{"invalid: reject because fragment present", "github.com/hashicorp/azr#anchor", nil, true},
{"invalid: leading and trailing slashes are removed", "/github.com/hashicorp/azr/", nil, true},
{"invalid: leading slashes are removed", "/github.com/hashicorp/azr", nil, true},
{"invalid: plugin name contains packer-", "/github.com/hashicorp/packer-azr", nil, true},
{"invalid: plugin name contains packer-plugin-", "/github.com/hashicorp/packer-plugin-azr", nil, true},
}
for _, tt := range tests {
t.Run(tt.args.str, func(t *testing.T) {
got, gotDiags := ParsePluginSourceString(tt.args.str)
t.Run(tt.name, func(t *testing.T) {
got, gotDiags := ParsePluginSourceString(tt.source)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParsePluginSourceString() got = %v, want %v", got, tt.want)
}
if tt.wantDiags == (len(gotDiags) == 0) {
t.Errorf("Unexpected diags %s", gotDiags)
if tt.wantDiags && len(gotDiags) == 0 {
t.Errorf("Expected diags, but got none")
}
if !tt.wantDiags && len(gotDiags) != 0 {
t.Errorf("Unexpected diags: %s", gotDiags)
}
})
}
}
func TestPluginName(t *testing.T) {
tests := []struct {
name string
pluginString string
expectName string
}{
{
"valid minimal name",
"github.com/hashicorp/amazon",
"amazon",
},
{
// Technically we can call `Name` on a plugin created manually
// but this is invalid as the Source's Name should not contain
// `packer-plugin-`.
"invalid name with prefix",
"github.com/hashicorp/packer-plugin-amazon",
"packer-plugin-amazon",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
plug := &Plugin{
Source: tt.pluginString,
}
name := plug.Name()
if name != tt.expectName {
t.Errorf("Expected plugin %q to have %q as name, got %q", tt.pluginString, tt.expectName, name)
}
})
}
}
func TestPluginParts(t *testing.T) {
tests := []struct {
name string
pluginSource string
expectedParts []string
}{
{
"valid with two parts",
"factiartory.com/packer",
[]string{"factiartory.com", "packer"},
},
{
"valid with four parts",
"factiartory.com/hashicrop/fields/packer",
[]string{"factiartory.com", "hashicrop", "fields", "packer"},
},
{
"valid, with double-slashes in the name",
"factiartory.com/hashicrop//fields/packer//",
[]string{"factiartory.com", "hashicrop", "fields", "packer"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
plugin := &Plugin{tt.pluginSource}
diff := cmp.Diff(plugin.Parts(), tt.expectedParts)
if diff != "" {
t.Errorf("Difference found between expected and computed parts: %s", diff)
}
})
}

View file

@ -474,9 +474,7 @@ func TestParser_no_init(t *testing.T) {
Name: "amazon",
Source: "github.com/hashicorp/amazon",
Type: &addrs.Plugin{
Type: "amazon",
Namespace: "hashicorp",
Hostname: "github.com",
Source: "github.com/hashicorp/amazon",
},
Requirement: VersionConstraint{
Required: mustVersionConstraints(version.NewConstraint(">= v0")),
@ -486,9 +484,7 @@ func TestParser_no_init(t *testing.T) {
Name: "amazon-v1",
Source: "github.com/hashicorp/amazon",
Type: &addrs.Plugin{
Type: "amazon",
Namespace: "hashicorp",
Hostname: "github.com",
Source: "github.com/hashicorp/amazon",
},
Requirement: VersionConstraint{
Required: mustVersionConstraints(version.NewConstraint(">= v1")),
@ -498,9 +494,7 @@ func TestParser_no_init(t *testing.T) {
Name: "amazon-v2",
Source: "github.com/hashicorp/amazon",
Type: &addrs.Plugin{
Type: "amazon",
Namespace: "hashicorp",
Hostname: "github.com",
Source: "github.com/hashicorp/amazon",
},
Requirement: VersionConstraint{
Required: mustVersionConstraints(version.NewConstraint(">= v2")),
@ -510,9 +504,7 @@ func TestParser_no_init(t *testing.T) {
Name: "amazon-v3",
Source: "github.com/hashicorp/amazon",
Type: &addrs.Plugin{
Type: "amazon",
Namespace: "hashicorp",
Hostname: "github.com",
Source: "github.com/hashicorp/amazon",
},
Requirement: VersionConstraint{
Required: mustVersionConstraints(version.NewConstraint(">= v3")),
@ -522,9 +514,7 @@ func TestParser_no_init(t *testing.T) {
Name: "amazon-v3-azr",
Source: "github.com/azr/amazon",
Type: &addrs.Plugin{
Type: "amazon",
Namespace: "azr",
Hostname: "github.com",
Source: "github.com/azr/amazon",
},
Requirement: VersionConstraint{
Required: mustVersionConstraints(version.NewConstraint(">= v3")),
@ -534,9 +524,7 @@ func TestParser_no_init(t *testing.T) {
Name: "amazon-v4",
Source: "github.com/hashicorp/amazon",
Type: &addrs.Plugin{
Type: "amazon",
Namespace: "hashicorp",
Hostname: "github.com",
Source: "github.com/hashicorp/amazon",
},
Requirement: VersionConstraint{
Required: mustVersionConstraints(version.NewConstraint(">= v4")),

View file

@ -42,7 +42,7 @@ func TestPackerConfig_required_plugin_parse(t *testing.T) {
"amazon": {
Name: "amazon",
Source: "github.com/hashicorp/amazon",
Type: &addrs.Plugin{Hostname: "github.com", Namespace: "hashicorp", Type: "amazon"},
Type: &addrs.Plugin{Source: "github.com/hashicorp/amazon"},
Requirement: VersionConstraint{
Required: mustVersionConstraints(version.NewConstraint("~> v1.2.3")),
},
@ -72,7 +72,7 @@ func TestPackerConfig_required_plugin_parse(t *testing.T) {
"amazon": {
Name: "amazon",
Source: "github.com/azr/amazon",
Type: &addrs.Plugin{Hostname: "github.com", Namespace: "azr", Type: "amazon"},
Type: &addrs.Plugin{Source: "github.com/azr/amazon"},
Requirement: VersionConstraint{
Required: mustVersionConstraints(version.NewConstraint("~> v1.2.3")),
},
@ -103,7 +103,7 @@ func TestPackerConfig_required_plugin_parse(t *testing.T) {
"amazon": {
Name: "amazon",
Source: "github.com/azr/amazon",
Type: &addrs.Plugin{Hostname: "github.com", Namespace: "azr", Type: "amazon"},
Type: &addrs.Plugin{Source: "github.com/azr/amazon"},
Requirement: VersionConstraint{
Required: mustVersionConstraints(version.NewConstraint("~> v1.2.3")),
},

View file

@ -14,10 +14,12 @@ import (
"log"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/google/go-github/v33/github"
"github.com/hashicorp/packer/hcl2template/addrs"
plugingetter "github.com/hashicorp/packer/packer/plugin-getter"
"golang.org/x/oauth2"
)
@ -156,10 +158,40 @@ func (t *HostSpecificTokenAuthTransport) base() http.RoundTripper {
return http.DefaultTransport
}
type GithubPlugin struct {
Hostname string
Namespace string
Type string
}
func NewGithubPlugin(source *addrs.Plugin) (*GithubPlugin, error) {
parts := source.Parts()
if len(parts) != 3 {
return nil, fmt.Errorf("Invalid github.com URI %q: a Github-compatible source must be in the github.com/<namespace>/<name> format.", source.String())
}
if parts[0] != defaultHostname {
return nil, fmt.Errorf("%q doesn't appear to be a valid %q source address; check source and try again.", source.String(), defaultHostname)
}
return &GithubPlugin{
Hostname: parts[0],
Namespace: parts[1],
Type: strings.Replace(parts[2], "packer-plugin-", "", 1),
}, nil
}
func (gp GithubPlugin) RealRelativePath() string {
return path.Join(
gp.Namespace,
fmt.Sprintf("packer-plugin-%s", gp.Type),
)
}
func (g *Getter) Get(what string, opts plugingetter.GetOptions) (io.ReadCloser, error) {
if opts.PluginRequirement.Identifier.Hostname != defaultHostname {
s := opts.PluginRequirement.Identifier.String() + " doesn't appear to be a valid " + defaultHostname + " source address; check source and try again."
return nil, errors.New(s)
ghURI, err := NewGithubPlugin(opts.PluginRequirement.Identifier)
if err != nil {
return nil, err
}
ctx := context.TODO()
@ -188,19 +220,18 @@ func (g *Getter) Get(what string, opts plugingetter.GetOptions) (io.ReadCloser,
}
var req *http.Request
var err error
transform := func(in io.ReadCloser) (io.ReadCloser, error) {
return in, nil
}
switch what {
case "releases":
u := filepath.ToSlash("/repos/" + opts.PluginRequirement.Identifier.RealRelativePath() + "/git/matching-refs/tags")
u := filepath.ToSlash("/repos/" + ghURI.RealRelativePath() + "/git/matching-refs/tags")
req, err = g.Client.NewRequest("GET", u, nil)
transform = transformVersionStream
case "sha256":
// something like https://github.com/sylviamoss/packer-plugin-comment/releases/download/v0.2.11/packer-plugin-comment_v0.2.11_x5_SHA256SUMS
u := filepath.ToSlash("https://github.com/" + opts.PluginRequirement.Identifier.RealRelativePath() + "/releases/download/" + opts.Version() + "/" + opts.PluginRequirement.FilenamePrefix() + opts.Version() + "_SHA256SUMS")
u := filepath.ToSlash("https://github.com/" + ghURI.RealRelativePath() + "/releases/download/" + opts.Version() + "/" + opts.PluginRequirement.FilenamePrefix() + opts.Version() + "_SHA256SUMS")
req, err = g.Client.NewRequest(
"GET",
u,
@ -208,7 +239,7 @@ func (g *Getter) Get(what string, opts plugingetter.GetOptions) (io.ReadCloser,
)
transform = transformChecksumStream()
case "zip":
u := filepath.ToSlash("https://github.com/" + opts.PluginRequirement.Identifier.RealRelativePath() + "/releases/download/" + opts.Version() + "/" + opts.ExpectedZipFilename())
u := filepath.ToSlash("https://github.com/" + ghURI.RealRelativePath() + "/releases/download/" + opts.Version() + "/" + opts.ExpectedZipFilename())
req, err = g.Client.NewRequest(
"GET",
u,

View file

@ -9,6 +9,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/fs"
"log"
"os"
"os/exec"
@ -96,13 +97,66 @@ func (pr Requirement) FilenamePrefix() string {
if pr.Identifier == nil {
return "packer-plugin-"
}
return "packer-plugin-" + pr.Identifier.Type + "_"
return "packer-plugin-" + pr.Identifier.Name() + "_"
}
func (opts BinaryInstallationOptions) FilenameSuffix() string {
return "_" + opts.OS + "_" + opts.ARCH + opts.Ext
}
// 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
}
return matches, err
}
// ListInstallations lists unique installed versions of plugin Requirement pr
// with opts as a filter.
//
@ -113,21 +167,13 @@ func (opts BinaryInstallationOptions) FilenameSuffix() string {
// considered.
func (pr Requirement) ListInstallations(opts ListInstallationsOptions) (InstallList, error) {
res := InstallList{}
FilenamePrefix := pr.FilenamePrefix()
filenameSuffix := opts.FilenameSuffix()
log.Printf("[TRACE] listing potential installations for %q that match %q. %#v", pr.Identifier, pr.VersionConstraints, opts)
glob := ""
if pr.Identifier == nil {
glob = filepath.Join(opts.PluginDirectory, "*", "*", "*", FilenamePrefix+"*"+filenameSuffix)
} else {
glob = filepath.Join(opts.PluginDirectory, pr.Identifier.Hostname, pr.Identifier.Namespace, pr.Identifier.Type, FilenamePrefix+"*"+filenameSuffix)
matches, err := pr.getPluginBinaries(opts)
if err != nil {
return nil, fmt.Errorf("ListInstallations: failed to list installed plugins: %s", err)
}
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 == "." {
@ -156,8 +202,8 @@ func (pr Requirement) ListInstallations(opts ListInstallationsOptions) (InstallL
}
// 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)
versionsStr := strings.TrimPrefix(fname, pr.FilenamePrefix())
versionsStr = strings.TrimSuffix(versionsStr, opts.FilenameSuffix())
if pr.Identifier == nil {
if idx := strings.Index(versionsStr, "_"); idx > 0 {

View file

@ -35,9 +35,7 @@ func TestChecksumFileEntry_init(t *testing.T) {
expectedVersion := "v0.3.0"
req := &Requirement{
Identifier: &addrs.Plugin{
Hostname: "github.com",
Namespace: "ddelnano",
Type: "xenserver",
Source: "github.com/ddelnano/xenserver",
},
}
@ -460,9 +458,10 @@ func (g *mockPluginGetter) Get(what string, options GetOptions) (io.ReadCloser,
}
toEncode = enc
case "zip":
acc := options.PluginRequirement.Identifier.Hostname + "/" +
options.PluginRequirement.Identifier.RealRelativePath() + "/" +
options.ExpectedZipFilename()
// Note: we'll act as if the plugin sources would always be github sources for now.
// This test will need to be updated if/when we move on to support other sources.
parts := options.PluginRequirement.Identifier.Parts()
acc := fmt.Sprintf("%s/%s/packer-plugin-%s/%s", parts[0], parts[1], parts[2], options.ExpectedZipFilename())
zip, found := g.Zips[acc]
if found == false {