1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-01-06 03:13:48 +02:00
goreleaser/internal/pipe/sbom/sbom_test.go
Carlos Alexandro Becker 874d698564
feat: add healthcheck cmd (#3826)
here's an idea: `goreleaser healthcheck`

It'll check if the needed dependencies (docker, git, etc) are available
in the path... this way users can preemptively run it before releasing
or to debug issues.

What do you think?

Here's how it looks like:

<img width="1007" alt="CleanShot 2023-03-02 at 23 24 26@2x"
src="https://user-images.githubusercontent.com/245435/222615682-d9cd0733-d900-43d1-9166-23b2be589b3a.png">

---------

Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
2023-03-03 09:50:15 -03:00

763 lines
19 KiB
Go

package sbom
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/internal/testctx"
"github.com/goreleaser/goreleaser/internal/testlib"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDescription(t *testing.T) {
require.NotEmpty(t, Pipe{}.String())
}
func TestSBOMCatalogDefault(t *testing.T) {
defaultArgs := []string{"$artifact", "--file", "$document", "--output", "spdx-json"}
defaultSboms := []string{
"{{ .ArtifactName }}.sbom",
}
defaultCmd := "syft"
tests := []struct {
configs []config.SBOM
artifact string
cmd string
sboms []string
args []string
env []string
err bool
}{
{
configs: []config.SBOM{
{
// empty
},
},
artifact: "archive",
cmd: defaultCmd,
sboms: defaultSboms,
args: defaultArgs,
env: []string{
"SYFT_FILE_METADATA_CATALOGER_ENABLED=true",
},
},
{
configs: []config.SBOM{
{
Artifacts: "package",
},
},
artifact: "package",
cmd: defaultCmd,
sboms: defaultSboms,
args: defaultArgs,
},
{
configs: []config.SBOM{
{
Artifacts: "archive",
},
},
artifact: "archive",
cmd: defaultCmd,
sboms: defaultSboms,
args: defaultArgs,
env: []string{
"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{
{
Artifacts: "any",
},
},
artifact: "any",
cmd: defaultCmd,
sboms: []string{},
args: defaultArgs,
},
{
configs: []config.SBOM{
{
Artifacts: "binary",
},
},
artifact: "binary",
cmd: defaultCmd,
sboms: []string{"{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.sbom"},
args: defaultArgs,
},
{
configs: []config.SBOM{
{
Artifacts: "source",
},
},
artifact: "source",
cmd: defaultCmd,
sboms: defaultSboms,
args: defaultArgs,
env: []string{
"SYFT_FILE_METADATA_CATALOGER_ENABLED=true",
},
},
{
// multiple documents are not allowed when artifacts != "any"
configs: []config.SBOM{
{
Artifacts: "binary",
Documents: []string{
"doc1",
"doc2",
},
},
},
err: true,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("artifact=%q", test.configs[0].Artifacts), func(t *testing.T) {
testlib.CheckPath(t, "syft")
ctx := testctx.NewWithCfg(config.Project{
SBOMs: test.configs,
})
err := Pipe{}.Default(ctx)
if test.err {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, ctx.Config.SBOMs[0].Cmd, test.cmd)
require.Equal(t, ctx.Config.SBOMs[0].Documents, test.sboms)
require.Equal(t, ctx.Config.SBOMs[0].Args, test.args)
require.Equal(t, ctx.Config.SBOMs[0].Env, test.env)
require.Equal(t, ctx.Config.SBOMs[0].Artifacts, test.artifact)
})
}
}
func TestSBOMCatalogInvalidArtifacts(t *testing.T) {
ctx := testctx.NewWithCfg(config.Project{
SBOMs: []config.SBOM{{Artifacts: "foo"}},
})
err := Pipe{}.Run(ctx)
require.EqualError(t, err, "invalid list of artifacts to catalog: foo")
}
func TestSeveralSBOMsWithTheSameID(t *testing.T) {
ctx := testctx.NewWithCfg(config.Project{
SBOMs: []config.SBOM{
{
ID: "a",
},
{
ID: "a",
},
},
})
require.EqualError(t, Pipe{}.Default(ctx), "found 2 sboms with the ID 'a', please fix your config")
}
func TestSkipCataloging(t *testing.T) {
t.Run("skip", func(t *testing.T) {
require.True(t, Pipe{}.Skip(testctx.New()))
})
t.Run("skip SBOM cataloging", func(t *testing.T) {
ctx := testctx.NewWithCfg(config.Project{
SBOMs: []config.SBOM{
{
Artifacts: "all",
},
},
})
ctx.SkipSBOMCataloging = true
require.True(t, Pipe{}.Skip(ctx))
})
t.Run("dont skip", func(t *testing.T) {
ctx := testctx.NewWithCfg(config.Project{
SBOMs: []config.SBOM{
{
Artifacts: "all",
},
},
})
require.False(t, Pipe{}.Skip(ctx))
})
}
func TestSBOMCatalogArtifacts(t *testing.T) {
tests := []struct {
desc string
ctx *context.Context
sbomPaths []string
sbomNames []string
expectedErrMsg string
}{
{
desc: "catalog errors",
expectedErrMsg: "cataloging artifacts: exit failed",
ctx: testctx.NewWithCfg(config.Project{
SBOMs: []config.SBOM{
{
Artifacts: "binary",
Cmd: "exit",
Args: []string{"1"},
},
},
}),
},
{
desc: "invalid args template",
expectedErrMsg: `cataloging artifacts failed: arg "${FOO}-{{ .foo }{{}}{": invalid template: template: tmpl:1: unexpected "}" in operand`,
ctx: testctx.NewWithCfg(config.Project{
SBOMs: []config.SBOM{
{
Artifacts: "binary",
Cmd: "exit",
Args: []string{"${FOO}-{{ .foo }{{}}{"},
},
},
Env: []string{
"FOO=BAR",
},
}),
},
{
desc: "catalog source archives",
ctx: testctx.NewWithCfg(config.Project{
SBOMs: []config.SBOM{
{Artifacts: "source"},
},
}),
sbomPaths: []string{"artifact5.tar.gz.sbom"},
sbomNames: []string{"artifact5.tar.gz.sbom"},
},
{
desc: "catalog archives",
ctx: testctx.NewWithCfg(config.Project{
SBOMs: []config.SBOM{
{Artifacts: "archive"},
},
}),
sbomPaths: []string{"artifact1.sbom", "artifact2.sbom"},
sbomNames: []string{"artifact1.sbom", "artifact2.sbom"},
},
{
desc: "catalog linux packages",
ctx: testctx.NewWithCfg(config.Project{
SBOMs: []config.SBOM{
{Artifacts: "package"},
},
}),
sbomPaths: []string{"package1.deb.sbom"},
sbomNames: []string{"package1.deb.sbom"},
},
{
desc: "catalog binaries",
ctx: testctx.NewWithCfg(config.Project{
SBOMs: []config.SBOM{
{Artifacts: "binary"},
},
}),
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",
},
},
{
desc: "manual cataloging",
ctx: testctx.NewWithCfg(config.Project{
SBOMs: []config.SBOM{
{
Artifacts: "any",
Args: []string{
"--file",
"$document0",
"--output",
"spdx-json",
"artifact5.tar.gz",
},
Documents: []string{
"final.sbom",
},
},
},
}),
sbomPaths: []string{"final.sbom"},
sbomNames: []string{"final.sbom"},
},
{
desc: "multiple SBOM configs",
ctx: testctx.NewWithCfg(config.Project{
Env: []string{
"SBOM_SUFFIX=s2-ish",
},
SBOMs: []config.SBOM{
{
ID: "s1",
Artifacts: "binary",
},
{
ID: "s2",
Artifacts: "archive",
Documents: []string{"{{ .ArtifactName }}.{{ .Env.SBOM_SUFFIX }}.sbom"},
},
},
}),
sbomPaths: []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",
},
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",
},
},
{
desc: "catalog artifacts with filtered by ID",
ctx: testctx.NewWithCfg(config.Project{
SBOMs: []config.SBOM{
{
Artifacts: "binary",
IDs: []string{"foo"},
},
},
}),
sbomPaths: []string{
"artifact3-name_1.2.2_linux_amd64.sbom",
},
sbomNames: []string{
"artifact3-name_1.2.2_linux_amd64.sbom",
},
},
{
desc: "catalog binary artifacts with env in arguments",
ctx: testctx.NewWithCfg(config.Project{
SBOMs: []config.SBOM{
{
Artifacts: "binary",
Args: []string{
"--file",
"$document",
"--output",
"spdx-json",
"$artifact",
},
Documents: []string{
"{{ .ArtifactName }}.{{ .Env.TEST_USER }}.sbom",
},
},
},
Env: []string{
"TEST_USER=test-user-name",
},
}),
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",
},
},
{
desc: "cataloging 'any' artifacts fails",
ctx: testctx.NewWithCfg(config.Project{
SBOMs: []config.SBOM{
{
Artifacts: "any",
Cmd: "false",
},
},
}),
expectedErrMsg: "cataloging artifacts: false failed: exit status 1: ",
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
testSBOMCataloging(t, test.ctx, test.sbomPaths, test.sbomNames, test.expectedErrMsg)
})
}
}
func testSBOMCataloging(tb testing.TB, ctx *context.Context, sbomPaths, sbomNames []string, expectedErrMsg string) {
tb.Helper()
testlib.CheckPath(tb, "syft")
tmpdir := tb.TempDir()
ctx.Config.Dist = tmpdir
ctx.Version = "1.2.2"
// create some fake artifacts
artifacts := []string{"artifact1", "artifact2", "artifact3", "package1.deb"}
require.NoError(tb, os.Mkdir(filepath.Join(tmpdir, "linux_amd64"), os.ModePerm))
for _, f := range artifacts {
file := filepath.Join(tmpdir, f)
require.NoError(tb, os.WriteFile(file, []byte("foo"), 0o644))
}
require.NoError(tb, os.WriteFile(filepath.Join(tmpdir, "linux_amd64", "artifact4"), []byte("foo"), 0o644))
artifacts = append(artifacts, "linux_amd64/artifact4")
require.NoError(tb, os.WriteFile(filepath.Join(tmpdir, "artifact5.tar.gz"), []byte("foo"), 0o644))
artifacts = append(artifacts, "artifact5.tar.gz")
ctx.Artifacts.Add(&artifact.Artifact{
Name: "artifact1",
Path: filepath.Join(tmpdir, "artifact1"),
Type: artifact.UploadableArchive,
Extra: map[string]interface{}{
artifact.ExtraID: "foo",
},
})
ctx.Artifacts.Add(&artifact.Artifact{
Name: "artifact2",
Path: filepath.Join(tmpdir, "artifact2"),
Type: artifact.UploadableArchive,
Extra: map[string]interface{}{
artifact.ExtraID: "foo3",
},
})
ctx.Artifacts.Add(&artifact.Artifact{
Name: "artifact3-name",
Path: filepath.Join(tmpdir, "artifact3"),
Goos: "linux",
Goarch: "amd64",
Type: artifact.UploadableBinary,
Extra: map[string]interface{}{
artifact.ExtraID: "foo",
artifact.ExtraBinary: "artifact3-name",
},
})
ctx.Artifacts.Add(&artifact.Artifact{
Name: "artifact4",
Path: filepath.Join(tmpdir, "linux_amd64", "artifact4"),
Goos: "linux",
Goarch: "amd64",
Type: artifact.Binary,
Extra: map[string]interface{}{
artifact.ExtraID: "foo3",
artifact.ExtraBinary: "artifact4-name",
},
})
ctx.Artifacts.Add(&artifact.Artifact{
Name: "artifact5.tar.gz",
Path: filepath.Join(tmpdir, "artifact5.tar.gz"),
Type: artifact.UploadableSourceArchive,
})
ctx.Artifacts.Add(&artifact.Artifact{
Name: "package1.deb",
Path: filepath.Join(tmpdir, "package1.deb"),
Type: artifact.LinuxPackage,
Extra: map[string]interface{}{
artifact.ExtraID: "foo",
},
})
// configure the pipeline
require.NoError(tb, Pipe{}.Default(ctx))
// run the pipeline
if expectedErrMsg != "" {
err := Pipe{}.Run(ctx)
require.Error(tb, err)
require.Contains(tb, err.Error(), expectedErrMsg)
return
}
require.NoError(tb, Pipe{}.Run(ctx))
// ensure all artifacts have an ID
for _, arti := range ctx.Artifacts.Filter(artifact.ByType(artifact.SBOM)).List() {
require.NotEmptyf(tb, arti.ID(), ".Extra.ID on %s", arti.Path)
}
// verify that only the artifacts and the sboms are in the dist dir
gotFiles := []string{}
require.NoError(tb, filepath.Walk(tmpdir,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
relPath, err := filepath.Rel(tmpdir, path)
if err != nil {
return err
}
gotFiles = append(gotFiles, relPath)
return nil
}),
)
wantFiles := append(artifacts, sbomPaths...)
sort.Strings(wantFiles)
require.ElementsMatch(tb, wantFiles, gotFiles, "SBOM paths differ")
var sbomArtifacts []string
for _, sig := range ctx.Artifacts.Filter(artifact.ByType(artifact.SBOM)).List() {
sbomArtifacts = append(sbomArtifacts, sig.Name)
}
require.ElementsMatch(tb, sbomArtifacts, sbomNames, "SBOM names differ")
}
func Test_subprocessDistPath(t *testing.T) {
cwd, err := os.Getwd()
require.NoError(t, err)
tests := []struct {
name string
distDir string
pathRelativeToCwd string
expects string
}{
{
name: "relative dist with anchor",
distDir: "./dist",
pathRelativeToCwd: "dist/my.sbom",
expects: "my.sbom",
},
{
name: "relative dist without anchor",
distDir: "dist",
pathRelativeToCwd: "dist/my.sbom",
expects: "my.sbom",
},
{
name: "relative dist with nested resource",
distDir: "dist",
pathRelativeToCwd: "dist/something/my.sbom",
expects: "something/my.sbom",
},
{
name: "absolute dist with nested resource",
distDir: filepath.Join(cwd, "dist/"),
pathRelativeToCwd: "dist/something/my.sbom",
expects: "something/my.sbom",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := subprocessDistPath(test.distDir, test.pathRelativeToCwd)
require.NoError(t, err)
assert.Equal(t, test.expects, actual)
})
}
}
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 := testctx.NewWithCfg(config.Project{
Dist: tt.dist,
}, testctx.WithVersion(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 {
k, v, ok := strings.Cut(str, "=")
require.True(t, ok)
actualEnv[k] = v
}
for k, v := range tt.expectedValues {
assert.Equal(t, v, actualEnv[k])
}
})
}
}
func TestDependencies(t *testing.T) {
ctx := testctx.NewWithCfg(config.Project{
SBOMs: []config.SBOM{
{Cmd: "syft"},
{Cmd: "foobar"},
},
})
require.Equal(t, []string{"syft", "foobar"}, Pipe{}.Dependencies(ctx))
}