imagetools: support oci-layout refs

Add oci-layout:// source and target support to imagetools create and
inspect while keeping merge, filter, and referrer logic shared.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi
2026-03-12 22:45:01 -07:00
parent 71edf28d8e
commit 9894189361
13 changed files with 869 additions and 192 deletions
+2 -3
View File
@@ -805,11 +805,10 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[
itpull := imagetools.New(imageopt) itpull := imagetools.New(imageopt)
ref, err := reference.ParseNormalizedNamed(names[0]) ref, err := imagetools.ParseLocation(names[0])
if err != nil { if err != nil {
return err return err
} }
ref = reference.TagNameOnly(ref)
srcs := make([]*imagetools.Source, len(descs)) srcs := make([]*imagetools.Source, len(descs))
for i, desc := range descs { for i, desc := range descs {
@@ -832,7 +831,7 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[
itpush := imagetools.New(imageopt) itpush := imagetools.New(imageopt)
for _, n := range names { for _, n := range names {
nn, err := reference.ParseNormalizedNamed(n) nn, err := imagetools.ParseLocation(n)
if err != nil { if err != nil {
return err return err
} }
+6 -33
View File
@@ -27,6 +27,7 @@ import (
"github.com/docker/buildx/util/buildflags" "github.com/docker/buildx/util/buildflags"
"github.com/docker/buildx/util/confutil" "github.com/docker/buildx/util/confutil"
"github.com/docker/buildx/util/dockerutil" "github.com/docker/buildx/util/dockerutil"
"github.com/docker/buildx/util/ocilayout"
"github.com/docker/buildx/util/osutil" "github.com/docker/buildx/util/osutil"
"github.com/docker/buildx/util/progress" "github.com/docker/buildx/util/progress"
"github.com/docker/buildx/util/sourcemeta" "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 // handle OCI layout
if localPath, ok := strings.CutPrefix(v.Path, "oci-layout://"); ok { 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 == "" { if dig == "" {
dig, err = resolveDigest(localPath, tag) dig, err = resolveDigest(localPath, tag)
if err != nil { if err != nil {
@@ -1402,38 +1407,6 @@ func isActive(ce *client.CacheOptionsEntry) bool {
return ce.Attrs["token"] != "" && (ce.Attrs["url"] != "" || ce.Attrs["url_v2"] != "") 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 { func defaultPlatform(bopts gateway.BuildOpts) *ocispecs.Platform {
pl := bopts.Workers[0].Platforms pl := bopts.Workers[0].Platforms
if len(pl) == 0 { if len(pl) == 0 {
-42
View File
@@ -40,48 +40,6 @@ func TestCacheOptions_DerivedVars(t *testing.T) {
}, CreateCaches(cacheFrom)) }, 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) { func TestCreateExports_RegistryUnpack(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
+28 -19
View File
@@ -10,7 +10,6 @@ import (
"github.com/containerd/containerd/v2/core/remotes" "github.com/containerd/containerd/v2/core/remotes"
"github.com/containerd/platforms" "github.com/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/buildx/builder" "github.com/docker/buildx/builder"
"github.com/docker/buildx/util/buildflags" "github.com/docker/buildx/util/buildflags"
"github.com/docker/buildx/util/cobrautil/completion" "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...) args = append(fileArgs, args...)
tags, err := parseRefs(in.tags) tags, err := parseLocations(in.tags)
if err != nil { if err != nil {
return err 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") 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 { if len(repos) == 1 {
for repo := range repos { for _, src := range srcs {
defaultRepo = &repo 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 { if defaultRepo == nil {
return errors.Errorf("multiple repositories specified, cannot infer repository for %q", args[i]) 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 != "" { 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 { if err != nil {
return err return err
} }
srcs[i].Ref = r srcs[i].Ref = r
sourceRefs = true sourceRefs = true
} else { } 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) eg, _ := errgroup.WithContext(ctx)
pw := progress.WithPrefix(printer, "internal", true) pw := progress.WithPrefix(printer, "internal", true)
tagsByRepo := map[string][]reference.Named{} tagsByRepo := map[string][]*imagetools.Location{}
for _, t := range tags { for _, t := range tags {
repo := t.Name() repo := t.Name()
tagsByRepo[repo] = append(tagsByRepo[repo], t) 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 { for _, desc := range manifests {
eg2.Go(func() error { 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)) 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, Ref: desc.Source.Ref,
Desc: desc.Descriptor, Desc: desc.Descriptor,
}, seed) }, 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 { 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 { for _, t := range repoTags {
sub.Log(1, fmt.Appendf(nil, "pushing %s to %s\n", desc.Digest.String(), t.String())) 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 { 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 return nil
@@ -302,10 +311,10 @@ func withMediaTypeKeyPrefix(ctx context.Context) context.Context {
return ctx return ctx
} }
func parseRefs(in []string) ([]reference.Named, error) { func parseLocations(in []string) ([]*imagetools.Location, error) {
refs := make([]reference.Named, len(in)) refs := make([]*imagetools.Location, len(in))
for i, in := range in { for i, in := range in {
n, err := reference.ParseNormalizedNamed(in) n, err := imagetools.ParseLocation(in)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -327,10 +336,10 @@ func parseSource(in string) (*imagetools.Source, error) {
return nil, err return nil, err
} }
ref, err := reference.ParseNormalizedNamed(in) loc, err := imagetools.ParseLocation(in)
if err == nil { if err == nil {
return &imagetools.Source{ return &imagetools.Source{
Ref: ref, Ref: loc,
}, nil }, nil
} else if !strings.HasPrefix(in, "{") { } else if !strings.HasPrefix(in, "{") {
return nil, err return nil, err
+178
View File
@@ -8,6 +8,7 @@ import (
"os/exec" "os/exec"
"path" "path"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/containerd/containerd/v2/core/content" "github.com/containerd/containerd/v2/core/content"
@@ -31,6 +32,10 @@ var imagetoolsTests = []func(t *testing.T, sb integration.Sandbox){
testImagetoolsCopyIndex, testImagetoolsCopyIndex,
testImagetoolsInspectAndFilter, testImagetoolsInspectAndFilter,
testImagetoolsCreatePlatformFilter, testImagetoolsCreatePlatformFilter,
testImagetoolsOCILayoutInspect,
testImagetoolsOCILayoutCreateSourceAndTarget,
testImagetoolsOCILayoutMergeSources,
testImagetoolsOCILayoutTargetDigest,
testImagetoolsAppend, testImagetoolsAppend,
testImagetoolsFile, testImagetoolsFile,
testImagetoolsAnnotation, testImagetoolsAnnotation,
@@ -359,6 +364,179 @@ func testImagetoolsCreatePlatformFilter(t *testing.T, sb integration.Sandbox) {
require.Equal(t, 1, attestationCount) 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))
}
// 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 // testImagetoolsAppend verifies create --append adds a new source onto an
// existing target image and rewrites it as a combined index. // existing target image and rewrites it as a combined index.
func testImagetoolsAppend(t *testing.T, sb integration.Sandbox) { func testImagetoolsAppend(t *testing.T, sb integration.Sandbox) {
+7
View File
@@ -15,6 +15,13 @@ func RegistryAuthForRef(ref string, auth authprovider.AuthConfigProvider) (strin
if auth == nil { if auth == nil {
return "", nil return "", nil
} }
loc, err := ParseLocation(ref)
if err != nil {
return "", err
}
if loc.IsOCILayout() {
return "", nil
}
r, err := parseRef(ref) r, err := parseRef(ref)
if err != nil { if err != nil {
return "", err return "", err
+157 -62
View File
@@ -5,7 +5,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"maps" "maps"
"net/url" "os"
"strings" "strings"
"github.com/containerd/containerd/v2/core/content" "github.com/containerd/containerd/v2/core/content"
@@ -13,7 +13,7 @@ import (
"github.com/containerd/containerd/v2/core/remotes" "github.com/containerd/containerd/v2/core/remotes"
"github.com/containerd/errdefs" "github.com/containerd/errdefs"
"github.com/containerd/platforms" "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/exporter/containerimage/exptypes"
"github.com/moby/buildkit/util/attestation" "github.com/moby/buildkit/util/attestation"
"github.com/moby/buildkit/util/contentutil" "github.com/moby/buildkit/util/contentutil"
@@ -38,7 +38,7 @@ var supportedArtifactTypes = map[string]struct{}{
type Source struct { type Source struct {
Desc ocispecs.Descriptor 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) { 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 { for i := range dts {
func(i int) { func(i int) {
eg.Go(func() error { 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 { if err != nil {
return err return err
} }
@@ -83,7 +83,7 @@ func (r *Resolver) combine(ctx context.Context, srcs []*Source, ann map[exptypes
p = &ocispecs.Platform{} p = &ocispecs.Platform{}
} }
if p.OS == "" || p.Architecture == "" { 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 return err
} }
} }
@@ -219,14 +219,20 @@ func (r *Resolver) combine(ctx context.Context, srcs []*Source, ann map[exptypes
}, sources, nil }, 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") 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 { if err != nil {
return errors.Wrapf(err, "failed to combine ref %s with digest %s", ref, desc.Digest) 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 { if err != nil {
return err return err
} }
@@ -245,72 +251,59 @@ func (r *Resolver) Push(ctx context.Context, ref reference.Named, desc ocispecs.
return err 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.in-toto+json", "intoto")
ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.oci.empty.v1+json", "empty") 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 := src.Desc
desc.Annotations = maps.Clone(desc.Annotations) desc.Annotations = maps.Clone(desc.Annotations)
if desc.Annotations == nil { if desc.Annotations == nil {
desc.Annotations = make(map[string]string) desc.Annotations = make(map[string]string)
} }
if src.Ref.IsRegistry() {
source, repo := u.Hostname(), strings.TrimPrefix(u.Path, "/") desc.Annotations["containerd.io/distribution.source."+src.Ref.Named().Name()] = src.Ref.Named().Name()
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())
} }
opts := []contentutil.CopyOption{ provider, err := r.providerForLocation(src.Ref)
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...)
if err != nil { if err != nil {
return err return err
} }
ingester, err := r.ingesterForLocation(dest)
if err != nil {
return err
}
recorder := &recordingReferrersProvider{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(recorder))
if err != nil {
return err
}
if dest.IsOCILayout() {
return r.writeRecordedReferrers(ctx, dest, recorder)
}
return nil 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 var manifest ocispecs.Manifest
if err := json.Unmarshal(dt, &manifest); err != nil { if err := json.Unmarshal(dt, &manifest); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
dt, err := r.GetDescriptor(ctx, in, manifest.Config) dt, err := r.GetDescriptor(ctx, loc, manifest.Config)
if err != nil { if err != nil {
return err return err
} }
@@ -450,15 +443,7 @@ func (r *Resolver) filterPlatforms(ctx context.Context, dt []byte, desc ocispecs
} }
src = defaultSource src = defaultSource
} }
f, err := r.resolver().Fetcher(ctx, src.Ref.String()) refs, err := r.FetchReferrers(ctx, src.Ref, d, remotes.WithReferrerArtifactTypes(artifactTypeAttestationManifest))
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))
if err != nil { if err != nil {
if errors.Is(err, errdefs.ErrNotFound) { if errors.Is(err, errdefs.ErrNotFound) {
continue continue
@@ -505,6 +490,116 @@ func (r *Resolver) filterPlatforms(ctx context.Context, dt []byte, desc ocispecs
return idxBytes, desc, mfstsWithSource, nil return idxBytes, desc, mfstsWithSource, nil
} }
type recordingReferrersProvider struct {
base referrersFunc
refs map[digest.Digest][]ocispecs.Descriptor
}
func (r *recordingReferrersProvider) Referrers(ctx context.Context, desc ocispecs.Descriptor) ([]ocispecs.Descriptor, error) {
out, err := r.base(ctx, desc)
if err != nil {
return nil, err
}
if r.refs == nil {
r.refs = map[digest.Digest][]ocispecs.Descriptor{}
}
r.refs[desc.Digest] = append(r.refs[desc.Digest], out...)
return out, nil
}
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 nil
}
return err
}
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() != "":
return idx.Put(desc)
case ref.Tag() != "":
return idx.Put(desc, ociindex.Tag(ref.Tag()))
default:
return idx.Put(desc, ociindex.Tag("latest"))
}
}
func (r *Resolver) writeRecordedReferrers(ctx context.Context, loc *Location, refs *recordingReferrersProvider) error {
if refs == nil || len(refs.refs) == 0 {
return nil
}
store, err := r.localStore(loc.OCILayout().Path)
if err != nil {
return err
}
idx := ociindex.NewStoreIndex(loc.OCILayout().Path)
for subject, manifests := range refs.refs {
fallback := ocispecs.Index{
Versioned: specs.Versioned{SchemaVersion: 2},
MediaType: ocispecs.MediaTypeImageIndex,
Manifests: manifests,
}
dt, err := json.Marshal(fallback)
if err != nil {
return err
}
desc := ocispecs.Descriptor{
MediaType: ocispecs.MediaTypeImageIndex,
Digest: digest.FromBytes(dt),
Size: int64(len(dt)),
}
w, err := store.Writer(ctx, content.WithRef(desc.Digest.String()), content.WithDescriptor(desc))
if err != nil && !errdefs.IsAlreadyExists(err) {
return err
}
if err == nil {
if err := content.Copy(ctx, w, bytes.NewReader(dt), desc.Size, desc.Digest); err != nil && !errdefs.IsAlreadyExists(err) {
return err
}
}
if err := idx.Put(desc, ociindex.Tag("sha256-"+subject.Encoded())); err != nil {
return err
}
}
return nil
}
func detectMediaType(dt []byte) (string, error) { func detectMediaType(dt []byte) (string, error) {
var mfst struct { var mfst struct {
MediaType string `json:"mediaType"` MediaType string `json:"mediaType"`
+171 -12
View File
@@ -3,19 +3,27 @@ package imagetools
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"io" "io"
"net/http" "net/http"
"sync"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/core/remotes" "github.com/containerd/containerd/v2/core/remotes"
"github.com/containerd/containerd/v2/core/remotes/docker" "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/containerd/log"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/docker/buildx/util/resolver" "github.com/docker/buildx/util/resolver"
"github.com/docker/buildx/util/resolver/auth" "github.com/docker/buildx/util/resolver/auth"
"github.com/moby/buildkit/client/ociindex"
"github.com/moby/buildkit/session/auth/authprovider" "github.com/moby/buildkit/session/auth/authprovider"
"github.com/moby/buildkit/util/contentutil" "github.com/moby/buildkit/util/contentutil"
"github.com/moby/buildkit/util/tracing" "github.com/moby/buildkit/util/tracing"
digest "github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1" ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@@ -25,9 +33,11 @@ type Opt struct {
} }
type Resolver struct { type Resolver struct {
auth docker.Authorizer auth docker.Authorizer
hosts docker.RegistryHosts hosts docker.RegistryHosts
buffer contentutil.Buffer buffer contentutil.Buffer
localStoreMu sync.Mutex
localStores map[string]content.Store
} }
func New(opt Opt) *Resolver { func New(opt Opt) *Resolver {
@@ -37,13 +47,14 @@ func New(opt Opt) *Resolver {
AuthConfig: opt.Auth, AuthConfig: opt.Auth,
} }
return &Resolver{ return &Resolver{
auth: auth, auth: auth,
hosts: resolver.NewRegistryConfig(opt.RegistryConfig), hosts: resolver.NewRegistryConfig(opt.RegistryConfig),
buffer: contentutil.NewBuffer(), 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{ return docker.NewResolver(docker.ResolverOptions{
Hosts: func(domain string) ([]docker.RegistryHost, error) { Hosts: func(domain string) ([]docker.RegistryHost, error) {
res, err := r.hosts(domain) 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 logger.Out = io.Discard
ctx = log.WithLogger(ctx, logrus.NewEntry(logger)) 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) ref, err := parseRef(in)
if err != nil { if err != nil {
return "", ocispecs.Descriptor{}, err return "", ocispecs.Descriptor{}, err
} }
in, desc, err := r.registryResolver().Resolve(ctx, ref.String())
in, desc, err := r.resolver().Resolve(ctx, ref.String())
if err != nil { if err != nil {
return "", ocispecs.Descriptor{}, err 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 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 { if err != nil {
return nil, ocispecs.Descriptor{}, err return nil, ocispecs.Descriptor{}, err
} }
return dt, desc, nil return dt, desc, nil
} }
func (r *Resolver) GetDescriptor(ctx context.Context, in string, desc ocispecs.Descriptor) ([]byte, error) { func (r *Resolver) GetDescriptor(ctx context.Context, loc *Location, desc ocispecs.Descriptor) ([]byte, error) {
fetcher, err := r.resolver().Fetcher(ctx, in) fetcher, err := r.fetcherForLocation(ctx, loc)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -113,6 +135,143 @@ func (r *Resolver) GetDescriptor(ctx context.Context, in string, desc ocispecs.D
return buf.Bytes(), nil 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 r.fetchOCILayoutReferrers(ctx, loc, dgst)
}
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 (r *Resolver) fetchOCILayoutReferrers(ctx context.Context, loc *Location, dgst digest.Digest) ([]ocispecs.Descriptor, error) {
idx := ociindex.NewStoreIndex(loc.OCILayout().Path)
// TODO: temporary fallback tag, should use annotations instead
desc, err := idx.Get("sha256-" + dgst.Encoded())
if err != nil {
return nil, err
}
if desc == nil {
return nil, errors.WithStack(errdefs.ErrNotFound)
}
dt, err := r.GetDescriptor(ctx, loc, *desc)
if err != nil {
return nil, err
}
if desc.MediaType != ocispecs.MediaTypeImageIndex {
return nil, errors.Errorf("unsupported referrers media type %s", desc.MediaType)
}
var referrersIndex ocispecs.Index
if err := json.Unmarshal(dt, &referrersIndex); err != nil {
return nil, err
}
return referrersIndex.Manifests, nil
}
func parseRef(s string) (reference.Named, error) { func parseRef(s string) (reference.Named, error) {
ref, err := reference.ParseNormalizedNamed(s) ref, err := reference.ParseNormalizedNamed(s)
if err != nil { if err != nil {
+10 -11
View File
@@ -16,7 +16,6 @@ import (
"github.com/containerd/containerd/v2/core/images" "github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/core/remotes" "github.com/containerd/containerd/v2/core/remotes"
"github.com/containerd/platforms" "github.com/containerd/platforms"
"github.com/distribution/reference"
intoto "github.com/in-toto/in-toto-golang/in_toto" intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/moby/buildkit/util/contentutil" "github.com/moby/buildkit/util/contentutil"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
@@ -43,8 +42,13 @@ type contentCache interface {
content.Ingester 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 { type loader struct {
resolver remotes.Resolver resolver loaderResolver
cache contentCache cache contentCache
} }
@@ -78,7 +82,7 @@ type result struct {
assets map[string]asset assets map[string]asset
} }
func newLoader(resolver remotes.Resolver) *loader { func newLoader(resolver loaderResolver) *loader {
return &loader{ return &loader{
resolver: resolver, resolver: resolver,
cache: contentutil.NewBuffer(), cache: contentutil.NewBuffer(),
@@ -86,22 +90,17 @@ func newLoader(resolver remotes.Resolver) *loader {
} }
func (l *loader) Load(ctx context.Context, ref string) (*result, error) { func (l *loader) Load(ctx context.Context, ref string) (*result, error) {
named, err := parseRef(ref) loc, err := ParseLocation(ref)
if err != nil { if err != nil {
return nil, err return nil, err
} }
_, desc, err := l.resolver.Resolve(ctx, named.String()) _, desc, err := l.resolver.Resolve(ctx, loc.String())
if err != nil { if err != nil {
return nil, err return nil, err
} }
canonical, err := reference.WithDigest(named, desc.Digest) fetcher, err := l.resolver.Fetcher(ctx, loc.String())
if err != nil {
return nil, err
}
fetcher, err := l.resolver.Fetcher(ctx, canonical.String())
if err != nil { if err != nil {
return nil, err return nil, err
} }
+187
View File
@@ -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://")
}
+13 -10
View File
@@ -12,7 +12,6 @@ import (
"github.com/containerd/containerd/v2/core/images" "github.com/containerd/containerd/v2/core/images"
"github.com/containerd/platforms" "github.com/containerd/platforms"
"github.com/distribution/reference"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1" ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
) )
@@ -27,7 +26,7 @@ type Printer struct {
format string format string
raw []byte raw []byte
ref reference.Named ref *Location
manifest ocispecs.Descriptor manifest ocispecs.Descriptor
index ocispecs.Index index ocispecs.Index
} }
@@ -35,7 +34,7 @@ type Printer struct {
func NewPrinter(ctx context.Context, opt Opt, name string, format string) (*Printer, error) { func NewPrinter(ctx context.Context, opt Opt, name string, format string) (*Printer, error) {
resolver := New(opt) resolver := New(opt)
ref, err := parseRef(name) ref, err := ParseLocation(name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -83,7 +82,7 @@ func (p *Printer) Print(raw bool, out io.Writer) error {
return nil 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 { if err != nil {
return err return err
} }
@@ -125,11 +124,11 @@ func (p *Printer) Print(raw bool, out io.Writer) error {
switch { switch {
// TODO: print formatted config // TODO: print formatted config
case strings.HasPrefix(format, "{{.Manifest"): case isWholeManifestTemplate(format):
w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0) w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0)
_, _ = fmt.Fprintf(w, "Name:\t%s\n", p.ref.String()) _, _ = fmt.Fprintf(w, "Name:\t%s\n", p.ref.String())
switch { switch {
case strings.HasPrefix(format, "{{.Manifest"): case isWholeManifestTemplate(format):
_, _ = fmt.Fprintf(w, "MediaType:\t%s\n", p.manifest.MediaType) _, _ = fmt.Fprintf(w, "MediaType:\t%s\n", p.manifest.MediaType)
_, _ = fmt.Fprintf(w, "Digest:\t%s\n", p.manifest.Digest) _, _ = fmt.Fprintf(w, "Digest:\t%s\n", p.manifest.Digest)
_ = w.Flush() _ = w.Flush()
@@ -162,6 +161,10 @@ func (p *Printer) Print(raw bool, out io.Writer) error {
return nil return nil
} }
func isWholeManifestTemplate(format string) bool {
return strings.TrimSpace(format) == "{{.Manifest}}"
}
func (p *Printer) printManifestList(out io.Writer) error { func (p *Printer) printManifestList(out io.Writer) error {
w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0) w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0)
_, _ = fmt.Fprintf(w, "\t\n") _, _ = fmt.Fprintf(w, "\t\n")
@@ -173,11 +176,11 @@ func (p *Printer) printManifestList(out io.Writer) error {
if i != 0 { if i != 0 {
_, _ = fmt.Fprintf(w, "\t\n") _, _ = fmt.Fprintf(w, "\t\n")
} }
cr, err := reference.WithDigest(p.ref, m.Digest) name := p.ref.String()
if err != nil { if ref, err := p.ref.WithDigest(m.Digest); err == nil {
return err 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) _, _ = fmt.Fprintf(w, "%sMediaType:\t%s\n", defaultPfx, m.MediaType)
if p := m.Platform; p != nil { if p := m.Platform; p != nil {
_, _ = fmt.Fprintf(w, "%sPlatform:\t%s\n", defaultPfx, platforms.Format(*p)) _, _ = fmt.Fprintf(w, "%sPlatform:\t%s\n", defaultPfx, platforms.Format(*p))
+60
View File
@@ -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
}
+50
View File
@@ -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)
}
}