Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

manifest push using yaml file #866

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cli/command/manifest/annotate.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ func runManifestAnnotate(dockerCli command.Cli, opts annotateOptions) error {
imageManifest.Descriptor.Platform.Variant = opts.variant
}

if !isValidOSArch(imageManifest.Descriptor.Platform.OS, imageManifest.Descriptor.Platform.Architecture) {
return errors.Errorf("manifest entry for image has unsupported os/arch combination: %s/%s", opts.os, opts.arch)
if err := validateOSArch(imageManifest.Descriptor.Platform.OS, imageManifest.Descriptor.Platform.Architecture); err != nil {
return err
}
return manifestStore.Save(targetRef, imgRef, imageManifest)
}
Expand Down
86 changes: 81 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 @@ -17,11 +18,13 @@ import (
"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 @@ -43,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 @@ -279,3 +298,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.Descriptor.Platform.OS, im.Descriptor.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.Descriptor.Platform.Variant = ym.Platform.Variant
}
if ym.Platform.OS != "" {
manifest.Descriptor.Platform.OS = ym.Platform.OS
}
if ym.Platform.Architecture != "" {
manifest.Descriptor.Platform.Architecture = ym.Platform.Architecture
}
if len(ym.Platform.OSFeatures) != 0 {
manifest.Descriptor.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
53 changes: 47 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,42 @@ 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
-
image: hello-world-amd64:latest
-
image: hello-world-amd64-windows:latest
platform:
os: windows
osversion: "10.0.14393.2189"
-
image: hello-world-390x:latest
platform:
architecture: s390x
os: linux
-
image: hello-world-armhf:latest
platform:
architecture: arm
os: linux
variant: v7
```


### Inspect a manifest list

```bash
Expand Down