Merge pull request #2633 from dmcgowan/import-docker

Support importing docker images
This commit is contained in:
Phil Estes 2018-09-19 09:52:47 -04:00 committed by GitHub
commit 1ac5ac652b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 510 additions and 323 deletions

View File

@ -20,10 +20,11 @@ import (
"fmt"
"io"
"os"
"time"
"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/images/archive"
"github.com/containerd/containerd/log"
"github.com/urfave/cli"
)
@ -34,45 +35,58 @@ var importCommand = cli.Command{
ArgsUsage: "[flags] <in>",
Description: `Import images from a tar stream.
Implemented formats:
- oci.v1 (default)
- oci.v1
- docker.v1.1
- docker.v1.2
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.
For OCI v1, you may need to specify --base-name because an OCI archive may
contain only partial image references (tags without the base image name).
If no base image name is provided, a name will be generated as "import-%{yyyy-MM-dd}".
e.g.
$ ctr images import --format oci.v1 --oci-name foo/bar foobar.tar
$ ctr images import --base-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: "format",
Value: "oci.v1",
Usage: "image format. See DESCRIPTION.",
Name: "base-name",
Value: "",
Usage: "base image name for added images, when provided only images with this name prefix are imported",
},
cli.BoolFlag{
Name: "digests",
Usage: "whether to create digest images (default: false)",
},
cli.StringFlag{
Name: "oci-name",
Value: "unknown/unknown",
Usage: "prefix added to either oci.v1 ref annotation or digest",
Name: "index-name",
Usage: "image name to keep index as, by default index is discarded",
},
// TODO(AkihiroSuda): support commands.LabelFlag (for all children objects)
}, commands.SnapshotterFlags...),
Action: func(context *cli.Context) error {
var (
in = context.Args().First()
imageImporter images.Importer
opts []containerd.ImportOpt
)
switch format := context.String("format"); format {
case "oci.v1":
imageImporter = &oci.V1Importer{
ImageName: context.String("oci-name"),
prefix := context.String("base-name")
if prefix == "" {
prefix = fmt.Sprintf("import-%s", time.Now().Format("2006-01-02"))
opts = append(opts, containerd.WithImageRefTranslator(archive.AddRefPrefix(prefix)))
} else {
// When provided, filter out references which do not match
opts = append(opts, containerd.WithImageRefTranslator(archive.FilterRefPrefix(prefix)))
}
default:
return fmt.Errorf("unknown format %s", format)
if context.Bool("digests") {
opts = append(opts, containerd.WithDigestRef(archive.DigestTranslator(prefix)))
}
if idxName := context.String("index-name"); idxName != "" {
opts = append(opts, containerd.WithIndexName(idxName))
}
client, ctx, cancel, err := commands.NewClient(context)
@ -90,20 +104,24 @@ If foobar.tar contains an OCI ref named "latest" and anonymous ref "sha256:deadb
return err
}
}
imgs, err := client.Import(ctx, imageImporter, r)
imgs, err := client.Import(ctx, r, opts...)
closeErr := r.Close()
if err != nil {
return err
}
if err = r.Close(); err != nil {
return err
if closeErr != nil {
return closeErr
}
log.G(ctx).Debugf("unpacking %d images", len(imgs))
for _, img := range imgs {
// TODO: Allow configuration of the platform
image := containerd.NewImage(client, img)
// TODO: Show unpack status
fmt.Printf("unpacking %s (%s)...", img.Name(), img.Target().Digest)
err = img.Unpack(ctx, context.String("snapshotter"))
fmt.Printf("unpacking %s (%s)...", img.Name, img.Target.Digest)
err = image.Unpack(ctx, context.String("snapshotter"))
if err != nil {
return err
}

View File

@ -70,7 +70,7 @@ func WriteBlob(ctx context.Context, cs Ingester, ref string, r io.Reader, desc o
cw, err := OpenWriter(ctx, cs, WithRef(ref), WithDescriptor(desc))
if err != nil {
if !errdefs.IsAlreadyExists(err) {
return err
return errors.Wrap(err, "failed to open writer")
}
return nil // all ready present
@ -127,7 +127,7 @@ func OpenWriter(ctx context.Context, cs Ingester, opts ...WriterOpt) (Writer, er
func Copy(ctx context.Context, cw Writer, r io.Reader, size int64, expected digest.Digest, opts ...Opt) error {
ws, err := cw.Status()
if err != nil {
return err
return errors.Wrap(err, "failed to get status")
}
if ws.Offset > 0 {
@ -138,7 +138,7 @@ func Copy(ctx context.Context, cw Writer, r io.Reader, size int64, expected dige
}
if _, err := copyWithBuffer(cw, r); err != nil {
return err
return errors.Wrap(err, "failed to copy")
}
if err := cw.Commit(ctx, size, expected, opts...); err != nil {

View File

@ -57,7 +57,7 @@ func (rw *remoteWriter) Status() (content.Status, error) {
Action: contentapi.WriteActionStat,
})
if err != nil {
return content.Status{}, errors.Wrap(err, "error getting writer status")
return content.Status{}, errors.Wrap(errdefs.FromGRPC(err), "error getting writer status")
}
return content.Status{
@ -82,7 +82,7 @@ func (rw *remoteWriter) Write(p []byte) (n int, err error) {
Data: p,
})
if err != nil {
return 0, err
return 0, errors.Wrap(errdefs.FromGRPC(err), "failed to send write")
}
n = int(resp.Offset - offset)
@ -112,7 +112,7 @@ func (rw *remoteWriter) Commit(ctx context.Context, size int64, expected digest.
Labels: base.Labels,
})
if err != nil {
return errdefs.FromGRPC(err)
return errors.Wrap(errdefs.FromGRPC(err), "commit failed")
}
if size != 0 && resp.Offset != size {

View File

@ -22,6 +22,7 @@ import (
"github.com/containerd/containerd/images"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
type exportOpts struct {
@ -51,7 +52,7 @@ func (c *Client) Export(ctx context.Context, exporter images.Exporter, desc ocis
}
pr, pw := io.Pipe()
go func() {
pw.CloseWithError(exporter.Export(ctx, c.ContentStore(), desc, pw))
pw.CloseWithError(errors.Wrap(exporter.Export(ctx, c.ContentStore(), desc, pw), "export failed"))
}()
return pr, nil
}

254
images/archive/importer.go Normal file
View File

@ -0,0 +1,254 @@
/*
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 archive provides a Docker and OCI compatible importer
package archive
import (
"archive/tar"
"bytes"
"context"
"encoding/json"
"io"
"io/ioutil"
"path"
"github.com/containerd/containerd/archive/compression"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/log"
digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
// ImportIndex imports an index from a tar achive image bundle
// - implements Docker v1.1, v1.2 and OCI v1.
// - prefers OCI v1 when provided
// - creates OCI index for Docker formats
// - normalizes Docker references and adds as OCI ref name
// e.g. alpine:latest -> docker.io/library/alpine:latest
// - existing OCI reference names are untouched
// - TODO: support option to compress layers on ingest
func ImportIndex(ctx context.Context, store content.Store, reader io.Reader) (ocispec.Descriptor, error) {
var (
tr = tar.NewReader(reader)
ociLayout ocispec.ImageLayout
mfsts []struct {
Config string
RepoTags []string
Layers []string
}
symlinks = make(map[string]string)
blobs = make(map[string]ocispec.Descriptor)
)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return ocispec.Descriptor{}, err
}
if hdr.Typeflag == tar.TypeSymlink {
symlinks[hdr.Name] = path.Join(path.Dir(hdr.Name), hdr.Linkname)
}
if hdr.Typeflag != tar.TypeReg && hdr.Typeflag != tar.TypeRegA {
if hdr.Typeflag != tar.TypeDir {
log.G(ctx).WithField("file", hdr.Name).Debug("file type ignored")
}
continue
}
hdrName := path.Clean(hdr.Name)
if hdrName == ocispec.ImageLayoutFile {
if err = onUntarJSON(tr, &ociLayout); err != nil {
return ocispec.Descriptor{}, errors.Wrapf(err, "untar oci layout %q", hdr.Name)
}
} else if hdrName == "manifest.json" {
if err = onUntarJSON(tr, &mfsts); err != nil {
return ocispec.Descriptor{}, errors.Wrapf(err, "untar manifest %q", hdr.Name)
}
} else {
dgst, err := onUntarBlob(ctx, tr, store, hdr.Size, "tar-"+hdrName)
if err != nil {
return ocispec.Descriptor{}, errors.Wrapf(err, "failed to ingest %q", hdr.Name)
}
blobs[hdrName] = ocispec.Descriptor{
Digest: dgst,
Size: hdr.Size,
}
}
}
// If OCI layout was given, interpret the tar as an OCI layout.
// When not provided, the layout of the tar will be interpretted
// as Docker v1.1 or v1.2.
if ociLayout.Version != "" {
if ociLayout.Version != ocispec.ImageLayoutVersion {
return ocispec.Descriptor{}, errors.Errorf("unsupported OCI version %s", ociLayout.Version)
}
idx, ok := blobs["index.json"]
if !ok {
return ocispec.Descriptor{}, errors.Errorf("missing index.json in OCI layout %s", ocispec.ImageLayoutVersion)
}
idx.MediaType = ocispec.MediaTypeImageIndex
return idx, nil
}
for name, linkname := range symlinks {
desc, ok := blobs[linkname]
if !ok {
return ocispec.Descriptor{}, errors.Errorf("no target for symlink layer from %q to %q", name, linkname)
}
blobs[name] = desc
}
var idx ocispec.Index
for _, mfst := range mfsts {
config, ok := blobs[mfst.Config]
if !ok {
return ocispec.Descriptor{}, errors.Errorf("image config %q not found", mfst.Config)
}
config.MediaType = ocispec.MediaTypeImageConfig
layers, err := resolveLayers(ctx, store, mfst.Layers, blobs)
if err != nil {
return ocispec.Descriptor{}, errors.Wrap(err, "failed to resolve layers")
}
manifest := ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2,
},
Config: config,
Layers: layers,
}
desc, err := writeManifest(ctx, store, manifest, ocispec.MediaTypeImageManifest)
if err != nil {
return ocispec.Descriptor{}, errors.Wrap(err, "write docker manifest")
}
platforms, err := images.Platforms(ctx, store, desc)
if err != nil {
return ocispec.Descriptor{}, errors.Wrap(err, "unable to resolve platform")
}
if len(platforms) > 0 {
// Only one platform can be resolved from non-index manifest,
// The platform can only come from the config included above,
// if the config has no platform it can be safely ommitted.
desc.Platform = &platforms[0]
}
if len(mfst.RepoTags) == 0 {
idx.Manifests = append(idx.Manifests, desc)
} else {
// Add descriptor per tag
for _, ref := range mfst.RepoTags {
mfstdesc := desc
normalized, err := normalizeReference(ref)
if err != nil {
return ocispec.Descriptor{}, err
}
mfstdesc.Annotations = map[string]string{
ocispec.AnnotationRefName: normalized,
}
idx.Manifests = append(idx.Manifests, mfstdesc)
}
}
}
return writeManifest(ctx, store, idx, ocispec.MediaTypeImageIndex)
}
func onUntarJSON(r io.Reader, j interface{}) error {
b, err := ioutil.ReadAll(r)
if err != nil {
return err
}
if err := json.Unmarshal(b, j); err != nil {
return err
}
return nil
}
func onUntarBlob(ctx context.Context, r io.Reader, store content.Ingester, size int64, ref string) (digest.Digest, error) {
dgstr := digest.Canonical.Digester()
if err := content.WriteBlob(ctx, store, ref, io.TeeReader(r, dgstr.Hash()), ocispec.Descriptor{Size: size}); err != nil {
return "", err
}
return dgstr.Digest(), nil
}
func resolveLayers(ctx context.Context, store content.Store, layerFiles []string, blobs map[string]ocispec.Descriptor) ([]ocispec.Descriptor, error) {
var layers []ocispec.Descriptor
for _, f := range layerFiles {
desc, ok := blobs[f]
if !ok {
return nil, errors.Errorf("layer %q not found", f)
}
// Open blob, resolve media type
ra, err := store.ReaderAt(ctx, desc)
if err != nil {
return nil, errors.Wrapf(err, "failed to open %q (%s)", f, desc.Digest)
}
s, err := compression.DecompressStream(content.NewReader(ra))
if err != nil {
return nil, errors.Wrapf(err, "failed to detect compression for %q", f)
}
if s.GetCompression() == compression.Uncompressed {
// TODO: Support compressing and writing back to content store
desc.MediaType = ocispec.MediaTypeImageLayer
} else {
desc.MediaType = ocispec.MediaTypeImageLayerGzip
}
s.Close()
layers = append(layers, desc)
}
return layers, nil
}
func writeManifest(ctx context.Context, cs content.Ingester, manifest interface{}, mediaType string) (ocispec.Descriptor, error) {
manifestBytes, err := json.Marshal(manifest)
if err != nil {
return ocispec.Descriptor{}, err
}
desc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(manifestBytes),
Size: int64(len(manifestBytes)),
}
if err := content.WriteBlob(ctx, cs, "manifest-"+desc.Digest.String(), bytes.NewReader(manifestBytes), desc); err != nil {
return ocispec.Descriptor{}, err
}
return desc, nil
}

View File

@ -0,0 +1,86 @@
/*
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 archive
import (
"strings"
"github.com/containerd/cri/pkg/util"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
// FilterRefPrefix restricts references to having the given image
// prefix. Tag-only references will have the prefix prepended.
func FilterRefPrefix(image string) func(string) string {
return refTranslator(image, true)
}
// AddRefPrefix prepends the given image prefix to tag-only references,
// while leaving returning full references unmodified.
func AddRefPrefix(image string) func(string) string {
return refTranslator(image, false)
}
// refTranslator creates a reference which only has a tag or verifies
// a full reference.
func refTranslator(image string, checkPrefix bool) func(string) string {
return func(ref string) string {
// Check if ref is full reference
if strings.ContainsAny(ref, "/:@") {
// If not prefixed, don't include image
if checkPrefix && !isImagePrefix(ref, image) {
return ""
}
return ref
}
return image + ":" + ref
}
}
func isImagePrefix(s, prefix string) bool {
if !strings.HasPrefix(s, prefix) {
return false
}
if len(s) > len(prefix) {
switch s[len(prefix)] {
case '/', ':', '@':
// Prevent matching partial namespaces
default:
return false
}
}
return true
}
func normalizeReference(ref string) (string, error) {
// TODO: Replace this function to not depend on reference package
normalized, err := util.NormalizeImageRef(ref)
if err != nil {
return "", errors.Wrapf(err, "normalize image ref %q", ref)
}
return normalized.String(), nil
}
// DigestTranslator creates a digest reference by adding the
// digest to an image name
func DigestTranslator(prefix string) func(digest.Digest) string {
return func(dgst digest.Digest) string {
return prefix + "@" + dgst.String()
}
}

View File

@ -27,7 +27,7 @@ import (
// Importer is the interface for image importer.
type Importer interface {
// Import imports an image from a tar stream.
Import(ctx context.Context, store content.Store, reader io.Reader) ([]Image, error)
Import(ctx context.Context, store content.Store, reader io.Reader) (ocispec.Descriptor, error)
}
// Exporter is the interface for image exporter.

View File

@ -94,7 +94,7 @@ func blobRecord(cs content.Provider, desc ocispec.Descriptor) tarRecord {
CopyTo: func(ctx context.Context, w io.Writer) (int64, error) {
r, err := cs.ReaderAt(ctx, desc)
if err != nil {
return 0, err
return 0, errors.Wrap(err, "failed to get reader")
}
defer r.Close()
@ -103,7 +103,7 @@ func blobRecord(cs content.Provider, desc ocispec.Descriptor) tarRecord {
n, err := io.Copy(io.MultiWriter(w, dgstr.Hash()), content.NewReader(r))
if err != nil {
return 0, err
return 0, errors.Wrap(err, "failed to copy to tar")
}
if dgstr.Digest() != desc.Digest {
return 0, errors.Errorf("unexpected digest %s copied", dgstr.Digest())

View File

@ -1,204 +0,0 @@
/*
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 oci provides the importer and the exporter for OCI Image Spec.
package oci
import (
"archive/tar"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"path"
"strings"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
// V1Importer implements OCI Image Spec v1.
type V1Importer struct {
// ImageName is preprended to either `:` + OCI ref name or `@` + digest (for anonymous refs).
// This field is mandatory atm, but may change in the future. maybe ref map[string]string as in moby/moby#33355
ImageName string
}
var _ images.Importer = &V1Importer{}
// Import implements Importer.
func (oi *V1Importer) Import(ctx context.Context, store content.Store, reader io.Reader) ([]images.Image, error) {
if oi.ImageName == "" {
return nil, errors.New("ImageName not set")
}
tr := tar.NewReader(reader)
var imgrecs []images.Image
foundIndexJSON := false
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if hdr.Typeflag != tar.TypeReg && hdr.Typeflag != tar.TypeRegA {
continue
}
hdrName := path.Clean(hdr.Name)
if hdrName == "index.json" {
if foundIndexJSON {
return nil, errors.New("duplicated index.json")
}
foundIndexJSON = true
imgrecs, err = onUntarIndexJSON(tr, oi.ImageName)
if err != nil {
return nil, err
}
continue
}
if strings.HasPrefix(hdrName, "blobs/") {
if err := onUntarBlob(ctx, tr, store, hdrName, hdr.Size); err != nil {
return nil, err
}
}
}
if !foundIndexJSON {
return nil, errors.New("no index.json found")
}
for _, img := range imgrecs {
err := setGCRefContentLabels(ctx, store, img.Target)
if err != nil {
return imgrecs, err
}
}
// FIXME(AkihiroSuda): set GC labels for unreferrenced blobs (i.e. with unknown media types)?
return imgrecs, nil
}
func onUntarIndexJSON(r io.Reader, imageName string) ([]images.Image, error) {
b, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
var idx ocispec.Index
if err := json.Unmarshal(b, &idx); err != nil {
return nil, err
}
var imgrecs []images.Image
for _, m := range idx.Manifests {
ref, err := normalizeImageRef(imageName, m)
if err != nil {
return nil, err
}
imgrecs = append(imgrecs, images.Image{
Name: ref,
Target: m,
})
}
return imgrecs, nil
}
func normalizeImageRef(imageName string, manifest ocispec.Descriptor) (string, error) {
digest := manifest.Digest
if digest == "" {
return "", errors.Errorf("manifest with empty digest: %v", manifest)
}
ociRef := manifest.Annotations[ocispec.AnnotationRefName]
if ociRef == "" {
return imageName + "@" + digest.String(), nil
}
return imageName + ":" + ociRef, nil
}
func onUntarBlob(ctx context.Context, r io.Reader, store content.Ingester, name string, size int64) error {
// name is like "blobs/sha256/deadbeef"
split := strings.Split(name, "/")
if len(split) != 3 {
return errors.Errorf("unexpected name: %q", name)
}
algo := digest.Algorithm(split[1])
if !algo.Available() {
return errors.Errorf("unsupported algorithm: %s", algo)
}
dgst := digest.NewDigestFromHex(algo.String(), split[2])
return content.WriteBlob(ctx, store, "unknown-"+dgst.String(), r, ocispec.Descriptor{Size: size, Digest: dgst})
}
// GetChildrenDescriptors returns children blob descriptors for the following supported types:
// - images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest
// - images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex
func GetChildrenDescriptors(r io.Reader, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
switch desc.MediaType {
case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
var manifest ocispec.Manifest
if err := json.NewDecoder(r).Decode(&manifest); err != nil {
return nil, err
}
return append([]ocispec.Descriptor{manifest.Config}, manifest.Layers...), nil
case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
var index ocispec.Index
if err := json.NewDecoder(r).Decode(&index); err != nil {
return nil, err
}
return index.Manifests, nil
}
return nil, nil
}
func setGCRefContentLabels(ctx context.Context, store content.Store, desc ocispec.Descriptor) error {
info, err := store.Info(ctx, desc.Digest)
if err != nil {
if errdefs.IsNotFound(err) {
// when the archive is created from multi-arch image,
// it may contain only blobs for a certain platform.
// So ErrNotFound (on manifest list) is expected here.
return nil
}
return err
}
ra, err := store.ReaderAt(ctx, desc)
if err != nil {
return err
}
defer ra.Close()
r := content.NewReader(ra)
children, err := GetChildrenDescriptors(r, desc)
if err != nil {
return err
}
if info.Labels == nil {
info.Labels = map[string]string{}
}
for i, child := range children {
// Note: child blob is not guaranteed to be written to the content store. (multi-arch)
info.Labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i)] = child.Digest.String()
}
if _, err := store.Update(ctx, info, "labels"); err != nil {
return err
}
for _, child := range children {
if err := setGCRefContentLabels(ctx, store, child); err != nil {
return err
}
}
return nil
}

View File

@ -1,53 +0,0 @@
/*
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 oci
import (
"testing"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/assert"
)
func TestNormalizeImageRef(t *testing.T) {
imageBaseName := "foo/bar"
for _, test := range []struct {
input ocispec.Descriptor
expect string
}{
{
input: ocispec.Descriptor{
Digest: digest.Digest("sha256:e22e93af8657d43d7f204b93d69604aeacf273f71d2586288cde312808c0ec77"),
},
expect: "foo/bar@sha256:e22e93af8657d43d7f204b93d69604aeacf273f71d2586288cde312808c0ec77",
},
{
input: ocispec.Descriptor{
Digest: digest.Digest("sha256:e22e93af8657d43d7f204b93d69604aeacf273f71d2586288cde312808c0ec77"),
Annotations: map[string]string{
ocispec.AnnotationRefName: "latest",
},
},
expect: "foo/bar:latest", // no @digest for simplicity
},
} {
normalized, err := normalizeImageRef(imageBaseName, test.input)
assert.NilError(t, err)
assert.Equal(t, test.expect, normalized)
}
}

129
import.go
View File

@ -18,36 +18,62 @@ package containerd
import (
"context"
"encoding/json"
"io"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/images/archive"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
type importOpts struct {
indexName string
imageRefT func(string) string
dgstRefT func(digest.Digest) string
}
// ImportOpt allows the caller to specify import specific options
type ImportOpt func(c *importOpts) error
type ImportOpt func(*importOpts) error
func resolveImportOpt(opts ...ImportOpt) (importOpts, error) {
var iopts importOpts
for _, o := range opts {
if err := o(&iopts); err != nil {
return iopts, err
// WithImageRefTranslator is used to translate the index reference
// to an image reference for the image store.
func WithImageRefTranslator(f func(string) string) ImportOpt {
return func(c *importOpts) error {
c.imageRefT = f
return nil
}
}
// WithDigestRef is used to create digest images for each
// manifest in the index.
func WithDigestRef(f func(digest.Digest) string) ImportOpt {
return func(c *importOpts) error {
c.dgstRefT = f
return nil
}
}
// WithIndexName creates a tag pointing to the imported index
func WithIndexName(name string) ImportOpt {
return func(c *importOpts) error {
c.indexName = name
return nil
}
return iopts, nil
}
// Import imports an image from a Tar stream using reader.
// 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 {
func (c *Client) Import(ctx context.Context, reader io.Reader, opts ...ImportOpt) ([]images.Image, error) {
var iopts importOpts
for _, o := range opts {
if err := o(&iopts); err != nil {
return nil, err
}
}
ctx, done, err := c.WithLease(ctx)
if err != nil {
@ -55,31 +81,86 @@ func (c *Client) Import(ctx context.Context, importer images.Importer, reader io
}
defer done(ctx)
imgrecs, err := importer.Import(ctx, c.ContentStore(), reader)
index, err := archive.ImportIndex(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 {
var (
imgs []images.Image
cs = c.ContentStore()
is = c.ImageService()
)
if iopts.indexName != "" {
imgs = append(imgs, images.Image{
Name: iopts.indexName,
Target: index,
})
}
var handler images.HandlerFunc
handler = func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
// Only save images at top level
if desc.Digest != index.Digest {
return images.Children(ctx, cs, desc)
}
p, err := content.ReadBlob(ctx, cs, desc)
if err != nil {
return nil, err
}
var idx ocispec.Index
if err := json.Unmarshal(p, &idx); err != nil {
return nil, err
}
for _, m := range idx.Manifests {
if ref := m.Annotations[ocispec.AnnotationRefName]; ref != "" {
if iopts.imageRefT != nil {
ref = iopts.imageRefT(ref)
}
if ref != "" {
imgs = append(imgs, images.Image{
Name: ref,
Target: m,
})
}
}
if iopts.dgstRefT != nil {
ref := iopts.dgstRefT(m.Digest)
if ref != "" {
imgs = append(imgs, images.Image{
Name: ref,
Target: m,
})
}
}
}
return idx.Manifests, nil
}
handler = images.SetChildrenLabels(cs, handler)
if err := images.Walk(ctx, handler, index); err != nil {
return nil, err
}
for i := range imgs {
img, err := is.Update(ctx, imgs[i], "target")
if err != nil {
if !errdefs.IsNotFound(err) {
return nil, err
}
created, err := is.Create(ctx, imgrec)
img, err = is.Create(ctx, imgs[i])
if err != nil {
return nil, err
}
imgrec = created
} else {
imgrec = updated
}
imgs[i] = img
}
images = append(images, NewImage(c, imgrec))
}
return images, nil
return imgs, nil
}

View File

@ -20,6 +20,7 @@ import (
"runtime"
"testing"
"github.com/containerd/containerd/images/archive"
"github.com/containerd/containerd/images/oci"
)
@ -49,13 +50,16 @@ func TestOCIExportAndImport(t *testing.T) {
t.Fatal(err)
}
imgrecs, err := client.Import(ctx, &oci.V1Importer{ImageName: "foo/bar:"}, exported)
opts := []ImportOpt{
WithImageRefTranslator(archive.AddRefPrefix("foo/bar")),
}
imgrecs, err := client.Import(ctx, exported, opts...)
if err != nil {
t.Fatal(err)
t.Fatalf("Import failed: %+v", err)
}
for _, imgrec := range imgrecs {
err = client.ImageService().Delete(ctx, imgrec.Name())
err = client.ImageService().Delete(ctx, imgrec.Name)
if err != nil {
t.Fatal(err)
}