diff --git a/commands/imagetools/create.go b/commands/imagetools/create.go new file mode 100644 index 00000000..e028a6b1 --- /dev/null +++ b/commands/imagetools/create.go @@ -0,0 +1,41 @@ +package commands + +import ( + "github.com/docker/cli/cli/command" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +type createOptions struct { + files []string + tags []string + dryrun bool + append bool +} + +func runCreate(dockerCli command.Cli, in createOptions, args []string) error { + return errors.Errorf("not-implemented") +} + +func createCmd(dockerCli command.Cli) *cobra.Command { + var options createOptions + + cmd := &cobra.Command{ + Use: "create [OPTIONS] [SOURCE...]", + Short: "Create a new image based on source images", + RunE: func(cmd *cobra.Command, args []string) error { + return runCreate(dockerCli, options, args) + }, + } + + flags := cmd.Flags() + + flags.StringArrayVarP(&options.files, "file", "f", []string{}, "Read source descriptor from file") + flags.StringArrayVarP(&options.tags, "tag", "t", []string{}, "Set reference for new image") + flags.BoolVar(&options.dryrun, "dry-run", false, "Show final image instead of pushing") + flags.BoolVar(&options.append, "append", false, "Append to existing manifest") + + _ = flags + + return cmd +} diff --git a/commands/imagetools/inspect.go b/commands/imagetools/inspect.go new file mode 100644 index 00000000..55843e5d --- /dev/null +++ b/commands/imagetools/inspect.go @@ -0,0 +1,68 @@ +package commands + +import ( + "fmt" + "os" + + "github.com/containerd/containerd/images" + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/moby/buildkit/util/appcontext" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/spf13/cobra" + "github.com/tonistiigi/buildx/util/imagetools" +) + +type inspectOptions struct { + raw bool +} + +func runInspect(dockerCli command.Cli, in inspectOptions, name string) error { + ctx := appcontext.Context() + + r := imagetools.New(imagetools.Opt{ + Auth: dockerCli.ConfigFile(), + }) + + dt, desc, err := r.Get(ctx, name) + if err != nil { + return err + } + + if in.raw { + fmt.Printf("%s\n", dt) + return nil + } + + switch desc.MediaType { + // case images.MediaTypeDockerSchema2Manifest, specs.MediaTypeImageManifest: + // TODO: handle distribution manifest and schema1 + case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex: + imagetools.PrintManifestList(dt, desc, name, os.Stdout) + default: + fmt.Printf("%s\n", dt) + } + + return nil +} + +func inspectCmd(dockerCli command.Cli) *cobra.Command { + var options inspectOptions + + cmd := &cobra.Command{ + Use: "inspect [OPTIONS] NAME", + Short: "Show details of image in the registry", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runInspect(dockerCli, options, args[0]) + }, + } + + flags := cmd.Flags() + + flags.BoolVar(&options.raw, "raw", false, "Show original JSON manifest") + + _ = flags + + return cmd +} diff --git a/commands/imagetools/root.go b/commands/imagetools/root.go new file mode 100644 index 00000000..ad1aa563 --- /dev/null +++ b/commands/imagetools/root.go @@ -0,0 +1,20 @@ +package commands + +import ( + "github.com/docker/cli/cli/command" + "github.com/spf13/cobra" +) + +func RootCmd(dockerCli command.Cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "imagetools", + Short: "Commands to work on images in registry", + } + + cmd.AddCommand( + inspectCmd(dockerCli), + createCmd(dockerCli), + ) + + return cmd +} diff --git a/commands/root.go b/commands/root.go index dd754c98..eb8ccd12 100644 --- a/commands/root.go +++ b/commands/root.go @@ -4,6 +4,7 @@ import ( "github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli/command" "github.com/spf13/cobra" + imagetoolscmd "github.com/tonistiigi/buildx/commands/imagetools" ) func NewRootCmd(name string, isPlugin bool, dockerCli command.Cli) *cobra.Command { @@ -31,5 +32,6 @@ func addCommands(cmd *cobra.Command, dockerCli command.Cli) { useCmd(dockerCli), inspectCmd(dockerCli), stopCmd(dockerCli), + imagetoolscmd.RootCmd(dockerCli), ) } diff --git a/go.mod b/go.mod index dea01013..1d87ab66 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/denisenkom/go-mssqldb v0.0.0-20190315220205-a8ed825ac853 // indirect github.com/docker/cli v0.0.0-20190321234815-f40f9c240ab0 github.com/docker/compose-on-kubernetes v0.4.19-0.20190128150448-356b2919c496 // indirect + github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible github.com/docker/docker v1.14.0-0.20190410063227-d9d9eccdc862 github.com/docker/docker-credential-helpers v0.6.1 // indirect github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect diff --git a/util/imagetools/inspect.go b/util/imagetools/inspect.go new file mode 100644 index 00000000..e73b9809 --- /dev/null +++ b/util/imagetools/inspect.go @@ -0,0 +1,92 @@ +package imagetools + +import ( + "bytes" + "context" + "io" + "net/http" + + "github.com/containerd/containerd/remotes" + "github.com/containerd/containerd/remotes/docker" + clitypes "github.com/docker/cli/cli/config/types" + "github.com/docker/distribution/reference" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +type Auth interface { + GetAuthConfig(registryHostname string) (clitypes.AuthConfig, error) +} + +type Opt struct { + Auth Auth +} + +type Resolver struct { + r remotes.Resolver +} + +func New(opt Opt) *Resolver { + resolver := docker.NewResolver(docker.ResolverOptions{ + Client: http.DefaultClient, + Credentials: toCredentialsFunc(opt.Auth), + }) + return &Resolver{ + r: resolver, + } +} + +func (r *Resolver) Get(ctx context.Context, in string) ([]byte, ocispec.Descriptor, error) { + ref, err := parseRef(in) + if err != nil { + return nil, ocispec.Descriptor{}, err + } + + in, desc, err := r.r.Resolve(ctx, ref.String()) + if err != nil { + return nil, ocispec.Descriptor{}, err + } + + fetcher, err := r.r.Fetcher(ctx, in) + if err != nil { + return nil, ocispec.Descriptor{}, err + } + + rc, err := fetcher.Fetch(ctx, desc) + if err != nil { + return nil, ocispec.Descriptor{}, err + } + + buf := &bytes.Buffer{} + _, err = io.Copy(buf, rc) + rc.Close() + if err != nil { + return nil, ocispec.Descriptor{}, err + } + + return buf.Bytes(), desc, nil +} + +func parseRef(s string) (reference.Named, error) { + ref, err := reference.ParseNormalizedNamed(s) + if err != nil { + return nil, err + } + ref = reference.TagNameOnly(ref) + return ref, nil +} + +func toCredentialsFunc(a Auth) func(string) (string, string, error) { + return func(host string) (string, string, error) { + if host == "registry-1.docker.io" { + host = "https://index.docker.io/v1/" + } + ac, err := a.GetAuthConfig(host) + if err != nil { + return "", "", err + } + if ac.IdentityToken != "" { + return "", ac.IdentityToken, nil + } + return ac.Username, ac.Password, nil + } +} diff --git a/util/imagetools/printers.go b/util/imagetools/printers.go new file mode 100644 index 00000000..633b017d --- /dev/null +++ b/util/imagetools/printers.go @@ -0,0 +1,74 @@ +package imagetools + +import ( + "encoding/json" + "fmt" + "io" + "os" + "strings" + "text/tabwriter" + + "github.com/containerd/containerd/platforms" + "github.com/docker/distribution/reference" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +func PrintManifestList(dt []byte, desc ocispec.Descriptor, refstr string, out io.Writer) error { + ref, err := parseRef(refstr) + if err != nil { + return err + } + + var mfst ocispec.Index + if err := json.Unmarshal(dt, &mfst); err != nil { + return err + } + + w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0) + + fmt.Fprintf(w, "Name:\t%s\n", ref.String()) + fmt.Fprintf(w, "MediaType:\t%s\n", desc.MediaType) + fmt.Fprintf(w, "Digest:\t%s\n", desc.Digest) + fmt.Fprintf(w, "\t\n") + + fmt.Fprintf(w, "Manifests:\t\n") + w.Flush() + + pfx := " " + + w = tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) + for i, m := range mfst.Manifests { + if i != 0 { + fmt.Fprintf(w, "\t\n") + } + cr, err := reference.WithDigest(ref, m.Digest) + if err != nil { + return err + } + fmt.Fprintf(w, "%sName:\t%s\n", pfx, cr.String()) + fmt.Fprintf(w, "%sMediaType:\t%s\n", pfx, m.MediaType) + if p := m.Platform; p != nil { + fmt.Fprintf(w, "%sPlatform:\t%s\n", pfx, platforms.Format(*p)) + if p.OSVersion != "" { + fmt.Fprintf(w, "%sOSVersion:\t%s\n", pfx, p.OSVersion) + } + if len(p.OSFeatures) > 0 { + fmt.Fprintf(w, "%sOSFeatures:\t%s\n", pfx, strings.Join(p.OSFeatures, ", ")) + } + if len(m.URLs) > 0 { + fmt.Fprintf(w, "%sURLs:\t%s\n", pfx, strings.Join(m.URLs, ", ")) + } + if len(m.Annotations) > 0 { + w.Flush() + w2 := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) + pfx2 := pfx + " " + for k, v := range m.Annotations { + fmt.Fprintf(w2, "%s%s:\t%s\n", pfx2, k, v) + } + w2.Flush() + } + } + } + + return w.Flush() +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 27fdb729..aa38751b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -28,10 +28,16 @@ github.com/beorn7/perks/quantile github.com/containerd/console # github.com/containerd/containerd v1.3.0-0.20190321141026-ceba56893a76 github.com/containerd/containerd/platforms +github.com/containerd/containerd/images +github.com/containerd/containerd/remotes +github.com/containerd/containerd/remotes/docker github.com/containerd/containerd/errdefs github.com/containerd/containerd/log github.com/containerd/containerd/content github.com/containerd/containerd/content/local +github.com/containerd/containerd/labels +github.com/containerd/containerd/reference +github.com/containerd/containerd/version github.com/containerd/containerd/filters github.com/containerd/containerd/sys github.com/containerd/containerd/api/services/content/v1 @@ -40,10 +46,7 @@ github.com/containerd/containerd/services/content/contentserver github.com/containerd/containerd/containers github.com/containerd/containerd/oci github.com/containerd/containerd -github.com/containerd/containerd/images github.com/containerd/containerd/namespaces -github.com/containerd/containerd/remotes -github.com/containerd/containerd/remotes/docker github.com/containerd/containerd/mount github.com/containerd/containerd/snapshots github.com/containerd/containerd/api/services/containers/v1 @@ -74,9 +77,6 @@ github.com/containerd/containerd/rootfs github.com/containerd/containerd/runtime/linux/runctypes github.com/containerd/containerd/runtime/v2/runc/options github.com/containerd/containerd/snapshots/proxy -github.com/containerd/containerd/labels -github.com/containerd/containerd/reference -github.com/containerd/containerd/version github.com/containerd/containerd/api/types/task github.com/containerd/containerd/events/exchange github.com/containerd/containerd/identifiers @@ -105,13 +105,13 @@ github.com/docker/cli/cli/flags github.com/docker/cli/cli github.com/docker/cli/cli/context/docker github.com/docker/cli/opts +github.com/docker/cli/cli/config/types github.com/docker/cli/cli/compose/interpolation github.com/docker/cli/cli/compose/schema github.com/docker/cli/cli/compose/template github.com/docker/cli/cli/config github.com/docker/cli/cli/config/configfile github.com/docker/cli/cli/connhelper -github.com/docker/cli/cli/config/types github.com/docker/cli/cli/context github.com/docker/cli/cli/context/kubernetes github.com/docker/cli/cli/context/store @@ -138,6 +138,7 @@ github.com/docker/compose-on-kubernetes/api/compose/clone github.com/docker/compose-on-kubernetes/api/compose/impersonation # github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible github.com/docker/distribution/reference +github.com/docker/distribution/digestset github.com/docker/distribution/manifest/manifestlist github.com/docker/distribution github.com/docker/distribution/manifest/schema2 @@ -147,7 +148,6 @@ github.com/docker/distribution/registry/client github.com/docker/distribution/registry/client/auth github.com/docker/distribution/registry/client/transport github.com/docker/distribution/registry/client/auth/challenge -github.com/docker/distribution/digestset github.com/docker/distribution/manifest github.com/docker/distribution/registry/storage/cache github.com/docker/distribution/registry/storage/cache/memory @@ -391,13 +391,13 @@ golang.org/x/crypto/pbkdf2 # golang.org/x/net v0.0.0-20180906233101-161cd47e91fd golang.org/x/net/http2 golang.org/x/net/context +golang.org/x/net/context/ctxhttp golang.org/x/net/trace golang.org/x/net/http/httpguts golang.org/x/net/http2/hpack golang.org/x/net/idna golang.org/x/net/proxy golang.org/x/net/internal/timeseries -golang.org/x/net/context/ctxhttp golang.org/x/net/internal/socks # golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f golang.org/x/sync/errgroup