X Tutup
Skip to content

Commit b518f11

Browse files
committed
client: add Import() and Export() for importing/exporting image in OCI format
Export as a tar (Note: "-" can be used for stdout): $ ctr images export /tmp/oci-busybox.tar docker.io/library/busybox:latest Import a tar (Note: "-" can be used for stdin): $ ctr images import foo/new:latest /tmp/oci-busybox.tar Note: media types are not converted at the moment: e.g. application/vnd.docker.image.rootfs.diff.tar.gzip -> application/vnd.oci.image.layer.v1.tar+gzip Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
1 parent 856b038 commit b518f11

File tree

13 files changed

+899
-279
lines changed

13 files changed

+899
-279
lines changed

client.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package containerd
33
import (
44
"context"
55
"fmt"
6+
"io"
67
"io/ioutil"
78
"log"
89
"net/http"
@@ -25,6 +26,7 @@ import (
2526
"github.com/containerd/containerd/errdefs"
2627
"github.com/containerd/containerd/images"
2728
"github.com/containerd/containerd/plugin"
29+
"github.com/containerd/containerd/reference"
2830
"github.com/containerd/containerd/remotes"
2931
"github.com/containerd/containerd/remotes/docker"
3032
"github.com/containerd/containerd/remotes/docker/schema1"
@@ -552,3 +554,120 @@ func (c *Client) Version(ctx context.Context) (Version, error) {
552554
Revision: response.Revision,
553555
}, nil
554556
}
557+
558+
type imageFormat string
559+
560+
const (
561+
ociImageFormat imageFormat = "oci"
562+
)
563+
564+
type importOpts struct {
565+
format imageFormat
566+
refObject string
567+
}
568+
569+
type ImportOpt func(c *importOpts) error
570+
571+
func WithOCIImportFormat() ImportOpt {
572+
return func(c *importOpts) error {
573+
if c.format != "" {
574+
return errors.New("format already set")
575+
}
576+
c.format = ociImageFormat
577+
return nil
578+
}
579+
}
580+
581+
// WithRefObject specifies the ref object to import.
582+
// If refObject is empty, it is copied from the ref argument of Import().
583+
func WithRefObject(refObject string) ImportOpt {
584+
return func(c *importOpts) error {
585+
c.refObject = refObject
586+
return nil
587+
}
588+
}
589+
590+
func resolveImportOpt(ref string, opts ...ImportOpt) (importOpts, error) {
591+
var iopts importOpts
592+
for _, o := range opts {
593+
if err := o(&iopts); err != nil {
594+
return iopts, err
595+
}
596+
}
597+
// use OCI as the default format
598+
if iopts.format == "" {
599+
iopts.format = ociImageFormat
600+
}
601+
// if refObject is not explicitly specified, use the one specified in ref
602+
if iopts.refObject == "" {
603+
refSpec, err := reference.Parse(ref)
604+
if err != nil {
605+
return iopts, err
606+
}
607+
iopts.refObject = refSpec.Object
608+
}
609+
return iopts, nil
610+
}
611+
612+
// Import imports an image from a Tar stream using reader.
613+
// OCI format is assumed by default.
614+
//
615+
// Note that unreferenced blobs are imported to the content store as well.
616+
func (c *Client) Import(ctx context.Context, ref string, reader io.Reader, opts ...ImportOpt) (Image, error) {
617+
iopts, err := resolveImportOpt(ref, opts...)
618+
if err != nil {
619+
return nil, err
620+
}
621+
switch iopts.format {
622+
case ociImageFormat:
623+
return c.importFromOCITar(ctx, ref, reader, iopts)
624+
default:
625+
return nil, errors.Errorf("unsupported format: %s", iopts.format)
626+
}
627+
}
628+
629+
type exportOpts struct {
630+
format imageFormat
631+
}
632+
633+
type ExportOpt func(c *exportOpts) error
634+
635+
func WithOCIExportFormat() ExportOpt {
636+
return func(c *exportOpts) error {
637+
if c.format != "" {
638+
return errors.New("format already set")
639+
}
640+
c.format = ociImageFormat
641+
return nil
642+
}
643+
}
644+
645+
// TODO: add WithMediaTypeTranslation that transforms media types according to the format.
646+
// e.g. application/vnd.docker.image.rootfs.diff.tar.gzip
647+
// -> application/vnd.oci.image.layer.v1.tar+gzip
648+
649+
// Export exports an image to a Tar stream.
650+
// OCI format is used by default.
651+
// It is up to caller to put "org.opencontainers.image.ref.name" annotation to desc.
652+
func (c *Client) Export(ctx context.Context, desc ocispec.Descriptor, opts ...ExportOpt) (io.ReadCloser, error) {
653+
var eopts exportOpts
654+
for _, o := range opts {
655+
if err := o(&eopts); err != nil {
656+
return nil, err
657+
}
658+
}
659+
// use OCI as the default format
660+
if eopts.format == "" {
661+
eopts.format = ociImageFormat
662+
}
663+
pr, pw := io.Pipe()
664+
switch eopts.format {
665+
case ociImageFormat:
666+
go func() {
667+
pw.CloseWithError(c.exportToOCITar(ctx, desc, pw, eopts))
668+
}()
669+
default:
670+
return nil, errors.Errorf("unsupported format: %s", eopts.format)
671+
}
672+
return pr, nil
673+
}

cmd/ctr/export.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package main
2+
3+
import (
4+
"io"
5+
"os"
6+
7+
"github.com/containerd/containerd/reference"
8+
digest "github.com/opencontainers/go-digest"
9+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
10+
"github.com/pkg/errors"
11+
"github.com/urfave/cli"
12+
)
13+
14+
var imagesExportCommand = cli.Command{
15+
Name: "export",
16+
Usage: "export an image",
17+
ArgsUsage: "[flags] <out> <image>",
18+
Description: `Export an image to a tar stream
19+
`,
20+
Flags: []cli.Flag{
21+
cli.StringFlag{
22+
Name: "oci-ref-name",
23+
Value: "",
24+
Usage: "Override org.opencontainers.image.ref.name annotation",
25+
},
26+
cli.StringFlag{
27+
Name: "manifest",
28+
Usage: "Digest of manifest",
29+
},
30+
cli.StringFlag{
31+
Name: "manifest-type",
32+
Usage: "Media type of manifest digest",
33+
Value: ocispec.MediaTypeImageManifest,
34+
},
35+
},
36+
Action: func(clicontext *cli.Context) error {
37+
var (
38+
out = clicontext.Args().First()
39+
local = clicontext.Args().Get(1)
40+
desc ocispec.Descriptor
41+
)
42+
43+
ctx, cancel := appContext(clicontext)
44+
defer cancel()
45+
46+
client, err := newClient(clicontext)
47+
if err != nil {
48+
return err
49+
}
50+
51+
if manifest := clicontext.String("manifest"); manifest != "" {
52+
desc.Digest, err = digest.Parse(manifest)
53+
if err != nil {
54+
return errors.Wrap(err, "invalid manifest digest")
55+
}
56+
desc.MediaType = clicontext.String("manifest-type")
57+
} else {
58+
img, err := client.ImageService().Get(ctx, local)
59+
if err != nil {
60+
return errors.Wrap(err, "unable to resolve image to manifest")
61+
}
62+
desc = img.Target
63+
}
64+
65+
if desc.Annotations == nil {
66+
desc.Annotations = make(map[string]string)
67+
}
68+
if s, ok := desc.Annotations[ocispec.AnnotationRefName]; !ok || s == "" {
69+
if ociRefName := determineOCIRefName(local); ociRefName != "" {
70+
desc.Annotations[ocispec.AnnotationRefName] = ociRefName
71+
}
72+
if ociRefName := clicontext.String("oci-ref-name"); ociRefName != "" {
73+
desc.Annotations[ocispec.AnnotationRefName] = ociRefName
74+
}
75+
}
76+
var w io.WriteCloser
77+
if out == "-" {
78+
w = os.Stdout
79+
} else {
80+
w, err = os.Create(out)
81+
if err != nil {
82+
return nil
83+
}
84+
}
85+
r, err := client.Export(ctx, desc)
86+
if err != nil {
87+
return err
88+
}
89+
if _, err := io.Copy(w, r); err != nil {
90+
return err
91+
}
92+
if err := w.Close(); err != nil {
93+
return err
94+
}
95+
return r.Close()
96+
},
97+
}
98+
99+
func determineOCIRefName(local string) string {
100+
refspec, err := reference.Parse(local)
101+
if err != nil {
102+
return ""
103+
}
104+
tag, _ := reference.SplitObject(refspec.Object)
105+
return tag
106+
}

cmd/ctr/images.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ var imageCommand = cli.Command{
2222
imagesListCommand,
2323
imageRemoveCommand,
2424
imagesSetLabelsCommand,
25+
imagesImportCommand,
26+
imagesExportCommand,
2527
},
2628
}
2729

