Skip to content

Commit 02719bd

Browse files
committed
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 <[email protected]> Signed-off-by: Daniel Nephin <[email protected]>
1 parent 17886d7 commit 02719bd

File tree

19 files changed

+1948
-12
lines changed

19 files changed

+1948
-12
lines changed

cli/command/cli.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,22 @@ import (
55
"net"
66
"net/http"
77
"os"
8+
"path/filepath"
89
"runtime"
910
"time"
1011

1112
"github.com/docker/cli/cli"
13+
"github.com/docker/cli/cli/config"
1214
cliconfig "github.com/docker/cli/cli/config"
1315
"github.com/docker/cli/cli/config/configfile"
1416
cliflags "github.com/docker/cli/cli/flags"
17+
manifeststore "github.com/docker/cli/cli/manifest/store"
18+
registryclient "github.com/docker/cli/cli/registry/client"
1519
"github.com/docker/cli/cli/trust"
1620
dopts "github.com/docker/cli/opts"
1721
"github.com/docker/docker/api"
22+
"github.com/docker/docker/api/types"
23+
registrytypes "github.com/docker/docker/api/types/registry"
1824
"github.com/docker/docker/client"
1925
"github.com/docker/go-connections/sockets"
2026
"github.com/docker/go-connections/tlsconfig"
@@ -45,6 +51,8 @@ type Cli interface {
4551
ClientInfo() ClientInfo
4652
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
4753
DefaultVersion() string
54+
ManifestStore() manifeststore.Store
55+
RegistryClient(bool) registryclient.RegistryClient
4856
}
4957

5058
// DockerCli is an instance the docker command line client.
@@ -114,6 +122,21 @@ func (cli *DockerCli) ClientInfo() ClientInfo {
114122
return cli.clientInfo
115123
}
116124

125+
// ManifestStore returns a store for local manifests
126+
func (cli *DockerCli) ManifestStore() manifeststore.Store {
127+
// TODO: support override default location from config file
128+
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
129+
}
130+
131+
// RegistryClient returns a client for communicating with a Docker distribution
132+
// registry
133+
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
134+
resolver := func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
135+
return ResolveAuthConfig(ctx, cli, index)
136+
}
137+
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
138+
}
139+
117140
// Initialize the dockerCli runs initialization that must happen after command
118141
// line flags are parsed.
119142
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {

cli/command/commands/commands.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/docker/cli/cli/command/config"
99
"github.com/docker/cli/cli/command/container"
1010
"github.com/docker/cli/cli/command/image"
11+
"github.com/docker/cli/cli/command/manifest"
1112
"github.com/docker/cli/cli/command/network"
1213
"github.com/docker/cli/cli/command/node"
1314
"github.com/docker/cli/cli/command/plugin"
@@ -39,12 +40,15 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
3940
image.NewImageCommand(dockerCli),
4041
image.NewBuildCommand(dockerCli),
4142

42-
// node
43-
node.NewNodeCommand(dockerCli),
43+
// manifest
44+
manifest.NewManifestCommand(dockerCli),
4445

4546
// network
4647
network.NewNetworkCommand(dockerCli),
4748

49+
// node
50+
node.NewNodeCommand(dockerCli),
51+
4852
// plugin
4953
plugin.NewPluginCommand(dockerCli),
5054

cli/command/manifest/annotate.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package manifest
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/docker/cli/cli"
7+
"github.com/docker/cli/cli/command"
8+
"github.com/docker/cli/cli/manifest/store"
9+
"github.com/pkg/errors"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
type annotateOptions struct {
14+
target string // the target manifest list name (also transaction ID)
15+
image string // the manifest to annotate within the list
16+
variant string // an architecture variant
17+
os string
18+
arch string
19+
osFeatures []string
20+
}
21+
22+
// NewAnnotateCommand creates a new `docker manifest annotate` command
23+
func newAnnotateCommand(dockerCli command.Cli) *cobra.Command {
24+
var opts annotateOptions
25+
26+
cmd := &cobra.Command{
27+
Use: "annotate [OPTIONS] MANIFEST_LIST MANIFEST",
28+
Short: "Add additional information to a local image manifest",
29+
Args: cli.ExactArgs(2),
30+
RunE: func(cmd *cobra.Command, args []string) error {
31+
opts.target = args[0]
32+
opts.image = args[1]
33+
return runManifestAnnotate(dockerCli, opts)
34+
},
35+
}
36+
37+
flags := cmd.Flags()
38+
39+
flags.StringVar(&opts.os, "os", "", "Set operating system")
40+
flags.StringVar(&opts.arch, "arch", "", "Set architecture")
41+
flags.StringSliceVar(&opts.osFeatures, "os-features", []string{}, "Set operating system feature")
42+
flags.StringVar(&opts.variant, "variant", "", "Set architecture variant")
43+
44+
return cmd
45+
}
46+
47+
func runManifestAnnotate(dockerCli command.Cli, opts annotateOptions) error {
48+
targetRef, err := normalizeReference(opts.target)
49+
if err != nil {
50+
return errors.Wrapf(err, "annotate: Error parsing name for manifest list (%s): %s", opts.target)
51+
}
52+
imgRef, err := normalizeReference(opts.image)
53+
if err != nil {
54+
return errors.Wrapf(err, "annotate: Error parsing name for manifest (%s): %s:", opts.image)
55+
}
56+
57+
manifestStore := dockerCli.ManifestStore()
58+
imageManifest, err := manifestStore.Get(targetRef, imgRef)
59+
switch {
60+
case store.IsNotFound(err):
61+
return fmt.Errorf("manifest for image %s does not exist in %s", opts.image, opts.target)
62+
case err != nil:
63+
return err
64+
}
65+
66+
// Update the mf
67+
if opts.os != "" {
68+
imageManifest.Platform.OS = opts.os
69+
}
70+
if opts.arch != "" {
71+
imageManifest.Platform.Architecture = opts.arch
72+
}
73+
for _, osFeature := range opts.osFeatures {
74+
imageManifest.Platform.OSFeatures = appendIfUnique(imageManifest.Platform.OSFeatures, osFeature)
75+
}
76+
if opts.variant != "" {
77+
imageManifest.Platform.Variant = opts.variant
78+
}
79+
80+
if !isValidOSArch(imageManifest.Platform.OS, imageManifest.Platform.Architecture) {
81+
return errors.Errorf("manifest entry for image has unsupported os/arch combination: %s/%s", opts.os, opts.arch)
82+
}
83+
return manifestStore.Save(targetRef, imgRef, imageManifest)
84+
}
85+
86+
func appendIfUnique(list []string, str string) []string {
87+
for _, s := range list {
88+
if s == str {
89+
return list
90+
}
91+
}
92+
return append(list, str)
93+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package manifest
2+
3+
import (
4+
manifesttypes "github.com/docker/cli/cli/manifest/types"
5+
"github.com/docker/cli/cli/registry/client"
6+
"github.com/docker/distribution/reference"
7+
"golang.org/x/net/context"
8+
)
9+
10+
type fakeRegistryClient struct {
11+
client.RegistryClient
12+
getManifestFunc func(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error)
13+
getManifestListFunc func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error)
14+
}
15+
16+
func (c *fakeRegistryClient) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
17+
if c.getManifestFunc != nil {
18+
return c.getManifestFunc(ctx, ref)
19+
}
20+
return manifesttypes.ImageManifest{}, nil
21+
}
22+
23+
func (c *fakeRegistryClient) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
24+
if c.getManifestListFunc != nil {
25+
return c.getManifestListFunc(ctx, ref)
26+
}
27+
return nil, nil
28+
}

