diff --git a/client/client_test.go b/client/client_test.go index 920b2153..1eca18b4 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -33,6 +33,7 @@ import ( "github.com/moby/buildkit/session" "github.com/moby/buildkit/session/secrets/secretsprovider" "github.com/moby/buildkit/session/sshforward/sshprovider" + "github.com/moby/buildkit/util/contentutil" "github.com/moby/buildkit/util/testutil" "github.com/moby/buildkit/util/testutil/httpserver" "github.com/moby/buildkit/util/testutil/integration" @@ -85,6 +86,7 @@ func TestClientIntegration(t *testing.T) { testSSHMount, testStdinClosed, testHostnameLookup, + testPushByDigest, testBasicInlineCacheImportExport, testExportBusyboxLocal, }, @@ -373,6 +375,50 @@ func testNetworkMode(t *testing.T, sb integration.Sandbox) { require.Contains(t, err.Error(), "network.host is not allowed") } +func testPushByDigest(t *testing.T, sb integration.Sandbox) { + requiresLinux(t) + c, err := New(context.TODO(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + registry, err := sb.NewRegistry() + if errors.Cause(err) == integration.ErrorRequirements { + t.Skip(err.Error()) + } + require.NoError(t, err) + + st := llb.Scratch().File(llb.Mkfile("foo", 0600, []byte("data"))) + + def, err := st.Marshal() + require.NoError(t, err) + + name := registry + "/foo/bar" + + resp, err := c.Solve(context.TODO(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: "image", + Attrs: map[string]string{ + "name": name, + "push": "true", + "push-by-digest": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + _, _, err = contentutil.ProviderFromRef(name + ":latest") + require.Error(t, err) + + desc, _, err := contentutil.ProviderFromRef(name + "@" + resp.ExporterResponse["containerimage.digest"]) + require.NoError(t, err) + + require.Equal(t, resp.ExporterResponse["containerimage.digest"], desc.Digest.String()) + require.Equal(t, images.MediaTypeDockerSchema2Manifest, desc.MediaType) + require.True(t, desc.Size > 0) +} + func testFrontendImageNaming(t *testing.T, sb integration.Sandbox) { requiresLinux(t) c, err := New(context.TODO(), sb.Address()) diff --git a/exporter/containerimage/export.go b/exporter/containerimage/export.go index a440f2d0..6e8f9525 100644 --- a/exporter/containerimage/export.go +++ b/exporter/containerimage/export.go @@ -16,10 +16,11 @@ import ( ) const ( - keyImageName = "name" - keyPush = "push" - keyInsecure = "registry.insecure" - ociTypes = "oci-mediatypes" + keyImageName = "name" + keyPush = "push" + keyPushByDigest = "push-by-digest" + keyInsecure = "registry.insecure" + ociTypes = "oci-mediatypes" ) type Opt struct { @@ -58,6 +59,16 @@ func (e *imageExporter) Resolve(ctx context.Context, opt map[string]string) (exp return nil, errors.Wrapf(err, "non-bool value specified for %s", k) } i.push = b + case keyPushByDigest: + if v == "" { + i.pushByDigest = true + continue + } + b, err := strconv.ParseBool(v) + if err != nil { + return nil, errors.Wrapf(err, "non-bool value specified for %s", k) + } + i.pushByDigest = b case keyInsecure: if v == "" { i.insecure = true @@ -90,11 +101,12 @@ func (e *imageExporter) Resolve(ctx context.Context, opt map[string]string) (exp type imageExporterInstance struct { *imageExporter - targetName string - push bool - insecure bool - ociTypes bool - meta map[string][]byte + targetName string + push bool + pushByDigest bool + insecure bool + ociTypes bool + meta map[string][]byte } func (e *imageExporterInstance) Name() string { @@ -146,7 +158,7 @@ func (e *imageExporterInstance) Export(ctx context.Context, src exporter.Source) tagDone(nil) } if e.push { - if err := push.Push(ctx, e.opt.SessionManager, e.opt.ImageWriter.ContentStore(), desc.Digest, targetName, e.insecure, e.opt.ResolverOpt); err != nil { + if err := push.Push(ctx, e.opt.SessionManager, e.opt.ImageWriter.ContentStore(), desc.Digest, targetName, e.insecure, e.opt.ResolverOpt, e.pushByDigest); err != nil { return nil, err } } diff --git a/util/push/push.go b/util/push/push.go index 735fced1..54dc5061 100644 --- a/util/push/push.go +++ b/util/push/push.go @@ -19,6 +19,7 @@ import ( "github.com/moby/buildkit/util/resolver" digest "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -40,7 +41,7 @@ func getCredentialsFunc(ctx context.Context, sm *session.Manager) func(string) ( } } -func Push(ctx context.Context, sm *session.Manager, cs content.Provider, dgst digest.Digest, ref string, insecure bool, rfn resolver.ResolveOptionsFunc) error { +func Push(ctx context.Context, sm *session.Manager, cs content.Provider, dgst digest.Digest, ref string, insecure bool, rfn resolver.ResolveOptionsFunc, byDigest bool) error { desc := ocispec.Descriptor{ Digest: dgst, } @@ -48,7 +49,15 @@ func Push(ctx context.Context, sm *session.Manager, cs content.Provider, dgst di if err != nil { return err } - ref = reference.TagNameOnly(parsed).String() + if byDigest && !reference.IsNameOnly(parsed) { + return errors.Errorf("can't push tagged ref %s by digest", parsed.String()) + } + + if byDigest { + ref = parsed.Name() + } else { + ref = reference.TagNameOnly(parsed).String() + } opt := rfn(ref) opt.Credentials = getCredentialsFunc(ctx, sm)