cmd/ctr/import.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
8+
"github.com/containerd/containerd"
9+
"github.com/containerd/containerd/log"
10+
"github.com/urfave/cli"
11+
)
12+
13+
var imagesImportCommand = cli.Command{
14+
Name: "import",
15+
Usage: "import an image",
16+
ArgsUsage: "[flags] <ref> <in>",
17+
Description: `Import an image from a tar stream
18+
`,
19+
Flags: []cli.Flag{
20+
cli.StringFlag{
21+
Name: "ref-object",
22+
Value: "",
23+
Usage: "reference object e.g. tag@digest (default: use the object specified in ref)",
24+
},
25+
},
26+
Action: func(clicontext *cli.Context) error {
27+
var (
28+
ref = clicontext.Args().First()
29+
in = clicontext.Args().Get(1)
30+
refObject = clicontext.String("ref-object")
31+
)
32+
33+
ctx, cancel := appContext(clicontext)
34+
defer cancel()
35+
36+
client, err := newClient(clicontext)
37+
if err != nil {
38+
return err
39+
}
40+
41+
var r io.ReadCloser
42+
if in == "-" {
43+
r = os.Stdin
44+
} else {
45+
r, err = os.Open(in)
46+
if err != nil {
47+
return err
48+
}
49+
}
50+
img, err := client.Import(ctx,
51+
ref,
52+
r,
53+
containerd.WithRefObject(refObject),
54+
)
55+
if err != nil {
56+
return err
57+
}
58+
if err = r.Close(); err != nil {
59+
return err
60+
}
61+
62+
log.G(ctx).WithField("image", ref).Debug("unpacking")
63+
64+
// TODO: Show unpack status
65+
fmt.Printf("unpacking %s...", img.Target().Digest)
66+
err = img.Unpack(ctx, clicontext.String("snapshotter"))
67+
fmt.Println("done")
68+
return err
69+
},
70+
}

