mirror of
https://github.com/docker/cli.git
synced 2026-01-26 15:41:42 +03:00
add manifest command
Enable inspection (aka "shallow pull") of images' manifest info, and also the creation of manifest lists (aka "fat manifests"). The workflow for creating a manifest list will be: `docker manifest create new-list-ref-name image-ref [image-ref...]` `docker manifest annotate new-list-ref-name image-ref --os linux --arch arm` `docker manifest push new-list-ref-name` The annotate step is optional. Most architectures are fine by default. There is also a `manifest inspect` command to allow for a "shallow pull" of an image's manifest: `docker manifest inspect manifest-or-manifest_list`. To be more in line with the existing external manifest tool, there is also a `-v` option for inspect that will show information depending on what the reference maps to (list or single manifest). Signed-off-by: Christy Perez <christy@linux.vnet.ibm.com> Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
committed by
Christy Norman Perez
parent
17886d7547
commit
02719bdbb5
@@ -5,16 +5,22 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/config"
|
||||
cliconfig "github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
manifeststore "github.com/docker/cli/cli/manifest/store"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
dopts "github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/sockets"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
@@ -45,6 +51,8 @@ type Cli interface {
|
||||
ClientInfo() ClientInfo
|
||||
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||
DefaultVersion() string
|
||||
ManifestStore() manifeststore.Store
|
||||
RegistryClient(bool) registryclient.RegistryClient
|
||||
}
|
||||
|
||||
// DockerCli is an instance the docker command line client.
|
||||
@@ -114,6 +122,21 @@ func (cli *DockerCli) ClientInfo() ClientInfo {
|
||||
return cli.clientInfo
|
||||
}
|
||||
|
||||
// ManifestStore returns a store for local manifests
|
||||
func (cli *DockerCli) ManifestStore() manifeststore.Store {
|
||||
// TODO: support override default location from config file
|
||||
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
|
||||
}
|
||||
|
||||
// RegistryClient returns a client for communicating with a Docker distribution
|
||||
// registry
|
||||
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
|
||||
resolver := func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
|
||||
return ResolveAuthConfig(ctx, cli, index)
|
||||
}
|
||||
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
|
||||
}
|
||||
|
||||
// Initialize the dockerCli runs initialization that must happen after command
|
||||
// line flags are parsed.
|
||||
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/docker/cli/cli/command/config"
|
||||
"github.com/docker/cli/cli/command/container"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/command/manifest"
|
||||
"github.com/docker/cli/cli/command/network"
|
||||
"github.com/docker/cli/cli/command/node"
|
||||
"github.com/docker/cli/cli/command/plugin"
|
||||
@@ -39,12 +40,15 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
|
||||
image.NewImageCommand(dockerCli),
|
||||
image.NewBuildCommand(dockerCli),
|
||||
|
||||
// node
|
||||
node.NewNodeCommand(dockerCli),
|
||||
// manifest
|
||||
manifest.NewManifestCommand(dockerCli),
|
||||
|
||||
// network
|
||||
network.NewNetworkCommand(dockerCli),
|
||||
|
||||
// node
|
||||
node.NewNodeCommand(dockerCli),
|
||||
|
||||
// plugin
|
||||
plugin.NewPluginCommand(dockerCli),
|
||||
|
||||
|
||||
93
cli/command/manifest/annotate.go
Normal file
93
cli/command/manifest/annotate.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/store"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type annotateOptions struct {
|
||||
target string // the target manifest list name (also transaction ID)
|
||||
image string // the manifest to annotate within the list
|
||||
variant string // an architecture variant
|
||||
os string
|
||||
arch string
|
||||
osFeatures []string
|
||||
}
|
||||
|
||||
// NewAnnotateCommand creates a new `docker manifest annotate` command
|
||||
func newAnnotateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts annotateOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "annotate [OPTIONS] MANIFEST_LIST MANIFEST",
|
||||
Short: "Add additional information to a local image manifest",
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.target = args[0]
|
||||
opts.image = args[1]
|
||||
return runManifestAnnotate(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.StringVar(&opts.os, "os", "", "Set operating system")
|
||||
flags.StringVar(&opts.arch, "arch", "", "Set architecture")
|
||||
flags.StringSliceVar(&opts.osFeatures, "os-features", []string{}, "Set operating system feature")
|
||||
flags.StringVar(&opts.variant, "variant", "", "Set architecture variant")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runManifestAnnotate(dockerCli command.Cli, opts annotateOptions) error {
|
||||
targetRef, err := normalizeReference(opts.target)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "annotate: Error parsing name for manifest list (%s): %s", opts.target)
|
||||
}
|
||||
imgRef, err := normalizeReference(opts.image)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "annotate: Error parsing name for manifest (%s): %s:", opts.image)
|
||||
}
|
||||
|
||||
manifestStore := dockerCli.ManifestStore()
|
||||
imageManifest, err := manifestStore.Get(targetRef, imgRef)
|
||||
switch {
|
||||
case store.IsNotFound(err):
|
||||
return fmt.Errorf("manifest for image %s does not exist in %s", opts.image, opts.target)
|
||||
case err != nil:
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the mf
|
||||
if opts.os != "" {
|
||||
imageManifest.Platform.OS = opts.os
|
||||
}
|
||||
if opts.arch != "" {
|
||||
imageManifest.Platform.Architecture = opts.arch
|
||||
}
|
||||
for _, osFeature := range opts.osFeatures {
|
||||
imageManifest.Platform.OSFeatures = appendIfUnique(imageManifest.Platform.OSFeatures, osFeature)
|
||||
}
|
||||
if opts.variant != "" {
|
||||
imageManifest.Platform.Variant = opts.variant
|
||||
}
|
||||
|
||||
if !isValidOSArch(imageManifest.Platform.OS, imageManifest.Platform.Architecture) {
|
||||
return errors.Errorf("manifest entry for image has unsupported os/arch combination: %s/%s", opts.os, opts.arch)
|
||||
}
|
||||
return manifestStore.Save(targetRef, imgRef, imageManifest)
|
||||
}
|
||||
|
||||
func appendIfUnique(list []string, str string) []string {
|
||||
for _, s := range list {
|
||||
if s == str {
|
||||
return list
|
||||
}
|
||||
}
|
||||
return append(list, str)
|
||||
}
|
||||
28
cli/command/manifest/client_test.go
Normal file
28
cli/command/manifest/client_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
manifesttypes "github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/distribution/reference"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type fakeRegistryClient struct {
|
||||
client.RegistryClient
|
||||
getManifestFunc func(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error)
|
||||
getManifestListFunc func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error)
|
||||
}
|
||||
|
||||
func (c *fakeRegistryClient) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
if c.getManifestFunc != nil {
|
||||
return c.getManifestFunc(ctx, ref)
|
||||
}
|
||||
return manifesttypes.ImageManifest{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeRegistryClient) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
|
||||
if c.getManifestListFunc != nil {
|
||||
return c.getManifestListFunc(ctx, ref)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
44
cli/command/manifest/cmd.go
Normal file
44
cli/command/manifest/cmd.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewManifestCommand returns a cobra command for `manifest` subcommands
|
||||
func NewManifestCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// use dockerCli as command.Cli
|
||||
cmd := &cobra.Command{
|
||||
Use: "manifest COMMAND",
|
||||
Short: "Manage Docker image manifests and manifest lists",
|
||||
Long: manifestDescription,
|
||||
Args: cli.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newCreateListCommand(dockerCli),
|
||||
newInspectCommand(dockerCli),
|
||||
newAnnotateCommand(dockerCli),
|
||||
newPushListCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
var manifestDescription = `
|
||||
The **docker manifest** command has subcommands for managing image manifests and
|
||||
manifest lists. A manifest list allows you to use one name to refer to the same image
|
||||
built for multiple architectures.
|
||||
|
||||
To see help for a subcommand, use:
|
||||
|
||||
docker manifest CMD --help
|
||||
|
||||
For full details on using docker manifest lists, see the registry v2 specification.
|
||||
|
||||
`
|
||||
82
cli/command/manifest/create_list.go
Normal file
82
cli/command/manifest/create_list.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/store"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type createOpts struct {
|
||||
amend bool
|
||||
insecure bool
|
||||
}
|
||||
|
||||
func newCreateListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts := createOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create MANFEST_LIST MANIFEST [MANIFEST...]",
|
||||
Short: "Create a local manifest list for annotating and pushing to a registry",
|
||||
Args: cli.RequiresMinArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return createManifestList(dockerCli, args, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.insecure, "insecure", false, "allow communication with an insecure registry")
|
||||
flags.BoolVarP(&opts.amend, "amend", "a", false, "Amend an existing manifest list")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func createManifestList(dockerCli command.Cli, args []string, opts createOpts) error {
|
||||
newRef := args[0]
|
||||
targetRef, err := normalizeReference(newRef)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error parsing name for manifest list (%s): %v", newRef)
|
||||
}
|
||||
|
||||
_, err = registry.ParseRepositoryInfo(targetRef)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error parsing repository name for manifest list (%s): %v", newRef)
|
||||
}
|
||||
|
||||
manifestStore := dockerCli.ManifestStore()
|
||||
_, err = manifestStore.GetList(targetRef)
|
||||
switch {
|
||||
case store.IsNotFound(err):
|
||||
// New manifest list
|
||||
case err != nil:
|
||||
return err
|
||||
case !opts.amend:
|
||||
return errors.Errorf("refusing to amend an existing manifest list with no --amend flag")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
// Now create the local manifest list transaction by looking up the manifest schemas
|
||||
// for the constituent images:
|
||||
manifests := args[1:]
|
||||
for _, manifestRef := range manifests {
|
||||
namedRef, err := normalizeReference(manifestRef)
|
||||
if err != nil {
|
||||
// TODO: wrap error?
|
||||
return err
|
||||
}
|
||||
|
||||
manifest, err := getManifest(ctx, dockerCli, targetRef, namedRef, opts.insecure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := manifestStore.Save(targetRef, namedRef, manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(dockerCli.Out(), "Created manifest list %s\n", targetRef.String())
|
||||
return nil
|
||||
}
|
||||
147
cli/command/manifest/inspect.go
Normal file
147
cli/command/manifest/inspect.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type inspectOptions struct {
|
||||
ref string
|
||||
list string
|
||||
verbose bool
|
||||
insecure bool
|
||||
}
|
||||
|
||||
// NewInspectCommand creates a new `docker manifest inspect` command
|
||||
func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts inspectOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [OPTIONS] [MANIFEST_LIST] MANIFEST",
|
||||
Short: "Display an image manifest, or manifest list",
|
||||
Args: cli.RequiresRangeArgs(1, 2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
switch len(args) {
|
||||
case 1:
|
||||
opts.ref = args[0]
|
||||
case 2:
|
||||
opts.list = args[0]
|
||||
opts.ref = args[1]
|
||||
}
|
||||
return runInspect(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.insecure, "insecure", false, "allow communication with an insecure registry")
|
||||
flags.BoolVarP(&opts.verbose, "verbose", "v", false, "Output additional info including layers and platform")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runInspect(dockerCli command.Cli, opts inspectOptions) error {
|
||||
namedRef, err := normalizeReference(opts.ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If list reference is provided, display the local manifest in a list
|
||||
if opts.list != "" {
|
||||
listRef, err := normalizeReference(opts.list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imageManifest, err := dockerCli.ManifestStore().Get(listRef, namedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printManifest(dockerCli, imageManifest, opts)
|
||||
}
|
||||
|
||||
// Try a local manifest list first
|
||||
localManifestList, err := dockerCli.ManifestStore().GetList(namedRef)
|
||||
if err == nil {
|
||||
return printManifestList(dockerCli, namedRef, localManifestList, opts)
|
||||
}
|
||||
|
||||
// Next try a remote manifest
|
||||
ctx := context.Background()
|
||||
registryClient := dockerCli.RegistryClient(opts.insecure)
|
||||
imageManifest, err := registryClient.GetManifest(ctx, namedRef)
|
||||
if err == nil {
|
||||
return printManifest(dockerCli, imageManifest, opts)
|
||||
}
|
||||
|
||||
// Finally try a remote manifest list
|
||||
manifestList, err := registryClient.GetManifestList(ctx, namedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printManifestList(dockerCli, namedRef, manifestList, opts)
|
||||
}
|
||||
|
||||
func printManifest(dockerCli command.Cli, manifest types.ImageManifest, opts inspectOptions) error {
|
||||
buffer := new(bytes.Buffer)
|
||||
if !opts.verbose {
|
||||
_, raw, err := manifest.Payload()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Indent(buffer, raw, "", "\t"); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Out(), buffer.String())
|
||||
return nil
|
||||
}
|
||||
jsonBytes, err := json.MarshalIndent(manifest, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dockerCli.Out().Write(append(jsonBytes, '\n'))
|
||||
return nil
|
||||
}
|
||||
|
||||
func printManifestList(dockerCli command.Cli, namedRef reference.Named, list []types.ImageManifest, opts inspectOptions) error {
|
||||
if !opts.verbose {
|
||||
targetRepo, err := registry.ParseRepositoryInfo(namedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifests := []manifestlist.ManifestDescriptor{}
|
||||
// More than one response. This is a manifest list.
|
||||
for _, img := range list {
|
||||
mfd, err := buildManifestDescriptor(targetRepo, img)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error assembling ManifestDescriptor")
|
||||
}
|
||||
manifests = append(manifests, mfd)
|
||||
}
|
||||
deserializedML, err := manifestlist.FromDescriptors(manifests)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jsonBytes, err := deserializedML.MarshalJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Out(), string(jsonBytes))
|
||||
return nil
|
||||
}
|
||||
jsonBytes, err := json.MarshalIndent(list, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dockerCli.Out().Write(append(jsonBytes, '\n'))
|
||||
return nil
|
||||
}
|
||||
131
cli/command/manifest/inspect_test.go
Normal file
131
cli/command/manifest/inspect_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/manifest/store"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
manifesttypes "github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func newTempManifestStore(t *testing.T) (store.Store, func()) {
|
||||
tmpdir, err := ioutil.TempDir("", "test-manifest-storage")
|
||||
require.NoError(t, err)
|
||||
|
||||
return store.NewStore(tmpdir), func() { os.RemoveAll(tmpdir) }
|
||||
}
|
||||
|
||||
func ref(t *testing.T, name string) reference.Named {
|
||||
named, err := reference.ParseNamed("example.com/" + name)
|
||||
require.NoError(t, err)
|
||||
return named
|
||||
}
|
||||
|
||||
func fullImageManifest(t *testing.T, ref reference.Named) types.ImageManifest {
|
||||
man, err := schema2.FromStruct(schema2.Manifest{
|
||||
Versioned: schema2.SchemaVersion,
|
||||
Config: distribution.Descriptor{
|
||||
Digest: "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d624560",
|
||||
Size: 1520,
|
||||
MediaType: schema2.MediaTypeImageConfig,
|
||||
},
|
||||
Layers: []distribution.Descriptor{
|
||||
{
|
||||
MediaType: schema2.MediaTypeLayer,
|
||||
Size: 1990402,
|
||||
Digest: "sha256:88286f41530e93dffd4b964e1db22ce4939fffa4a4c665dab8591fbab03d4926",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// TODO: include image data for verbose inspect
|
||||
return types.NewImageManifest(ref, digest.Digest("abcd"), types.Image{}, man)
|
||||
}
|
||||
|
||||
func TestInspectCommandLocalManifestNotFound(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
err := cmd.Execute()
|
||||
assert.EqualError(t, err, "No such manifest: example.com/alpine:3.0")
|
||||
}
|
||||
|
||||
func TestInspectCommandNotFound(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
cli.SetRegistryClient(&fakeRegistryClient{
|
||||
getManifestFunc: func(_ context.Context, _ reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
return manifesttypes.ImageManifest{}, errors.New("missing")
|
||||
},
|
||||
getManifestListFunc: func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
|
||||
return nil, errors.Errorf("No such manifest: %s", ref)
|
||||
},
|
||||
})
|
||||
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs([]string{"example.com/alpine:3.0"})
|
||||
err := cmd.Execute()
|
||||
assert.EqualError(t, err, "No such manifest: example.com/alpine:3.0")
|
||||
}
|
||||
|
||||
func TestInspectCommandLocalManifest(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
namedRef := ref(t, "alpine:3.0")
|
||||
imageManifest := fullImageManifest(t, namedRef)
|
||||
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
require.NoError(t, cmd.Execute())
|
||||
actual := cli.OutBuffer()
|
||||
expected := golden.Get(t, "inspect-manifest.golden")
|
||||
assert.Equal(t, string(expected), actual.String())
|
||||
}
|
||||
|
||||
func TestInspectcommandRemoteManifest(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
cli.SetRegistryClient(&fakeRegistryClient{
|
||||
getManifestFunc: func(_ context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
return fullImageManifest(t, ref), nil
|
||||
},
|
||||
})
|
||||
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs([]string{"example.com/alpine:3.0"})
|
||||
require.NoError(t, cmd.Execute())
|
||||
actual := cli.OutBuffer()
|
||||
expected := golden.Get(t, "inspect-manifest.golden")
|
||||
assert.Equal(t, string(expected), actual.String())
|
||||
}
|
||||
272
cli/command/manifest/push.go
Normal file
272
cli/command/manifest/push.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type pushOpts struct {
|
||||
insecure bool
|
||||
purge bool
|
||||
target string
|
||||
}
|
||||
|
||||
type mountRequest struct {
|
||||
ref reference.Named
|
||||
manifest types.ImageManifest
|
||||
}
|
||||
|
||||
type manifestBlob struct {
|
||||
canonical reference.Canonical
|
||||
os string
|
||||
}
|
||||
|
||||
type pushRequest struct {
|
||||
targetRef reference.Named
|
||||
list *manifestlist.DeserializedManifestList
|
||||
mountRequests []mountRequest
|
||||
manifestBlobs []manifestBlob
|
||||
insecure bool
|
||||
}
|
||||
|
||||
func newPushListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts := pushOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "push [OPTIONS] MANIFEST_LIST",
|
||||
Short: "Push a manifest list to a repository",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.target = args[0]
|
||||
return runPush(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.purge, "purge", "p", false, "Remove the local manifest list after push")
|
||||
flags.BoolVar(&opts.insecure, "insecure", false, "Allow push to an insecure registry")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runPush(dockerCli command.Cli, opts pushOpts) error {
|
||||
|
||||
targetRef, err := normalizeReference(opts.target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifests, err := dockerCli.ManifestStore().GetList(targetRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(manifests) == 0 {
|
||||
return errors.Errorf("%s not found", targetRef)
|
||||
}
|
||||
|
||||
pushRequest, err := buildPushRequest(manifests, targetRef, opts.insecure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := pushList(ctx, dockerCli, pushRequest); err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.purge {
|
||||
return dockerCli.ManifestStore().Remove(targetRef)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildPushRequest(manifests []types.ImageManifest, targetRef reference.Named, insecure bool) (pushRequest, error) {
|
||||
req := pushRequest{targetRef: targetRef, insecure: insecure}
|
||||
|
||||
var err error
|
||||
req.list, err = buildManifestList(manifests, targetRef)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
targetRepo, err := registry.ParseRepositoryInfo(targetRef)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
targetRepoName, err := registryclient.RepoNameForReference(targetRepo.Name)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
for _, imageManifest := range manifests {
|
||||
manifestRepoName, err := registryclient.RepoNameForReference(imageManifest.Ref)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
repoName, _ := reference.WithName(manifestRepoName)
|
||||
if repoName.Name() != targetRepoName {
|
||||
blobs, err := buildBlobRequestList(imageManifest, repoName)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
req.manifestBlobs = append(req.manifestBlobs, blobs...)
|
||||
|
||||
manifestPush, err := buildPutManifestRequest(imageManifest, targetRef)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
req.mountRequests = append(req.mountRequests, manifestPush)
|
||||
}
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func buildManifestList(manifests []types.ImageManifest, targetRef reference.Named) (*manifestlist.DeserializedManifestList, error) {
|
||||
targetRepoInfo, err := registry.ParseRepositoryInfo(targetRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
descriptors := []manifestlist.ManifestDescriptor{}
|
||||
for _, imageManifest := range manifests {
|
||||
if imageManifest.Platform.Architecture == "" || imageManifest.Platform.OS == "" {
|
||||
return nil, errors.Errorf(
|
||||
"manifest %s must have an OS and Architecture to be pushed to a registry", imageManifest.Ref)
|
||||
}
|
||||
descriptor, err := buildManifestDescriptor(targetRepoInfo, imageManifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
descriptors = append(descriptors, descriptor)
|
||||
}
|
||||
|
||||
return manifestlist.FromDescriptors(descriptors)
|
||||
}
|
||||
|
||||
func buildManifestDescriptor(targetRepo *registry.RepositoryInfo, imageManifest types.ImageManifest) (manifestlist.ManifestDescriptor, error) {
|
||||
repoInfo, err := registry.ParseRepositoryInfo(imageManifest.Ref)
|
||||
if err != nil {
|
||||
return manifestlist.ManifestDescriptor{}, err
|
||||
}
|
||||
|
||||
manifestRepoHostname := reference.Domain(repoInfo.Name)
|
||||
targetRepoHostname := reference.Domain(targetRepo.Name)
|
||||
if manifestRepoHostname != targetRepoHostname {
|
||||
return manifestlist.ManifestDescriptor{}, errors.Errorf("cannot use source images from a different registry than the target image: %s != %s", manifestRepoHostname, targetRepoHostname)
|
||||
}
|
||||
|
||||
mediaType, raw, err := imageManifest.Payload()
|
||||
if err != nil {
|
||||
return manifestlist.ManifestDescriptor{}, err
|
||||
}
|
||||
|
||||
manifest := manifestlist.ManifestDescriptor{
|
||||
Platform: imageManifest.Platform,
|
||||
}
|
||||
manifest.Descriptor.Digest = imageManifest.Digest
|
||||
manifest.Size = int64(len(raw))
|
||||
manifest.MediaType = mediaType
|
||||
|
||||
if err = manifest.Descriptor.Digest.Validate(); err != nil {
|
||||
return manifestlist.ManifestDescriptor{}, errors.Wrapf(err,
|
||||
"digest parse of image %q failed with error: %v", imageManifest.Ref)
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func buildBlobRequestList(imageManifest types.ImageManifest, repoName reference.Named) ([]manifestBlob, error) {
|
||||
var blobReqs []manifestBlob
|
||||
|
||||
for _, blobDigest := range imageManifest.Blobs() {
|
||||
canonical, err := reference.WithDigest(repoName, blobDigest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blobReqs = append(blobReqs, manifestBlob{canonical: canonical, os: imageManifest.Platform.OS})
|
||||
}
|
||||
return blobReqs, nil
|
||||
}
|
||||
|
||||
func buildPutManifestRequest(imageManifest types.ImageManifest, targetRef reference.Named) (mountRequest, error) {
|
||||
refWithoutTag, err := reference.WithName(targetRef.Name())
|
||||
if err != nil {
|
||||
return mountRequest{}, err
|
||||
}
|
||||
mountRef, err := reference.WithDigest(refWithoutTag, imageManifest.Digest)
|
||||
if err != nil {
|
||||
return mountRequest{}, err
|
||||
}
|
||||
|
||||
// This indentation has to be added to ensure sha parity with the registry
|
||||
v2ManifestBytes, err := json.MarshalIndent(imageManifest.SchemaV2Manifest, "", " ")
|
||||
if err != nil {
|
||||
return mountRequest{}, err
|
||||
}
|
||||
// indent only the DeserializedManifest portion of this, in order to maintain parity with the registry
|
||||
// and not alter the sha
|
||||
var v2Manifest schema2.DeserializedManifest
|
||||
if err = v2Manifest.UnmarshalJSON(v2ManifestBytes); err != nil {
|
||||
return mountRequest{}, err
|
||||
}
|
||||
imageManifest.SchemaV2Manifest = &v2Manifest
|
||||
|
||||
return mountRequest{ref: mountRef, manifest: imageManifest}, err
|
||||
}
|
||||
|
||||
func pushList(ctx context.Context, dockerCli command.Cli, req pushRequest) error {
|
||||
rclient := dockerCli.RegistryClient(req.insecure)
|
||||
|
||||
if err := mountBlobs(ctx, rclient, req.targetRef, req.manifestBlobs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := pushReferences(ctx, dockerCli.Out(), rclient, req.mountRequests); err != nil {
|
||||
return err
|
||||
}
|
||||
dgst, err := rclient.PutManifest(ctx, req.targetRef, req.list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(dockerCli.Out(), dgst.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func pushReferences(ctx context.Context, out io.Writer, client registryclient.RegistryClient, mounts []mountRequest) error {
|
||||
for _, mount := range mounts {
|
||||
newDigest, err := client.PutManifest(ctx, mount.ref, mount.manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(out, "Pushed ref %s with digest: %s\n", mount.ref, newDigest)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mountBlobs(ctx context.Context, client registryclient.RegistryClient, ref reference.Named, blobs []manifestBlob) error {
|
||||
for _, blob := range blobs {
|
||||
err := client.MountBlob(ctx, blob.canonical, ref)
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
case registryclient.ErrBlobCreated:
|
||||
if blob.os != "windows" {
|
||||
return fmt.Errorf("error mounting %s to %s", blob.canonical, ref)
|
||||
}
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
16
cli/command/manifest/testdata/inspect-manifest.golden
vendored
Normal file
16
cli/command/manifest/testdata/inspect-manifest.golden
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 1520,
|
||||
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d624560"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 1990402,
|
||||
"digest": "sha256:88286f41530e93dffd4b964e1db22ce4939fffa4a4c665dab8591fbab03d4926"
|
||||
}
|
||||
]
|
||||
}
|
||||
79
cli/command/manifest/util.go
Normal file
79
cli/command/manifest/util.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/store"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type osArch struct {
|
||||
os string
|
||||
arch string
|
||||
}
|
||||
|
||||
// Remove any unsupported os/arch combo
|
||||
// list of valid os/arch values (see "Optional Environment Variables" section
|
||||
// of https://golang.org/doc/install/source
|
||||
// Added linux/s390x as we know System z support already exists
|
||||
var validOSArches = map[osArch]bool{
|
||||
{os: "darwin", arch: "386"}: true,
|
||||
{os: "darwin", arch: "amd64"}: true,
|
||||
{os: "darwin", arch: "arm"}: true,
|
||||
{os: "darwin", arch: "arm64"}: true,
|
||||
{os: "dragonfly", arch: "amd64"}: true,
|
||||
{os: "freebsd", arch: "386"}: true,
|
||||
{os: "freebsd", arch: "amd64"}: true,
|
||||
{os: "freebsd", arch: "arm"}: true,
|
||||
{os: "linux", arch: "386"}: true,
|
||||
{os: "linux", arch: "amd64"}: true,
|
||||
{os: "linux", arch: "arm"}: true,
|
||||
{os: "linux", arch: "arm64"}: true,
|
||||
{os: "linux", arch: "ppc64le"}: true,
|
||||
{os: "linux", arch: "mips64"}: true,
|
||||
{os: "linux", arch: "mips64le"}: true,
|
||||
{os: "linux", arch: "s390x"}: true,
|
||||
{os: "netbsd", arch: "386"}: true,
|
||||
{os: "netbsd", arch: "amd64"}: true,
|
||||
{os: "netbsd", arch: "arm"}: true,
|
||||
{os: "openbsd", arch: "386"}: true,
|
||||
{os: "openbsd", arch: "amd64"}: true,
|
||||
{os: "openbsd", arch: "arm"}: true,
|
||||
{os: "plan9", arch: "386"}: true,
|
||||
{os: "plan9", arch: "amd64"}: true,
|
||||
{os: "solaris", arch: "amd64"}: true,
|
||||
{os: "windows", arch: "386"}: true,
|
||||
{os: "windows", arch: "amd64"}: true,
|
||||
}
|
||||
|
||||
func isValidOSArch(os string, arch string) bool {
|
||||
// check for existence of this combo
|
||||
_, ok := validOSArches[osArch{os, arch}]
|
||||
return ok
|
||||
}
|
||||
|
||||
func normalizeReference(ref string) (reference.Named, error) {
|
||||
namedRef, err := reference.ParseNormalizedNamed(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, isDigested := namedRef.(reference.Canonical); !isDigested {
|
||||
return reference.TagNameOnly(namedRef), nil
|
||||
}
|
||||
return namedRef, nil
|
||||
}
|
||||
|
||||
// getManifest from the local store, and fallback to the remote registry if it
|
||||
// doesn't exist locally
|
||||
func getManifest(ctx context.Context, dockerCli command.Cli, listRef, namedRef reference.Named, insecure bool) (types.ImageManifest, error) {
|
||||
data, err := dockerCli.ManifestStore().Get(listRef, namedRef)
|
||||
switch {
|
||||
case store.IsNotFound(err):
|
||||
return dockerCli.RegistryClient(insecure).GetManifest(ctx, namedRef)
|
||||
case err != nil:
|
||||
return types.ImageManifest{}, err
|
||||
default:
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
@@ -10,14 +10,13 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/pkg/term"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ElectAuthServer returns the default registry to use (by asking the daemon)
|
||||
|
||||
Reference in New Issue
Block a user