package containerd import ( "archive/tar" "context" "encoding/json" "io" "sort" "github.com/containerd/containerd/content" "github.com/containerd/containerd/images" ocispecs "github.com/opencontainers/image-spec/specs-go" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" ) func (c *Client) exportToOCITar(ctx context.Context, desc ocispec.Descriptor, writer io.Writer, eopts exportOpts) error { tw := tar.NewWriter(writer) defer tw.Close() records := []tarRecord{ ociLayoutFile(""), ociIndexRecord(desc), } cs := c.ContentStore() algorithms := map[string]struct{}{} exportHandler := func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { records = append(records, blobRecord(cs, desc)) algorithms[desc.Digest.Algorithm().String()] = struct{}{} return nil, nil } handlers := images.Handlers( images.ChildrenHandler(cs), images.HandlerFunc(exportHandler), ) // Walk sequentially since the number of fetchs is likely one and doing in // parallel requires locking the export handler if err := images.Walk(ctx, handlers, desc); err != nil { return err } if len(algorithms) > 0 { records = append(records, directoryRecord("blobs/", 0755)) for alg := range algorithms { records = append(records, directoryRecord("blobs/"+alg+"/", 0755)) } } return writeTar(ctx, tw, records) } type tarRecord struct { Header *tar.Header CopyTo func(context.Context, io.Writer) (int64, error) } func blobRecord(cs content.Store, desc ocispec.Descriptor) tarRecord { path := "blobs/" + desc.Digest.Algorithm().String() + "/" + desc.Digest.Hex() return tarRecord{ Header: &tar.Header{ Name: path, Mode: 0444, Size: desc.Size, Typeflag: tar.TypeReg, }, CopyTo: func(ctx context.Context, w io.Writer) (int64, error) { r, err := cs.ReaderAt(ctx, desc.Digest) if err != nil { return 0, err } defer r.Close() // Verify digest dgstr := desc.Digest.Algorithm().Digester() n, err := io.Copy(io.MultiWriter(w, dgstr.Hash()), content.NewReader(r)) if err != nil { return 0, err } if dgstr.Digest() != desc.Digest { return 0, errors.Errorf("unexpected digest %s copied", dgstr.Digest()) } return n, nil }, } } func directoryRecord(name string, mode int64) tarRecord { return tarRecord{ Header: &tar.Header{ Name: name, Mode: mode, Typeflag: tar.TypeDir, }, } } func ociLayoutFile(version string) tarRecord { if version == "" { version = ocispec.ImageLayoutVersion } layout := ocispec.ImageLayout{ Version: version, } b, err := json.Marshal(layout) if err != nil { panic(err) } return tarRecord{ Header: &tar.Header{ Name: ocispec.ImageLayoutFile, Mode: 0444, Size: int64(len(b)), Typeflag: tar.TypeReg, }, CopyTo: func(ctx context.Context, w io.Writer) (int64, error) { n, err := w.Write(b) return int64(n), err }, } } func ociIndexRecord(manifests ...ocispec.Descriptor) tarRecord { index := ocispec.Index{ Versioned: ocispecs.Versioned{ SchemaVersion: 2, }, Manifests: manifests, } b, err := json.Marshal(index) if err != nil { panic(err) } return tarRecord{ Header: &tar.Header{ Name: "index.json", Mode: 0644, Size: int64(len(b)), Typeflag: tar.TypeReg, }, CopyTo: func(ctx context.Context, w io.Writer) (int64, error) { n, err := w.Write(b) return int64(n), err }, } } func writeTar(ctx context.Context, tw *tar.Writer, records []tarRecord) error { sort.Sort(tarRecordsByName(records)) for _, record := range records { if err := tw.WriteHeader(record.Header); err != nil { return err } if record.CopyTo != nil { n, err := record.CopyTo(ctx, tw) if err != nil { return err } if n != record.Header.Size { return errors.Errorf("unexpected copy size for %s", record.Header.Name) } } else if record.Header.Size > 0 { return errors.Errorf("no content to write to record with non-zero size for %s", record.Header.Name) } } return nil } type tarRecordsByName []tarRecord func (t tarRecordsByName) Len() int { return len(t) } func (t tarRecordsByName) Swap(i, j int) { t[i], t[j] = t[j], t[i] } func (t tarRecordsByName) Less(i, j int) bool { return t[i].Header.Name < t[j].Header.Name }