Skip to content

Commit

Permalink
Merge pull request #3172 from dmcgowan/export-docker-compatibility
Browse files Browse the repository at this point in the history
Update image export to support Docker format
  • Loading branch information
fuweid committed May 17, 2019
2 parents cd5369b + 4754d2a commit e61f7f4
Show file tree
Hide file tree
Showing 9 changed files with 643 additions and 354 deletions.
123 changes: 50 additions & 73 deletions cmd/ctr/commands/images/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,33 @@ import (
"os"

"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/images/oci"
"github.com/containerd/containerd/reference"
digest "github.com/opencontainers/go-digest"
"github.com/containerd/containerd/images/archive"
"github.com/containerd/containerd/platforms"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/urfave/cli"
)

var exportCommand = cli.Command{
Name: "export",
Usage: "export an image",
ArgsUsage: "[flags] <out> <image>",
Description: `Export an image to a tar stream.
Currently, only OCI format is supported.
Usage: "export images",
ArgsUsage: "[flags] <out> <image> ...",
Description: `Export images to an OCI tar archive.
Tar output is formatted as an OCI archive, a Docker manifest is provided for the platform.
Use '--skip-manifest-json' to avoid including the Docker manifest.json file.
Use '--platform' to define the output platform.
When '--all-platforms' is given all images in a manifest list must be available.
`,
Flags: []cli.Flag{
// TODO(AkihiroSuda): make this map[string]string as in moby/moby#33355?
cli.StringFlag{
Name: "oci-ref-name",
Value: "",
Usage: "override org.opencontainers.image.ref.name annotation",
},
cli.StringFlag{
Name: "manifest",
Usage: "digest of manifest",
cli.BoolFlag{
Name: "skip-manifest-json",
Usage: "do not add Docker compatible manifest.json to archive",
},
cli.StringFlag{
Name: "manifest-type",
Usage: "media type of manifest digest",
Value: ocispec.MediaTypeImageManifest,
cli.StringSliceFlag{
Name: "platform",
Usage: "Pull content from a specific platform",
Value: &cli.StringSlice{},
},
cli.BoolFlag{
Name: "all-platforms",
Expand All @@ -59,43 +56,47 @@ Currently, only OCI format is supported.
},
Action: func(context *cli.Context) error {
var (
out = context.Args().First()
local = context.Args().Get(1)
desc ocispec.Descriptor
out = context.Args().First()
images = context.Args().Tail()
exportOpts = []archive.ExportOpt{}
)
if out == "" || local == "" {
if out == "" || len(images) == 0 {
return errors.New("please provide both an output filename and an image reference to export")
}

if pss := context.StringSlice("platform"); len(pss) > 0 {
var all []ocispec.Platform
for _, ps := range pss {
p, err := platforms.Parse(ps)
if err != nil {
return errors.Wrapf(err, "invalid platform %q", ps)
}
all = append(all, p)
}
exportOpts = append(exportOpts, archive.WithPlatform(platforms.Ordered(all...)))
} else {
exportOpts = append(exportOpts, archive.WithPlatform(platforms.Default()))
}

if context.Bool("all-platforms") {
exportOpts = append(exportOpts, archive.WithAllPlatforms())
}

if context.Bool("skip-manifest-json") {
exportOpts = append(exportOpts, archive.WithSkipDockerManifest())
}

client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
if manifest := context.String("manifest"); manifest != "" {
desc.Digest, err = digest.Parse(manifest)
if err != nil {
return errors.Wrap(err, "invalid manifest digest")
}
desc.MediaType = context.String("manifest-type")
} else {
img, err := client.ImageService().Get(ctx, local)
if err != nil {
return errors.Wrap(err, "unable to resolve image to manifest")
}
desc = img.Target
}

if desc.Annotations == nil {
desc.Annotations = make(map[string]string)
}
if s, ok := desc.Annotations[ocispec.AnnotationRefName]; !ok || s == "" {
if ociRefName := determineOCIRefName(local); ociRefName != "" {
desc.Annotations[ocispec.AnnotationRefName] = ociRefName
}
if ociRefName := context.String("oci-ref-name"); ociRefName != "" {
desc.Annotations[ocispec.AnnotationRefName] = ociRefName
}
is := client.ImageService()
for _, img := range images {
exportOpts = append(exportOpts, archive.WithImage(is, img))
}

var w io.WriteCloser
if out == "-" {
w = os.Stdout
Expand All @@ -105,32 +106,8 @@ Currently, only OCI format is supported.
return nil
}
}
defer w.Close()

var (
exportOpts []oci.V1ExporterOpt
)

exportOpts = append(exportOpts, oci.WithAllPlatforms(context.Bool("all-platforms")))

r, err := client.Export(ctx, desc, exportOpts...)
if err != nil {
return err
}
if _, err := io.Copy(w, r); err != nil {
return err
}
if err := w.Close(); err != nil {
return err
}
return r.Close()
return client.Export(ctx, w, exportOpts...)
},
}

func determineOCIRefName(local string) string {
refspec, err := reference.Parse(local)
if err != nil {
return ""
}
tag, _ := reference.SplitObject(refspec.Object)
return tag
}
26 changes: 6 additions & 20 deletions export.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,12 @@ import (
"context"
"io"

"github.com/containerd/containerd/images/oci"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/containerd/containerd/images/archive"
)

// Export exports an image to a Tar stream.
// OCI format is used by default.
// It is up to caller to put "org.opencontainers.image.ref.name" annotation to desc.
// TODO(AkihiroSuda): support exporting multiple descriptors at once to a single archive stream.
func (c *Client) Export(ctx context.Context, desc ocispec.Descriptor, opts ...oci.V1ExporterOpt) (io.ReadCloser, error) {

exporter, err := oci.ResolveV1ExportOpt(opts...)
if err != nil {
return nil, err
}

pr, pw := io.Pipe()
go func() {
pw.CloseWithError(errors.Wrap(exporter.Export(ctx, c.ContentStore(), desc, pw), "export failed"))
}()
return pr, nil
// Export exports images to a Tar stream.
// The tar archive is in OCI format with a Docker compatible manifest
// when a single target platform is given.
func (c *Client) Export(ctx context.Context, w io.Writer, opts ...archive.ExportOpt) error {
return archive.Export(ctx, c.ContentStore(), w, opts...)
}
15 changes: 10 additions & 5 deletions export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ package containerd

import (
"archive/tar"
"bytes"
"io"
"runtime"
"testing"

"github.com/containerd/containerd/images/archive"
"github.com/containerd/containerd/platforms"
)

// TestOCIExport exports testImage as a tar stream
func TestOCIExport(t *testing.T) {
// TestExport exports testImage as a tar stream
func TestExport(t *testing.T) {
// TODO: support windows
if testing.Short() || runtime.GOOS == "windows" {
t.Skip()
Expand All @@ -38,15 +42,16 @@ func TestOCIExport(t *testing.T) {
}
defer client.Close()

pulled, err := client.Fetch(ctx, testImage)
_, err = client.Fetch(ctx, testImage)
if err != nil {
t.Fatal(err)
}
exportedStream, err := client.Export(ctx, pulled.Target)
wb := bytes.NewBuffer(nil)
err = client.Export(ctx, wb, archive.WithPlatform(platforms.Default()), archive.WithImage(client.ImageService(), testImage))
if err != nil {
t.Fatal(err)
}
assertOCITar(t, exportedStream)
assertOCITar(t, bytes.NewReader(wb.Bytes()))
}

func assertOCITar(t *testing.T, r io.Reader) {
Expand Down
28 changes: 28 additions & 0 deletions images/annotations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package images

const (
// AnnotationImageName is an annotation on a Descriptor in an index.json
// containing the `Name` value as used by an `Image` struct
AnnotationImageName = "io.containerd.image.name"

// AnnotationImageNamePrefix is used the same way as AnnotationImageName
// but may be used to refer to additional names in the annotation map
// using user-defined suffixes (i.e. "extra.1")
AnnotationImageNamePrefix = AnnotationImageName + "."
)
Loading

0 comments on commit e61f7f4

Please sign in to comment.