Skip to content

Commit

Permalink
Add manifest command
Browse files Browse the repository at this point in the history
Enable inspection (aka "shallow pull") of an image's manifest info, and
also the creation of manifest lists (aka "fat manifests" or "multi-arch
images").

The workflow for creating a manifest list will be:

`docker manifest create new-list-ref-name manifest [manifests...]`
`docker manifest annotate new-list-ref-name manifest --os linux --arch
arm`
`docker manifest push new-list-ref-name`

\- or -

`docker manifest push -f annotated-manifests.yaml`

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`.
These by default show a ManifestDescriptor in the case of a single
manifest, or a DeserialedManifestList.

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 <christy@linux.vnet.ibm.com>
  • Loading branch information
clnperez committed Jun 4, 2017
1 parent a0066a1 commit dc0fc04
Show file tree
Hide file tree
Showing 141 changed files with 19,091 additions and 3 deletions.
8 changes: 6 additions & 2 deletions cli/command/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/docker/cli/cli/command/config"
"github.com/docker/cli/cli/command/container"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/command/manifest"
"github.com/docker/cli/cli/command/network"
"github.com/docker/cli/cli/command/node"
"github.com/docker/cli/cli/command/plugin"
Expand Down Expand Up @@ -38,12 +39,15 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
image.NewImageCommand(dockerCli),
image.NewBuildCommand(dockerCli),

// node
node.NewNodeCommand(dockerCli),
// manfiest
manifest.NewManifestCommand(dockerCli),

// network
network.NewNetworkCommand(dockerCli),

// node
node.NewNodeCommand(dockerCli),

// plugin
plugin.NewPluginCommand(dockerCli),

Expand Down
128 changes: 128 additions & 0 deletions cli/command/manifest/annotate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package manifest

import (
"fmt"

"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/distribution/reference"

"github.com/Sirupsen/logrus"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

type annotateOptions struct {
target string // the target manifest list name (also transaction ID)
image string // the manifest to annotate within the list
variant string // an architecture variant
os string
arch string
cpuFeatures []string
osFeatures []string
}

// NewAnnotateCommand creates a new `docker manifest annotate` command
func newAnnotateCommand(dockerCli *command.DockerCli) *cobra.Command {
var opts annotateOptions

cmd := &cobra.Command{
Use: "annotate NAME[:TAG] [OPTIONS]",
Short: "Add additional information to an image's manifest.",
Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
opts.target = args[0]
opts.image = args[1]
return runManifestAnnotate(dockerCli, opts)
},
}

flags := cmd.Flags()

flags.StringVar(&opts.os, "os", "", "Add ios info to a manifest before pushing it.")
flags.StringVar(&opts.arch, "arch", "", "Add arch info to a manifest before pushing it.")
flags.StringSliceVar(&opts.cpuFeatures, "cpu-features", []string{}, "Add feature info to a manifest before pushing it.")
flags.StringSliceVar(&opts.osFeatures, "os-features", []string{}, "Add feature info to a manifest before pushing it.")
flags.StringVar(&opts.variant, "variant", "", "Add arch variant to a manifest before pushing it.")

return cmd
}

func runManifestAnnotate(dockerCli *command.DockerCli, opts annotateOptions) error {

// Make sure the manifests are pulled, find the file you need, unmarshal the json, edit the file, and done.
targetRef, err := reference.ParseNormalizedNamed(opts.target)
if err != nil {
return errors.Wrapf(err, "annotate: Error parsing name for manifest list (%s): %s", opts.target)
}
imgRef, err := reference.ParseNormalizedNamed(opts.image)
if err != nil {
return errors.Wrapf(err, "annotate: Error prasing name for manifest (%s): %s:", opts.image)
}

// Make sure we've got tags or digests:
if _, isDigested := targetRef.(reference.Canonical); !isDigested {
targetRef = reference.TagNameOnly(targetRef)
}
if _, isDigested := imgRef.(reference.Canonical); !isDigested {
imgRef = reference.TagNameOnly(imgRef)
}
transactionID := makeFilesafeName(targetRef.String())
imgID := makeFilesafeName(imgRef.String())
logrus.Debugf("beginning annotate for %s/%s", transactionID, imgID)

imgInspect, _, err := getImageData(dockerCli, imgRef.String(), targetRef.String(), false)
if err != nil {
return err
}

if len(imgInspect) > 1 {
return fmt.Errorf("cannot annotate manifest list. Please pass an image (not list) name")
}

mf := imgInspect[0]

newMf, err := unmarshalIntoManifestInspect(imgID, transactionID)
if err != nil {
return err
}

// Update the mf
if opts.os != "" {
newMf.OS = opts.os
}
if opts.arch != "" {
newMf.Architecture = opts.arch
}
for _, cpuFeature := range opts.cpuFeatures {
newMf.Features = appendIfUnique(mf.Features, cpuFeature)
}
for _, osFeature := range opts.osFeatures {
newMf.OSFeatures = appendIfUnique(mf.OSFeatures, osFeature)
}
if opts.variant != "" {
newMf.Variant = opts.variant
}

// validate os/arch input
if !isValidOSArch(newMf.OS, newMf.Architecture) {
return fmt.Errorf("manifest entry for image has unsupported os/arch combination: %s/%s", opts.os, opts.arch)
}
// @TODO
// dgst := digest.FromBytes(b) can't use b/c not of the json.

if err := updateMfFile(newMf, imgID, transactionID); err != nil {
return err
}

logrus.Debugf("annotated %s with options %v", mf.RefName, opts)
return nil
}
func appendIfUnique(list []string, str string) []string {
for _, s := range list {
if s == str {
return list
}
}
return append(list, str)
}
44 changes: 44 additions & 0 deletions cli/command/manifest/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package manifest

import (
"fmt"

"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"

"github.com/spf13/cobra"
)

// NewManifestCommand returns a cobra command for `manifest` subcommands
func NewManifestCommand(dockerCli *command.DockerCli) *cobra.Command {
cmd := &cobra.Command{
Use: "manifest COMMAND",
Short: "Manage Docker image manifests and lists",
Long: manifestDescription,
Args: cli.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
},
}
cmd.AddCommand(
//newListFetchCommand(dockerCli),
newCreateListCommand(dockerCli),
newInspectCommand(dockerCli),
newAnnotateCommand(dockerCli),
newPushListCommand(dockerCli),
)
return cmd
}

var manifestDescription = `
The **docker manifest** command has subcommands for managing image manifests and
manifest lists. A manifest list allows you to use one name to refer to the same image
built for multiple architectures.
To see help for a subcommand, use:
docker manifest CMD help
For full details on using docker manifest lists view the registry v2 specification.
`
81 changes: 81 additions & 0 deletions cli/command/manifest/create_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package manifest

import (
"fmt"

"github.com/Sirupsen/logrus"
"github.com/pkg/errors"
"github.com/spf13/cobra"

"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/distribution/reference"
"github.com/docker/docker/registry"
)

type annotateOpts struct {
amend bool
}

func newCreateListCommand(dockerCli *command.DockerCli) *cobra.Command {

opts := annotateOpts{}

cmd := &cobra.Command{
Use: "create newRef manifest [manifest...]",
Short: "Create a local manifest list for annotating and pushing to a registry",
Args: cli.RequiresMinArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
return createManifestList(dockerCli, args, opts)
},
}

flags := cmd.Flags()
flags.BoolVarP(&opts.amend, "amend", "a", false, "Amend an existing manifest list transaction")
return cmd
}

func createManifestList(dockerCli *command.DockerCli, args []string, opts annotateOpts) error {

// Just do some basic verification here, and leave the rest for when the user pushes the list
newRef := args[0]
targetRef, err := reference.ParseNormalizedNamed(newRef)
if err != nil {
return errors.Wrapf(err, "error parsing name for manifest list (%s): %v", newRef)
}
_, err = registry.ParseRepositoryInfo(targetRef)
if err != nil {
return errors.Wrapf(err, "error parsing repository name for manifest list (%s): %v", newRef)
}

// Check locally for this list transaction before proceeding
if _, isDigested := targetRef.(reference.Canonical); !isDigested {
targetRef = reference.TagNameOnly(targetRef)
}
manifestFiles, err := getListFilenames(makeFilesafeName(targetRef.String()))
if err != nil {
return err
}
if len(manifestFiles) > 0 && !opts.amend {
return fmt.Errorf("refusing to continue over an existing manifest list transaction with no --amend flag")
}

// Now create the local manifest list transaction by looking up the manifest schemas
// for the constituent images:
manifests := args[1:]
logrus.Info("retrieving digests of images...")
for _, manifestRef := range manifests {

mfstData, _, err := getImageData(dockerCli, manifestRef, targetRef.String(), false)
if err != nil {
return err
}

if len(mfstData) > 1 {
// too many responses--can only happen if a manifest list was returned for the name lookup
return fmt.Errorf("manifest lists cannot embed another manifest list")
}

}
return nil
}
28 changes: 28 additions & 0 deletions cli/command/manifest/ensurehome_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// +build linux

package manifest

import (
"os"

"github.com/Sirupsen/logrus"
"github.com/docker/docker/dockerversion"
"github.com/docker/docker/pkg/homedir"
)

// ensureHomeIfIAmStatic ensure $HOME to be set if dockerversion.IAmStatic is "true".
// In a static binary, os/user.Current() leads to segfault due to a glibc issue that won't be fixed
// in the foreseeable future. (golang/go#13470, https://sourceware.org/bugzilla/show_bug.cgi?id=19341)
// So we forcibly set HOME so as to avoid call to os/user/Current()
func ensureHomeIfIAmStatic() error {
// Note: dockerversion.IAmStatic and homedir.GetStatic() is only available for linux.
if dockerversion.IAmStatic == "true" && os.Getenv("HOME") == "" {
home, err := homedir.GetStatic()
if err != nil {
return err
}
logrus.Warnf("docker manifest requires HOME to be set for static client binary. Forcibly setting HOME to %s.", home)
os.Setenv("HOME", home)
}
return nil
}
7 changes: 7 additions & 0 deletions cli/command/manifest/ensurehome_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// +build !linux

package manifest

func ensureHomeIfIAmStatic() error {
return nil
}
Loading

0 comments on commit dc0fc04

Please sign in to comment.