Skip to content

Commit ae7bcd6

Browse files
committed
manifest push using yaml file
Instead of: manifest create list image1 image2 manifest annotate list image1 manifest push list You can do: manifiest push --file myfile.yaml registry/repo/image:tag Signed-off-by: Christy Norman <christy@linux.vnet.ibm.com>
1 parent 95a9b4d commit ae7bcd6

File tree

17 files changed

+480
-49
lines changed

17 files changed

+480
-49
lines changed

cli/command/manifest/annotate.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ func runManifestAnnotate(dockerCli command.Cli, opts annotateOptions) error {
7777
imageManifest.Platform.Variant = opts.variant
7878
}
7979

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)
80+
if err := validateOSArch(imageManifest.Platform.OS, imageManifest.Platform.Architecture); err != nil {
81+
return err
8282
}
8383
return manifestStore.Save(targetRef, imgRef, imageManifest)
8484
}

cli/command/manifest/push.go

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"io"
8+
"io/ioutil"
89

910
"github.com/docker/cli/cli"
1011
"github.com/docker/cli/cli/command"
@@ -14,13 +15,16 @@ import (
1415
"github.com/docker/distribution/manifest/schema2"
1516
"github.com/docker/distribution/reference"
1617
"github.com/docker/docker/registry"
18+
1719
"github.com/pkg/errors"
1820
"github.com/spf13/cobra"
21+
yaml "gopkg.in/yaml.v2"
1922
)
2023

2124
type pushOpts struct {
2225
insecure bool
2326
purge bool
27+
file string
2428
target string
2529
}
2630

@@ -42,32 +46,48 @@ type pushRequest struct {
4246
insecure bool
4347
}
4448

49+
type yamlManifestList struct {
50+
Image string
51+
Manifests []yamlManifest
52+
}
53+
54+
type yamlManifest struct {
55+
Image string
56+
Platform manifestlist.PlatformSpec
57+
}
58+
4559
func newPushListCommand(dockerCli command.Cli) *cobra.Command {
4660
opts := pushOpts{}
4761

4862
cmd := &cobra.Command{
4963
Use: "push [OPTIONS] MANIFEST_LIST",
50-
Short: "Push a manifest list to a repository",
64+
Short: "Push a manifest list to a repository, either after a create, or from a file",
5165
Args: cli.ExactArgs(1),
5266
RunE: func(cmd *cobra.Command, args []string) error {
5367
opts.target = args[0]
5468
return runPush(dockerCli, opts)
5569
},
5670
}
57-
5871
flags := cmd.Flags()
59-
flags.BoolVarP(&opts.purge, "purge", "p", false, "Remove the local manifest list after push")
60-
flags.BoolVar(&opts.insecure, "insecure", false, "Allow push to an insecure registry")
72+
flags.BoolVarP(&opts.purge, "purge", "p", false, "remove the local manifests after push")
73+
flags.BoolVar(&opts.insecure, "insecure", false, "allow push to an insecure registry")
74+
flags.StringVar(&opts.file, "file", "", "file containing the yaml representation of manifest list")
6175
return cmd
6276
}
6377

