diff --git a/acctest/plugin/component_acc_test.go b/acctest/plugin/component_acc_test.go index 53a49ad22..14d0e5702 100644 --- a/acctest/plugin/component_acc_test.go +++ b/acctest/plugin/component_acc_test.go @@ -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", diff --git a/acctest/plugin/plugin_acc_test.go b/acctest/plugin/plugin_acc_test.go index b2afe7e81..c9eec1fbc 100644 --- a/acctest/plugin/plugin_acc_test.go +++ b/acctest/plugin/plugin_acc_test.go @@ -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 cleanupPluginInstallation(plugin addrs.Plugin) error { - home, err := homedir.Dir() +func pluginDirectory(plugin addrs.Plugin) (string, error) { + pluginDir, err := packer.PluginFolder() if err != nil { - return err + 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) - } + pluginParts := []string{pluginDir} + pluginParts = append(pluginParts, plugin.Parts()...) + return filepath.Join(pluginParts...), nil +} +func cleanupPluginInstallation(plugin addrs.Plugin) error { + pluginPath, err := pluginDirectory(plugin) + if err != nil { + return err + } 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()) } diff --git a/command/plugins_install.go b/command/plugins_install.go index 4360de411..59e197a3f 100644 --- a/command/plugins_install.go +++ b/command/plugins_install.go @@ -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, ) diff --git a/hcl2template/addrs/plugin.go b/hcl2template/addrs/plugin.go index 9db29b317..2baae5703 100644 --- a/hcl2template/addrs/plugin.go +++ b/hcl2template/addrs/plugin.go @@ -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 + Source string } -func (p Plugin) RealRelativePath() string { - return p.Namespace + "/packer-plugin-" + p.Type +// 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 strings.FieldsFunc(p.Source, func(r rune) bool { + return r == '/' + }) } -func (p Plugin) Parts() []string { - return []string{p.Hostname, p.Namespace, p.Type} +// 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,67 +110,66 @@ 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.") } - // check the 'name' portion, which is always the last part - givenName := parts[len(parts)-1] - name, err := ParsePluginPart(givenName) + 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 { - diags = diags.Append(&hcl.Diagnostic{ + 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: "Invalid plugin type", - Detail: fmt.Sprintf(`Invalid plugin type %q in source %q: %s"`, givenName, str, err), + 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), }) - return nil, diags } - ret.Type = name - // the namespace is always the second-to-last part - givenNamespace := parts[len(parts)-2] - namespace, err := ParsePluginPart(givenNamespace) + // check the 'name' portion, which is always the last part + _, givenName := path.Split(str) + _, err = ParsePluginPart(givenName) 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), + Summary: "Invalid plugin type", + Detail: fmt.Sprintf(`Invalid plugin type %q in source %q: %s"`, givenName, 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 } diff --git a/hcl2template/addrs/plugin_test.go b/hcl2template/addrs/plugin_test.go index e99a72416..60af038be 100644 --- a/hcl2template/addrs/plugin_test.go +++ b/hcl2template/addrs/plugin_test.go @@ -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) } }) } diff --git a/hcl2template/types.packer_config_test.go b/hcl2template/types.packer_config_test.go index eec35860e..10a0e73a7 100644 --- a/hcl2template/types.packer_config_test.go +++ b/hcl2template/types.packer_config_test.go @@ -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")), diff --git a/hcl2template/types.required_plugins_test.go b/hcl2template/types.required_plugins_test.go index 0329d1505..8a44b6e17 100644 --- a/hcl2template/types.required_plugins_test.go +++ b/hcl2template/types.required_plugins_test.go @@ -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")), }, diff --git a/packer/plugin-getter/github/getter.go b/packer/plugin-getter/github/getter.go index a4942a421..e4cd4f603 100644 --- a/packer/plugin-getter/github/getter.go +++ b/packer/plugin-getter/github/getter.go @@ -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// 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, diff --git a/packer/plugin-getter/plugins.go b/packer/plugin-getter/plugins.go index 402ab92f1..69c493870 100644 --- a/packer/plugin-getter/plugins.go +++ b/packer/plugin-getter/plugins.go @@ -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-____ 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 := filepath.Glob(glob) + matches, err := pr.getPluginBinaries(opts) if err != nil { - return nil, fmt.Errorf("ListInstallations: %q failed to list binaries in folder: %v", pr.Identifier.String(), err) + return nil, fmt.Errorf("ListInstallations: failed to list installed plugins: %s", 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 { diff --git a/packer/plugin-getter/plugins_test.go b/packer/plugin-getter/plugins_test.go index e92e636b1..85bcab42e 100644 --- a/packer/plugin-getter/plugins_test.go +++ b/packer/plugin-getter/plugins_test.go @@ -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 {