Skip to content

Commit 11dfa23

Browse files
author
Vincent Demeester
authored
Merge pull request docker#138 from clnperez/manifest-cmd
Add manifest command
2 parents 4f55001 + 6c6ce22 commit 11dfa23

File tree

30 files changed

+2945
-4
lines changed

30 files changed

+2945
-4
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", 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", 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: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package manifest
2+
3+
import (
4+
"io/ioutil"
5+
"testing"
6+
7+
"github.com/docker/cli/internal/test"
8+
"github.com/docker/cli/internal/test/testutil"
9+
"github.com/gotestyourself/gotestyourself/golden"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestManifestAnnotateError(t *testing.T) {
15+
testCases := []struct {
16+
args []string
17+
expectedError string
18+
}{
19+
{
20+
args: []string{"too-few-arguments"},
21+
expectedError: "requires exactly 2 arguments",
22+
},
23+
{
24+
args: []string{"th!si'sa/fa!ke/li$t/name", "example.com/alpine:3.0"},
25+
expectedError: "error parsing name for manifest list",
26+
},
27+
{
28+
args: []string{"example.com/list:v1", "th!si'sa/fa!ke/im@ge/nam32"},
29+
expectedError: "error parsing name for manifest",
30+
},
31+
}
32+
33+
for _, tc := range testCases {
34+
cli := test.NewFakeCli(nil)
35+
cmd := newAnnotateCommand(cli)
36+
cmd.SetArgs(tc.args)
37+
cmd.SetOutput(ioutil.Discard)
38+
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
39+
}
40+
}
41+
42+
func TestManifestAnnotate(t *testing.T) {
43+
store, cleanup := newTempManifestStore(t)
44+
defer cleanup()
45+
46+
cli := test.NewFakeCli(nil)
47+
cli.SetManifestStore(store)
48+
namedRef := ref(t, "alpine:3.0")
49+
imageManifest := fullImageManifest(t, namedRef)
50+
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
51+
require.NoError(t, err)
52+
53+
cmd := newAnnotateCommand(cli)
54+
cmd.SetArgs([]string{"example.com/list:v1", "example.com/fake:0.0"})
55+
cmd.SetOutput(ioutil.Discard)
56+
expectedError := "manifest for image example.com/fake:0.0 does not exist"
57+
testutil.ErrorContains(t, cmd.Execute(), expectedError)
58+
59+
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
60+
cmd.Flags().Set("os", "freebsd")
61+
cmd.Flags().Set("arch", "fake")
62+
cmd.Flags().Set("os-features", "feature1")
63+
cmd.Flags().Set("variant", "v7")
64+
expectedError = "manifest entry for image has unsupported os/arch combination"
65+
testutil.ErrorContains(t, cmd.Execute(), expectedError)
66+
67+
cmd.Flags().Set("arch", "arm")
68+
require.NoError(t, cmd.Execute())
69+
70+
cmd = newInspectCommand(cli)
71+
err = cmd.Flags().Set("verbose", "true")
72+
require.NoError(t, err)
73+
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
74+
require.NoError(t, cmd.Execute())
75+
actual := cli.OutBuffer()
76+
expected := golden.Get(t, "inspect-annotate.golden")
77+
assert.Equal(t, string(expected), actual.String())
78+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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"
7+
"github.com/docker/distribution/reference"
8+
"github.com/opencontainers/go-digest"
9+
"golang.org/x/net/context"
10+
)
11+
12+
type fakeRegistryClient struct {
13+
client.RegistryClient
14+
getManifestFunc func(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error)
15+
getManifestListFunc func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error)
16+
mountBlobFunc func(ctx context.Context, source reference.Canonical, target reference.Named) error
17+
putManifestFunc func(ctx context.Context, source reference.Named, mf distribution.Manifest) (digest.Digest, error)
18+
}
19+
20+
func (c *fakeRegistryClient) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
21+
if c.getManifestFunc != nil {
22+
return c.getManifestFunc(ctx, ref)
23+
}
24+
return manifesttypes.ImageManifest{}, nil
25+
}
26+
27+
func (c *fakeRegistryClient) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
28+
if c.getManifestListFunc != nil {
29+
return c.getManifestListFunc(ctx, ref)
30+
}
31+
return nil, nil
32+
}
33+
34+
func (c *fakeRegistryClient) MountBlob(ctx context.Context, source reference.Canonical, target reference.Named) error {
35+
if c.mountBlobFunc != nil {
36+
return c.mountBlobFunc(ctx, source, target)
37+
}
38+
return nil
39+
}
40+
41+
func (c *fakeRegistryClient) PutManifest(ctx context.Context, ref reference.Named, mf distribution.Manifest) (digest.Digest, error) {
42+
if c.putManifestFunc != nil {
43+
return c.putManifestFunc(ctx, ref, mf)
44+
}
45+
return digest.Digest(""), nil
46+
}

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+
`

0 commit comments

Comments
 (0)