Skip to content
Merged
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
159 changes: 49 additions & 110 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import (
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/plugin"
"github.com/containerd/containerd/reference"
"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
"github.com/containerd/containerd/remotes/docker/schema1"
Expand Down Expand Up @@ -494,95 +493,27 @@ func (c *Client) Version(ctx context.Context) (Version, error) {
}, nil
}

type imageFormat string

const (
ociImageFormat imageFormat = "oci"
)

type importOpts struct {
format imageFormat
refObject string
labels map[string]string
}

// ImportOpt allows the caller to specify import specific options
type ImportOpt func(c *importOpts) error

// WithImportLabel sets a label to be associated with an imported image
func WithImportLabel(key, value string) ImportOpt {
return func(opts *importOpts) error {
if opts.labels == nil {
opts.labels = make(map[string]string)
}

opts.labels[key] = value
return nil
}
}

// WithImportLabels associates a set of labels to an imported image
func WithImportLabels(labels map[string]string) ImportOpt {
return func(opts *importOpts) error {
if opts.labels == nil {
opts.labels = make(map[string]string)
}

for k, v := range labels {
opts.labels[k] = v
}
return nil
}
}

// WithOCIImportFormat sets the import format for an OCI image format
func WithOCIImportFormat() ImportOpt {
return func(c *importOpts) error {
if c.format != "" {
return errors.New("format already set")
}
c.format = ociImageFormat
return nil
}
}

// WithRefObject specifies the ref object to import.
// If refObject is empty, it is copied from the ref argument of Import().
func WithRefObject(refObject string) ImportOpt {
return func(c *importOpts) error {
c.refObject = refObject
return nil
}
}

