Skip to content

Commit

Permalink
manifest push using yaml file
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
clnperez committed Jul 2, 2018
1 parent f5393c9 commit 317c10b
Show file tree
Hide file tree
Showing 16 changed files with 479 additions and 48 deletions.
4 changes: 2 additions & 2 deletions cli/command/manifest/annotate.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ func runManifestAnnotate(dockerCli command.Cli, opts annotateOptions) error {
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)
if err := validateOSArch(imageManifest.Platform.OS, imageManifest.Platform.Architecture); err != nil {
return err
}
return manifestStore.Save(targetRef, imgRef, imageManifest)
}
Expand Down
87 changes: 82 additions & 5 deletions cli/command/manifest/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"

"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
Expand All @@ -14,13 +15,16 @@ import (
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/reference"
"github.com/docker/docker/registry"

"github.com/pkg/errors"
"github.com/spf13/cobra"
yaml "gopkg.in/yaml.v2"
)

type pushOpts struct {
insecure bool
purge bool
file string
target string
}

Expand All @@ -42,32 +46,48 @@ type pushRequest struct {
insecure bool
}

type yamlManifestList struct {
Image string
Manifests []yamlManifest
}

type yamlManifest struct {
Image string
Platform manifestlist.PlatformSpec
}

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",
Short: "Push a manifest list to a repository, either after a create, or from a file",
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")
flags.BoolVarP(&opts.purge, "purge", "p", false, "remove the local manifests after push")
flags.BoolVar(&opts.insecure, "insecure", false, "allow push to an insecure registry")
flags.StringVar(&opts.file, "file", "", "file containing the yaml representation of manifest list")
return cmd
}

func runPush(dockerCli command.Cli, opts pushOpts) error {

targetRef, err := normalizeReference(opts.target)
if err != nil {
return err
}
if opts.file != "" {
return pushListFromYaml(dockerCli, targetRef, opts)
}

return pushListFromStore(dockerCli, targetRef, opts)
}

