From dcf533e75687526b729704e12dc7c6233534eb47 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:28:01 +0200 Subject: [PATCH] build: fix oci-layout named context serialization Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- build/opt.go | 16 ++++-- build/opt_test.go | 94 ++++++++++++++++++++++++++++++ tests/build.go | 110 ++++++++++++++++++++++++++++++++++++ util/imagetools/location.go | 6 -- 4 files changed, 214 insertions(+), 12 deletions(-) diff --git a/build/opt.go b/build/opt.go index ed9ca5a02..be50b8187 100644 --- a/build/opt.go +++ b/build/opt.go @@ -917,29 +917,33 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro } // handle OCI layout - if localPath, ok := strings.CutPrefix(v.Path, "oci-layout://"); ok { - ref, _, err := ocilayout.Parse("oci-layout://" + localPath) + if ref, ok, err := ocilayout.Parse(v.Path); ok { if err != nil { return nil, err } - localPath, dig, tag := ref.Path, ref.Digest.String(), ref.Tag - if dig == "" { - dig, err = resolveDigest(localPath, tag) + localPath := ref.Path + + if ref.Digest == "" { + dig, err := resolveDigest(localPath, ref.Tag) if err != nil { return nil, errors.Wrapf(err, "oci-layout reference %q could not be resolved", v.Path) } + ref.Digest = digest.Digest(dig) } + store, err := local.NewStore(localPath) if err != nil { return nil, errors.Wrapf(err, "invalid store at %s", localPath) } + storeName := identity.NewID() if target.OCIStores == nil { target.OCIStores = map[string]content.Store{} } target.OCIStores[storeName] = store - target.FrontendAttrs["context:"+k] = "oci-layout://" + storeName + ":" + tag + "@" + dig + ref.Path = storeName + target.FrontendAttrs["context:"+k] = ref.String() continue } diff --git a/build/opt_test.go b/build/opt_test.go index 0e4499a98..6f522c543 100644 --- a/build/opt_test.go +++ b/build/opt_test.go @@ -1,10 +1,16 @@ package build import ( + "context" "testing" "github.com/docker/buildx/util/buildflags" + "github.com/docker/buildx/util/ocilayout" + "github.com/docker/buildx/util/progress" "github.com/moby/buildkit/client" + "github.com/moby/buildkit/client/ociindex" + "github.com/opencontainers/go-digest" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -154,3 +160,91 @@ func TestProxyArgKeyExists(t *testing.T) { }) } } + +func TestLoadInputsOCILayoutNamedContext(t *testing.T) { + layoutPath := t.TempDir() + + idx := ociindex.NewStoreIndex(layoutPath) + manifestDigest := digest.FromString("manifest") + err := idx.Put(ocispecs.Descriptor{ + MediaType: ocispecs.MediaTypeImageManifest, + Digest: manifestDigest, + Size: 1, + }, ociindex.Tag("latest")) + require.NoError(t, err) + + tests := []struct { + name string + ref string + wantRef ocilayout.Ref + }{ + { + name: "digest only", + ref: "oci-layout://" + layoutPath + "@" + manifestDigest.String(), + wantRef: ocilayout.Ref{ + Digest: manifestDigest, + }, + }, + { + name: "tag only", + ref: "oci-layout://" + layoutPath + ":latest", + wantRef: ocilayout.Ref{ + Tag: "latest", + Digest: manifestDigest, + }, + }, + { + name: "tag and digest", + ref: "oci-layout://" + layoutPath + ":latest@" + manifestDigest.String(), + wantRef: ocilayout.Ref{ + Tag: "latest", + Digest: manifestDigest, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + target := &client.SolveOpt{ + FrontendAttrs: map[string]string{}, + } + inp := &Inputs{ + ContextPath: "https://example.com/context.tar.gz", + NamedContexts: map[string]NamedContext{ + "proxy": { + Path: tt.ref, + }, + }, + } + + release, err := loadInputs(context.Background(), nil, inp, testProgressWriter{}, target) + require.NoError(t, err) + require.NotNil(t, release) + t.Cleanup(release) + + attr, ok := target.FrontendAttrs["context:proxy"] + require.True(t, ok) + require.Len(t, target.OCIStores, 1) + + parsed, ok, err := ocilayout.Parse(attr) + require.True(t, ok) + require.NoError(t, err) + require.NotEmpty(t, parsed.Path) + assert.Equal(t, tt.wantRef.Tag, parsed.Tag) + assert.Equal(t, tt.wantRef.Digest, parsed.Digest) + target.OCIStores = nil + }) + } +} + +type testProgressWriter struct{} + +func (testProgressWriter) Write(*client.SolveStatus) {} + +func (testProgressWriter) WriteBuildRef(string, string) {} + +func (testProgressWriter) ValidateLogSource(digest.Digest, any) bool { return true } + +func (testProgressWriter) ClearLogSource(any) {} + +var _ progress.Writer = testProgressWriter{} diff --git a/tests/build.go b/tests/build.go index 6c2d22972..95c4c8579 100644 --- a/tests/build.go +++ b/tests/build.go @@ -1,7 +1,9 @@ package tests import ( + "archive/tar" "bytes" + "context" "encoding/json" "fmt" "io" @@ -13,6 +15,8 @@ import ( "strings" "testing" + "github.com/containerd/containerd/v2/core/content" + "github.com/containerd/containerd/v2/plugins/content/local" "github.com/containerd/continuity/fs/fstest" "github.com/containerd/platforms" "github.com/creack/pty" @@ -21,6 +25,7 @@ import ( "github.com/docker/buildx/util/gitutil" "github.com/docker/buildx/util/gitutil/gittestutil" "github.com/moby/buildkit/client" + "github.com/moby/buildkit/client/ociindex" "github.com/moby/buildkit/frontend/subrequests/lint" "github.com/moby/buildkit/frontend/subrequests/outline" "github.com/moby/buildkit/frontend/subrequests/targets" @@ -32,6 +37,8 @@ import ( "github.com/moby/buildkit/util/testutil" "github.com/moby/buildkit/util/testutil/integration" "github.com/opencontainers/go-digest" + "github.com/opencontainers/image-spec/specs-go" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -50,6 +57,7 @@ var buildTests = []func(t *testing.T, sb integration.Sandbox){ testBuildStdin, testBuildRemote, testBuildRemoteAuth, + testBuildNamedContextOCILayoutDigestOnly, testBuildLocalState, testBuildLocalStateStdin, testBuildLocalStateRemote, @@ -305,6 +313,35 @@ COPY foo /foo require.FileExists(t, filepath.Join(dirDest, "foo")) } +func testBuildNamedContextOCILayoutDigestOnly(t *testing.T, sb integration.Sandbox) { + if isMobyWorker(sb) { + t.Skip("oci-layout named contexts are not supported by the docker worker") + } + + dir := tmpdir(t, fstest.CreateFile("Dockerfile", []byte(` +FROM scratch +COPY --from=proxy /foo /foo +`), 0o600)) + layoutPath := filepath.Join(dir, "layout") + expected := "from-oci-layout" + manifestDigest := createOCILayoutImage(t, layoutPath, "foo", []byte(expected), "latest") + dirDest := t.TempDir() + + out, err := buildCmd(sb, + withDir(dir), + withArgs( + "--build-context", "proxy=oci-layout://layout@"+manifestDigest.String(), + "--output=type=local,dest="+dirDest, + dir, + ), + ) + require.NoError(t, err, out) + + dt, err := os.ReadFile(filepath.Join(dirDest, "foo")) + require.NoError(t, err) + require.Equal(t, expected, string(dt)) +} + func testBuildLocalState(t *testing.T, sb integration.Sandbox) { dockerfile := []byte(` FROM busybox:latest AS base @@ -1665,3 +1702,76 @@ COPY --from=base /etc/bar /bar ) return dir } + +func createOCILayoutImage(t *testing.T, layoutPath, fileName string, fileContents []byte, tag string) digest.Digest { + t.Helper() + + store, err := local.NewStore(layoutPath) + require.NoError(t, err) + + layerBytes := bytes.NewBuffer(nil) + tw := tar.NewWriter(layerBytes) + err = tw.WriteHeader(&tar.Header{ + Name: fileName, + Mode: 0o644, + Size: int64(len(fileContents)), + }) + require.NoError(t, err) + _, err = tw.Write(fileContents) + require.NoError(t, err) + require.NoError(t, tw.Close()) + + ctx := context.Background() + layerDesc := ocispecs.Descriptor{ + MediaType: ocispecs.MediaTypeImageLayer, + Digest: digest.FromBytes(layerBytes.Bytes()), + Size: int64(layerBytes.Len()), + } + err = content.WriteBlob(ctx, store, "layer-"+layerDesc.Digest.String(), bytes.NewReader(layerBytes.Bytes()), layerDesc) + require.NoError(t, err) + + cfgBytes, err := json.Marshal(ocispecs.Image{ + Platform: ocispecs.Platform{ + Architecture: "amd64", + OS: "linux", + }, + Config: ocispecs.ImageConfig{ + WorkingDir: "/", + }, + RootFS: ocispecs.RootFS{ + Type: "layers", + DiffIDs: []digest.Digest{layerDesc.Digest}, + }, + }) + require.NoError(t, err) + cfgDesc := ocispecs.Descriptor{ + MediaType: ocispecs.MediaTypeImageConfig, + Digest: digest.FromBytes(cfgBytes), + Size: int64(len(cfgBytes)), + } + err = content.WriteBlob(ctx, store, "config-"+cfgDesc.Digest.String(), bytes.NewReader(cfgBytes), cfgDesc) + require.NoError(t, err) + + manifestBytes, err := json.Marshal(ocispecs.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + MediaType: ocispecs.MediaTypeImageManifest, + Config: cfgDesc, + Layers: []ocispecs.Descriptor{layerDesc}, + }) + require.NoError(t, err) + manifestDesc := ocispecs.Descriptor{ + MediaType: ocispecs.MediaTypeImageManifest, + Digest: digest.FromBytes(manifestBytes), + Size: int64(len(manifestBytes)), + } + err = content.WriteBlob(ctx, store, "manifest-"+manifestDesc.Digest.String(), bytes.NewReader(manifestBytes), manifestDesc) + require.NoError(t, err) + + idx := ociindex.NewStoreIndex(layoutPath) + err = idx.Put(manifestDesc, ociindex.Tag(tag)) + require.NoError(t, err) + + return manifestDesc.Digest +} diff --git a/util/imagetools/location.go b/util/imagetools/location.go index 737f3044d..215cd64d5 100644 --- a/util/imagetools/location.go +++ b/util/imagetools/location.go @@ -1,8 +1,6 @@ package imagetools import ( - "strings" - "github.com/distribution/reference" "github.com/docker/buildx/util/ocilayout" digest "github.com/opencontainers/go-digest" @@ -181,7 +179,3 @@ func (l *Location) ValidateTargetDigest(desc digest.Digest) error { } return nil } - -func IsOCILayout(s string) bool { - return strings.HasPrefix(s, "oci-layout://") -}