func resolveImportOpt(ref string, opts ...ImportOpt) (importOpts, error) {
func resolveImportOpt(opts ...ImportOpt) (importOpts, error) {
var iopts importOpts
for _, o := range opts {
if err := o(&iopts); err != nil {
return iopts, err
}
}
// use OCI as the default format
if iopts.format == "" {
iopts.format = ociImageFormat
}
// if refObject is not explicitly specified, use the one specified in ref
if iopts.refObject == "" {
refSpec, err := reference.Parse(ref)
if err != nil {
return iopts, err
}
iopts.refObject = refSpec.Object
}
return iopts, nil
}

// Import imports an image from a Tar stream using reader.
// OCI format is assumed by default.
//
// Note that unreferenced blobs are imported to the content store as well.
func (c *Client) Import(ctx context.Context, ref string, reader io.Reader, opts ...ImportOpt) (Image, error) {
iopts, err := resolveImportOpt(ref, opts...)
// Caller needs to specify importer. Future version may use oci.v1 as the default.
// Note that unreferrenced blobs may be imported to the content store as well.
func (c *Client) Import(ctx context.Context, importer images.Importer, reader io.Reader, opts ...ImportOpt) ([]Image, error) {
_, err := resolveImportOpt(opts...) // unused now
if err != nil {
return nil, err
}
Expand All @@ -593,58 +524,66 @@ func (c *Client) Import(ctx context.Context, ref string, reader io.Reader, opts
}
defer done()

switch iopts.format {
case ociImageFormat:
return c.importFromOCITar(ctx, ref, reader, iopts)
default:
return nil, errors.Errorf("unsupported format: %s", iopts.format)
imgrecs, err := importer.Import(ctx, c.ContentStore(), reader)
if err != nil {
// is.Update() is not called on error
return nil, err
}

is := c.ImageService()
var images []Image
for _, imgrec := range imgrecs {
if updated, err := is.Update(ctx, imgrec, "target"); err != nil {
if !errdefs.IsNotFound(err) {
return nil, err
}

created, err := is.Create(ctx, imgrec)
if err != nil {
return nil, err
}

imgrec = created
} else {
imgrec = updated
}

images = append(images, &image{
client: c,
i: imgrec,
})
}
return images, nil
}

type exportOpts struct {
format imageFormat
}

// ExportOpt allows callers to set export options
// ExportOpt allows the caller to specify export-specific options
type ExportOpt func(c *exportOpts) error

// WithOCIExportFormat sets the OCI image format as the export target
func WithOCIExportFormat() ExportOpt {
return func(c *exportOpts) error {
if c.format != "" {
return errors.New("format already set")
func resolveExportOpt(opts ...ExportOpt) (exportOpts, error) {
var eopts exportOpts
for _, o := range opts {
if err := o(&eopts); err != nil {
return eopts, err
}
c.format = ociImageFormat
return nil
}
return eopts, nil
}

// TODO: add WithMediaTypeTranslation that transforms media types according to the format.
// e.g. application/vnd.docker.image.rootfs.diff.tar.gzip
// -> application/vnd.oci.image.layer.v1.tar+gzip

// 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.
func (c *Client) Export(ctx context.Context, desc ocispec.Descriptor, opts ...ExportOpt) (io.ReadCloser, error) {
var eopts exportOpts
for _, o := range opts {
if err := o(&eopts); err != nil {
return nil, err
}
}
// use OCI as the default format
if eopts.format == "" {
eopts.format = ociImageFormat
// TODO(AkihiroSuda): support exporting multiple descriptors at once to a single archive stream.
func (c *Client) Export(ctx context.Context, exporter images.Exporter, desc ocispec.Descriptor, opts ...ExportOpt) (io.ReadCloser, error) {
_, err := resolveExportOpt(opts...) // unused now
if err != nil {
return nil, err
}
pr, pw := io.Pipe()
switch eopts.format {
case ociImageFormat:
go func() {
pw.CloseWithError(c.exportToOCITar(ctx, desc, pw, eopts))
}()
default:
return nil, errors.Errorf("unsupported format: %s", eopts.format)
}
go func() {
pw.CloseWithError(exporter.Export(ctx, c.ContentStore(), desc, pw))
}()
return pr, nil
}
14 changes: 9 additions & 5 deletions cmd/ctr/commands/images/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"

"github.com/containerd/containerd/cmd/ctr/commands"
oci "github.com/containerd/containerd/images/oci"
"github.com/containerd/containerd/reference"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
Expand All @@ -13,11 +14,14 @@ import (
)

var exportCommand = cli.Command{
Name: "export",
Usage: "export an image",
ArgsUsage: "[flags] <out> <image>",
Description: "export an image to a tar stream",
Name: "export",
Usage: "export an image",
ArgsUsage: "[flags] <out> <image>",
Description: `Export an image to a tar stream.
Currently, only OCI format is supported.
`,
Flags: []cli.Flag{
// TODO(AkihiroSuda): make this map[string]string as in moby/moby#33355?
cli.StringFlag{
Name: "oci-ref-name",
Value: "",
Expand Down Expand Up @@ -78,7 +82,7 @@ var exportCommand = cli.Command{
return nil
}
}
r, err := client.Export(ctx, desc)
r, err := client.Export(ctx, &oci.V1Exporter{}, desc)
if err != nil {
return err
}
Expand Down
83 changes: 56 additions & 27 deletions cmd/ctr/commands/images/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,66 @@ import (
"io"
"os"

"github.com/containerd/containerd"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/images"
oci "github.com/containerd/containerd/images/oci"
"github.com/containerd/containerd/log"
"github.com/urfave/cli"
)

var importCommand = cli.Command{
Name: "import",
Usage: "import an image",
ArgsUsage: "[flags] <ref> <in>",
Description: "import an image from a tar stream",
Flags: []cli.Flag{
Name: "import",
Usage: "import images",
ArgsUsage: "[flags] <in>",
Description: `Import images from a tar stream.
Implemented formats:
- oci.v1 (default)


For oci.v1 format, you need to specify --oci-name because an OCI archive contains image refs (tags)
but does not contain the base image name.

e.g.
$ ctr images import --format oci.v1 --oci-name foo/bar foobar.tar

If foobar.tar contains an OCI ref named "latest" and anonymous ref "sha256:deadbeef", the command will create
"foo/bar:latest" and "foo/bar@sha256:deadbeef" images in the containerd store.
`,
Flags: append([]cli.Flag{
cli.StringFlag{
Name: "ref-object",
Value: "",
Usage: "reference object e.g. tag@digest (default: use the object specified in ref)",
Name: "format",
Value: "oci.v1",
Usage: "image format. See DESCRIPTION.",
},
commands.LabelFlag,
},
cli.StringFlag{
Name: "oci-name",
Value: "unknown/unknown",
Usage: "prefix added to either oci.v1 ref annotation or digest",
},
// TODO(AkihiroSuda): support commands.LabelFlag (for all children objects)
}, commands.SnapshotterFlags...),

Action: func(context *cli.Context) error {
var (
ref = context.Args().First()
in = context.Args().Get(1)
refObject = context.String("ref-object")
labels = commands.LabelArgs(context.StringSlice("label"))
in = context.Args().First()
imageImporter images.Importer
)

switch format := context.String("format"); format {
case "oci.v1":
imageImporter = &oci.V1Importer{
ImageName: context.String("oci-name"),
}
default:
return fmt.Errorf("unknown format %s", format)
}

client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()

var r io.ReadCloser
if in == "-" {
r = os.Stdin
Expand All @@ -45,25 +74,25 @@ var importCommand = cli.Command{
return err
}
}
img, err := client.Import(ctx,
ref,
r,
containerd.WithRefObject(refObject),
containerd.WithImportLabels(labels),
)
imgs, err := client.Import(ctx, imageImporter, r)
if err != nil {
return err
}
if err = r.Close(); err != nil {
return err
}

log.G(ctx).WithField("image", ref).Debug("unpacking")
log.G(ctx).Debugf("unpacking %d images", len(imgs))

// TODO: Show unpack status
fmt.Printf("unpacking %s...", img.Target().Digest)
err = img.Unpack(ctx, context.String("snapshotter"))
fmt.Println("done")
return err
for _, img := range imgs {
// TODO: Show unpack status
fmt.Printf("unpacking %s (%s)...", img.Name(), img.Target().Digest)
err = img.Unpack(ctx, context.String("snapshotter"))
if err != nil {
return err
}
fmt.Println("done")
}
return nil
},
}
9 changes: 5 additions & 4 deletions export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import (
"io"
"runtime"
"testing"

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

// TestExport exports testImage as a tar stream
func TestExport(t *testing.T) {
// TestOCIExport exports testImage as a tar stream
func TestOCIExport(t *testing.T) {
// TODO: support windows
if testing.Short() || runtime.GOOS == "windows" {
t.Skip()
Expand All @@ -26,8 +28,7 @@ func TestExport(t *testing.T) {
if err != nil {
t.Fatal(err)
}

exportedStream, err := client.Export(ctx, pulled.Target())
exportedStream, err := client.Export(ctx, &oci.V1Exporter{}, pulled.Target())
if err != nil {
t.Fatal(err)
}
Expand Down
Loading