6478
func runPush(dockerCli command.Cli, opts pushOpts) error {
65-
6679
targetRef, err := normalizeReference(opts.target)
6780
if err != nil {
6881
return err
6982
}
83+
if opts.file != "" {
84+
return pushListFromYaml(dockerCli, targetRef, opts)
85+
}
7086

87+
return pushListFromStore(dockerCli, targetRef, opts)
88+
}
89+
90+
func pushListFromStore(dockerCli command.Cli, targetRef reference.Named, opts pushOpts) error {
7191
manifests, err := dockerCli.ManifestStore().GetList(targetRef)
7292
if err != nil {
7393
return err
@@ -271,3 +291,60 @@ func mountBlobs(ctx context.Context, client registryclient.RegistryClient, ref r
271291
}
272292
return nil
273293
}
294+
295+
func pushListFromYaml(dockerCli command.Cli, targetRef reference.Named, opts pushOpts) error {
296+
yamlInput, err := getYamlManifestList(opts.file)
297+
if err != nil {
298+
return err
299+
}
300+
if len(yamlInput.Manifests) == 0 {
301+
return errors.New("no manifests specified in file input")
302+
}
303+
ctx := context.Background()
304+
var manifests []types.ImageManifest
305+
for _, manifest := range yamlInput.Manifests {
306+
imageRef, err := normalizeReference(manifest.Image)
307+
if err != nil {
308+
return err
309+
}
310+
im, err := dockerCli.RegistryClient(opts.insecure).GetManifest(ctx, imageRef)
311+
if err != nil {
312+
return err
313+
}
314+
addYamlAnnotations(&im, manifest)
315+
if err := validateOSArch(im.Platform.OS, im.Platform.Architecture); err != nil {
316+
return err
317+
}
318+
manifests = append(manifests, im)
319+
}
320+
321+
pushRequest, err := buildPushRequest(manifests, targetRef, opts.insecure)
322+
if err != nil {
323+
return err
324+
}
325+
return pushList(ctx, dockerCli, pushRequest)
326+
}
327+
328+
func addYamlAnnotations(manifest *types.ImageManifest, ym yamlManifest) {
329+
if ym.Platform.Variant != "" {
330+
manifest.Platform.Variant = ym.Platform.Variant
331+
}
332+
if ym.Platform.OS != "" {
333+
manifest.Platform.OS = ym.Platform.OS
334+
}
335+
if ym.Platform.Architecture != "" {
336+
manifest.Platform.Architecture = ym.Platform.Architecture
337+
}
338+
if len(ym.Platform.OSFeatures) != 0 {
339+
manifest.Platform.OSFeatures = ym.Platform.OSFeatures
340+
}
341+
}
342+
343+
func getYamlManifestList(yamlFile string) (yamlManifestList, error) {
344+
yamlBuf, err := ioutil.ReadFile(yamlFile)
345+
if err != nil {
346+
return yamlManifestList{}, err
347+
}
348+
var yamlInput yamlManifestList
349+
return yamlInput, yaml.UnmarshalStrict(yamlBuf, &yamlInput)
350+
}

cli/command/manifest/push_test.go

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ import (
1515
func newFakeRegistryClient() *fakeRegistryClient {
1616
return &fakeRegistryClient{
1717
getManifestFunc: func(_ context.Context, _ reference.Named) (manifesttypes.ImageManifest, error) {
18-
return manifesttypes.ImageManifest{}, errors.New("")
18+
return manifesttypes.ImageManifest{}, errors.New("getManifestFunc not implemented")
1919
},
2020
getManifestListFunc: func(_ context.Context, _ reference.Named) ([]manifesttypes.ImageManifest, error) {
21-
return nil, errors.Errorf("")
21+
return nil, errors.Errorf("getManifestListFunc not implemented")
2222
},
2323
}
2424
}
@@ -67,3 +67,60 @@ func TestManifestPush(t *testing.T) {
6767
err = cmd.Execute()
6868
assert.NilError(t, err)
6969
}
70+
71+
func TestPushFromYaml(t *testing.T) {
72+
cli := test.NewFakeCli(nil)
73+
cli.SetRegistryClient(&fakeRegistryClient{
74+
getManifestFunc: func(_ context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
75+
return fullImageManifest(t, ref), nil
76+
},
77+
})
78+
79+
cmd := newPushListCommand(cli)
80+
cmd.Flags().Set("file", "testdata/test-push.yaml")
81+
cmd.SetArgs([]string{"pushtest/pass:latest"})
82+
assert.NilError(t, cmd.Execute())
83+
}
84+
85+
func TestManifestPushYamlErrors(t *testing.T) {
86+
testCases := []struct {
87+
flags map[string]string
88+
args []string
89+
expectedError string
90+
}{
91+
{
92+
flags: map[string]string{"file": "testdata/test-push-fail.yaml"},
93+
args: []string{"pushtest/fail:latest"},
94+
expectedError: "manifest entry for image has unsupported os/arch combination: linux/nope",
95+
},
96+
{
97+
flags: map[string]string{"file": "testdata/test-push-empty.yaml"},
98+
args: []string{"pushtest/fail:latest"},
99+
expectedError: "no manifests specified in file input",
100+
},
101+
{
102+
args: []string{"testdata/test-push-empty.yaml"},
103+
expectedError: "No such manifest: docker.io/testdata/test-push-empty.yaml:latest",
104+
},
105+
}
106+
107+
store, sCleanup := newTempManifestStore(t)
108+
defer sCleanup()
109+
for _, tc := range testCases {
110+
cli := test.NewFakeCli(nil)
111+
cli.SetRegistryClient(&fakeRegistryClient{
112+
getManifestFunc: func(_ context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
113+
return fullImageManifest(t, ref), nil
114+
},
115+
})
116+
117+
cli.SetManifestStore(store)
118+
cmd := newPushListCommand(cli)
119+
for k, v := range tc.flags {
120+
cmd.Flags().Set(k, v)
121+
}
122+
cmd.SetArgs(tc.args)
123+
cmd.SetOutput(ioutil.Discard)
124+
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
125+
}
126+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
manifests:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
manifests:
2+
-
3+
image: test/hello-world-ppc64le:latest
4+
platform:
5+
architecture: nope
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
manifests:
2+
-
3+
image: test/hello-world-ppc64le:latest
4+
platform:
5+
architecture: ppc64le
6+
-
7+
image: test/hello-world-amd64:latest
8+
platform:
9+
architecture: amd64
10+
os: linux
11+
-
12+
image: test/hello-world-s390x:latest
13+
platform:
14+
architecture: s390x
15+
os: linux
16+
osversion: 1.1
17+
variant: xyz
18+
osfeatures: [a,b,c]
19+
-
20+
image: test/hello-world-armv5:latest
21+
platform:
22+
-
23+
image: test/hello-world:armhf
24+
platform:
25+
architecture: arm
26+
os: linux
27+
variant: abc
28+

cli/command/manifest/util.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package manifest
22

33
import (
44
"context"
5+
"fmt"
56

67
"github.com/docker/cli/cli/command"
78
"github.com/docker/cli/cli/manifest/store"
@@ -14,6 +15,18 @@ type osArch struct {
1415
arch string
1516
}
1617

18+
type invalidOSArchErr struct {
19+
osArch
20+
}
21+
22+
func (e *invalidOSArchErr) Error() string {
23+
return fmt.Sprintf("manifest entry for image has unsupported os/arch combination: %s/%s", e.os, e.arch)
24+
}
25+
26+
func newInvalidOSArchErr(os1 string, arch1 string) *invalidOSArchErr {
27+
return &invalidOSArchErr{osArch{os: os1, arch: arch1}}
28+
}
29+
1730
// Remove any unsupported os/arch combo
1831
// list of valid os/arch values (see "Optional Environment Variables" section
1932
// of https://golang.org/doc/install/source
@@ -48,10 +61,13 @@ var validOSArches = map[osArch]bool{
4861
{os: "windows", arch: "amd64"}: true,
4962
}
5063

51-
func isValidOSArch(os string, arch string) bool {
64+
func validateOSArch(os string, arch string) error {
5265
// check for existence of this combo
5366
_, ok := validOSArches[osArch{os, arch}]
54-
return ok
67+
if !ok {
68+
return newInvalidOSArchErr(os, arch)
69+
}
70+
return nil
5571
}
5672

5773
func normalizeReference(ref string) (reference.Named, error) {

docs/reference/commandline/manifest.md

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,19 @@ Usage: docker manifest push [OPTIONS] MANIFEST_LIST
9797
Push a manifest list to a repository
9898

9999
Options:
100-
--help Print usage
101-
--insecure Allow push to an insecure registry
102-
-p, --purge Remove the local manifest list after push
100+
--help Print usage
101+
--insecure Allow push to an insecure registry
102+
--file string File containing the yaml representation of manifest list
103+
-p, --purge Remove the local manifest list after push
103104
```
104105

106+
### Working with manifest lists
107+
108+
Manifest lists can only be used in conjunction with a registry. The idea of manifest lists is that they push the complexity of dealing with multiple platforms' and architectures' images down to the image owners. Application developers need not know the names of all the different images living on a remote registry. Since the manifest list is to simplify pulling from a registry, no information about a manifest list is stored for later use by the docker engine. This means that in order to create a manifest list, its constituent images must first have been pushed to the registry from which it will laber be accessed. This then allows the registry, during the manifest push operation, to link directly to the image layers contained within its filesystem.
109+
105110
### Working with insecure registries
106111

107-
The manifest command interacts solely with a Docker registry. Because of this, it has no way to query the engine for the list of allowed insecure registries. To allow the CLI to interact with an insecure registry, some `docker manifest` commands have an `--insecure` flag. For each transaction, such as a `create`, which queries a registry, the `--insecure` flag must be specified. This flag tells the CLI that this registry call may ignore security concerns like missing or self-signed certificates. Likewise, on a `manifest push` to an insecure registry, the `--insecure` flag must be specified. If this is not used with an insecure registry, the manifest command fails to find a registry that meets the default requirements.
112+
Because the manifest command interacts soley with a registry, it has no way to query the engine for the list of allowed insecure registries. To allow the CLI to interact with an insecure registry, some `docker manifest` commands have an `--insecure` flag. For each transaction, such as a `create`, which queries a registry, the `--insecure` flag must be specified. This flag tells the CLI that this registry call may ignore security concerns like missing or self-signed certificates. Likewise, on a `manifest push` to an insecure registry, the `--insecure` flag must be specified. If this is not used with an insecure registry, the manifest command fails to find a registry that meets the default requirements.
108113

109114
## Examples
110115

@@ -168,9 +173,9 @@ $ docker manifest inspect --verbose hello-world
168173
}
169174
```
170175

171-
### Create and push a manifest list
176+
### Create, annotate and push a manifest list
172177

173-
To create a manifest list, you first `create` the manifest list locally by specifying the constituent images you would
178+
To create a manifest list, you may first `create` the manifest list locally by specifying the constituent images you would
174179
like to have included in your manifest list. Keep in mind that this is pushed to a registry, so if you want to push
175180
to a registry other than the docker registry, you need to create your manifest list with the registry name or IP and port.
176181
This is similar to tagging an image and pushing it to a foreign registry.
@@ -205,6 +210,44 @@ sha256:050b213d49d7673ba35014f21454c573dcbec75254a08f4a7c34f66a47c06aba
205210

206211
```
207212

213+
### Push a manifest list using yaml
214+
215+
Instead of using three cli commands (or more, depending on your annotations), you can push a manifest list using a single yaml file.
216+
217+
218+
```
219+
docker manifest push --file my-hello-world.yaml myregistry:port/my-hello-world:latest
220+
```
221+
222+
Sample file referencing four images:
223+
224+
```
225+
manifests:
226+
-
227+
image: hello-world-ppc64le:latest
228+
platform:
229+
architecture: ppc64le
230+
-
231+
image: clnperez/hello-world-amd64:latest
232+
platform:
233+
architecture: amd64
234+
os: linux
235+
-
236+
image: clnperez/hello-world-amd64-windows:latest
237+
platform:
238+
architecture: amd64
239+
os: windows
240+
osversion: "10.0.14393.2189"
241+
-
242+
image: clnperez/hello-world-s390x:latest
243+
platform:
244+
architecture: s390x
245+
os: linux
246+
osversion: 1.1
247+
variant: xyz
248+
```
249+
250+
208251
### Inspect a manifest list
209252

210253
```bash

vendor.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ golang.org/x/time a4bde12657593d5e90d0533a3e4fd95e635124cb
8383
google.golang.org/genproto 694d95ba50e67b2e363f3483057db5d4910c18f9
8484
google.golang.org/grpc v1.12.0
8585
gopkg.in/inf.v0 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4
86-
gopkg.in/yaml.v2 4c78c975fe7c825c6d1466c42be594d1d6f3aba6
86+
gopkg.in/yaml.v2 d670f9405373e636a5a2765eea47fac0c9bc91a4
8787
k8s.io/api kubernetes-1.8.2
8888
k8s.io/apimachinery kubernetes-1.8.2
8989
k8s.io/client-go kubernetes-1.8.2

0 commit comments

Comments
 (0)