From d68fcc4dc4cf866783b1b8e338d94bb951c15595 Mon Sep 17 00:00:00 2001 From: Jon Johnson Date: Sat, 14 Dec 2024 10:24:27 -0800 Subject: [PATCH] Lock image configs before building The embedded /etc/apko.json included both archs in the config, which was a little weird. It also wasn't locked, which is maybe on purpose, but locking feels better than not locking. Will make sure this is acceptable before merging. Signed-off-by: Jon Johnson --- internal/cli/build.go | 73 +++++++----------- internal/cli/publish_test.go | 8 +- ...bd19671fe54c227a6cfd33c1164cb8f67a86baadea | 1 + ...342d2b70e72d4f213642f488250fdb62ccd65a4ba7 | 1 + ...4331454f74315d2edbcf68e2af172f8efa5c95fe19 | 1 - ...3be08869342df17c951370416999e19cda0d36624} | 2 +- ...c918022ba897a381c264722a6b1d5c986471e9751e | Bin 3267 -> 0 bytes ...1d8d4aff54d14817a2675575b4f557499655097f3d | Bin 3256 -> 0 bytes ...182560718f22427628a3c112902377fa7ff897f9f7 | Bin 0 -> 3272 bytes ...fd026cb52cac94ff0f057996c829ac546f6819c75} | 2 +- ...82332b61325ae8e6f8ddc49159e7bafc30453bfc17 | 1 - ...6e7a1d961317ffb6352fc775b133b1af8656a6d0bb | Bin 0 -> 3273 bytes internal/cli/testdata/golden/index.json | 2 +- .../golden/sboms/sbom-aarch64.spdx.json | 24 +++--- .../golden/sboms/sbom-index.spdx.json | 42 +++++----- .../golden/sboms/sbom-x86_64.spdx.json | 24 +++--- 16 files changed, 83 insertions(+), 98 deletions(-) create mode 100644 internal/cli/testdata/golden/blobs/sha256/0c3a3431cb3d08f1de9b3ebd19671fe54c227a6cfd33c1164cb8f67a86baadea create mode 100644 internal/cli/testdata/golden/blobs/sha256/2daa2a0b7c784bbda06b9a342d2b70e72d4f213642f488250fdb62ccd65a4ba7 delete mode 100644 internal/cli/testdata/golden/blobs/sha256/43e3f561cb0910cb25ea304331454f74315d2edbcf68e2af172f8efa5c95fe19 rename internal/cli/testdata/golden/blobs/sha256/{9b913f37c3adc743715bd578f7cc823f22a2e45b73c939023888109a66a97951 => 5e0be8f8dbc1497d60148df3be08869342df17c951370416999e19cda0d36624} (78%) delete mode 100644 internal/cli/testdata/golden/blobs/sha256/82c91678ac0f40848c5cdac918022ba897a381c264722a6b1d5c986471e9751e delete mode 100644 internal/cli/testdata/golden/blobs/sha256/8854f6c83258e956f0811a1d8d4aff54d14817a2675575b4f557499655097f3d create mode 100644 internal/cli/testdata/golden/blobs/sha256/89c9e3bd4d4968247bdf82182560718f22427628a3c112902377fa7ff897f9f7 rename internal/cli/testdata/golden/blobs/sha256/{35af2fe3834e23d725a234d852303b84996b78b7d941b4c9b08a41e0a77d1b18 => cff7a34bf75690ef289b741fd026cb52cac94ff0f057996c829ac546f6819c75} (78%) delete mode 100644 internal/cli/testdata/golden/blobs/sha256/dcc96a7dc51e9f68b3402c82332b61325ae8e6f8ddc49159e7bafc30453bfc17 create mode 100644 internal/cli/testdata/golden/blobs/sha256/fab53f6bbeb155d8f33acb6e7a1d961317ffb6352fc775b133b1af8656a6d0bb diff --git a/internal/cli/build.go b/internal/cli/build.go index fcccfd284..d9da06519 100644 --- a/internal/cli/build.go +++ b/internal/cli/build.go @@ -22,6 +22,7 @@ import ( "log/slog" "os" "path/filepath" + "slices" "sync" "github.com/google/go-containerregistry/pkg/v1/layout" @@ -38,6 +39,7 @@ import ( "chainguard.dev/apko/pkg/build/oci" "chainguard.dev/apko/pkg/build/types" "chainguard.dev/apko/pkg/sbom" + "chainguard.dev/apko/pkg/tarfs" ) func buildCmd() *cobra.Command { @@ -236,6 +238,7 @@ func buildImageComponents(ctx context.Context, workDir string, archs []types.Arc // computation. multiArchBDE := o.SourceDateEpoch + configs, _, err := build.LockImageConfiguration(ctx, *ic, opts...) mc, err := build.NewMultiArch(ctx, archs, opts...) if err != nil { return nil, nil, err @@ -247,26 +250,24 @@ func buildImageComponents(ctx context.Context, workDir string, archs []types.Arc } } - // This is a little different, but we use a multiarch builder to call BuildLayers because we want - // each architecture to be aware of the other architectures during the solve stage. We don't want - // to select any packages unless they are available on every architecture because we want solutions - // to match across architectures. - // - // Eventually, we probably want to do something similar for all this logic around stitching images together. - layers, err := mc.BuildLayers(ctx) - if err != nil { - return nil, nil, fmt.Errorf("building layers: %w", err) - } - - for _, arch := range archs { - arch := arch - + for arch, ic := range configs { errg.Go(func() error { + if arch == "index" { + return nil + } + + arch := types.ParseArchitecture(arch) log := clog.New(slog.Default().Handler()).With("arch", arch.ToAPK()) ctx := clog.WithLogger(ctx, log) - bc := mc.Contexts[arch] - layer := layers[arch] + opts := slices.Clone(opts) + opts = append(opts, build.WithArch(arch), build.WithImageConfiguration(*ic)) + + bc, err := build.New(ctx, tarfs.New(), opts...) + _, layer, err := bc.BuildLayer(ctx) + if err != nil { + return fmt.Errorf("building %q layer: %w", arch, err) + } // Compute the "build date epoch" from the packages that were // installed. The "build date epoch" is the MAX of the builddate @@ -285,6 +286,14 @@ func buildImageComponents(ctx context.Context, workDir string, archs []types.Arc return fmt.Errorf("failed to build OCI image for %q: %w", arch, err) } + var outputs []types.SBOM + if len(o.SBOMFormats) != 0 { + outputs, err = bc.GenerateImageSBOM(ctx, arch, img) + if err != nil { + return fmt.Errorf("generating sbom for %s: %w", arch, err) + } + } + mtx.Lock() defer mtx.Unlock() @@ -294,6 +303,10 @@ func buildImageComponents(ctx context.Context, workDir string, archs []types.Arc multiArchBDE = bde } + if len(o.SBOMFormats) != 0 { + sboms = append(sboms, outputs...) + } + return nil }) } @@ -323,34 +336,6 @@ func buildImageComponents(ctx context.Context, workDir string, archs []types.Arc // the sboms are saved to the same working directory as the image components if len(o.SBOMFormats) != 0 { - var ( - g errgroup.Group - mtx sync.Mutex - ) - for arch, img := range imgs { - arch, img := arch, img - bc := mc.Contexts[arch] - - g.Go(func() error { - log := clog.New(slog.Default().Handler()).With("arch", arch.ToAPK()) - ctx := clog.WithLogger(ctx, log) - - outputs, err := bc.GenerateImageSBOM(ctx, arch, img) - if err != nil { - return fmt.Errorf("generating sbom for %s: %w", arch, err) - } - mtx.Lock() - defer mtx.Unlock() - - sboms = append(sboms, outputs...) - return nil - }) - } - - if err := g.Wait(); err != nil { - return nil, nil, err - } - files, err := build.GenerateIndexSBOM(ctx, *o, *ic, finalDigest, imgs) if err != nil { return nil, nil, fmt.Errorf("generating index SBOM: %w", err) diff --git a/internal/cli/publish_test.go b/internal/cli/publish_test.go index 157fb593b..8d73c6fc5 100644 --- a/internal/cli/publish_test.go +++ b/internal/cli/publish_test.go @@ -91,7 +91,7 @@ func TestPublish(t *testing.T) { // This test will fail if we ever make a change in apko that changes the image. // Sometimes, this is intentional, and we need to change this and bump the version. - want := "sha256:df77d3071fed05b55a2c76ce0af54b4e8174404ee29962162b627aaa42b43b91" + want := "sha256:df086c6b126032e9ed1b4aa6fecdb6988b45d86d9c1e8d2eb81dea647f4c7a7f" require.Equal(t, want, digest.String()) sdst := fmt.Sprintf("%s:%s.sbom", dst, strings.ReplaceAll(want, ":", "-")) @@ -109,7 +109,7 @@ func TestPublish(t *testing.T) { // This test will fail if we ever make a change in apko that changes the SBOM. // Sometimes, this is intentional, and we need to change this and bump the version. - swant := "sha256:18b8dc7cd228ab657a248b94c256ec4b3e12a69901161d027ee4b6bbee0e87b6" + swant := "sha256:d1faff2316aa480d1400e6c86af431402cee84f980b97a06f096bdab9a52075f" require.Equal(t, swant, got) im, err := idx.IndexManifest() @@ -118,8 +118,8 @@ func TestPublish(t *testing.T) { // We also want to check the children SBOMs because the index SBOM does not have // references to the children SBOMs, just the children! wantBoms := []string{ - "sha256:b30707314d196b3f65b1e9b05f34b795588f0031343bf3e3e75f256aff3fc7e6", - "sha256:1449715ad955e02f8a8f8f5b9adff17d89c2443e199af2555f8336df18720fe7", + "sha256:c483478580314a253c2170b32de7686d1664ec936be3a4df51ef2bba92c46261", + "sha256:18d9c631ac5656d52d2595bc80195ad1c68c517db8bded1ec229f775ee682d98", } for i, m := range im.Manifests { diff --git a/internal/cli/testdata/golden/blobs/sha256/0c3a3431cb3d08f1de9b3ebd19671fe54c227a6cfd33c1164cb8f67a86baadea b/internal/cli/testdata/golden/blobs/sha256/0c3a3431cb3d08f1de9b3ebd19671fe54c227a6cfd33c1164cb8f67a86baadea new file mode 100644 index 000000000..fb6d92e24 --- /dev/null +++ b/internal/cli/testdata/golden/blobs/sha256/0c3a3431cb3d08f1de9b3ebd19671fe54c227a6cfd33c1164cb8f67a86baadea @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":593,"digest":"sha256:5e0be8f8dbc1497d60148df3be08869342df17c951370416999e19cda0d36624"},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","size":3272,"digest":"sha256:89c9e3bd4d4968247bdf82182560718f22427628a3c112902377fa7ff897f9f7"}],"annotations":{"org.opencontainers.image.created":"1970-01-01T00:00:00Z"}} \ No newline at end of file diff --git a/internal/cli/testdata/golden/blobs/sha256/2daa2a0b7c784bbda06b9a342d2b70e72d4f213642f488250fdb62ccd65a4ba7 b/internal/cli/testdata/golden/blobs/sha256/2daa2a0b7c784bbda06b9a342d2b70e72d4f213642f488250fdb62ccd65a4ba7 new file mode 100644 index 000000000..13030a9ab --- /dev/null +++ b/internal/cli/testdata/golden/blobs/sha256/2daa2a0b7c784bbda06b9a342d2b70e72d4f213642f488250fdb62ccd65a4ba7 @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":593,"digest":"sha256:cff7a34bf75690ef289b741fd026cb52cac94ff0f057996c829ac546f6819c75"},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","size":3273,"digest":"sha256:fab53f6bbeb155d8f33acb6e7a1d961317ffb6352fc775b133b1af8656a6d0bb"}],"annotations":{"org.opencontainers.image.created":"1970-01-01T00:00:00Z"}} \ No newline at end of file diff --git a/internal/cli/testdata/golden/blobs/sha256/43e3f561cb0910cb25ea304331454f74315d2edbcf68e2af172f8efa5c95fe19 b/internal/cli/testdata/golden/blobs/sha256/43e3f561cb0910cb25ea304331454f74315d2edbcf68e2af172f8efa5c95fe19 deleted file mode 100644 index 172334364..000000000 --- a/internal/cli/testdata/golden/blobs/sha256/43e3f561cb0910cb25ea304331454f74315d2edbcf68e2af172f8efa5c95fe19 +++ /dev/null @@ -1 +0,0 @@ -{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":593,"digest":"sha256:9b913f37c3adc743715bd578f7cc823f22a2e45b73c939023888109a66a97951"},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","size":3256,"digest":"sha256:8854f6c83258e956f0811a1d8d4aff54d14817a2675575b4f557499655097f3d"}],"annotations":{"org.opencontainers.image.created":"1970-01-01T00:00:00Z"}} \ No newline at end of file diff --git a/internal/cli/testdata/golden/blobs/sha256/9b913f37c3adc743715bd578f7cc823f22a2e45b73c939023888109a66a97951 b/internal/cli/testdata/golden/blobs/sha256/5e0be8f8dbc1497d60148df3be08869342df17c951370416999e19cda0d36624 similarity index 78% rename from internal/cli/testdata/golden/blobs/sha256/9b913f37c3adc743715bd578f7cc823f22a2e45b73c939023888109a66a97951 rename to internal/cli/testdata/golden/blobs/sha256/5e0be8f8dbc1497d60148df3be08869342df17c951370416999e19cda0d36624 index 3362b2a4c..3f087552e 100644 --- a/internal/cli/testdata/golden/blobs/sha256/9b913f37c3adc743715bd578f7cc823f22a2e45b73c939023888109a66a97951 +++ b/internal/cli/testdata/golden/blobs/sha256/5e0be8f8dbc1497d60148df3be08869342df17c951370416999e19cda0d36624 @@ -1 +1 @@ -{"architecture":"amd64","author":"github.com/chainguard-dev/apko","created":"1970-01-01T00:00:00Z","history":[{"author":"apko","created":"1970-01-01T00:00:00Z","created_by":"apko","comment":"This is an apko single-layer image"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:b606e55d50a4a23dd4092a9983b45887dd7c979a38acd5dc9736a09cc899e600"]},"config":{"Entrypoint":["/bin/sh","-l"],"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"],"Labels":{"org.opencontainers.image.created":"1970-01-01T00:00:00Z"}}} \ No newline at end of file +{"architecture":"amd64","author":"github.com/chainguard-dev/apko","created":"1970-01-01T00:00:00Z","history":[{"author":"apko","created":"1970-01-01T00:00:00Z","created_by":"apko","comment":"This is an apko single-layer image"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:8f9c734d8ec1cd189067042886090e052d1e32c6622124061dd7baf8be1c1e56"]},"config":{"Entrypoint":["/bin/sh","-l"],"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"],"Labels":{"org.opencontainers.image.created":"1970-01-01T00:00:00Z"}}} \ No newline at end of file diff --git a/internal/cli/testdata/golden/blobs/sha256/82c91678ac0f40848c5cdac918022ba897a381c264722a6b1d5c986471e9751e b/internal/cli/testdata/golden/blobs/sha256/82c91678ac0f40848c5cdac918022ba897a381c264722a6b1d5c986471e9751e deleted file mode 100644 index 8a8fdf0fac65cad6f1c9bfec0447c7b363de6e0c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3267 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 Bvp@g< diff --git a/internal/cli/testdata/golden/blobs/sha256/8854f6c83258e956f0811a1d8d4aff54d14817a2675575b4f557499655097f3d b/internal/cli/testdata/golden/blobs/sha256/8854f6c83258e956f0811a1d8d4aff54d14817a2675575b4f557499655097f3d deleted file mode 100644 index 0eb84d3bb4089715ac32c4c90a7a302c23b63c69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3256 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