containerd/oci/oci.go
Akihiro Suda b518f11dba 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>
2017-07-28 04:47:53 +00:00

294 lines
7.6 KiB
Go

// Package oci provides basic operations for manipulating OCI images.
// This package can be used even outside of containerd, and contains some
// functions not used in containerd itself.
package oci
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
spec "github.com/opencontainers/image-spec/specs-go/v1"
)
// BlobWriter writes an OCI blob and returns a digest when committed.
type BlobWriter interface {
// Close is expected to be called after Commit() when commission is needed.
io.WriteCloser
// Digest may return empty digest or panics until committed.
Digest() digest.Digest
// Commit commits the blob (but no roll-back is guaranteed on an error).
// size and expected can be zero-value when unknown.
Commit(size int64, expected digest.Digest) error
}
// ErrUnexpectedSize can be returned from BlobWriter.Commit()
type ErrUnexpectedSize struct {
Expected int64
Actual int64
}
func (e ErrUnexpectedSize) Error() string {
if e.Expected > 0 && e.Expected != e.Actual {
return fmt.Sprintf("unexpected size: %d != %d", e.Expected, e.Actual)
}
return fmt.Sprintf("malformed ErrUnexpectedSize(%+v)", e)
}
// ErrUnexpectedDigest can be returned from BlobWriter.Commit()
type ErrUnexpectedDigest struct {
Expected digest.Digest
Actual digest.Digest
}
func (e ErrUnexpectedDigest) Error() string {
if e.Expected.String() != "" && e.Expected.String() != e.Actual.String() {
return fmt.Sprintf("unexpected digest: %v != %v", e.Expected, e.Actual)
}
return fmt.Sprintf("malformed ErrUnexpectedDigest(%+v)", e)
}
// ImageDriver corresponds to the representation of an image.
// Path uses os.PathSeparator as the separator.
// The methods of ImageDriver should only be called from oci package.
type ImageDriver interface {
Init() error
Remove(path string) error
Reader(path string) (io.ReadCloser, error)
Writer(path string, perm os.FileMode) (io.WriteCloser, error)
BlobWriter(algo digest.Algorithm) (BlobWriter, error)
}
type InitOpts struct {
// imageLayoutVersion can be an empty string for specifying the default version.
ImageLayoutVersion string
// skip creating oci-layout
SkipCreateImageLayout bool
// skip creating index.json
SkipCreateIndex bool
}
// Init initializes an OCI image structure.
// Init calls img.Init, creates `oci-layout`(0444), and creates `index.json`(0644).
//
func Init(img ImageDriver, opts InitOpts) error {
if err := img.Init(); err != nil {
return err
}
// Create oci-layout
if !opts.SkipCreateImageLayout {
imageLayoutVersion := opts.ImageLayoutVersion
if imageLayoutVersion == "" {
imageLayoutVersion = spec.ImageLayoutVersion
}
if err := WriteImageLayout(img, spec.ImageLayout{Version: imageLayoutVersion}); err != nil {
return err
}
}
// Create index.json
if !opts.SkipCreateIndex {
if err := WriteIndex(img, spec.Index{Versioned: specs.Versioned{SchemaVersion: 2}}); err != nil {
return err
}
}
return nil
}
func blobPath(d digest.Digest) string {
return filepath.Join("blobs", d.Algorithm().String(), d.Hex())
}
const (
indexPath = "index.json"
)
// GetBlobReader returns io.ReadCloser for a blob.
func GetBlobReader(img ImageDriver, d digest.Digest) (io.ReadCloser, error) {
// we return a reader rather than the full *os.File here so as to prohibit write operations.
return img.Reader(blobPath(d))
}
// ReadBlob reads an OCI blob.
func ReadBlob(img ImageDriver, d digest.Digest) ([]byte, error) {
r, err := GetBlobReader(img, d)
if err != nil {
return nil, err
}
defer r.Close()
return ioutil.ReadAll(r)
}
// WriteBlob writes bytes as an OCI blob and returns its digest using the canonical digest algorithm.
// If you need to specify certain algorithm, you can use NewBlobWriter(img string, algo digest.Algorithm).
func WriteBlob(img ImageDriver, b []byte) (digest.Digest, error) {
w, err := img.BlobWriter(digest.Canonical)
if err != nil {
return "", err
}
n, err := w.Write(b)
if err != nil {
return "", err
}
if n < len(b) {
return "", io.ErrShortWrite
}
if err := w.Close(); err != nil {
return "", err
}
return w.Digest(), err
}
// NewBlobWriter returns a BlobWriter.
func NewBlobWriter(img ImageDriver, algo digest.Algorithm) (BlobWriter, error) {
return img.BlobWriter(algo)
}
// DeleteBlob deletes an OCI blob.
func DeleteBlob(img ImageDriver, d digest.Digest) error {
return img.Remove(blobPath(d))
}
// ReadImageLayout returns the image layout.
func ReadImageLayout(img ImageDriver) (spec.ImageLayout, error) {
r, err := img.Reader(spec.ImageLayoutFile)
if err != nil {
return spec.ImageLayout{}, err
}
b, err := ioutil.ReadAll(r)
if err != nil {
return spec.ImageLayout{}, err
}
if err := r.Close(); err != nil {
return spec.ImageLayout{}, err
}
var layout spec.ImageLayout
if err := json.Unmarshal(b, &layout); err != nil {
return spec.ImageLayout{}, err
}
return layout, nil
}
// WriteImageLayout writes the image layout.
func WriteImageLayout(img ImageDriver, layout spec.ImageLayout) error {
b, err := json.Marshal(layout)
if err != nil {
return err
}
w, err := img.Writer(spec.ImageLayoutFile, 0444)
if err != nil {
return err
}
n, err := w.Write(b)
if err != nil {
return err
}
if n < len(b) {
return io.ErrShortWrite
}
return w.Close()
}
// ReadIndex returns the index.
func ReadIndex(img ImageDriver) (spec.Index, error) {
r, err := img.Reader(indexPath)
if err != nil {
return spec.Index{}, err
}
b, err := ioutil.ReadAll(r)
if err != nil {
return spec.Index{}, err
}
if err := r.Close(); err != nil {
return spec.Index{}, err
}
var idx spec.Index
if err := json.Unmarshal(b, &idx); err != nil {
return spec.Index{}, err
}
return idx, nil
}
// WriteIndex writes the index.
func WriteIndex(img ImageDriver, idx spec.Index) error {
b, err := json.Marshal(idx)
if err != nil {
return err
}
w, err := img.Writer(indexPath, 0644)
if err != nil {
return err
}
n, err := w.Write(b)
if err != nil {
return err
}
if n < len(b) {
return io.ErrShortWrite
}
return w.Close()
}
// RemoveManifestDescriptorFromIndex removes the manifest descriptor from the index.
// Returns nil error when the entry not found.
func RemoveManifestDescriptorFromIndex(img ImageDriver, refName string) error {
if refName == "" {
return errors.New("empty refName specified")
}
src, err := ReadIndex(img)
if err != nil {
return err
}
dst := src
dst.Manifests = nil
for _, m := range src.Manifests {
mRefName, ok := m.Annotations[spec.AnnotationRefName]
if ok && mRefName == refName {
continue
}
dst.Manifests = append(dst.Manifests, m)
}
return WriteIndex(img, dst)
}
// PutManifestDescriptorToIndex puts a manifest descriptor to the index.
// If ref name is set and conflicts with the existing descriptors, the old ones are removed.
func PutManifestDescriptorToIndex(img ImageDriver, desc spec.Descriptor) error {
refName, ok := desc.Annotations[spec.AnnotationRefName]
if ok && refName != "" {
if err := RemoveManifestDescriptorFromIndex(img, refName); err != nil {
return err
}
}
idx, err := ReadIndex(img)
if err != nil {
return err
}
idx.Manifests = append(idx.Manifests, desc)
return WriteIndex(img, idx)
}
// WriteJSONBlob is an utility function that writes x as a JSON blob with the specified media type, and returns the descriptor.
func WriteJSONBlob(img ImageDriver, x interface{}, mediaType string) (spec.Descriptor, error) {
b, err := json.Marshal(x)
if err != nil {
return spec.Descriptor{}, err
}
d, err := WriteBlob(img, b)
if err != nil {
return spec.Descriptor{}, err
}
return spec.Descriptor{
MediaType: mediaType,
Digest: d,
Size: int64(len(b)),
}, nil
}