func pushListFromStore(dockerCli command.Cli, targetRef reference.Named, opts pushOpts) error {
manifests, err := dockerCli.ManifestStore().GetList(targetRef)
if err != nil {
return err
Expand Down Expand Up @@ -271,3 +291,60 @@ func mountBlobs(ctx context.Context, client registryclient.RegistryClient, ref r
}
return nil
}

func pushListFromYaml(dockerCli command.Cli, targetRef reference.Named, opts pushOpts) error {
yamlInput, err := getYamlManifestList(opts.file)
if err != nil {
return err
}
if len(yamlInput.Manifests) == 0 {
return errors.New("no manifests specified in file input")
}
ctx := context.Background()
var manifests []types.ImageManifest
for _, manifest := range yamlInput.Manifests {
imageRef, err := normalizeReference(manifest.Image)
if err != nil {
return err
}
im, err := dockerCli.RegistryClient(opts.insecure).GetManifest(ctx, imageRef)
if err != nil {
return err
}
addYamlAnnotations(&im, manifest)
if err := validateOSArch(im.Platform.OS, im.Platform.Architecture); err != nil {
return err
}
manifests = append(manifests, im)
}

pushRequest, err := buildPushRequest(manifests, targetRef, opts.insecure)
if err != nil {
return err
}
return pushList(ctx, dockerCli, pushRequest)
}

func addYamlAnnotations(manifest *types.ImageManifest, ym yamlManifest) {
if ym.Platform.Variant != "" {
manifest.Platform.Variant = ym.Platform.Variant
}
if ym.Platform.OS != "" {
manifest.Platform.OS = ym.Platform.OS
}
if ym.Platform.Architecture != "" {
manifest.Platform.Architecture = ym.Platform.Architecture
}
if len(ym.Platform.OSFeatures) != 0 {
manifest.Platform.OSFeatures = ym.Platform.OSFeatures
}
}

func getYamlManifestList(yamlFile string) (yamlManifestList, error) {
yamlBuf, err := ioutil.ReadFile(yamlFile)
if err != nil {
return yamlManifestList{}, err
}
var yamlInput yamlManifestList
return yamlInput, yaml.UnmarshalStrict(yamlBuf, &yamlInput)
}
61 changes: 59 additions & 2 deletions cli/command/manifest/push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import (
func newFakeRegistryClient() *fakeRegistryClient {
return &fakeRegistryClient{
getManifestFunc: func(_ context.Context, _ reference.Named) (manifesttypes.ImageManifest, error) {
return manifesttypes.ImageManifest{}, errors.New("")
return manifesttypes.ImageManifest{}, errors.New("getManifestFunc not implemented")
},
getManifestListFunc: func(_ context.Context, _ reference.Named) ([]manifesttypes.ImageManifest, error) {
return nil, errors.Errorf("")
return nil, errors.Errorf("getManifestListFunc not implemented")
},
}
}
Expand Down Expand Up @@ -67,3 +67,60 @@ func TestManifestPush(t *testing.T) {
err = cmd.Execute()
assert.NilError(t, err)
}

func TestPushFromYaml(t *testing.T) {
cli := test.NewFakeCli(nil)
cli.SetRegistryClient(&fakeRegistryClient{
getManifestFunc: func(_ context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
return fullImageManifest(t, ref), nil
},
})

cmd := newPushListCommand(cli)
cmd.Flags().Set("file", "testdata/test-push.yaml")
cmd.SetArgs([]string{"pushtest/pass:latest"})
assert.NilError(t, cmd.Execute())
}

func TestManifestPushYamlErrors(t *testing.T) {
testCases := []struct {
flags map[string]string
args []string
expectedError string
}{
{
flags: map[string]string{"file": "testdata/test-push-fail.yaml"},
args: []string{"pushtest/fail:latest"},
expectedError: "manifest entry for image has unsupported os/arch combination: linux/nope",
},
{
flags: map[string]string{"file": "testdata/test-push-empty.yaml"},
args: []string{"pushtest/fail:latest"},
expectedError: "no manifests specified in file input",
},
{
args: []string{"testdata/test-push-empty.yaml"},
expectedError: "No such manifest: docker.io/testdata/test-push-empty.yaml:latest",
},
}

store, sCleanup := newTempManifestStore(t)
defer sCleanup()
for _, tc := range testCases {
cli := test.NewFakeCli(nil)
cli.SetRegistryClient(&fakeRegistryClient{
getManifestFunc: func(_ context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
return fullImageManifest(t, ref), nil
},
})

cli.SetManifestStore(store)
cmd := newPushListCommand(cli)
for k, v := range tc.flags {
cmd.Flags().Set(k, v)
}
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
1 change: 1 addition & 0 deletions cli/command/manifest/testdata/test-push-empty.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
manifests:
5 changes: 5 additions & 0 deletions cli/command/manifest/testdata/test-push-fail.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
manifests:
-
image: test/hello-world-ppc64le:latest
platform:
architecture: nope
28 changes: 28 additions & 0 deletions cli/command/manifest/testdata/test-push.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
manifests:
-
image: test/hello-world-ppc64le:latest
platform:
architecture: ppc64le
-
image: test/hello-world-amd64:latest
platform:
architecture: amd64
os: linux
-
image: test/hello-world-s390x:latest
platform:
architecture: s390x
os: linux
osversion: 1.1
variant: xyz
osfeatures: [a,b,c]
-
image: test/hello-world-armv5:latest
platform:
-
image: test/hello-world:armhf
platform:
architecture: arm
os: linux
variant: abc

20 changes: 18 additions & 2 deletions cli/command/manifest/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package manifest

import (
"context"
"fmt"

"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/manifest/store"
Expand All @@ -14,6 +15,18 @@ type osArch struct {
arch string
}

type invalidOSArchErr struct {
osArch
}

func (e *invalidOSArchErr) Error() string {
return fmt.Sprintf("manifest entry for image has unsupported os/arch combination: %s/%s", e.os, e.arch)
}

func newInvalidOSArchErr(os1 string, arch1 string) *invalidOSArchErr {
return &invalidOSArchErr{osArch{os: os1, arch: arch1}}
}

// Remove any unsupported os/arch combo
// list of valid os/arch values (see "Optional Environment Variables" section
// of https://golang.org/doc/install/source
Expand Down Expand Up @@ -48,10 +61,13 @@ var validOSArches = map[osArch]bool{
{os: "windows", arch: "amd64"}: true,
}

func isValidOSArch(os string, arch string) bool {
func validateOSArch(os string, arch string) error {
// check for existence of this combo
_, ok := validOSArches[osArch{os, arch}]
return ok
if !ok {
return newInvalidOSArchErr(os, arch)
}
return nil
}

func normalizeReference(ref string) (reference.Named, error) {
Expand Down
55 changes: 49 additions & 6 deletions docs/reference/commandline/manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,19 @@ Usage: docker manifest push [OPTIONS] MANIFEST_LIST
Push a manifest list to a repository

Options:
--help Print usage
--insecure Allow push to an insecure registry
-p, --purge Remove the local manifest list after push
--help Print usage
--insecure Allow push to an insecure registry
--file string File containing the yaml representation of manifest list
-p, --purge Remove the local manifest list after push
```

### Working with manifest lists

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.

### Working with insecure registries

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.
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.

## Examples

Expand Down Expand Up @@ -168,9 +173,9 @@ $ docker manifest inspect --verbose hello-world
}
```

### Create and push a manifest list
### Create, annotate and push a manifest list

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

```

### Push a manifest list using yaml

Instead of using three cli commands (or more, depending on your annotations), you can push a manifest list using a single yaml file.


```
docker manifest push --file my-hello-world.yaml myregistry:port/my-hello-world:latest
```

Sample file referencing four images:

```
manifests:
-
image: hello-world-ppc64le:latest
platform:
architecture: ppc64le
-
image: clnperez/hello-world-amd64:latest
platform:
architecture: amd64
os: linux
-
image: clnperez/hello-world-amd64-windows:latest
platform:
architecture: amd64
os: windows
osversion: "10.0.14393.2189"
-
image: clnperez/hello-world-s390x:latest
platform:
architecture: s390x
os: linux
osversion: 1.1
variant: xyz
```


### Inspect a manifest list

```bash
Expand Down
Loading

0 comments on commit 317c10b

Please sign in to comment.