Merge pull request #3811 from crazy-max/fix-oci-layout
build: fix oci-layout named context serialization
This commit is contained in:
+10
-6
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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://")
|
||||
}
|
||||
|
||||
+11
-3
@@ -34,7 +34,7 @@ func Parse(s string) (Ref, bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if i := strings.LastIndex(localPath, ":"); i >= 0 {
|
||||
if i := strings.LastIndex(localPath, ":"); i >= 0 && !isWindowsDrivePath(localPath, i) {
|
||||
after := localPath[i+1:]
|
||||
if reference.TagRegexp.MatchString(after) {
|
||||
localPath, out.Tag = localPath[:i], after
|
||||
@@ -51,10 +51,18 @@ func Parse(s string) (Ref, bool, error) {
|
||||
func (r Ref) String() string {
|
||||
s := prefix + r.Path
|
||||
if r.Tag != "" {
|
||||
return s + ":" + r.Tag
|
||||
s += ":" + r.Tag
|
||||
}
|
||||
if r.Digest != "" {
|
||||
return s + "@" + r.Digest.String()
|
||||
s += "@" + r.Digest.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func isWindowsDrivePath(path string, colon int) bool {
|
||||
if colon != 1 || len(path) < 2 {
|
||||
return false
|
||||
}
|
||||
c := path[0]
|
||||
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package ocilayout
|
||||
import (
|
||||
"testing"
|
||||
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -39,6 +40,27 @@ func TestParse(t *testing.T) {
|
||||
path: "/path/to/oci/@/layout",
|
||||
tag: "latest",
|
||||
},
|
||||
{
|
||||
s: `oci-layout://C:\path\to\oci\layout`,
|
||||
path: `C:\path\to\oci\layout`,
|
||||
tag: "latest",
|
||||
},
|
||||
{
|
||||
s: `oci-layout://C:\path\to\oci\layout:1.3`,
|
||||
path: `C:\path\to\oci\layout`,
|
||||
tag: "1.3",
|
||||
},
|
||||
{
|
||||
s: `oci-layout://C:\path\to\oci\layout@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`,
|
||||
path: `C:\path\to\oci\layout`,
|
||||
dgst: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
{
|
||||
s: `oci-layout://C:\path\to\oci\layout:1.3@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`,
|
||||
path: `C:\path\to\oci\layout`,
|
||||
tag: "1.3",
|
||||
dgst: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
} {
|
||||
ref, ok, err := Parse(tt.s)
|
||||
require.True(t, ok)
|
||||
@@ -48,3 +70,13 @@ func TestParse(t *testing.T) {
|
||||
assert.Equal(t, tt.tag, ref.Tag, "comparing tag: %s", tt.s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefString(t *testing.T) {
|
||||
ref := Ref{
|
||||
Path: "/path/to/oci/layout",
|
||||
Tag: "1.3",
|
||||
Digest: digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
|
||||
}
|
||||
|
||||
assert.Equal(t, "oci-layout:///path/to/oci/layout:1.3@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ref.String())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user