diff --git a/go.mod b/go.mod index ad3f7ef7a2f..0322269af0b 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/muesli/coral v1.0.0 github.com/muesli/mango-coral v1.0.1 github.com/muesli/roff v0.1.0 + github.com/scylladb/go-set v1.0.2 github.com/slack-go/slack v0.10.2 github.com/stretchr/testify v1.7.0 github.com/ulikunitz/xz v0.5.10 diff --git a/go.sum b/go.sum index 205e4719200..a499f0d83c7 100644 --- a/go.sum +++ b/go.sum @@ -322,6 +322,8 @@ github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= +github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= @@ -674,6 +676,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/scylladb/go-set v1.0.2 h1:SkvlMCKhP0wyyct6j+0IHJkBkSZL+TDzZ4E7f7BCcRE= +github.com/scylladb/go-set v1.0.2/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go index 6050d5e8b35..04ea07ea9ab 100644 --- a/internal/artifact/artifact.go +++ b/internal/artifact/artifact.go @@ -17,6 +17,7 @@ import ( "sync" "github.com/apex/log" + "github.com/scylladb/go-set/strset" ) // Type defines the type of an artifact. @@ -379,6 +380,39 @@ func ByIDs(ids ...string) Filter { return Or(filters...) } +// ByBinaryLikeArtifacts filter artifacts down to artifacts that are Binary, UploadableBinary, or UniversalBinary, +// deduplicating artifacts by path (preferring UploadableBinary over all others). Note: this filter is unique in the +// sense that it cannot act in isolation of the state of other artifacts; the filter requires the whole list of +// artifacts in advance to perform deduplication. +func ByBinaryLikeArtifacts(arts Artifacts) Filter { + // find all of the paths for any uploadable binary artifacts + uploadableBins := arts.Filter(ByType(UploadableBinary)).List() + uploadableBinPaths := strset.New() + for _, a := range uploadableBins { + uploadableBinPaths.Add(a.Path) + } + + // we want to keep any matching artifact that is not a binary that already has a path accounted for + // by another uploadable binary. We always prefer uploadable binary artifacts over binary artifacts. + deduplicateByPath := func(a *Artifact) bool { + if a.Type == UploadableBinary { + return true + } + return !uploadableBinPaths.Has(a.Path) + } + + return And( + // allow all of the binary-like artifacts as possible... + Or( + ByType(Binary), + ByType(UploadableBinary), + ByType(UniversalBinary), + ), + // ... but remove any duplicates found + deduplicateByPath, + ) +} + // Or performs an OR between all given filters. func Or(filters ...Filter) Filter { return func(a *Artifact) bool { diff --git a/internal/artifact/artifact_test.go b/internal/artifact/artifact_test.go index 2a839203ce2..bc20572ebd3 100644 --- a/internal/artifact/artifact_test.go +++ b/internal/artifact/artifact_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/goreleaser/goreleaser/internal/golden" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" ) @@ -593,3 +594,259 @@ func TestMarshalJSON(t *testing.T) { require.NoError(t, err) golden.RequireEqualJSON(t, bts) } + +func Test_ByBinaryLikeArtifacts(t *testing.T) { + tests := []struct { + name string + initial []*Artifact + expected []*Artifact + }{ + { + name: "keep all unique paths", + initial: []*Artifact{ + { + Path: "binary-path", + Type: Binary, + }, + { + Path: "uploadable-binary-path", + Type: UploadableBinary, + }, + { + Path: "universal-binary-path", + Type: UniversalBinary, + }, + }, + expected: []*Artifact{ + { + Path: "binary-path", + Type: Binary, + }, + { + Path: "uploadable-binary-path", + Type: UploadableBinary, + }, + { + Path: "universal-binary-path", + Type: UniversalBinary, + }, + }, + }, + { + name: "duplicate path between binaries ignored (odd configuration)", + initial: []*Artifact{ + { + Path: "!!!duplicate!!!", + Type: Binary, + }, + { + Path: "uploadable-binary-path", + Type: UploadableBinary, + }, + { + Path: "!!!duplicate!!!", + Type: UniversalBinary, + }, + }, + expected: []*Artifact{ + { + Path: "!!!duplicate!!!", + Type: Binary, + }, + { + Path: "uploadable-binary-path", + Type: UploadableBinary, + }, + { + Path: "!!!duplicate!!!", + Type: UniversalBinary, + }, + }, + }, + { + name: "remove duplicate binary", + initial: []*Artifact{ + { + Path: "!!!duplicate!!!", + Type: Binary, + }, + { + Path: "!!!duplicate!!!", + Type: UploadableBinary, + }, + { + Path: "universal-binary-path", + Type: UniversalBinary, + }, + }, + expected: []*Artifact{ + { + Path: "!!!duplicate!!!", + Type: UploadableBinary, + }, + { + Path: "universal-binary-path", + Type: UniversalBinary, + }, + }, + }, + { + name: "remove duplicate universal binary", + initial: []*Artifact{ + { + Path: "binary-path", + Type: Binary, + }, + { + Path: "!!!duplicate!!!", + Type: UploadableBinary, + }, + { + Path: "!!!duplicate!!!", + Type: UniversalBinary, + }, + }, + expected: []*Artifact{ + { + Path: "binary-path", + Type: Binary, + }, + { + Path: "!!!duplicate!!!", + Type: UploadableBinary, + }, + }, + }, + { + name: "remove multiple duplicates", + initial: []*Artifact{ + { + Path: "!!!duplicate!!!", + Type: Binary, + }, + { + Path: "!!!duplicate!!!", + Type: Binary, + }, + { + Path: "!!!duplicate!!!", + Type: UploadableBinary, + }, + { + Path: "!!!duplicate!!!", + Type: UniversalBinary, + }, + { + Path: "!!!duplicate!!!", + Type: UniversalBinary, + }, + }, + expected: []*Artifact{ + { + Path: "!!!duplicate!!!", + Type: UploadableBinary, + }, + }, + }, + { + name: "keep duplicate uploadable binaries (odd configuration)", + initial: []*Artifact{ + { + Path: "!!!duplicate!!!", + Type: Binary, + }, + { + Path: "!!!duplicate!!!", + Type: Binary, + }, + { + Path: "!!!duplicate!!!", + Type: UploadableBinary, + }, + { + Path: "!!!duplicate!!!", + Type: UploadableBinary, + }, + { + Path: "!!!duplicate!!!", + Type: UniversalBinary, + }, + { + Path: "!!!duplicate!!!", + Type: UniversalBinary, + }, + }, + expected: []*Artifact{ + { + Path: "!!!duplicate!!!", + Type: UploadableBinary, + }, + { + Path: "!!!duplicate!!!", + Type: UploadableBinary, + }, + }, + }, + { + name: "keeps duplicates when there is no uploadable binary", + initial: []*Artifact{ + { + Path: "!!!duplicate!!!", + Type: Binary, + }, + { + Path: "!!!duplicate!!!", + Type: Binary, + }, + { + Path: "!!!duplicate!!!", + Type: UniversalBinary, + }, + { + Path: "!!!duplicate!!!", + Type: UniversalBinary, + }, + }, + expected: []*Artifact{ + { + Path: "!!!duplicate!!!", + Type: Binary, + }, + { + Path: "!!!duplicate!!!", + Type: Binary, + }, + { + Path: "!!!duplicate!!!", + Type: UniversalBinary, + }, + { + Path: "!!!duplicate!!!", + Type: UniversalBinary, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + arts := New() + for _, a := range tt.initial { + arts.Add(a) + } + actual := arts.Filter(ByBinaryLikeArtifacts(arts)).List() + assert.Equal(t, tt.expected, actual) + + if t.Failed() { + t.Log("expected:") + for _, a := range tt.expected { + t.Logf(" %s: %s", a.Type.String(), a.Path) + } + + t.Log("got:") + for _, a := range actual { + t.Logf(" %s: %s", a.Type.String(), a.Path) + } + } + }) + } +} diff --git a/internal/pipe/sbom/sbom.go b/internal/pipe/sbom/sbom.go index 70bb56d9325..7653a2c3e64 100644 --- a/internal/pipe/sbom/sbom.go +++ b/internal/pipe/sbom/sbom.go @@ -36,43 +36,49 @@ func (Pipe) Default(ctx *context.Context) error { ids := ids.New("sboms") for i := range ctx.Config.SBOMs { cfg := &ctx.Config.SBOMs[i] - if cfg.Cmd == "" { - cfg.Cmd = "syft" + if err := setConfigDefaults(cfg); err != nil { + return err } - if cfg.Artifacts == "" { - cfg.Artifacts = "archive" + ids.Inc(cfg.ID) + } + return ids.Validate() +} + +func setConfigDefaults(cfg *config.SBOM) error { + if cfg.Cmd == "" { + cfg.Cmd = "syft" + } + if cfg.Artifacts == "" { + cfg.Artifacts = "archive" + } + if len(cfg.Documents) == 0 { + switch cfg.Artifacts { + case "binary": + cfg.Documents = []string{"{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.sbom"} + case "any": + cfg.Documents = []string{} + default: + cfg.Documents = []string{"{{ .ArtifactName }}.sbom"} } - if len(cfg.Documents) == 0 { - switch cfg.Artifacts { - case "binary": - cfg.Documents = []string{"{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.sbom"} - case "any": - cfg.Documents = []string{} - default: - cfg.Documents = []string{"{{ .ArtifactName }}.sbom"} - } + } + if cfg.Cmd == "syft" { + if len(cfg.Args) == 0 { + cfg.Args = []string{"$artifact", "--file", "$document", "--output", "spdx-json"} } - if cfg.Cmd == "syft" { - if len(cfg.Args) == 0 { - cfg.Args = []string{"$artifact", "--file", "$document", "--output", "spdx-json"} - } - if len(cfg.Env) == 0 && cfg.Artifacts == "source" || cfg.Artifacts == "archive" { - cfg.Env = []string{ - "SYFT_FILE_METADATA_CATALOGER_ENABLED=true", - } + if len(cfg.Env) == 0 && (cfg.Artifacts == "source" || cfg.Artifacts == "archive") { + cfg.Env = []string{ + "SYFT_FILE_METADATA_CATALOGER_ENABLED=true", } } - if cfg.ID == "" { - cfg.ID = "default" - } - - if cfg.Artifacts != "any" && len(cfg.Documents) > 1 { - return fmt.Errorf("multiple SBOM outputs when artifacts=%q is unsupported", cfg.Artifacts) - } + } + if cfg.ID == "" { + cfg.ID = "default" + } - ids.Inc(cfg.ID) + if cfg.Artifacts != "any" && len(cfg.Documents) > 1 { + return fmt.Errorf("multiple SBOM outputs when artifacts=%q is unsupported", cfg.Artifacts) } - return ids.Validate() + return nil } // Run executes the Pipe. @@ -96,7 +102,7 @@ func catalogTask(ctx *context.Context, cfg config.SBOM) func() error { case "archive": filters = append(filters, artifact.ByType(artifact.UploadableArchive)) case "binary": - filters = append(filters, artifact.ByType(artifact.UploadableBinary)) + filters = append(filters, artifact.ByBinaryLikeArtifacts(ctx.Artifacts)) case "package": filters = append(filters, artifact.ByType(artifact.LinuxPackage)) case "any": @@ -152,49 +158,16 @@ func subprocessDistPath(distDir string, pathRelativeToCwd string) (string, error } func catalogArtifact(ctx *context.Context, cfg config.SBOM, a *artifact.Artifact) ([]*artifact.Artifact, error) { - env := ctx.Env.Copy() artifactDisplayName := "(any)" - templater := tmpl.New(ctx).WithEnv(env) + args, envs, paths, err := applyTemplate(ctx, cfg, a) + if err != nil { + return nil, fmt.Errorf("cataloging artifacts failed: %w", err) + } if a != nil { - procPath, err := subprocessDistPath(ctx.Config.Dist, a.Path) - if err != nil { - return nil, fmt.Errorf("cataloging artifacts failed: cannot determine artifact path for %q: %w", a.Path, err) - } - env["artifact"] = procPath - env["artifactID"] = a.ID() - - templater = templater.WithArtifact(a, nil) artifactDisplayName = a.Path } - var paths []string - for idx, sbom := range cfg.Documents { - input := filepath.Join(ctx.Config.Dist, expand(sbom, env)) - - path, err := templater.Apply(input) - if err != nil { - return nil, fmt.Errorf("cataloging artifacts failed: %s: invalid template: %w", input, err) - } - - path, err = filepath.Abs(path) - if err != nil { - return nil, fmt.Errorf("cataloging artifacts failed: unable to create artifact path %q: %w", sbom, err) - } - - procPath, err := subprocessDistPath(ctx.Config.Dist, path) - if err != nil { - return nil, fmt.Errorf("cataloging artifacts failed: cannot determine document path for %q: %w", path, err) - } - - env[fmt.Sprintf("document%d", idx)] = procPath - if idx == 0 { - env["document"] = procPath - } - - paths = append(paths, procPath) - } - var names []string for _, p := range paths { names = append(names, filepath.Base(p)) @@ -202,16 +175,6 @@ func catalogArtifact(ctx *context.Context, cfg config.SBOM, a *artifact.Artifact fields := log.Fields{"cmd": cfg.Cmd, "artifact": artifactDisplayName, "sboms": strings.Join(names, ", ")} - // nolint:prealloc - var args []string - for _, arg := range cfg.Args { - renderedArg, err := templater.Apply(expand(arg, env)) - if err != nil { - return nil, fmt.Errorf("cataloging artifacts failed: %s: invalid template: %w", arg, err) - } - args = append(args, renderedArg) - } - // The GoASTScanner flags this as a security risk. // However, this works as intended. The nosec annotation // tells the scanner to ignore this. @@ -223,7 +186,7 @@ func catalogArtifact(ctx *context.Context, cfg config.SBOM, a *artifact.Artifact cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value)) } } - cmd.Env = append(cmd.Env, cfg.Env...) + cmd.Env = append(cmd.Env, envs...) cmd.Dir = ctx.Config.Dist var b bytes.Buffer @@ -238,27 +201,19 @@ func catalogArtifact(ctx *context.Context, cfg config.SBOM, a *artifact.Artifact var artifacts []*artifact.Artifact - for _, sbom := range cfg.Documents { - templater = tmpl.New(ctx).WithEnv(env) - if a != nil { - env["artifact"] = a.Name - templater = templater.WithArtifact(a, nil) + for _, path := range paths { + if !filepath.IsAbs(path) { + path = filepath.Join(ctx.Config.Dist, path) } - name, err := templater.Apply(expand(sbom, env)) + matches, err := filepath.Glob(path) if err != nil { - return nil, fmt.Errorf("cataloging artifacts failed: %s: invalid template: %w", a, err) - } - - search := filepath.Join(ctx.Config.Dist, name) - matches, err := filepath.Glob(search) - if err != nil { - return nil, fmt.Errorf("cataloging artifacts: failed to find SBOM artifact %q: %w", search, err) + return nil, fmt.Errorf("cataloging artifacts: failed to find SBOM artifact %q: %w", path, err) } for _, match := range matches { artifacts = append(artifacts, &artifact.Artifact{ Type: artifact.SBOM, - Name: name, + Name: filepath.Base(path), Path: match, Extra: map[string]interface{}{ artifact.ExtraID: cfg.ID, @@ -271,6 +226,85 @@ func catalogArtifact(ctx *context.Context, cfg config.SBOM, a *artifact.Artifact return artifacts, nil } +func applyTemplate(ctx *context.Context, cfg config.SBOM, a *artifact.Artifact) ([]string, []string, []string, error) { + env := ctx.Env.Copy() + var extraEnvs []string + templater := tmpl.New(ctx).WithEnv(env) + + if a != nil { + procPath, err := subprocessDistPath(ctx.Config.Dist, a.Path) + if err != nil { + return nil, nil, nil, fmt.Errorf("cataloging artifacts failed: cannot determine artifact path for %q: %w", a.Path, err) + } + extraEnvs = appendExtraEnv("artifact", procPath, extraEnvs, env) + extraEnvs = appendExtraEnv("artifactID", a.ID(), extraEnvs, env) + + templater = templater.WithArtifact(a, nil) + } + + for _, keyValue := range cfg.Env { + renderedKeyValue, err := templater.Apply(expand(keyValue, env)) + if err != nil { + return nil, nil, nil, fmt.Errorf("env %q: invalid template: %w", keyValue, err) + } + extraEnvs = append(extraEnvs, renderedKeyValue) + + fields := strings.Split(renderedKeyValue, "=") + key := fields[0] + renderedValue := strings.Join(fields[1:], "=") + env[key] = renderedValue + } + + var paths []string + for idx, sbom := range cfg.Documents { + input := expand(sbom, env) + if !filepath.IsAbs(input) { + // assume any absolute path is handled correctly and assume that any relative path is not already + // adjusted to reference the dist path + input = filepath.Join(ctx.Config.Dist, input) + } + + path, err := templater.Apply(input) + if err != nil { + return nil, nil, nil, fmt.Errorf("input %q: invalid template: %w", input, err) + } + + path, err = filepath.Abs(path) + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to create artifact path %q: %w", sbom, err) + } + + procPath, err := subprocessDistPath(ctx.Config.Dist, path) + if err != nil { + return nil, nil, nil, fmt.Errorf("cannot determine document path for %q: %w", path, err) + } + + extraEnvs = appendExtraEnv(fmt.Sprintf("document%d", idx), procPath, extraEnvs, env) + if idx == 0 { + extraEnvs = appendExtraEnv("document", procPath, extraEnvs, env) + } + + paths = append(paths, procPath) + } + + // nolint:prealloc + var args []string + for _, arg := range cfg.Args { + renderedArg, err := templater.Apply(expand(arg, env)) + if err != nil { + return nil, nil, nil, fmt.Errorf("arg %q: invalid template: %w", arg, err) + } + args = append(args, renderedArg) + } + + return args, extraEnvs, paths, nil +} + +func appendExtraEnv(key, value string, envs []string, env map[string]string) []string { + env[key] = value + return append(envs, fmt.Sprintf("%s=%s", key, value)) +} + func expand(s string, env map[string]string) string { return os.Expand(s, func(key string) string { return env[key] diff --git a/internal/pipe/sbom/sbom_test.go b/internal/pipe/sbom/sbom_test.go index f4fde14a15e..646f8a5af85 100644 --- a/internal/pipe/sbom/sbom_test.go +++ b/internal/pipe/sbom/sbom_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "sort" + "strings" "testing" "github.com/goreleaser/goreleaser/internal/artifact" @@ -72,6 +73,23 @@ func TestSBOMCatalogDefault(t *testing.T) { "SYFT_FILE_METADATA_CATALOGER_ENABLED=true", }, }, + { + configs: []config.SBOM{ + { + Artifacts: "archive", + Env: []string{ + "something=something-else", + }, + }, + }, + artifact: "archive", + cmd: defaultCmd, + sboms: defaultSboms, + args: defaultArgs, + env: []string{ + "something=something-else", + }, + }, { configs: []config.SBOM{ { @@ -224,7 +242,7 @@ func TestSBOMCatalogArtifacts(t *testing.T) { }, { desc: "invalid args template", - expectedErrMsg: `cataloging artifacts failed: ${FOO}-{{ .foo }{{}}{: invalid template: template: tmpl:1: unexpected "}" in operand`, + expectedErrMsg: `cataloging artifacts failed: arg "${FOO}-{{ .foo }{{}}{": invalid template: template: tmpl:1: unexpected "}" in operand`, ctx: context.New( config.Project{ SBOMs: []config.SBOM{ @@ -287,9 +305,11 @@ func TestSBOMCatalogArtifacts(t *testing.T) { ), sbomPaths: []string{ "artifact3-name_1.2.2_linux_amd64.sbom", + "artifact4-name_1.2.2_linux_amd64.sbom", }, sbomNames: []string{ "artifact3-name_1.2.2_linux_amd64.sbom", + "artifact4-name_1.2.2_linux_amd64.sbom", }, }, { @@ -340,11 +360,13 @@ func TestSBOMCatalogArtifacts(t *testing.T) { "artifact1.s2-ish.sbom", "artifact2.s2-ish.sbom", "artifact3-name_1.2.2_linux_amd64.sbom", + "artifact4-name_1.2.2_linux_amd64.sbom", }, sbomNames: []string{ "artifact1.s2-ish.sbom", "artifact2.s2-ish.sbom", "artifact3-name_1.2.2_linux_amd64.sbom", + "artifact4-name_1.2.2_linux_amd64.sbom", }, }, { @@ -392,9 +414,11 @@ func TestSBOMCatalogArtifacts(t *testing.T) { ), sbomPaths: []string{ "artifact3-name.test-user-name.sbom", + "artifact4.test-user-name.sbom", }, sbomNames: []string{ "artifact3-name.test-user-name.sbom", + "artifact4.test-user-name.sbom", }, }, { @@ -583,3 +607,170 @@ func Test_subprocessDistPath(t *testing.T) { }) } } + +func Test_templateNames(t *testing.T) { + art := artifact.Artifact{ + Name: "name-it", + Path: "to/a/place", + Goos: "darwin", + Goarch: "amd64", + Type: artifact.Binary, + Extra: map[string]interface{}{ + artifact.ExtraID: "id-it", + "Binary": "binary-name", + }, + } + + wd, err := os.Getwd() + require.NoError(t, err) + + tests := []struct { + name string + dist string + version string + cfg config.SBOM + artifact artifact.Artifact + expectedValues map[string]string + expectedPaths []string + }{ + { + name: "default configuration", + artifact: art, + cfg: config.SBOM{}, + dist: "/somewhere/to/dist", + expectedPaths: []string{ + "/somewhere/to/dist/name-it.sbom", + }, + expectedValues: map[string]string{ + "artifact": "to/a/place", + "artifactID": "id-it", + "document": "/somewhere/to/dist/name-it.sbom", + "document0": "/somewhere/to/dist/name-it.sbom", + }, + }, + { + name: "default configuration + relative dist", + artifact: art, + cfg: config.SBOM{}, + dist: "somewhere/to/dist", + expectedPaths: []string{ + filepath.Join(wd, "somewhere/to/dist/name-it.sbom"), + }, + expectedValues: map[string]string{ + "artifact": "to/a/place", // note: this is always relative to ${dist} + "artifactID": "id-it", + "document": filepath.Join(wd, "somewhere/to/dist/name-it.sbom"), + "document0": filepath.Join(wd, "somewhere/to/dist/name-it.sbom"), + }, + }, + { + name: "custom document using $artifact", + // note: this configuration is probably a misconfiguration since it is placing SBOMs within each bin + // directory, however, it will behave as correctly as possible. + artifact: art, + cfg: config.SBOM{ + Documents: []string{ + // note: the artifact name is probably an incorrect value here since it can't express all attributes + // of the binary (os, arch, etc), so builds with multiple architectures will create SBOMs with the + // same name. + "${artifact}.cdx.sbom", + }, + }, + dist: "somewhere/to/dist", + expectedPaths: []string{ + filepath.Join(wd, "somewhere/to/dist/to/a/place.cdx.sbom"), + }, + expectedValues: map[string]string{ + "artifact": "to/a/place", + "artifactID": "id-it", + "document": filepath.Join(wd, "somewhere/to/dist/to/a/place.cdx.sbom"), + "document0": filepath.Join(wd, "somewhere/to/dist/to/a/place.cdx.sbom"), + }, + }, + { + name: "custom document using build vars", + artifact: art, + cfg: config.SBOM{ + Documents: []string{ + "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.cdx.sbom", + }, + }, + version: "1.0.0", + dist: "somewhere/to/dist", + expectedPaths: []string{ + filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"), + }, + expectedValues: map[string]string{ + "artifact": "to/a/place", + "artifactID": "id-it", + "document": filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"), + "document0": filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"), + }, + }, + { + name: "env vars with go templated options", + artifact: art, + cfg: config.SBOM{ + Documents: []string{ + "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.cdx.sbom", + }, + Env: []string{ + "with-env-var=value", + "custom-os={{ .Os }}-unique", + "custom-arch={{ .Arch }}-unique", + }, + }, + version: "1.0.0", + dist: "somewhere/to/dist", + expectedPaths: []string{ + filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"), + }, + expectedValues: map[string]string{ + "artifact": "to/a/place", + "artifactID": "id-it", + "with-env-var": "value", + "custom-os": "darwin-unique", + "custom-arch": "amd64-unique", + "document": filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"), + "document0": filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.New(config.Project{ + Dist: tt.dist, + }) + ctx.Version = tt.version + + cfg := tt.cfg + require.NoError(t, setConfigDefaults(&cfg)) + + var inputArgs []string + var expectedArgs []string + for key, value := range tt.expectedValues { + inputArgs = append(inputArgs, fmt.Sprintf("${%s}", key)) + expectedArgs = append(expectedArgs, value) + } + cfg.Args = inputArgs + + actualArgs, actualEnvs, actualPaths, err := applyTemplate(ctx, cfg, &tt.artifact) + require.NoError(t, err) + + assert.Equal(t, tt.expectedPaths, actualPaths, "paths differ") + + assert.Equal(t, expectedArgs, actualArgs, "arguments differ") + + actualEnv := make(map[string]string) + for _, str := range actualEnvs { + key := strings.Split(str, "=")[0] + value := strings.Join(strings.Split(str, "=")[1:], "=") + actualEnv[key] = value + } + + for k, v := range tt.expectedValues { + assert.Equal(t, v, actualEnv[k]) + } + }) + } +}