cli/command/manifest/cmd.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package manifest
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/docker/cli/cli"
7+
"github.com/docker/cli/cli/command"
8+
9+
"github.com/spf13/cobra"
10+
)
11+
12+
// NewManifestCommand returns a cobra command for `manifest` subcommands
13+
func NewManifestCommand(dockerCli command.Cli) *cobra.Command {
14+
// use dockerCli as command.Cli
15+
cmd := &cobra.Command{
16+
Use: "manifest COMMAND",
17+
Short: "Manage Docker image manifests and manifest lists",
18+
Long: manifestDescription,
19+
Args: cli.NoArgs,
20+
Run: func(cmd *cobra.Command, args []string) {
21+
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
22+
},
23+
}
24+
cmd.AddCommand(
25+
newCreateListCommand(dockerCli),
26+
newInspectCommand(dockerCli),
27+
newAnnotateCommand(dockerCli),
28+
newPushListCommand(dockerCli),
29+
)
30+
return cmd
31+
}
32+
33+
var manifestDescription = `
34+
The **docker manifest** command has subcommands for managing image manifests and
35+
manifest lists. A manifest list allows you to use one name to refer to the same image
36+
built for multiple architectures.
37+
38+
To see help for a subcommand, use:
39+
40+
docker manifest CMD --help
41+
42+
For full details on using docker manifest lists, see the registry v2 specification.
43+
44+
`
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package manifest
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/docker/cli/cli"
7+
"github.com/docker/cli/cli/command"
8+
"github.com/docker/cli/cli/manifest/store"
9+
"github.com/docker/docker/registry"
10+
"github.com/pkg/errors"
11+
"github.com/spf13/cobra"
12+
"golang.org/x/net/context"
13+
)
14+
15+
type createOpts struct {
16+
amend bool
17+
insecure bool
18+
}
19+
20+
func newCreateListCommand(dockerCli command.Cli) *cobra.Command {
21+
opts := createOpts{}
22+
23+
cmd := &cobra.Command{
24+
Use: "create MANFEST_LIST MANIFEST [MANIFEST...]",
25+
Short: "Create a local manifest list for annotating and pushing to a registry",
26+
Args: cli.RequiresMinArgs(2),
27+
RunE: func(cmd *cobra.Command, args []string) error {
28+
return createManifestList(dockerCli, args, opts)
29+
},
30+
}
31+
32+
flags := cmd.Flags()
33+
flags.BoolVar(&opts.insecure, "insecure", false, "allow communication with an insecure registry")
34+
flags.BoolVarP(&opts.amend, "amend", "a", false, "Amend an existing manifest list")
35+
return cmd
36+
}
37+
38+
func createManifestList(dockerCli command.Cli, args []string, opts createOpts) error {
39+
newRef := args[0]
40+
targetRef, err := normalizeReference(newRef)
41+
if err != nil {
42+
return errors.Wrapf(err, "error parsing name for manifest list (%s): %v", newRef)
43+
}
44+
45+
_, err = registry.ParseRepositoryInfo(targetRef)
46+
if err != nil {
47+
return errors.Wrapf(err, "error parsing repository name for manifest list (%s): %v", newRef)
48+
}
49+
50+
manifestStore := dockerCli.ManifestStore()
51+
_, err = manifestStore.GetList(targetRef)
52+
switch {
53+
case store.IsNotFound(err):
54+
// New manifest list
55+
case err != nil:
56+
return err
57+
case !opts.amend:
58+
return errors.Errorf("refusing to amend an existing manifest list with no --amend flag")
59+
}
60+
61+
ctx := context.Background()
62+
// Now create the local manifest list transaction by looking up the manifest schemas
63+
// for the constituent images:
64+
manifests := args[1:]
65+
for _, manifestRef := range manifests {
66+
namedRef, err := normalizeReference(manifestRef)
67+
if err != nil {
68+
// TODO: wrap error?
69+
return err
70+
}
71+
72+
manifest, err := getManifest(ctx, dockerCli, targetRef, namedRef, opts.insecure)
73+
if err != nil {
74+
return err
75+
}
76+
if err := manifestStore.Save(targetRef, namedRef, manifest); err != nil {
77+
return err
78+
}
79+
}
80+
fmt.Fprintf(dockerCli.Out(), "Created manifest list %s\n", targetRef.String())
81+
return nil
82+
}

0 commit comments

Comments
 (0)