diff --git a/agent/integration/job_verification_integration_test.go b/agent/integration/job_verification_integration_test.go index d5b99c637e..df2f52d763 100644 --- a/agent/integration/job_verification_integration_test.go +++ b/agent/integration/job_verification_integration_test.go @@ -30,7 +30,7 @@ var ( Step: pipeline.CommandStep{ Command: "echo hello world", Plugins: pipeline.Plugins{{ - Name: "some-plugin#v1.0.0", + Source: "some#v1.0.0", Config: map[string]string{ "key": "value", }, @@ -41,7 +41,7 @@ var ( }, Env: map[string]string{ "BUILDKITE_COMMAND": "echo hello world", - "BUILDKITE_PLUGINS": `[{"some-plugin#v1.0.0":{"key":"value"}}]`, + "BUILDKITE_PLUGINS": `[{"github.com/buildkite-plugins/some-buildkite-plugin#v1.0.0":{"key":"value"}}]`, "DEPLOY": "0", }, } @@ -64,7 +64,7 @@ var ( Step: pipeline.CommandStep{ Command: "echo hello world", Plugins: pipeline.Plugins{{ - Name: "some-plugin#v1.0.0", + Source: "some#v1.0.0", Config: map[string]string{ "key": "value", }, @@ -72,7 +72,7 @@ var ( }, Env: map[string]string{ "BUILDKITE_COMMAND": "echo hello world", - "BUILDKITE_PLUGINS": `[{"crimes-plugin#v1.0.0":{"steal":"everything"}}]`, + "BUILDKITE_PLUGINS": `[{"github.com/buildkite-plugins/crimes-buildkite-plugin#v1.0.0":{"steal":"everything"}}]`, "DEPLOY": "0", }, } diff --git a/internal/pipeline/parser_test.go b/internal/pipeline/parser_test.go index 0a4596b6ff..ece881234e 100644 --- a/internal/pipeline/parser_test.go +++ b/internal/pipeline/parser_test.go @@ -929,7 +929,7 @@ steps: Command: "echo foo", Plugins: Plugins{ { - Name: "ecr#v2.7.0", + Source: "ecr#v2.7.0", Config: ordered.MapFromItems( ordered.TupleSA{Key: "login", Value: true}, ordered.TupleSA{Key: "account_ids", Value: "0123456789"}, @@ -1017,13 +1017,13 @@ steps: Command: "script/buildkite/xxx.sh", Plugins: Plugins{ { - Name: "xxx/aws-assume-role#v0.1.0", + Source: "xxx/aws-assume-role#v0.1.0", Config: ordered.MapFromItems( ordered.TupleSA{Key: "role", Value: "arn:aws:iam::xxx:role/xxx"}, ), }, { - Name: "ecr#v1.1.4", + Source: "ecr#v1.1.4", Config: ordered.MapFromItems( ordered.TupleSA{Key: "login", Value: true}, ordered.TupleSA{Key: "account_ids", Value: "xxx"}, @@ -1031,7 +1031,7 @@ steps: ), }, { - Name: "docker-compose#v2.5.1", + Source: "docker-compose#v2.5.1", Config: ordered.MapFromItems( ordered.TupleSA{Key: "run", Value: "xxx"}, ordered.TupleSA{Key: "config", Value: ".buildkite/docker/docker-compose.yml"}, @@ -1070,19 +1070,19 @@ steps: "name": ":s3: xxx", "plugins": [ { - "xxx/aws-assume-role#v0.1.0": { + "github.com/xxx/aws-assume-role-buildkite-plugin#v0.1.0": { "role": "arn:aws:iam::xxx:role/xxx" } }, { - "ecr#v1.1.4": { + "github.com/buildkite-plugins/ecr-buildkite-plugin#v1.1.4": { "login": true, "account_ids": "xxx", "registry_region": "us-east-1" } }, { - "docker-compose#v2.5.1": { + "github.com/buildkite-plugins/docker-compose-buildkite-plugin#v2.5.1": { "run": "xxx", "config": ".buildkite/docker/docker-compose.yml", "env": [ @@ -1124,13 +1124,13 @@ func TestParserParsesScalarPlugins(t *testing.T) { Command: "script/buildkite/xxx.sh", Plugins: Plugins{ { - Name: "example-plugin#v1.0.0", + Source: "example-plugin#v1.0.0", }, { - Name: "another-plugin#v0.0.1-beta43", + Source: "another-plugin#v0.0.1-beta43", }, { - Name: "docker-compose#v2.5.1", + Source: "docker-compose#v2.5.1", Config: ordered.MapFromItems( ordered.TupleSA{Key: "config", Value: ".buildkite/docker/docker-compose.yml"}, ), @@ -1156,10 +1156,10 @@ func TestParserParsesScalarPlugins(t *testing.T) { "command": "script/buildkite/xxx.sh", "name": ":s3: xxx", "plugins": [ - "example-plugin#v1.0.0", - "another-plugin#v0.0.1-beta43", + "github.com/buildkite-plugins/example-plugin-buildkite-plugin#v1.0.0", + "github.com/buildkite-plugins/another-plugin-buildkite-plugin#v0.0.1-beta43", { - "docker-compose#v2.5.1": { + "github.com/buildkite-plugins/docker-compose-buildkite-plugin#v2.5.1": { "config": ".buildkite/docker/docker-compose.yml" } } diff --git a/internal/pipeline/plugin.go b/internal/pipeline/plugin.go index 3c0ec4e47a..b8a595749e 100644 --- a/internal/pipeline/plugin.go +++ b/internal/pipeline/plugin.go @@ -2,6 +2,9 @@ package pipeline import ( "encoding/json" + "net/url" + "path" + "strings" "github.com/buildkite/interpolate" "gopkg.in/yaml.v3" @@ -19,12 +22,13 @@ var ( // // Standard caveats apply - see the package comment. type Plugin struct { - Name string + Source string Config any } // MarshalJSON returns the plugin in "one-key object" form, or "single string" -// form (no config, only plugin name). +// form (no config, only plugin name). Plugin sources are marshalled into "full" +// form. func (p *Plugin) MarshalJSON() ([]byte, error) { // NB: MarshalYAML (as seen below) never returns an error. o, _ := p.MarshalYAML() @@ -32,19 +36,70 @@ func (p *Plugin) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the plugin in either "one-item map" form, or "scalar" -// form (no config, only plugin name). +// form (no config, only plugin name). Plugin sources are marshalled into "full" +// form. func (p *Plugin) MarshalYAML() (any, error) { if p.Config == nil { - return p.Name, nil + return p.FullSource(), nil } return map[string]any{ - p.Name: p.Config, + p.FullSource(): p.Config, }, nil } +// FullSource attempts to canonicalise Source. If it fails, it returns Source +// unaltered. Otherwise, it resolves sources in a manner described at +// https://buildkite.com/docs/plugins/using#plugin-sources. +func (p *Plugin) FullSource() string { + if p.Source == "" { + return "" + } + + // Looks like an absolute or relative file path. + if strings.HasPrefix(p.Source, "/") || strings.HasPrefix(p.Source, ".") || strings.HasPrefix(p.Source, `\`) { + return p.Source + } + + u, err := url.Parse(p.Source) + if err != nil { + return p.Source + } + + // They wrote something like ssh://..., https://..., or C:\... + // in which case they _mean it_. + if u.Scheme != "" || u.Opaque != "" { + return p.Source + } + + // thing => thing-buildkite-plugin + // thing#main => thing-buildkite-plugin#main + lastSegment := func(n, f string) string { + n += "-buildkite-plugin" + if f == "" { + return n + } + return n + "#" + f + } + + paths := strings.Split(strings.TrimPrefix(u.Path, "/"), "/") + switch len(paths) { + case 1: + // trimmed path contained no slash + return path.Join("github.com", "buildkite-plugins", lastSegment(paths[0], u.Fragment)) + + case 2: + // trimmed path contained one slash + return path.Join("github.com", paths[0], lastSegment(paths[1], u.Fragment)) + + default: + // trimmed path contained more than one slash - apply no smarts + return p.Source + } +} + func (p *Plugin) interpolate(env interpolate.Env) error { - name, err := interpolate.Interpolate(env, p.Name) + name, err := interpolate.Interpolate(env, p.Source) if err != nil { return err } @@ -52,7 +107,7 @@ func (p *Plugin) interpolate(env interpolate.Env) error { if err != nil { return err } - p.Name = name + p.Source = name p.Config = cfg return nil } diff --git a/internal/pipeline/plugin_test.go b/internal/pipeline/plugin_test.go new file mode 100644 index 0000000000..41850b7e1e --- /dev/null +++ b/internal/pipeline/plugin_test.go @@ -0,0 +1,77 @@ +package pipeline + +import "testing" + +func TestPluginFullSource(t *testing.T) { + t.Parallel() + + tests := []struct { + source, want string + }{ + { + source: "thing", + want: "github.com/buildkite-plugins/thing-buildkite-plugin", + }, + { + source: "thing#main", + want: "github.com/buildkite-plugins/thing-buildkite-plugin#main", + }, + { + source: "my-org/thing", + want: "github.com/my-org/thing-buildkite-plugin", + }, + { + source: "./.buildkite/plugins/llamas/rock", + want: "./.buildkite/plugins/llamas/rock", + }, + { + source: `.\.buildkite\plugins\llamas\rock`, + want: `.\.buildkite\plugins\llamas\rock`, + }, + { + source: `C:\llamas\rock`, + want: `C:\llamas\rock`, + }, + { + source: `\\\\?\C:\user\docs`, + want: `\\\\?\C:\user\docs`, + }, + { + source: "/a-plugin", + want: "/a-plugin", + }, + { + source: "/my-org/a-plugin", + want: "/my-org/a-plugin", + }, + { + source: "https://my-plugin.git", + want: "https://my-plugin.git", + }, + { + source: "file:///Users/user/Desktop/my-plugin.git", + want: "file:///Users/user/Desktop/my-plugin.git", + }, + { + source: "git@github.com:buildkite/private-buildkite-plugin.git", + want: "git@github.com:buildkite/private-buildkite-plugin.git", + }, + { + source: "ssh://git@github.com:buildkite/private-buildkite-plugin.git", + want: "ssh://git@github.com:buildkite/private-buildkite-plugin.git", + }, + { + source: "my:plugin", + want: "my:plugin", + }, + } + + for _, test := range tests { + p := Plugin{ + Source: test.source, + } + if got, want := p.FullSource(), test.want; got != want { + t.Errorf("%#v.FullSource() = %q, want %q", p, got, want) + } + } +} diff --git a/internal/pipeline/plugins.go b/internal/pipeline/plugins.go index 7a9111072a..04d0225034 100644 --- a/internal/pipeline/plugins.go +++ b/internal/pipeline/plugins.go @@ -23,7 +23,7 @@ func (p *Plugins) UnmarshalOrdered(o any) error { unmarshalMap := func(m *ordered.MapSA) error { return m.Range(func(k string, v any) error { *p = append(*p, &Plugin{ - Name: k, + Source: k, Config: v, }) return nil @@ -51,7 +51,7 @@ func (p *Plugins) UnmarshalOrdered(o any) error { // - plugin#1.0.0 // (no config, only plugin) *p = append(*p, &Plugin{ - Name: ct, + Source: ct, Config: nil, }) diff --git a/internal/pipeline/sign_test.go b/internal/pipeline/sign_test.go index 176e039327..21bf269b6a 100644 --- a/internal/pipeline/sign_test.go +++ b/internal/pipeline/sign_test.go @@ -17,11 +17,11 @@ func TestSignVerify(t *testing.T) { Command: "llamas", Plugins: Plugins{ { - Name: "some-plugin#v1.0.0", + Source: "some-plugin#v1.0.0", Config: nil, }, { - Name: "another-plugin#v3.4.5", + Source: "another-plugin#v3.4.5", Config: ordered.MapFromItems( ordered.TupleSA{ Key: "llama", @@ -56,19 +56,19 @@ func TestSignVerify(t *testing.T) { name: "HMAC-SHA256", generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set) { return newSymmetricKeyPair(t, "alpacas", alg) }, alg: jwa.HS256, - expectedDeterministicSignature: "eyJhbGciOiJIUzI1NiIsImtpZCI6IlRlc3RTaWduVmVyaWZ5In0..0kDPckkYX838NHBKfRA_be4FqpKpBabqohpgU5sGSGI", + expectedDeterministicSignature: "eyJhbGciOiJIUzI1NiIsImtpZCI6IlRlc3RTaWduVmVyaWZ5In0..Xd7udcMRc3Gg236JdiV2vggGrqxAfgfLZdCLUpgAN34", }, { name: "HMAC-SHA384", generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set) { return newSymmetricKeyPair(t, "alpacas", alg) }, alg: jwa.HS384, - expectedDeterministicSignature: "eyJhbGciOiJIUzM4NCIsImtpZCI6IlRlc3RTaWduVmVyaWZ5In0..GSufqnr_XZkobn0SYnoA2T_rciQJNenP7XMNuXPPcZai98KrE1kbD_FhVZn_D-d4", + expectedDeterministicSignature: "eyJhbGciOiJIUzM4NCIsImtpZCI6IlRlc3RTaWduVmVyaWZ5In0..g-_B2RO6o_oZjPoM2UyCHDANbPeeqLBUexLRl_MoW7BdpLC7r6mLc0wgRIzJy6ih", }, { name: "HMAC-SHA512", generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set) { return newSymmetricKeyPair(t, "alpacas", alg) }, alg: jwa.HS512, - expectedDeterministicSignature: "eyJhbGciOiJIUzUxMiIsImtpZCI6IlRlc3RTaWduVmVyaWZ5In0..QP7CAzIZLKylXhJ-t7eEPxIro2j0-BR03PpUGLfDgT0-5oycmHYJWaF8UNFLM425VEKhW88Tr749nYByVy4eZQ", + expectedDeterministicSignature: "eyJhbGciOiJIUzUxMiIsImtpZCI6IlRlc3RTaWduVmVyaWZ5In0..iW8eaMBrcK7Ehj41DRzgQp3haYBf70JgA_n0C4d_acRZCdVUm-GJv9pdxQ5O0pYd7gJC_wMmaNMkuj4TXqlPvg", }, { name: "RSA-PSS 256",