build: fix oci-layout named context serialization

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2026-04-23 12:26:02 +02:00
parent 2d58ee6f98
commit dcf533e756
4 changed files with 214 additions and 12 deletions
+10 -6
View File
@@ -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
}
+94
View File
@@ -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{}
+110
View File
@@ -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
}
-6
View File
@@ -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://")
}