content/content.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"io"
66
"time"
77

8+
"github.com/containerd/containerd/oci"
89
"github.com/opencontainers/go-digest"
910
)
1011

@@ -77,10 +78,8 @@ type IngestManager interface {
7778
}
7879

7980
type Writer interface {
80-
io.WriteCloser
81+
oci.BlobWriter
8182
Status() (Status, error)
82-
Digest() digest.Digest
83-
Commit(size int64, expected digest.Digest) error
8483
Truncate(size int64) error
8584
}
8685

content/local/writer.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/containerd/containerd/content"
1010
"github.com/containerd/containerd/errdefs"
11+
"github.com/containerd/containerd/oci"
1112
"github.com/opencontainers/go-digest"
1213
"github.com/pkg/errors"
1314
)
@@ -78,7 +79,7 @@ func (w *writer) Commit(size int64, expected digest.Digest) error {
7879
}
7980

8081
if size > 0 && size != fi.Size() {
81-
return errors.Errorf("%q failed size validation: %v != %v", w.ref, fi.Size(), size)
82+
return oci.ErrUnexpectedSize{Expected: size, Actual: fi.Size()}
8283
}
8384

8485
if err := w.fp.Close(); err != nil {
@@ -87,7 +88,7 @@ func (w *writer) Commit(size int64, expected digest.Digest) error {
8788

8889
dgst := w.digester.Digest()
8990
if expected != "" && expected != dgst {
90-
return errors.Errorf("unexpected digest: %v != %v", dgst, expected)
91+
return oci.ErrUnexpectedDigest{Expected: expected, Actual: dgst}
9192
}
9293

9394
var (

0 commit comments

Comments
 (0)
X Tutup