diff --git a/pkg/container_backend/docker_server_backend.go b/pkg/container_backend/docker_server_backend.go index 6c6bb0b3e6..7affaf1e44 100644 --- a/pkg/container_backend/docker_server_backend.go +++ b/pkg/container_backend/docker_server_backend.go @@ -472,7 +472,7 @@ func (backend *DockerServerBackend) GenerateSBOM(ctx context.Context, scanOpts s }) contextAddFiles = append(contextAddFiles, workingTree.Containerfile()) - archive := newSbomContextArchiver(workingTree.RootDir()) + archive := NewSbomContextArchiver(workingTree.RootDir()) if err := archive.Create(ctx, BuildContextArchiveCreateOptions{ DockerfileRelToContextPath: workingTree.Containerfile(), diff --git a/pkg/container_backend/sbom_build_context_archiver.go b/pkg/container_backend/sbom_build_context_archiver.go index 0ca2f376c7..3f972d6334 100644 --- a/pkg/container_backend/sbom_build_context_archiver.go +++ b/pkg/container_backend/sbom_build_context_archiver.go @@ -4,22 +4,23 @@ import ( "archive/tar" "context" "fmt" + "io" "os" "path/filepath" ) -type sbomBuildContextArchiver struct { +type SbomBuildContextArchiver struct { rootDir string tarPath string } -func newSbomContextArchiver(rootDir string) *sbomBuildContextArchiver { - return &sbomBuildContextArchiver{ +func NewSbomContextArchiver(rootDir string) *SbomBuildContextArchiver { + return &SbomBuildContextArchiver{ rootDir: rootDir, } } -func (a *sbomBuildContextArchiver) Create(_ context.Context, opts BuildContextArchiveCreateOptions) error { +func (a *SbomBuildContextArchiver) Create(_ context.Context, opts BuildContextArchiveCreateOptions) error { tarFile, err := os.Create(filepath.Join(a.rootDir, "sbom-docker.tar")) if err != nil { return fmt.Errorf("unable to create tar file: %w", err) @@ -50,14 +51,8 @@ func (a *sbomBuildContextArchiver) Create(_ context.Context, opts BuildContextAr return fmt.Errorf("unable to write tar header %v: %w", hdr, err) } - fileContent := make([]byte, stat.Size()) - - if _, err = file.Read(fileContent); err != nil { - return fmt.Errorf("unable to read file %q: %w", file.Name(), err) - } - - if _, err = tarWriter.Write(fileContent); err != nil { - return fmt.Errorf("unable to write to tar %q: %w", tarFile.Name(), err) + if _, err = io.Copy(tarWriter, file); err != nil { + return fmt.Errorf("unable to copy file %q: %w", file.Name(), err) } if err = file.Close(); err != nil { @@ -72,21 +67,21 @@ func (a *sbomBuildContextArchiver) Create(_ context.Context, opts BuildContextAr return nil } -func (a *sbomBuildContextArchiver) Path() string { +func (a *SbomBuildContextArchiver) Path() string { return a.tarPath } -func (a *sbomBuildContextArchiver) ExtractOrGetExtractedDir(ctx context.Context) (string, error) { +func (a *SbomBuildContextArchiver) ExtractOrGetExtractedDir(ctx context.Context) (string, error) { return "", nil } -func (a *sbomBuildContextArchiver) CalculatePathsChecksum(ctx context.Context, paths []string) (string, error) { +func (a *SbomBuildContextArchiver) CalculatePathsChecksum(ctx context.Context, paths []string) (string, error) { return "", nil } -func (a *sbomBuildContextArchiver) CalculateGlobsChecksum(ctx context.Context, globs []string, checkForArchive bool) (string, error) { +func (a *SbomBuildContextArchiver) CalculateGlobsChecksum(ctx context.Context, globs []string, checkForArchive bool) (string, error) { return "", nil } -func (a *sbomBuildContextArchiver) CleanupExtractedDir(ctx context.Context) { +func (a *SbomBuildContextArchiver) CleanupExtractedDir(ctx context.Context) { } diff --git a/pkg/container_backend/sbom_build_context_archiver_test.go b/pkg/container_backend/sbom_build_context_archiver_test.go new file mode 100644 index 0000000000..b00f710645 --- /dev/null +++ b/pkg/container_backend/sbom_build_context_archiver_test.go @@ -0,0 +1,121 @@ +package container_backend_test + +import ( + "archive/tar" + "bytes" + "errors" + "io" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" + + "github.com/werf/werf/v2/pkg/container_backend" +) + +var _ = Describe("SbomBuildContextArchiver", func() { + DescribeTable("Create() should", + func(ctx SpecContext, tc testCase) { + tmpDir := GinkgoT().TempDir() + + // arrange: create input files + prepareFixtures(tmpDir, tc.fixtures) + + archiver := container_backend.NewSbomContextArchiver(tmpDir) + + err := archiver.Create(ctx, container_backend.BuildContextArchiveCreateOptions{ + ContextAddFiles: tc.contextAdd, + }) + Expect(err).To(tc.createErrMatcher) + + Expect(archiver.Path()).To(Equal(filepath.Join(tmpDir, "sbom-docker.tar"))) + + verifyArchive(tmpDir, tc) + }, + Entry("successfully creates tar and adds files from mode 0600", testCase{ + fixtures: map[string][]byte{ + "foo.txt": []byte("hello"), + "dir/bar.json": []byte(`{"x":1}`), + "empty.bin": []byte(""), + "dir/nested.md": []byte("# t"), + }, + contextAdd: []string{ + "foo.txt", + "dir/bar.json", + "empty.bin", + }, + createErrMatcher: Succeed(), + verifyArchive: true, + verifyEntries: map[string][]byte{ + "foo.txt": []byte("hello"), + "dir/bar.json": []byte(`{"x":1}`), + "empty.bin": []byte(""), + }, + }), + Entry("error if the added file is missing", testCase{ + fixtures: map[string][]byte{ + "exists.txt": []byte("ok"), + }, + contextAdd: []string{ + "exists.txt", + "nope.txt", + }, + createErrMatcher: MatchError(ContainSubstring("nope.txt: no such file or directory")), + verifyArchive: false, + }), + ) +}) + +type testCase struct { + fixtures map[string][]byte // path -> content (relative to root) + contextAdd []string + createErrMatcher types.GomegaMatcher + verifyArchive bool + verifyEntries map[string][]byte // name -> content +} + +func prepareFixtures(tmpDir string, fixtures map[string][]byte) { + // arrange: create input files + for rel, content := range fixtures { + abs := filepath.Join(tmpDir, rel) + Expect(os.MkdirAll(filepath.Dir(abs), 0o755)).To(Succeed()) + Expect(os.WriteFile(abs, content, 0o644)).To(Succeed()) + } +} + +func verifyArchive(tmpDir string, tc testCase) { + if tc.verifyArchive { + f, openErr := os.Open(filepath.Join(tmpDir, "sbom-docker.tar")) + Expect(openErr).NotTo(HaveOccurred()) + defer f.Close() + + tr := tar.NewReader(f) + seen := map[string]struct{}{} + + for { + hdr, rErr := tr.Next() + if errors.Is(rErr, io.EOF) { + break + } + Expect(rErr).NotTo(HaveOccurred()) + + seen[hdr.Name] = struct{}{} + + Expect(hdr.Mode).To(Equal(int64(0o600))) + + if want, ok := tc.verifyEntries[hdr.Name]; ok { + got, readErr := io.ReadAll(tr) + Expect(readErr).NotTo(HaveOccurred()) + Expect(bytes.Equal(got, want)).To(BeTrue(), "content mismatch for %s", hdr.Name) + Expect(hdr.Size).To(Equal(int64(len(want)))) + } + } + + for name := range tc.verifyEntries { + _, ok := seen[name] + Expect(ok).To(BeTrue(), "expected tar entry %q to exist", name) + } + } +}