Merge pull request #3721 from tonistiigi/imagetools-oci-layout
imagetools: add oci-layout support
This commit is contained in:
+2
-3
@@ -805,11 +805,10 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[
|
||||
|
||||
itpull := imagetools.New(imageopt)
|
||||
|
||||
ref, err := reference.ParseNormalizedNamed(names[0])
|
||||
ref, err := imagetools.ParseLocation(names[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ref = reference.TagNameOnly(ref)
|
||||
|
||||
srcs := make([]*imagetools.Source, len(descs))
|
||||
for i, desc := range descs {
|
||||
@@ -832,7 +831,7 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[
|
||||
itpush := imagetools.New(imageopt)
|
||||
|
||||
for _, n := range names {
|
||||
nn, err := reference.ParseNormalizedNamed(n)
|
||||
nn, err := imagetools.ParseLocation(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+6
-33
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/docker/buildx/util/buildflags"
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/docker/buildx/util/dockerutil"
|
||||
"github.com/docker/buildx/util/ocilayout"
|
||||
"github.com/docker/buildx/util/osutil"
|
||||
"github.com/docker/buildx/util/progress"
|
||||
"github.com/docker/buildx/util/sourcemeta"
|
||||
@@ -913,7 +914,11 @@ 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 {
|
||||
localPath, dig, tag := parseOCILayoutPath(localPath)
|
||||
ref, _, err := ocilayout.Parse("oci-layout://" + localPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
localPath, dig, tag := ref.Path, ref.Digest.String(), ref.Tag
|
||||
if dig == "" {
|
||||
dig, err = resolveDigest(localPath, tag)
|
||||
if err != nil {
|
||||
@@ -1402,38 +1407,6 @@ func isActive(ce *client.CacheOptionsEntry) bool {
|
||||
return ce.Attrs["token"] != "" && (ce.Attrs["url"] != "" || ce.Attrs["url_v2"] != "")
|
||||
}
|
||||
|
||||
// parseOCILayoutPath handles the oci-layout url accepted by buildx.
|
||||
func parseOCILayoutPath(s string) (localPath, dgst, tag string) {
|
||||
localPath = s
|
||||
|
||||
// Look for the digest reference. There might be multiple @ symbols
|
||||
// in the path and the @ symbol may be part of the path or part of
|
||||
// the digest. If we find the @ symbol, verify that it's a valid
|
||||
// digest reference instead of just assuming it is because it
|
||||
// might be part of the file path.
|
||||
if i := strings.LastIndex(localPath, "@"); i >= 0 {
|
||||
after := localPath[i+1:]
|
||||
if reference.DigestRegexp.MatchString(after) {
|
||||
localPath, dgst = localPath[:i], after
|
||||
}
|
||||
}
|
||||
|
||||
// Do the same with the tag. This isn't as necessary since colons
|
||||
// aren't valid as file paths on Linux/Unix systems, but they are valid
|
||||
// on Windows systems so we might as well just be safe.
|
||||
if i := strings.LastIndex(localPath, ":"); i >= 0 {
|
||||
after := localPath[i+1:]
|
||||
if reference.TagRegexp.MatchString(after) {
|
||||
localPath, tag = localPath[:i], after
|
||||
}
|
||||
}
|
||||
|
||||
if tag == "" {
|
||||
tag = "latest"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func defaultPlatform(bopts gateway.BuildOpts) *ocispecs.Platform {
|
||||
pl := bopts.Workers[0].Platforms
|
||||
if len(pl) == 0 {
|
||||
|
||||
@@ -40,48 +40,6 @@ func TestCacheOptions_DerivedVars(t *testing.T) {
|
||||
}, CreateCaches(cacheFrom))
|
||||
}
|
||||
|
||||
func TestParseOCILayoutPath(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
s string
|
||||
path string
|
||||
dgst string
|
||||
tag string
|
||||
}{
|
||||
{
|
||||
s: "/path/to/oci/layout",
|
||||
path: "/path/to/oci/layout",
|
||||
tag: "latest",
|
||||
},
|
||||
{
|
||||
s: "/path/to/oci/layout:1.3",
|
||||
path: "/path/to/oci/layout",
|
||||
tag: "1.3",
|
||||
},
|
||||
{
|
||||
s: "/path/to/oci/layout@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
path: "/path/to/oci/layout",
|
||||
dgst: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
tag: "latest",
|
||||
},
|
||||
{
|
||||
s: "/path/to/oci/@/layout@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
path: "/path/to/oci/@/layout",
|
||||
dgst: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
tag: "latest",
|
||||
},
|
||||
{
|
||||
s: "/path/to/oci/@/layout",
|
||||
path: "/path/to/oci/@/layout",
|
||||
tag: "latest",
|
||||
},
|
||||
} {
|
||||
path, dgst, tag := parseOCILayoutPath(tt.s)
|
||||
assert.Equal(t, tt.path, path, "comparing path: %s", tt.s)
|
||||
assert.Equal(t, tt.dgst, dgst, "comparing digest: %s", tt.s)
|
||||
assert.Equal(t, tt.tag, tag, "comparing tag: %s", tt.s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateExports_RegistryUnpack(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/containerd/containerd/v2/core/remotes"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/util/buildflags"
|
||||
"github.com/docker/buildx/util/cobrautil/completion"
|
||||
@@ -60,7 +59,7 @@ func runCreate(ctx context.Context, dockerCli command.Cli, in createOptions, arg
|
||||
|
||||
args = append(fileArgs, args...)
|
||||
|
||||
tags, err := parseRefs(in.tags)
|
||||
tags, err := parseLocations(in.tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -102,10 +101,16 @@ func runCreate(ctx context.Context, dockerCli command.Cli, in createOptions, arg
|
||||
return errors.Errorf("no repositories specified, please set a reference in tag or source")
|
||||
}
|
||||
|
||||
var defaultRepo *string
|
||||
var defaultRepo *imagetools.Location
|
||||
if len(repos) == 1 {
|
||||
for repo := range repos {
|
||||
defaultRepo = &repo
|
||||
for _, src := range srcs {
|
||||
if src.Ref != nil {
|
||||
defaultRepo = src.Ref
|
||||
break
|
||||
}
|
||||
}
|
||||
if defaultRepo == nil && len(tags) > 0 {
|
||||
defaultRepo = tags[0]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,19 +119,19 @@ func runCreate(ctx context.Context, dockerCli command.Cli, in createOptions, arg
|
||||
if defaultRepo == nil {
|
||||
return errors.Errorf("multiple repositories specified, cannot infer repository for %q", args[i])
|
||||
}
|
||||
n, err := reference.ParseNormalizedNamed(*defaultRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.Desc.MediaType == "" && s.Desc.Digest != "" {
|
||||
r, err := reference.WithDigest(n, s.Desc.Digest)
|
||||
r, err := defaultRepo.WithDigest(s.Desc.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcs[i].Ref = r
|
||||
sourceRefs = true
|
||||
} else {
|
||||
srcs[i].Ref = reference.TagNameOnly(n)
|
||||
r, err := defaultRepo.TagNameOnly()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcs[i].Ref = r
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,7 +214,7 @@ func runCreate(ctx context.Context, dockerCli command.Cli, in createOptions, arg
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
pw := progress.WithPrefix(printer, "internal", true)
|
||||
|
||||
tagsByRepo := map[string][]reference.Named{}
|
||||
tagsByRepo := map[string][]*imagetools.Location{}
|
||||
for _, t := range tags {
|
||||
repo := t.Name()
|
||||
tagsByRepo[repo] = append(tagsByRepo[repo], t)
|
||||
@@ -224,10 +229,14 @@ func runCreate(ctx context.Context, dockerCli command.Cli, in createOptions, arg
|
||||
for _, desc := range manifests {
|
||||
eg2.Go(func() error {
|
||||
sub.Log(1, fmt.Appendf(nil, "copying %s from %s to %s\n", desc.Digest.String(), desc.Source.Ref.String(), repo))
|
||||
return r.Copy(ctx, &imagetools.Source{
|
||||
err := r.Copy(ctx, &imagetools.Source{
|
||||
Ref: desc.Source.Ref,
|
||||
Desc: desc.Descriptor,
|
||||
}, seed)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "copy %s from %s to %s", desc.Digest.String(), desc.Source.Ref.String(), seed.String())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := eg2.Wait(); err != nil {
|
||||
@@ -236,7 +245,7 @@ func runCreate(ctx context.Context, dockerCli command.Cli, in createOptions, arg
|
||||
for _, t := range repoTags {
|
||||
sub.Log(1, fmt.Appendf(nil, "pushing %s to %s\n", desc.Digest.String(), t.String()))
|
||||
if err := r.Push(ctx, t, desc, dt); err != nil {
|
||||
return err
|
||||
return errors.Wrapf(err, "publish %s to %s", desc.Digest.String(), t.String())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -302,10 +311,10 @@ func withMediaTypeKeyPrefix(ctx context.Context) context.Context {
|
||||
return ctx
|
||||
}
|
||||
|
||||
func parseRefs(in []string) ([]reference.Named, error) {
|
||||
refs := make([]reference.Named, len(in))
|
||||
func parseLocations(in []string) ([]*imagetools.Location, error) {
|
||||
refs := make([]*imagetools.Location, len(in))
|
||||
for i, in := range in {
|
||||
n, err := reference.ParseNormalizedNamed(in)
|
||||
n, err := imagetools.ParseLocation(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -327,10 +336,10 @@ func parseSource(in string) (*imagetools.Source, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ref, err := reference.ParseNormalizedNamed(in)
|
||||
loc, err := imagetools.ParseLocation(in)
|
||||
if err == nil {
|
||||
return &imagetools.Source{
|
||||
Ref: ref,
|
||||
Ref: loc,
|
||||
}, nil
|
||||
} else if !strings.HasPrefix(in, "{") {
|
||||
return nil, err
|
||||
|
||||
+421
-10
@@ -8,6 +8,7 @@ import (
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/content"
|
||||
@@ -31,6 +32,12 @@ var imagetoolsTests = []func(t *testing.T, sb integration.Sandbox){
|
||||
testImagetoolsCopyIndex,
|
||||
testImagetoolsInspectAndFilter,
|
||||
testImagetoolsCreatePlatformFilter,
|
||||
testImagetoolsOCILayoutInspect,
|
||||
testImagetoolsOCILayoutCreateSourceAndTarget,
|
||||
testImagetoolsOCILayoutReferrers,
|
||||
testImagetoolsOCILayoutExistingContent,
|
||||
testImagetoolsOCILayoutMergeSources,
|
||||
testImagetoolsOCILayoutTargetDigest,
|
||||
testImagetoolsAppend,
|
||||
testImagetoolsFile,
|
||||
testImagetoolsAnnotation,
|
||||
@@ -359,6 +366,378 @@ func testImagetoolsCreatePlatformFilter(t *testing.T, sb integration.Sandbox) {
|
||||
require.Equal(t, 1, attestationCount)
|
||||
}
|
||||
|
||||
// testImagetoolsOCILayoutInspect verifies inspect works against local OCI layout sources.
|
||||
func testImagetoolsOCILayoutInspect(t *testing.T, sb integration.Sandbox) {
|
||||
if !isDockerContainerWorker(sb) {
|
||||
t.Skip("only testing with docker-container worker, imagetools only runs on docker-container")
|
||||
}
|
||||
|
||||
dir := createDockerfileWithArches(t, "amd64", "arm64")
|
||||
registry, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
source := registry + "/buildx/imtools-oci-layout-inspect-src:latest"
|
||||
out, err := buildCmd(sb, withArgs("-t", source, "--push", "--platform=linux/amd64,linux/arm64", "--provenance=false", dir))
|
||||
require.NoError(t, err, string(out))
|
||||
|
||||
cmd := buildxCmd(sb, withArgs("imagetools", "inspect", source, "--raw"))
|
||||
dt, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
sourceDigest := digest.FromBytes(dt)
|
||||
|
||||
layoutPath := filepath.Join(dir, "layout-inspect")
|
||||
layoutRef := "oci-layout://" + layoutPath + ":latest"
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", layoutRef, source))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "inspect", layoutRef, "--raw"))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
require.Equal(t, sourceDigest, digest.FromBytes(dt))
|
||||
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "inspect", "oci-layout://"+layoutPath+"@"+sourceDigest.String(), "--raw"))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
require.Equal(t, sourceDigest, digest.FromBytes(dt))
|
||||
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "inspect", layoutRef, "--format", "{{.Manifest.Digest}}"))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
require.Equal(t, sourceDigest.String(), strings.TrimSpace(string(dt)))
|
||||
}
|
||||
|
||||
// testImagetoolsOCILayoutCreateSourceAndTarget verifies create can round-trip
|
||||
// between registry and OCI layout sources and targets.
|
||||
func testImagetoolsOCILayoutCreateSourceAndTarget(t *testing.T, sb integration.Sandbox) {
|
||||
if !isDockerContainerWorker(sb) {
|
||||
t.Skip("only testing with docker-container worker, imagetools only runs on docker-container")
|
||||
}
|
||||
|
||||
dir := createDockerfileWithArches(t, "amd64", "arm64")
|
||||
registry1, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
registry2, err := sb.NewRegistry()
|
||||
require.NoError(t, err)
|
||||
|
||||
source := registry1 + "/buildx/imtools-oci-layout-roundtrip-src:latest"
|
||||
out, err := buildCmd(sb, withArgs("-t", source, "--push", "--platform=linux/amd64,linux/arm64", "--provenance=false", dir))
|
||||
require.NoError(t, err, string(out))
|
||||
|
||||
cmd := buildxCmd(sb, withArgs("imagetools", "inspect", source, "--raw"))
|
||||
dt, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
sourceDigest := digest.FromBytes(dt)
|
||||
|
||||
layoutPath := filepath.Join(dir, "layout-roundtrip")
|
||||
layoutRef := "oci-layout://" + layoutPath + ":v1"
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", layoutRef, source))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
target := registry2 + "/buildx/imtools-oci-layout-roundtrip-dst:latest"
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", target, layoutRef))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target, "--raw"))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
require.Equal(t, sourceDigest, digest.FromBytes(dt))
|
||||
}
|
||||
|
||||
// testImagetoolsOCILayoutReferrers verifies standalone referrers are recorded
|
||||
// directly in OCI layout index.json with a subject annotation, while reachable
|
||||
// attestation manifests are not duplicated there.
|
||||
func testImagetoolsOCILayoutReferrers(t *testing.T, sb integration.Sandbox) {
|
||||
if !isDockerContainerWorker(sb) {
|
||||
t.Skip("only testing with docker-container worker, imagetools only runs on docker-container")
|
||||
}
|
||||
|
||||
dir := createDockerfileWithArches(t, "amd64", "arm64")
|
||||
registry1, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
registry2, err := sb.NewRegistry()
|
||||
require.NoError(t, err)
|
||||
|
||||
source := registry1 + "/buildx/imtools-oci-layout-referrers-src:latest"
|
||||
out, err := buildCmd(sb, withArgs(
|
||||
"--output", "type=image,name="+source+",push=true,oci-mediatypes=true,oci-artifact=true",
|
||||
"--platform=linux/amd64,linux/arm64",
|
||||
"--provenance=mode=min",
|
||||
dir,
|
||||
))
|
||||
require.NoError(t, err, string(out))
|
||||
|
||||
cmd := buildxCmd(sb, withArgs("imagetools", "inspect", source, "--raw"))
|
||||
dt, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
var srcIdx ocispecs.Index
|
||||
err = json.Unmarshal(dt, &srcIdx)
|
||||
require.NoError(t, err)
|
||||
|
||||
var attestations []ocispecs.Descriptor
|
||||
for _, mfst := range srcIdx.Manifests {
|
||||
if mfst.Annotations["vnd.docker.reference.type"] == "attestation-manifest" {
|
||||
attestations = append(attestations, mfst)
|
||||
}
|
||||
}
|
||||
require.Len(t, attestations, 2)
|
||||
|
||||
signatures := make([]ocispecs.Descriptor, 0, len(attestations))
|
||||
for _, attestation := range attestations {
|
||||
signatures = append(signatures, pushFakeSignatureReferrer(t, source, attestation))
|
||||
}
|
||||
|
||||
layoutPath := filepath.Join(dir, "layout-referrers")
|
||||
layoutRef := "oci-layout://" + layoutPath + ":latest"
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", layoutRef, source))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
idxBytes, err := os.ReadFile(filepath.Join(layoutPath, "index.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var layoutIdx ocispecs.Index
|
||||
err = json.Unmarshal(idxBytes, &layoutIdx)
|
||||
require.NoError(t, err)
|
||||
|
||||
directReferrers := map[digest.Digest]ocispecs.Descriptor{}
|
||||
directReferrerCount := 0
|
||||
for _, desc := range layoutIdx.Manifests {
|
||||
if desc.Annotations["io.containerd.manifest.subject"] != "" {
|
||||
directReferrerCount++
|
||||
directReferrers[desc.Digest] = desc
|
||||
}
|
||||
}
|
||||
require.Len(t, directReferrers, directReferrerCount)
|
||||
require.Len(t, directReferrers, len(signatures))
|
||||
for i, sig := range signatures {
|
||||
desc, ok := directReferrers[sig.Digest]
|
||||
require.True(t, ok)
|
||||
require.Equal(t, attestations[i].Digest.String(), desc.Annotations["io.containerd.manifest.subject"])
|
||||
}
|
||||
for _, attestation := range attestations {
|
||||
_, ok := directReferrers[attestation.Digest]
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
target := registry2 + "/buildx/imtools-oci-layout-referrers-dst:latest"
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", target, layoutRef))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target, "--raw"))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
var dstIdx ocispecs.Index
|
||||
err = json.Unmarshal(dt, &dstIdx)
|
||||
require.NoError(t, err)
|
||||
|
||||
copiedAttestations := map[digest.Digest]struct{}{}
|
||||
for _, mfst := range dstIdx.Manifests {
|
||||
if mfst.Annotations["vnd.docker.reference.type"] == "attestation-manifest" {
|
||||
copiedAttestations[mfst.Digest] = struct{}{}
|
||||
}
|
||||
}
|
||||
require.Len(t, copiedAttestations, len(attestations))
|
||||
|
||||
for _, sig := range signatures {
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target+"@"+sig.Digest.String(), "--raw"))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
var sigManifest ocispecs.Manifest
|
||||
err = json.Unmarshal(dt, &sigManifest)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, sigManifest.Subject)
|
||||
_, ok := copiedAttestations[sigManifest.Subject.Digest]
|
||||
require.True(t, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// testImagetoolsOCILayoutExistingContent verifies importing into an existing OCI
|
||||
// layout still updates index.json state when the top-level descriptor blob is
|
||||
// already present.
|
||||
func testImagetoolsOCILayoutExistingContent(t *testing.T, sb integration.Sandbox) {
|
||||
if !isDockerContainerWorker(sb) {
|
||||
t.Skip("only testing with docker-container worker, imagetools only runs on docker-container")
|
||||
}
|
||||
|
||||
dir := createDockerfileWithArches(t, "amd64", "arm64")
|
||||
registry, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
source := registry + "/buildx/imtools-oci-layout-existing-content-src:latest"
|
||||
out, err := buildCmd(sb, withArgs(
|
||||
"--output", "type=image,name="+source+",push=true,oci-mediatypes=true,oci-artifact=true",
|
||||
"--platform=linux/amd64,linux/arm64",
|
||||
"--provenance=mode=min",
|
||||
dir,
|
||||
))
|
||||
require.NoError(t, err, string(out))
|
||||
|
||||
layoutPath := filepath.Join(dir, "layout-existing-content")
|
||||
initialRef := "oci-layout://" + layoutPath + ":latest"
|
||||
cmd := buildxCmd(sb, withArgs("imagetools", "create", "-t", initialRef, source))
|
||||
dt, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "inspect", source, "--raw"))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
var srcIdx ocispecs.Index
|
||||
err = json.Unmarshal(dt, &srcIdx)
|
||||
require.NoError(t, err)
|
||||
|
||||
var attestations []ocispecs.Descriptor
|
||||
for _, mfst := range srcIdx.Manifests {
|
||||
if mfst.Annotations["vnd.docker.reference.type"] == "attestation-manifest" {
|
||||
attestations = append(attestations, mfst)
|
||||
}
|
||||
}
|
||||
require.Len(t, attestations, 2)
|
||||
|
||||
signatures := make([]ocispecs.Descriptor, 0, len(attestations))
|
||||
for _, attestation := range attestations {
|
||||
signatures = append(signatures, pushFakeSignatureReferrer(t, source, attestation))
|
||||
}
|
||||
|
||||
secondRef := "oci-layout://" + layoutPath + ":second"
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", secondRef, source))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "inspect", secondRef, "--raw"))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
idxBytes, err := os.ReadFile(filepath.Join(layoutPath, "index.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var layoutIdx ocispecs.Index
|
||||
err = json.Unmarshal(idxBytes, &layoutIdx)
|
||||
require.NoError(t, err)
|
||||
|
||||
directReferrers := map[digest.Digest]ocispecs.Descriptor{}
|
||||
directReferrerCount := 0
|
||||
for _, desc := range layoutIdx.Manifests {
|
||||
if desc.Annotations["io.containerd.manifest.subject"] != "" {
|
||||
directReferrerCount++
|
||||
directReferrers[desc.Digest] = desc
|
||||
}
|
||||
}
|
||||
require.Len(t, directReferrers, directReferrerCount)
|
||||
require.Len(t, directReferrers, len(signatures))
|
||||
for i, sig := range signatures {
|
||||
desc, ok := directReferrers[sig.Digest]
|
||||
require.True(t, ok)
|
||||
require.Equal(t, attestations[i].Digest.String(), desc.Annotations["io.containerd.manifest.subject"])
|
||||
}
|
||||
}
|
||||
|
||||
// testImagetoolsOCILayoutMergeSources verifies create merges registry and local OCI layout sources.
|
||||
func testImagetoolsOCILayoutMergeSources(t *testing.T, sb integration.Sandbox) {
|
||||
if !isDockerContainerWorker(sb) {
|
||||
t.Skip("only testing with docker-container worker, imagetools only runs on docker-container")
|
||||
}
|
||||
|
||||
dir := createDockerfileWithArches(t, "amd64", "arm64")
|
||||
registry1, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
registry2, err := sb.NewRegistry()
|
||||
require.NoError(t, err)
|
||||
registry3, err := sb.NewRegistry()
|
||||
require.NoError(t, err)
|
||||
|
||||
srcLocalSeed := registry1 + "/buildx/imtools-oci-layout-merge-local:latest"
|
||||
out, err := buildCmd(sb, withArgs("-t", srcLocalSeed, "--push", "--platform=linux/amd64", "--provenance=false", dir))
|
||||
require.NoError(t, err, string(out))
|
||||
|
||||
layoutPath := filepath.Join(dir, "layout-merge")
|
||||
layoutRef := "oci-layout://" + layoutPath + ":latest"
|
||||
cmd := buildxCmd(sb, withArgs("imagetools", "create", "-t", layoutRef, srcLocalSeed))
|
||||
dt, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
srcRegistry := registry2 + "/buildx/imtools-oci-layout-merge-registry:latest"
|
||||
out, err = buildCmd(sb, withArgs("-t", srcRegistry, "--push", "--platform=linux/arm64", "--provenance=false", dir))
|
||||
require.NoError(t, err, string(out))
|
||||
|
||||
target := registry3 + "/buildx/imtools-oci-layout-merge-target:latest"
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", target, layoutRef, srcRegistry))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target, "--raw"))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
var idx ocispecs.Index
|
||||
err = json.Unmarshal(dt, &idx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, idx.Manifests, 2)
|
||||
}
|
||||
|
||||
// testImagetoolsOCILayoutTargetDigest verifies exact digest targets work for
|
||||
// OCI layouts and mismatched digests fail.
|
||||
func testImagetoolsOCILayoutTargetDigest(t *testing.T, sb integration.Sandbox) {
|
||||
if !isDockerContainerWorker(sb) {
|
||||
t.Skip("only testing with docker-container worker, imagetools only runs on docker-container")
|
||||
}
|
||||
|
||||
dir := createDockerfile(t)
|
||||
registry, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
source := registry + "/buildx/imtools-oci-layout-digest-src:latest"
|
||||
out, err := buildCmd(sb, withArgs("-t", source, "--push", "--platform=linux/amd64", "--provenance=false", dir))
|
||||
require.NoError(t, err, string(out))
|
||||
|
||||
cmd := buildxCmd(sb, withArgs("imagetools", "inspect", source, "--raw"))
|
||||
dt, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
sourceDigest := digest.FromBytes(dt)
|
||||
|
||||
layoutPath := filepath.Join(dir, "layout-digest")
|
||||
exactRef := "oci-layout://" + layoutPath + "@" + sourceDigest.String()
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", exactRef, "--prefer-index=false", source))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "inspect", exactRef, "--raw"))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
require.Equal(t, sourceDigest, digest.FromBytes(dt))
|
||||
|
||||
wrongRef := "oci-layout://" + filepath.Join(dir, "layout-digest-wrong") + "@sha256:1111111111111111111111111111111111111111111111111111111111111111"
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", wrongRef, "--prefer-index=false", source))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.Error(t, err, string(dt))
|
||||
require.Contains(t, string(dt), "requested digest")
|
||||
}
|
||||
|
||||
// testImagetoolsAppend verifies create --append adds a new source onto an
|
||||
// existing target image and rewrites it as a combined index.
|
||||
func testImagetoolsAppend(t *testing.T, sb integration.Sandbox) {
|
||||
@@ -623,6 +1002,21 @@ func testImagetoolsCopyAttestationWithSignature(t *testing.T, sb integration.San
|
||||
require.Len(t, platformManifests, 2)
|
||||
require.Len(t, attestations, 2)
|
||||
|
||||
// Negative controls: signatures on image manifests, and unsupported
|
||||
// artifact types, should not be copied by imagetools create.
|
||||
platformSignatures := make(map[digest.Digest]ocispecs.Descriptor, len(platformManifests))
|
||||
platformUnsupportedReferrers := make(map[digest.Digest]ocispecs.Descriptor, len(platformManifests))
|
||||
for platformDigest, platformDesc := range platformManifests {
|
||||
platformSignatures[platformDigest] = pushFakeSignatureReferrer(t, source, platformDesc)
|
||||
platformUnsupportedReferrers[platformDigest] = pushFakeReferrer(
|
||||
t,
|
||||
source,
|
||||
platformDesc,
|
||||
"application/vnd.example.attachment.v1+json",
|
||||
map[string]string{"example.type": "unsupported"},
|
||||
)
|
||||
}
|
||||
|
||||
signatures := make(map[digest.Digest]ocispecs.Descriptor, len(attestations))
|
||||
for _, attestationDesc := range attestations {
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "inspect", source+"@"+string(attestationDesc.Digest), "--raw"))
|
||||
@@ -679,6 +1073,20 @@ func testImagetoolsCopyAttestationWithSignature(t *testing.T, sb integration.San
|
||||
require.Equal(t, attestationDesc.Digest, signatureManifest.Subject.Digest)
|
||||
require.Equal(t, "dsse-envelope", signatureManifest.Annotations["dev.sigstore.bundle.content"])
|
||||
}
|
||||
|
||||
// Only attestation signatures should be present after the copy. The
|
||||
// negative-control referrers attached to image manifests must not exist.
|
||||
for _, platformDesc := range platformManifests {
|
||||
signatureDesc := platformSignatures[platformDesc.Digest]
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target+"@"+string(signatureDesc.Digest), "--raw"))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.Error(t, err, string(dt))
|
||||
|
||||
unsupportedDesc := platformUnsupportedReferrers[platformDesc.Digest]
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target+"@"+string(unsupportedDesc.Digest), "--raw"))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.Error(t, err, string(dt))
|
||||
}
|
||||
}
|
||||
|
||||
type imagetoolsMergeMode int
|
||||
@@ -790,6 +1198,14 @@ func prepareSinglePlatformFallbackAsset(t *testing.T, sb integration.Sandbox, di
|
||||
}
|
||||
|
||||
func pushFakeSignatureReferrer(t *testing.T, sourceRef string, subject ocispecs.Descriptor) ocispecs.Descriptor {
|
||||
return pushFakeReferrer(t, sourceRef, subject, "application/vnd.dev.sigstore.bundle.v0.3+json", map[string]string{
|
||||
"dev.sigstore.bundle.content": "dsse-envelope",
|
||||
"dev.sigstore.bundle.predicateType": "https://sigstore.dev/cosign/sign/v1",
|
||||
"org.opencontainers.image.created": "2025-12-05T10:16:57Z",
|
||||
})
|
||||
}
|
||||
|
||||
func pushFakeReferrer(t *testing.T, sourceRef string, subject ocispecs.Descriptor, artifactType string, annotations map[string]string) ocispecs.Descriptor {
|
||||
t.Helper()
|
||||
|
||||
repoName := mustRepoName(t, sourceRef)
|
||||
@@ -797,27 +1213,22 @@ func pushFakeSignatureReferrer(t *testing.T, sourceRef string, subject ocispecs.
|
||||
configBytes := []byte("{}")
|
||||
configDesc := ocispecs.Descriptor{
|
||||
MediaType: "application/vnd.oci.empty.v1+json",
|
||||
ArtifactType: "application/vnd.dev.sigstore.bundle.v0.3+json",
|
||||
ArtifactType: artifactType,
|
||||
Digest: digest.FromBytes(configBytes),
|
||||
Size: int64(len(configBytes)),
|
||||
}
|
||||
|
||||
layerBytes := []byte(`{"kind":"fake-sigstore-bundle"}`)
|
||||
layerBytes := []byte(`{"kind":"fake-referrer"}`)
|
||||
layerDesc := ocispecs.Descriptor{
|
||||
MediaType: "application/vnd.dev.sigstore.bundle.v0.3+json",
|
||||
MediaType: artifactType,
|
||||
Digest: digest.FromBytes(layerBytes),
|
||||
Size: int64(len(layerBytes)),
|
||||
}
|
||||
|
||||
annotations := map[string]string{
|
||||
"dev.sigstore.bundle.content": "dsse-envelope",
|
||||
"dev.sigstore.bundle.predicateType": "https://sigstore.dev/cosign/sign/v1",
|
||||
"org.opencontainers.image.created": "2025-12-05T10:16:57Z",
|
||||
}
|
||||
signatureManifest := ocispecs.Manifest{
|
||||
Versioned: specsVersioned(),
|
||||
MediaType: ocispecs.MediaTypeImageManifest,
|
||||
ArtifactType: "application/vnd.dev.sigstore.bundle.v0.3+json",
|
||||
ArtifactType: artifactType,
|
||||
Config: configDesc,
|
||||
Layers: []ocispecs.Descriptor{layerDesc},
|
||||
Subject: &subject,
|
||||
@@ -828,7 +1239,7 @@ func pushFakeSignatureReferrer(t *testing.T, sourceRef string, subject ocispecs.
|
||||
|
||||
signatureDesc := ocispecs.Descriptor{
|
||||
MediaType: ocispecs.MediaTypeImageManifest,
|
||||
ArtifactType: "application/vnd.dev.sigstore.bundle.v0.3+json",
|
||||
ArtifactType: artifactType,
|
||||
Digest: digest.FromBytes(signatureBytes),
|
||||
Size: int64(len(signatureBytes)),
|
||||
Annotations: annotations,
|
||||
|
||||
@@ -15,6 +15,13 @@ func RegistryAuthForRef(ref string, auth authprovider.AuthConfigProvider) (strin
|
||||
if auth == nil {
|
||||
return "", nil
|
||||
}
|
||||
loc, err := ParseLocation(ref)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if loc.IsOCILayout() {
|
||||
return "", nil
|
||||
}
|
||||
r, err := parseRef(ref)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
+143
-62
@@ -5,7 +5,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"maps"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/content"
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/containerd/containerd/v2/core/remotes"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/moby/buildkit/client/ociindex"
|
||||
"github.com/moby/buildkit/exporter/containerimage/exptypes"
|
||||
"github.com/moby/buildkit/util/attestation"
|
||||
"github.com/moby/buildkit/util/contentutil"
|
||||
@@ -38,7 +38,7 @@ var supportedArtifactTypes = map[string]struct{}{
|
||||
|
||||
type Source struct {
|
||||
Desc ocispecs.Descriptor
|
||||
Ref reference.Named
|
||||
Ref *Location
|
||||
}
|
||||
|
||||
func (r *Resolver) Combine(ctx context.Context, srcs []*Source, ann map[exptypes.AnnotationKey]string, preferIndex bool, platforms []ocispecs.Platform) ([]byte, ocispecs.Descriptor, []DescWithSource, error) {
|
||||
@@ -60,7 +60,7 @@ func (r *Resolver) combine(ctx context.Context, srcs []*Source, ann map[exptypes
|
||||
for i := range dts {
|
||||
func(i int) {
|
||||
eg.Go(func() error {
|
||||
dt, err := r.GetDescriptor(ctx, srcs[i].Ref.String(), srcs[i].Desc)
|
||||
dt, err := r.GetDescriptor(ctx, srcs[i].Ref, srcs[i].Desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -83,7 +83,7 @@ func (r *Resolver) combine(ctx context.Context, srcs []*Source, ann map[exptypes
|
||||
p = &ocispecs.Platform{}
|
||||
}
|
||||
if p.OS == "" || p.Architecture == "" {
|
||||
if err := r.loadPlatform(ctx, p, srcs[i].Ref.String(), dt); err != nil {
|
||||
if err := r.loadPlatform(ctx, srcs[i].Ref, p, dt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -219,14 +219,20 @@ func (r *Resolver) combine(ctx context.Context, srcs []*Source, ann map[exptypes
|
||||
}, sources, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) Push(ctx context.Context, ref reference.Named, desc ocispecs.Descriptor, dt []byte) error {
|
||||
func (r *Resolver) Push(ctx context.Context, ref *Location, desc ocispecs.Descriptor, dt []byte) error {
|
||||
ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.in-toto+json", "intoto")
|
||||
if ref.IsOCILayout() {
|
||||
if err := ref.ValidateTargetDigest(desc.Digest); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.pushOCILayout(ctx, ref, desc, dt)
|
||||
}
|
||||
|
||||
fullRef, err := reference.WithDigest(reference.TagNameOnly(ref), desc.Digest)
|
||||
fullRef, err := ref.WithDigest(desc.Digest)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to combine ref %s with digest %s", ref, desc.Digest)
|
||||
}
|
||||
p, err := r.resolver().Pusher(ctx, fullRef.String())
|
||||
p, err := r.registryResolver().Pusher(ctx, fullRef.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -245,72 +251,61 @@ func (r *Resolver) Push(ctx context.Context, ref reference.Named, desc ocispecs.
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Resolver) Copy(ctx context.Context, src *Source, dest reference.Named) error {
|
||||
func (r *Resolver) Copy(ctx context.Context, src *Source, dest *Location) error {
|
||||
ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.in-toto+json", "intoto")
|
||||
ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.oci.empty.v1+json", "empty")
|
||||
|
||||
// push by digest
|
||||
p, err := r.resolver().Pusher(ctx, dest.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srcRef := reference.TagNameOnly(src.Ref)
|
||||
f, err := r.resolver().Fetcher(ctx, srcRef.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
refspec := reference.TrimNamed(src.Ref).String()
|
||||
u, err := url.Parse("dummy://" + refspec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc := src.Desc
|
||||
desc.Annotations = maps.Clone(desc.Annotations)
|
||||
if desc.Annotations == nil {
|
||||
desc.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
source, repo := u.Hostname(), strings.TrimPrefix(u.Path, "/")
|
||||
desc.Annotations["containerd.io/distribution.source."+source] = repo
|
||||
|
||||
referrersFetcher, ok := f.(remotes.ReferrersFetcher)
|
||||
if !ok {
|
||||
return errors.Errorf("fetcher for %s does not support referrers", src.Ref.String())
|
||||
if src.Ref.IsRegistry() {
|
||||
desc.Annotations["containerd.io/distribution.source."+src.Ref.Named().Name()] = src.Ref.Named().Name()
|
||||
}
|
||||
|
||||
opts := []contentutil.CopyOption{
|
||||
contentutil.WithReferrers(referrersFunc(func(ctx context.Context, desc ocispecs.Descriptor) ([]ocispecs.Descriptor, error) {
|
||||
descs, err := referrersFetcher.FetchReferrers(ctx, desc.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var filtered []ocispecs.Descriptor
|
||||
for _, d := range descs {
|
||||
if _, ok := supportedArtifactTypes[d.ArtifactType]; ok {
|
||||
filtered = append(filtered, d)
|
||||
}
|
||||
}
|
||||
return filtered, nil
|
||||
})),
|
||||
}
|
||||
|
||||
err = contentutil.CopyChain(ctx, contentutil.FromPusher(p), contentutil.FromFetcher(f), desc, opts...)
|
||||
provider, err := r.providerForLocation(src.Ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ingester, err := r.ingesterForLocation(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
referrers := &referrersProvider{base: referrersFunc(func(ctx context.Context, subject ocispecs.Descriptor) ([]ocispecs.Descriptor, error) {
|
||||
descs, err := r.FetchReferrers(ctx, src.Ref, subject.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var filtered []ocispecs.Descriptor
|
||||
for _, d := range descs {
|
||||
if _, ok := supportedArtifactTypes[d.ArtifactType]; ok {
|
||||
filtered = append(filtered, d)
|
||||
}
|
||||
}
|
||||
return filtered, nil
|
||||
})}
|
||||
|
||||
err = contentutil.CopyChain(ctx, ingester, provider, desc, contentutil.WithReferrers(referrers))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dest.IsOCILayout() {
|
||||
for subject, descs := range referrers.refs {
|
||||
r.ociReferrers.record(dest.OCILayout().Path, subject, descs)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Resolver) loadPlatform(ctx context.Context, p2 *ocispecs.Platform, in string, dt []byte) error {
|
||||
func (r *Resolver) loadPlatform(ctx context.Context, loc *Location, p2 *ocispecs.Platform, dt []byte) error {
|
||||
var manifest ocispecs.Manifest
|
||||
if err := json.Unmarshal(dt, &manifest); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
dt, err := r.GetDescriptor(ctx, in, manifest.Config)
|
||||
dt, err := r.GetDescriptor(ctx, loc, manifest.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -450,15 +445,7 @@ func (r *Resolver) filterPlatforms(ctx context.Context, dt []byte, desc ocispecs
|
||||
}
|
||||
src = defaultSource
|
||||
}
|
||||
f, err := r.resolver().Fetcher(ctx, src.Ref.String())
|
||||
if err != nil {
|
||||
return nil, ocispecs.Descriptor{}, nil, err
|
||||
}
|
||||
rf, ok := f.(remotes.ReferrersFetcher)
|
||||
if !ok {
|
||||
return nil, ocispecs.Descriptor{}, nil, errors.Errorf("fetcher for %s does not support referrers", srcMap[d].Ref.String())
|
||||
}
|
||||
refs, err := rf.FetchReferrers(ctx, d, remotes.WithReferrerArtifactTypes(artifactTypeAttestationManifest))
|
||||
refs, err := r.FetchReferrers(ctx, src.Ref, d, remotes.WithReferrerArtifactTypes(artifactTypeAttestationManifest))
|
||||
if err != nil {
|
||||
if errors.Is(err, errdefs.ErrNotFound) {
|
||||
continue
|
||||
@@ -505,6 +492,100 @@ func (r *Resolver) filterPlatforms(ctx context.Context, dt []byte, desc ocispecs
|
||||
return idxBytes, desc, mfstsWithSource, nil
|
||||
}
|
||||
|
||||
type referrersProvider struct {
|
||||
base referrersFunc
|
||||
refs map[digest.Digest][]ocispecs.Descriptor
|
||||
}
|
||||
|
||||
func (r *referrersProvider) Referrers(ctx context.Context, desc ocispecs.Descriptor) ([]ocispecs.Descriptor, error) {
|
||||
out, err := r.base(ctx, desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = dedupeDescriptors(out)
|
||||
if r.refs == nil {
|
||||
r.refs = map[digest.Digest][]ocispecs.Descriptor{}
|
||||
}
|
||||
r.refs[desc.Digest] = dedupeDescriptors(append(r.refs[desc.Digest], out...))
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func dedupeDescriptors(descs []ocispecs.Descriptor) []ocispecs.Descriptor {
|
||||
if len(descs) < 2 {
|
||||
return descs
|
||||
}
|
||||
seen := make(map[digest.Digest]struct{}, len(descs))
|
||||
out := descs[:0]
|
||||
for _, desc := range descs {
|
||||
if _, ok := seen[desc.Digest]; ok {
|
||||
continue
|
||||
}
|
||||
seen[desc.Digest] = struct{}{}
|
||||
out = append(out, desc)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Resolver) ingesterForLocation(loc *Location) (content.Ingester, error) {
|
||||
if loc.IsRegistry() {
|
||||
p, err := r.registryResolver().Pusher(context.TODO(), loc.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return contentutil.FromPusher(p), nil
|
||||
}
|
||||
return r.localStore(loc.OCILayout().Path)
|
||||
}
|
||||
|
||||
func (r *Resolver) providerForLocation(loc *Location) (content.Provider, error) {
|
||||
if loc.IsRegistry() {
|
||||
f, err := r.registryResolver().Fetcher(context.TODO(), loc.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return contentutil.FromFetcher(f), nil
|
||||
}
|
||||
return r.localStore(loc.OCILayout().Path)
|
||||
}
|
||||
|
||||
func (r *Resolver) pushOCILayout(ctx context.Context, ref *Location, desc ocispecs.Descriptor, dt []byte) error {
|
||||
if err := os.MkdirAll(ref.OCILayout().Path, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
store, err := r.localStore(ref.OCILayout().Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w, err := store.Writer(ctx, content.WithRef(desc.Digest.String()), content.WithDescriptor(desc))
|
||||
if err != nil {
|
||||
if !errdefs.IsAlreadyExists(err) {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err = content.Copy(ctx, w, bytes.NewReader(dt), desc.Size, desc.Digest)
|
||||
if err != nil && !errdefs.IsAlreadyExists(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
idx := ociindex.NewStoreIndex(ref.OCILayout().Path)
|
||||
switch {
|
||||
case ref.Digest() != "":
|
||||
if err := idx.Put(desc); err != nil {
|
||||
return err
|
||||
}
|
||||
case ref.Tag() != "":
|
||||
if err := idx.Put(desc, ociindex.Tag(ref.Tag())); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if err := idx.Put(desc, ociindex.Tag("latest")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return writePendingOCILayoutReferrers(ctx, r.ociReferrers.take(ref.OCILayout().Path), r.GetDescriptor, idx, ref)
|
||||
}
|
||||
|
||||
func detectMediaType(dt []byte) (string, error) {
|
||||
var mfst struct {
|
||||
MediaType string `json:"mediaType"`
|
||||
|
||||
+147
-12
@@ -5,17 +5,24 @@ import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/content"
|
||||
"github.com/containerd/containerd/v2/core/remotes"
|
||||
"github.com/containerd/containerd/v2/core/remotes/docker"
|
||||
contentlocal "github.com/containerd/containerd/v2/plugins/content/local"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/containerd/log"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/buildx/util/resolver"
|
||||
"github.com/docker/buildx/util/resolver/auth"
|
||||
"github.com/moby/buildkit/client/ociindex"
|
||||
"github.com/moby/buildkit/session/auth/authprovider"
|
||||
"github.com/moby/buildkit/util/contentutil"
|
||||
"github.com/moby/buildkit/util/tracing"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -25,9 +32,12 @@ type Opt struct {
|
||||
}
|
||||
|
||||
type Resolver struct {
|
||||
auth docker.Authorizer
|
||||
hosts docker.RegistryHosts
|
||||
buffer contentutil.Buffer
|
||||
auth docker.Authorizer
|
||||
hosts docker.RegistryHosts
|
||||
buffer contentutil.Buffer
|
||||
localStoreMu sync.Mutex
|
||||
localStores map[string]content.Store
|
||||
ociReferrers ociLayoutReferrerRecorder
|
||||
}
|
||||
|
||||
func New(opt Opt) *Resolver {
|
||||
@@ -37,13 +47,14 @@ func New(opt Opt) *Resolver {
|
||||
AuthConfig: opt.Auth,
|
||||
}
|
||||
return &Resolver{
|
||||
auth: auth,
|
||||
hosts: resolver.NewRegistryConfig(opt.RegistryConfig),
|
||||
buffer: contentutil.NewBuffer(),
|
||||
auth: auth,
|
||||
hosts: resolver.NewRegistryConfig(opt.RegistryConfig),
|
||||
buffer: contentutil.NewBuffer(),
|
||||
localStores: map[string]content.Store{},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resolver) resolver() remotes.Resolver {
|
||||
func (r *Resolver) registryResolver() remotes.Resolver {
|
||||
return docker.NewResolver(docker.ResolverOptions{
|
||||
Hosts: func(domain string) ([]docker.RegistryHost, error) {
|
||||
res, err := r.hosts(domain)
|
||||
@@ -66,12 +77,19 @@ func (r *Resolver) Resolve(ctx context.Context, in string) (string, ocispecs.Des
|
||||
logger.Out = io.Discard
|
||||
ctx = log.WithLogger(ctx, logrus.NewEntry(logger))
|
||||
|
||||
loc, err := ParseLocation(in)
|
||||
if err != nil {
|
||||
return "", ocispecs.Descriptor{}, err
|
||||
}
|
||||
if loc.IsOCILayout() {
|
||||
return r.resolveOCILayout(ctx, loc)
|
||||
}
|
||||
|
||||
ref, err := parseRef(in)
|
||||
if err != nil {
|
||||
return "", ocispecs.Descriptor{}, err
|
||||
}
|
||||
|
||||
in, desc, err := r.resolver().Resolve(ctx, ref.String())
|
||||
in, desc, err := r.registryResolver().Resolve(ctx, ref.String())
|
||||
if err != nil {
|
||||
return "", ocispecs.Descriptor{}, err
|
||||
}
|
||||
@@ -85,15 +103,19 @@ func (r *Resolver) Get(ctx context.Context, in string) ([]byte, ocispecs.Descrip
|
||||
return nil, ocispecs.Descriptor{}, err
|
||||
}
|
||||
|
||||
dt, err := r.GetDescriptor(ctx, in, desc)
|
||||
loc, err := ParseLocation(in)
|
||||
if err != nil {
|
||||
return nil, ocispecs.Descriptor{}, err
|
||||
}
|
||||
dt, err := r.GetDescriptor(ctx, loc, desc)
|
||||
if err != nil {
|
||||
return nil, ocispecs.Descriptor{}, err
|
||||
}
|
||||
return dt, desc, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) GetDescriptor(ctx context.Context, in string, desc ocispecs.Descriptor) ([]byte, error) {
|
||||
fetcher, err := r.resolver().Fetcher(ctx, in)
|
||||
func (r *Resolver) GetDescriptor(ctx context.Context, loc *Location, desc ocispecs.Descriptor) ([]byte, error) {
|
||||
fetcher, err := r.fetcherForLocation(ctx, loc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -113,6 +135,119 @@ func (r *Resolver) GetDescriptor(ctx context.Context, in string, desc ocispecs.D
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (r *Resolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) {
|
||||
loc, err := ParseLocation(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.fetcherForLocation(ctx, loc)
|
||||
}
|
||||
|
||||
func (r *Resolver) fetcherForLocation(ctx context.Context, loc *Location) (remotes.Fetcher, error) {
|
||||
if loc.IsRegistry() {
|
||||
return r.registryResolver().Fetcher(ctx, loc.String())
|
||||
}
|
||||
store, err := r.localStore(loc.OCILayout().Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &providerFetcher{Provider: store}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) localStore(path string) (content.Store, error) {
|
||||
r.localStoreMu.Lock()
|
||||
defer r.localStoreMu.Unlock()
|
||||
|
||||
if store, ok := r.localStores[path]; ok {
|
||||
return store, nil
|
||||
}
|
||||
|
||||
store, err := contentlocal.NewStore(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.localStores[path] = store
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) FetchReferrers(ctx context.Context, loc *Location, dgst digest.Digest, opts ...remotes.FetchReferrersOpt) ([]ocispecs.Descriptor, error) {
|
||||
if loc.IsOCILayout() {
|
||||
return fetchOCILayoutReferrers(ctx, r.GetDescriptor, loc, dgst, opts...)
|
||||
}
|
||||
f, err := r.registryResolver().Fetcher(ctx, loc.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rf, ok := f.(remotes.ReferrersFetcher)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("fetcher for %s does not support referrers", loc.String())
|
||||
}
|
||||
return rf.FetchReferrers(ctx, dgst, opts...)
|
||||
}
|
||||
|
||||
type providerFetcher struct {
|
||||
content.Provider
|
||||
}
|
||||
|
||||
func (f *providerFetcher) Fetch(ctx context.Context, desc ocispecs.Descriptor) (io.ReadCloser, error) {
|
||||
ra, err := f.ReaderAt(ctx, desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &readerAtReadCloser{ReaderAt: ra}, nil
|
||||
}
|
||||
|
||||
type readerAtReadCloser struct {
|
||||
content.ReaderAt
|
||||
offset int64
|
||||
}
|
||||
|
||||
func (r *readerAtReadCloser) Read(dt []byte) (int, error) {
|
||||
n, err := r.ReadAt(dt, r.offset)
|
||||
r.offset += int64(n)
|
||||
if n > 0 && errors.Is(err, io.EOF) {
|
||||
return n, nil
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *readerAtReadCloser) Close() error {
|
||||
return r.ReaderAt.Close()
|
||||
}
|
||||
|
||||
func (r *Resolver) resolveOCILayout(ctx context.Context, loc *Location) (string, ocispecs.Descriptor, error) {
|
||||
store, err := r.localStore(loc.OCILayout().Path)
|
||||
if err != nil {
|
||||
return "", ocispecs.Descriptor{}, err
|
||||
}
|
||||
if loc.Digest() != "" {
|
||||
info, err := store.Info(ctx, loc.Digest())
|
||||
if err != nil {
|
||||
return "", ocispecs.Descriptor{}, err
|
||||
}
|
||||
desc := ocispecs.Descriptor{Digest: info.Digest, Size: info.Size}
|
||||
dt, err := content.ReadBlob(ctx, store, desc)
|
||||
if err != nil {
|
||||
return "", ocispecs.Descriptor{}, err
|
||||
}
|
||||
mt, err := detectMediaType(dt)
|
||||
if err != nil {
|
||||
return "", ocispecs.Descriptor{}, err
|
||||
}
|
||||
desc.MediaType = mt
|
||||
return loc.String(), desc, nil
|
||||
}
|
||||
idx := ociindex.NewStoreIndex(loc.OCILayout().Path)
|
||||
desc, err := idx.Get(loc.Tag())
|
||||
if err != nil {
|
||||
return "", ocispecs.Descriptor{}, err
|
||||
}
|
||||
if desc == nil {
|
||||
return "", ocispecs.Descriptor{}, errors.Wrapf(errdefs.ErrNotFound, "reference %s not found", loc.String())
|
||||
}
|
||||
return loc.String(), *desc, nil
|
||||
}
|
||||
|
||||
func parseRef(s string) (reference.Named, error) {
|
||||
ref, err := reference.ParseNormalizedNamed(s)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package imagetools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/images"
|
||||
"github.com/containerd/containerd/v2/core/remotes"
|
||||
"github.com/moby/buildkit/client/ociindex"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFetchReferrersOCILayoutArtifactTypeFilter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
idx := ociindex.NewStoreIndex(dir)
|
||||
subject := digest.FromString("subject")
|
||||
|
||||
attestation := ocispecs.Descriptor{
|
||||
MediaType: ocispecs.MediaTypeImageManifest,
|
||||
ArtifactType: artifactTypeAttestationManifest,
|
||||
Digest: digest.FromString("attestation"),
|
||||
Size: 123,
|
||||
Annotations: map[string]string{
|
||||
images.AnnotationManifestSubject: subject.String(),
|
||||
},
|
||||
}
|
||||
require.NoError(t, idx.Put(attestation))
|
||||
|
||||
signature := ocispecs.Descriptor{
|
||||
MediaType: ocispecs.MediaTypeImageManifest,
|
||||
ArtifactType: "application/vnd.dev.sigstore.bundle.v0.3+json",
|
||||
Digest: digest.FromString("signature"),
|
||||
Size: 456,
|
||||
Annotations: map[string]string{
|
||||
images.AnnotationManifestSubject: subject.String(),
|
||||
},
|
||||
}
|
||||
require.NoError(t, idx.Put(signature))
|
||||
|
||||
loc, err := ParseLocation("oci-layout://" + dir + ":latest")
|
||||
require.NoError(t, err)
|
||||
|
||||
r := New(Opt{})
|
||||
refs, err := r.FetchReferrers(context.Background(), loc, subject, remotes.WithReferrerArtifactTypes(artifactTypeAttestationManifest))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, refs, 1)
|
||||
require.Equal(t, attestation.Digest, refs[0].Digest)
|
||||
require.Equal(t, attestation.ArtifactType, refs[0].ArtifactType)
|
||||
}
|
||||
+10
-11
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/containerd/containerd/v2/core/images"
|
||||
"github.com/containerd/containerd/v2/core/remotes"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/moby/buildkit/util/contentutil"
|
||||
"github.com/opencontainers/go-digest"
|
||||
@@ -43,8 +42,13 @@ type contentCache interface {
|
||||
content.Ingester
|
||||
}
|
||||
|
||||
type loaderResolver interface {
|
||||
Resolve(ctx context.Context, ref string) (string, ocispecs.Descriptor, error)
|
||||
Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error)
|
||||
}
|
||||
|
||||
type loader struct {
|
||||
resolver remotes.Resolver
|
||||
resolver loaderResolver
|
||||
cache contentCache
|
||||
}
|
||||
|
||||
@@ -78,7 +82,7 @@ type result struct {
|
||||
assets map[string]asset
|
||||
}
|
||||
|
||||
func newLoader(resolver remotes.Resolver) *loader {
|
||||
func newLoader(resolver loaderResolver) *loader {
|
||||
return &loader{
|
||||
resolver: resolver,
|
||||
cache: contentutil.NewBuffer(),
|
||||
@@ -86,22 +90,17 @@ func newLoader(resolver remotes.Resolver) *loader {
|
||||
}
|
||||
|
||||
func (l *loader) Load(ctx context.Context, ref string) (*result, error) {
|
||||
named, err := parseRef(ref)
|
||||
loc, err := ParseLocation(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, desc, err := l.resolver.Resolve(ctx, named.String())
|
||||
_, desc, err := l.resolver.Resolve(ctx, loc.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
canonical, err := reference.WithDigest(named, desc.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fetcher, err := l.resolver.Fetcher(ctx, canonical.String())
|
||||
fetcher, err := l.resolver.Fetcher(ctx, loc.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package imagetools
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/buildx/util/ocilayout"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type LocationKind int
|
||||
|
||||
const (
|
||||
LocationKindRegistry LocationKind = iota
|
||||
LocationKindOCILayout
|
||||
)
|
||||
|
||||
type Location struct {
|
||||
kind LocationKind
|
||||
|
||||
original string
|
||||
|
||||
named reference.Named
|
||||
oci ocilayout.Ref
|
||||
}
|
||||
|
||||
func ParseLocation(s string) (*Location, error) {
|
||||
if ref, ok, err := ocilayout.Parse(s); ok {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Location{
|
||||
kind: LocationKindOCILayout,
|
||||
original: s,
|
||||
oci: ref,
|
||||
}, nil
|
||||
}
|
||||
|
||||
ref, err := reference.ParseNormalizedNamed(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ref = reference.TagNameOnly(ref)
|
||||
return &Location{
|
||||
kind: LocationKindRegistry,
|
||||
original: s,
|
||||
named: ref,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *Location) String() string {
|
||||
if l == nil {
|
||||
return ""
|
||||
}
|
||||
switch l.kind {
|
||||
case LocationKindOCILayout:
|
||||
return l.oci.String()
|
||||
default:
|
||||
return l.named.String()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Location) Kind() LocationKind {
|
||||
return l.kind
|
||||
}
|
||||
|
||||
func (l *Location) IsRegistry() bool {
|
||||
return l != nil && l.kind == LocationKindRegistry
|
||||
}
|
||||
|
||||
func (l *Location) IsOCILayout() bool {
|
||||
return l != nil && l.kind == LocationKindOCILayout
|
||||
}
|
||||
|
||||
func (l *Location) Name() string {
|
||||
if l == nil {
|
||||
return ""
|
||||
}
|
||||
if l.IsRegistry() {
|
||||
return l.named.Name()
|
||||
}
|
||||
return l.oci.Path
|
||||
}
|
||||
|
||||
func (l *Location) Named() reference.Named {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return l.named
|
||||
}
|
||||
|
||||
func (l *Location) OCILayout() ocilayout.Ref {
|
||||
return l.oci
|
||||
}
|
||||
|
||||
func (l *Location) Tag() string {
|
||||
if l == nil {
|
||||
return ""
|
||||
}
|
||||
if l.IsRegistry() {
|
||||
if tagged, ok := l.named.(reference.Tagged); ok {
|
||||
return tagged.Tag()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return l.oci.Tag
|
||||
}
|
||||
|
||||
func (l *Location) Digest() digest.Digest {
|
||||
if l == nil {
|
||||
return ""
|
||||
}
|
||||
if l.IsRegistry() {
|
||||
if digested, ok := l.named.(reference.Digested); ok {
|
||||
return digested.Digest()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return l.oci.Digest
|
||||
}
|
||||
|
||||
func (l *Location) WithDigest(dgst digest.Digest) (*Location, error) {
|
||||
if l.IsRegistry() {
|
||||
d, err := reference.WithDigest(l.named, dgst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Location{kind: LocationKindRegistry, original: d.String(), named: d}, nil
|
||||
}
|
||||
ref := l.oci
|
||||
ref.Tag = ""
|
||||
ref.Digest = dgst
|
||||
return &Location{kind: LocationKindOCILayout, original: ref.String(), oci: ref}, nil
|
||||
}
|
||||
|
||||
func (l *Location) WithTag(tag string) (*Location, error) {
|
||||
if l.IsRegistry() {
|
||||
n, err := reference.ParseNormalizedNamed(l.named.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t, err := reference.WithTag(n, tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Location{kind: LocationKindRegistry, original: t.String(), named: t}, nil
|
||||
}
|
||||
ref := l.oci
|
||||
ref.Tag = tag
|
||||
ref.Digest = ""
|
||||
return &Location{kind: LocationKindOCILayout, original: ref.String(), oci: ref}, nil
|
||||
}
|
||||
|
||||
func (l *Location) TagNameOnly() (*Location, error) {
|
||||
if l.IsRegistry() {
|
||||
n, err := reference.ParseNormalizedNamed(l.named.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Location{
|
||||
kind: LocationKindRegistry,
|
||||
original: reference.TagNameOnly(n).String(),
|
||||
named: reference.TagNameOnly(n),
|
||||
}, nil
|
||||
}
|
||||
ref := l.oci
|
||||
ref.Digest = ""
|
||||
if ref.Tag == "" {
|
||||
ref.Tag = "latest"
|
||||
}
|
||||
return &Location{kind: LocationKindOCILayout, original: ref.String(), oci: ref}, nil
|
||||
}
|
||||
|
||||
func (l *Location) ValidateTargetDigest(desc digest.Digest) error {
|
||||
if l == nil || l.Digest() == "" {
|
||||
return nil
|
||||
}
|
||||
if l.Digest() != desc {
|
||||
return errors.Errorf("target %s requested digest %s but produced %s", l.String(), l.Digest(), desc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsOCILayout(s string) bool {
|
||||
return strings.HasPrefix(s, "oci-layout://")
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package imagetools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/images"
|
||||
"github.com/containerd/containerd/v2/core/remotes"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/moby/buildkit/client/ociindex"
|
||||
"github.com/moby/buildkit/util/attestation"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ociLayoutReferrerRecorder struct {
|
||||
mu sync.Mutex
|
||||
refs map[string]map[digest.Digest][]ocispecs.Descriptor
|
||||
}
|
||||
|
||||
func (r *ociLayoutReferrerRecorder) record(path string, subject digest.Digest, descs []ocispecs.Descriptor) {
|
||||
if len(descs) == 0 {
|
||||
return
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.refs == nil {
|
||||
r.refs = map[string]map[digest.Digest][]ocispecs.Descriptor{}
|
||||
}
|
||||
if r.refs[path] == nil {
|
||||
r.refs[path] = map[digest.Digest][]ocispecs.Descriptor{}
|
||||
}
|
||||
r.refs[path][subject] = dedupeDescriptors(append(r.refs[path][subject], descs...))
|
||||
}
|
||||
|
||||
func (r *ociLayoutReferrerRecorder) take(path string) map[digest.Digest][]ocispecs.Descriptor {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.refs == nil {
|
||||
return nil
|
||||
}
|
||||
out := r.refs[path]
|
||||
delete(r.refs, path)
|
||||
return out
|
||||
}
|
||||
|
||||
func hasSubjectAnnotation(desc ocispecs.Descriptor) bool {
|
||||
return desc.Annotations[images.AnnotationManifestSubject] != ""
|
||||
}
|
||||
|
||||
// fetchOCILayoutReferrers resolves referrers for a subject from an OCI layout by
|
||||
// combining directly indexed subject entries with referrers reachable from the
|
||||
// regular named roots in index.json.
|
||||
func fetchOCILayoutReferrers(ctx context.Context, getDescriptor func(context.Context, *Location, ocispecs.Descriptor) ([]byte, error), loc *Location, subject digest.Digest, opts ...remotes.FetchReferrersOpt) ([]ocispecs.Descriptor, error) {
|
||||
idx, err := ociindex.NewStoreIndex(loc.OCILayout().Path).Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := map[digest.Digest]ocispecs.Descriptor{}
|
||||
visited := map[digest.Digest]struct{}{}
|
||||
for _, desc := range idx.Manifests {
|
||||
if hasSubjectAnnotation(desc) {
|
||||
continue
|
||||
}
|
||||
if err := collectReachableOCILayoutReferrers(ctx, getDescriptor, loc, desc, subject, visited, out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, desc := range idx.Manifests {
|
||||
if desc.Annotations[images.AnnotationManifestSubject] == subject.String() {
|
||||
out[desc.Digest] = desc
|
||||
}
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
return nil, errors.WithStack(errdefs.ErrNotFound)
|
||||
}
|
||||
|
||||
refs := make([]ocispecs.Descriptor, 0, len(out))
|
||||
for _, desc := range out {
|
||||
refs = append(refs, desc)
|
||||
}
|
||||
return filterOCILayoutReferrers(ctx, refs, opts...)
|
||||
}
|
||||
|
||||
func filterOCILayoutReferrers(ctx context.Context, refs []ocispecs.Descriptor, opts ...remotes.FetchReferrersOpt) ([]ocispecs.Descriptor, error) {
|
||||
var cfg remotes.FetchReferrersConfig
|
||||
for _, opt := range opts {
|
||||
if err := opt(ctx, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(cfg.ArtifactTypes) == 0 {
|
||||
return refs, nil
|
||||
}
|
||||
out := make([]ocispecs.Descriptor, 0, len(refs))
|
||||
for _, ref := range refs {
|
||||
if slices.Contains(cfg.ArtifactTypes, ref.ArtifactType) {
|
||||
out = append(out, ref)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// collectReachableOCILayoutReferrers walks a regular OCI layout root and records
|
||||
// referrer manifests for the requested subject that are already reachable from it.
|
||||
func collectReachableOCILayoutReferrers(ctx context.Context, getDescriptor func(context.Context, *Location, ocispecs.Descriptor) ([]byte, error), loc *Location, desc ocispecs.Descriptor, subject digest.Digest, visited map[digest.Digest]struct{}, out map[digest.Digest]ocispecs.Descriptor) error {
|
||||
if _, ok := visited[desc.Digest]; ok {
|
||||
return nil
|
||||
}
|
||||
visited[desc.Digest] = struct{}{}
|
||||
|
||||
if desc.Annotations[attestation.DockerAnnotationReferenceDigest] == subject.String() {
|
||||
out[desc.Digest] = desc
|
||||
}
|
||||
|
||||
switch desc.MediaType {
|
||||
case ocispecs.MediaTypeImageIndex:
|
||||
dt, err := getDescriptor(ctx, loc, desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var idx ocispecs.Index
|
||||
if err := json.Unmarshal(dt, &idx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
for _, child := range idx.Manifests {
|
||||
if err := collectReachableOCILayoutReferrers(ctx, getDescriptor, loc, child, subject, visited, out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case ocispecs.MediaTypeImageManifest:
|
||||
dt, err := getDescriptor(ctx, loc, desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var mfst ocispecs.Manifest
|
||||
if err := json.Unmarshal(dt, &mfst); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if mfst.Subject != nil && mfst.Subject.Digest == subject {
|
||||
out[desc.Digest] = desc
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writePendingOCILayoutReferrers adds copied referrers to index.json only when
|
||||
// they are not already reachable from the regular top-level roots.
|
||||
func writePendingOCILayoutReferrers(
|
||||
ctx context.Context,
|
||||
pending map[digest.Digest][]ocispecs.Descriptor,
|
||||
getDescriptor func(context.Context, *Location, ocispecs.Descriptor) ([]byte, error),
|
||||
idx ociindex.StoreIndex,
|
||||
loc *Location,
|
||||
) error {
|
||||
if len(pending) == 0 {
|
||||
return nil
|
||||
}
|
||||
current, err := idx.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reachable := map[digest.Digest]struct{}{}
|
||||
visited := map[digest.Digest]struct{}{}
|
||||
for _, desc := range current.Manifests {
|
||||
if err := collectReachableDigests(ctx, getDescriptor, loc, desc, visited, reachable); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for subject, manifests := range pending {
|
||||
for _, desc := range manifests {
|
||||
if err := putSubjectReferrerIndexEntry(idx, reachable, subject, desc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectReachableDigests records descriptors reachable from regular OCI layout
|
||||
// roots so standalone subject-indexed referrers are not duplicated in index.json.
|
||||
func collectReachableDigests(ctx context.Context, getDescriptor func(context.Context, *Location, ocispecs.Descriptor) ([]byte, error), loc *Location, desc ocispecs.Descriptor, visited map[digest.Digest]struct{}, reachable map[digest.Digest]struct{}) error {
|
||||
if hasSubjectAnnotation(desc) {
|
||||
return nil
|
||||
}
|
||||
if _, ok := visited[desc.Digest]; ok {
|
||||
return nil
|
||||
}
|
||||
visited[desc.Digest] = struct{}{}
|
||||
reachable[desc.Digest] = struct{}{}
|
||||
|
||||
if desc.MediaType != ocispecs.MediaTypeImageIndex {
|
||||
return nil
|
||||
}
|
||||
|
||||
dt, err := getDescriptor(ctx, loc, desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var idx ocispecs.Index
|
||||
if err := json.Unmarshal(dt, &idx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
for _, child := range idx.Manifests {
|
||||
if err := collectReachableDigests(ctx, getDescriptor, loc, child, visited, reachable); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func putSubjectReferrerIndexEntry(idx ociindex.StoreIndex, reachable map[digest.Digest]struct{}, subject digest.Digest, desc ocispecs.Descriptor) error {
|
||||
if _, ok := reachable[desc.Digest]; ok {
|
||||
return nil
|
||||
}
|
||||
if desc.Annotations == nil {
|
||||
desc.Annotations = map[string]string{}
|
||||
}
|
||||
if !hasSubjectAnnotation(desc) {
|
||||
desc.Annotations[images.AnnotationManifestSubject] = subject.String()
|
||||
}
|
||||
return idx.Put(desc)
|
||||
}
|
||||
+13
-10
@@ -12,7 +12,6 @@ import (
|
||||
|
||||
"github.com/containerd/containerd/v2/core/images"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
@@ -27,7 +26,7 @@ type Printer struct {
|
||||
format string
|
||||
|
||||
raw []byte
|
||||
ref reference.Named
|
||||
ref *Location
|
||||
manifest ocispecs.Descriptor
|
||||
index ocispecs.Index
|
||||
}
|
||||
@@ -35,7 +34,7 @@ type Printer struct {
|
||||
func NewPrinter(ctx context.Context, opt Opt, name string, format string) (*Printer, error) {
|
||||
resolver := New(opt)
|
||||
|
||||
ref, err := parseRef(name)
|
||||
ref, err := ParseLocation(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -83,7 +82,7 @@ func (p *Printer) Print(raw bool, out io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
res, err := newLoader(p.resolver.resolver()).Load(p.ctx, p.name)
|
||||
res, err := newLoader(p.resolver).Load(p.ctx, p.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -125,11 +124,11 @@ func (p *Printer) Print(raw bool, out io.Writer) error {
|
||||
|
||||
switch {
|
||||
// TODO: print formatted config
|
||||
case strings.HasPrefix(format, "{{.Manifest"):
|
||||
case isWholeManifestTemplate(format):
|
||||
w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0)
|
||||
_, _ = fmt.Fprintf(w, "Name:\t%s\n", p.ref.String())
|
||||
switch {
|
||||
case strings.HasPrefix(format, "{{.Manifest"):
|
||||
case isWholeManifestTemplate(format):
|
||||
_, _ = fmt.Fprintf(w, "MediaType:\t%s\n", p.manifest.MediaType)
|
||||
_, _ = fmt.Fprintf(w, "Digest:\t%s\n", p.manifest.Digest)
|
||||
_ = w.Flush()
|
||||
@@ -162,6 +161,10 @@ func (p *Printer) Print(raw bool, out io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func isWholeManifestTemplate(format string) bool {
|
||||
return strings.TrimSpace(format) == "{{.Manifest}}"
|
||||
}
|
||||
|
||||
func (p *Printer) printManifestList(out io.Writer) error {
|
||||
w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0)
|
||||
_, _ = fmt.Fprintf(w, "\t\n")
|
||||
@@ -173,11 +176,11 @@ func (p *Printer) printManifestList(out io.Writer) error {
|
||||
if i != 0 {
|
||||
_, _ = fmt.Fprintf(w, "\t\n")
|
||||
}
|
||||
cr, err := reference.WithDigest(p.ref, m.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
name := p.ref.String()
|
||||
if ref, err := p.ref.WithDigest(m.Digest); err == nil {
|
||||
name = ref.String()
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%sName:\t%s\n", defaultPfx, cr.String())
|
||||
_, _ = fmt.Fprintf(w, "%sName:\t%s\n", defaultPfx, name)
|
||||
_, _ = fmt.Fprintf(w, "%sMediaType:\t%s\n", defaultPfx, m.MediaType)
|
||||
if p := m.Platform; p != nil {
|
||||
_, _ = fmt.Fprintf(w, "%sPlatform:\t%s\n", defaultPfx, platforms.Format(*p))
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package ocilayout
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
type Ref struct {
|
||||
Path string
|
||||
Tag string
|
||||
Digest digest.Digest
|
||||
}
|
||||
|
||||
const prefix = "oci-layout://"
|
||||
|
||||
func Parse(s string) (Ref, bool, error) {
|
||||
if !strings.HasPrefix(s, prefix) {
|
||||
return Ref{}, false, nil
|
||||
}
|
||||
|
||||
localPath := strings.TrimPrefix(s, prefix)
|
||||
var out Ref
|
||||
|
||||
if i := strings.LastIndex(localPath, "@"); i >= 0 {
|
||||
after := localPath[i+1:]
|
||||
if reference.DigestRegexp.MatchString(after) {
|
||||
dgst, err := digest.Parse(after)
|
||||
if err != nil {
|
||||
return Ref{}, true, err
|
||||
}
|
||||
localPath, out.Digest = localPath[:i], dgst
|
||||
}
|
||||
}
|
||||
|
||||
if i := strings.LastIndex(localPath, ":"); i >= 0 {
|
||||
after := localPath[i+1:]
|
||||
if reference.TagRegexp.MatchString(after) {
|
||||
localPath, out.Tag = localPath[:i], after
|
||||
}
|
||||
}
|
||||
|
||||
out.Path = localPath
|
||||
if out.Tag == "" && out.Digest == "" {
|
||||
out.Tag = "latest"
|
||||
}
|
||||
return out, true, nil
|
||||
}
|
||||
|
||||
func (r Ref) String() string {
|
||||
s := prefix + r.Path
|
||||
if r.Tag != "" {
|
||||
return s + ":" + r.Tag
|
||||
}
|
||||
if r.Digest != "" {
|
||||
return s + "@" + r.Digest.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package ocilayout
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
s string
|
||||
path string
|
||||
dgst string
|
||||
tag string
|
||||
}{
|
||||
{
|
||||
s: "oci-layout:///path/to/oci/layout",
|
||||
path: "/path/to/oci/layout",
|
||||
tag: "latest",
|
||||
},
|
||||
{
|
||||
s: "oci-layout:///path/to/oci/layout:1.3",
|
||||
path: "/path/to/oci/layout",
|
||||
tag: "1.3",
|
||||
},
|
||||
{
|
||||
s: "oci-layout:///path/to/oci/layout@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
path: "/path/to/oci/layout",
|
||||
dgst: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
{
|
||||
s: "oci-layout:///path/to/oci/@/layout@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
path: "/path/to/oci/@/layout",
|
||||
dgst: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
{
|
||||
s: "oci-layout:///path/to/oci/@/layout",
|
||||
path: "/path/to/oci/@/layout",
|
||||
tag: "latest",
|
||||
},
|
||||
} {
|
||||
ref, ok, err := Parse(tt.s)
|
||||
require.True(t, ok)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.path, ref.Path, "comparing path: %s", tt.s)
|
||||
assert.Equal(t, tt.dgst, ref.Digest.String(), "comparing digest: %s", tt.s)
|
||||
assert.Equal(t, tt.tag, ref.Tag, "comparing tag: %s", tt.s)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user