From bfde944f9c659722e1ea298eb2cb9f5a98d338f5 Mon Sep 17 00:00:00 2001 From: Shiwei Zhang Date: Mon, 29 Mar 2021 14:31:48 +0800 Subject: [PATCH] support dry run Signed-off-by: Shiwei Zhang --- cmd/oras/push.go | 12 ++++- internal/resolver/dummy.go | 103 +++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 internal/resolver/dummy.go diff --git a/cmd/oras/push.go b/cmd/oras/push.go index eec1d293d..509ddf886 100644 --- a/cmd/oras/push.go +++ b/cmd/oras/push.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" + iresolver "github.com/deislabs/oras/internal/resolver" "github.com/deislabs/oras/pkg/content" ctxo "github.com/deislabs/oras/pkg/context" "github.com/deislabs/oras/pkg/oras" @@ -33,6 +34,7 @@ type pushOptions struct { artifactRefs []string pathValidationDisabled bool verbose bool + dryRun bool debug bool configs []string @@ -88,6 +90,7 @@ Example - Push file to the HTTP registry: cmd.Flags().StringVarP(&opts.password, "password", "p", "", "registry password") cmd.Flags().BoolVarP(&opts.insecure, "insecure", "", false, "allow connections to SSL registry without certs") cmd.Flags().BoolVarP(&opts.plainHTTP, "plain-http", "", false, "use plain http and not https") + cmd.Flags().BoolVarP(&opts.dryRun, "dry-run", "", false, "push to a dummy registry instead of the real remote") return cmd } @@ -99,9 +102,16 @@ func runPush(opts pushOptions) error { ctx = ctxo.WithLoggerDiscarded(ctx) } + // specify resolver + var resolver remotes.Resolver + if opts.dryRun { + resolver = iresolver.Dummy() + } else { + resolver = newResolver(opts.username, opts.password, opts.insecure, opts.plainHTTP, opts.configs...) + } + // bake artifact var pushOpts []oras.PushOpt - resolver := newResolver(opts.username, opts.password, opts.insecure, opts.plainHTTP, opts.configs...) if opts.artifactType != "" { manifests, err := loadReferences(ctx, resolver, opts.artifactRefs) if err != nil { diff --git a/internal/resolver/dummy.go b/internal/resolver/dummy.go new file mode 100644 index 000000000..39267eaea --- /dev/null +++ b/internal/resolver/dummy.go @@ -0,0 +1,103 @@ +package resolver + +import ( + "context" + "io" + "time" + + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/remotes" + artifactspec "github.com/notaryproject/artifacts/specs-go/v2" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" +) + +// dummyResolver is a dummy resolver, which resolves nothing. +// It accepts any pushes but ignores them. +type dummyResolver struct{} + +var dummyResolverInstance = &dummyResolver{} + +// Dummy creates a new dummy resolver +func Dummy() remotes.Resolver { + return dummyResolverInstance +} + +func (r *dummyResolver) Resolve(ctx context.Context, ref string) (name string, desc ocispec.Descriptor, err error) { + return "", ocispec.Descriptor{}, errors.Wrap(errdefs.ErrNotFound, "dummy resolver") +} + +func (r *dummyResolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) { + return remotes.FetcherFunc(func(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) { + return nil, errors.Wrap(errdefs.ErrNotFound, "dummy resolver") + }), nil +} + +// Pusher returns a new pusher for the provided reference +func (r *dummyResolver) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) { + return remotes.PusherFunc(func(ctx context.Context, desc ocispec.Descriptor) (content.Writer, error) { + now := time.Now() + return &dummyWriter{ + actual: digest.Canonical.Digester(), + status: content.Status{ + Ref: ref, + Total: desc.Size, + Expected: desc.Digest, + StartedAt: now, + UpdatedAt: now, + }, + }, nil + }), nil +} + +// Discoverer returns a new discoverer for the provided reference +func (r *dummyResolver) Discoverer(ctx context.Context, ref string) (remotes.Discoverer, error) { + return remotes.DiscovererFunc(func(ctx context.Context, desc ocispec.Descriptor, artifactType string) ([]artifactspec.Artifact, error) { + return nil, errors.Wrap(errdefs.ErrNotFound, "dummy resolver") + }), nil +} + +type dummyWriter struct { + actual digest.Digester + status content.Status +} + +func (dw *dummyWriter) Write(p []byte) (n int, err error) { + n, err = dw.actual.Hash().Write(p) + dw.status.Offset += int64(n) + dw.status.UpdatedAt = time.Now() + return +} + +func (dw *dummyWriter) Close() error { + return nil +} + +func (dw *dummyWriter) Status() (content.Status, error) { + return dw.status, nil +} + +func (dw *dummyWriter) Digest() digest.Digest { + return dw.status.Expected +} + +func (dw *dummyWriter) Commit(ctx context.Context, size int64, expected digest.Digest, opts ...content.Opt) error { + if size > 0 && size != dw.status.Offset { + return errors.Errorf("unexpected size %d, expected %d", dw.status.Offset, size) + } + if expected == "" { + expected = dw.status.Expected + } + + actual := dw.actual.Digest() + if actual != expected { + return errors.Errorf("got digest %s, expected %s", actual, expected) + } + return nil +} + +func (dw *dummyWriter) Truncate(size int64) error { + return errors.New("cannot truncate dummy upload") +}