github.com/demonoid81/containerd@v1.3.4/images/archive/exporter.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package archive
    18  
    19  import (
    20  	"archive/tar"
    21  	"context"
    22  	"encoding/json"
    23  	"io"
    24  	"path"
    25  	"sort"
    26  
    27  	"github.com/containerd/containerd/content"
    28  	"github.com/containerd/containerd/errdefs"
    29  	"github.com/containerd/containerd/images"
    30  	"github.com/containerd/containerd/platforms"
    31  	digest "github.com/opencontainers/go-digest"
    32  	ocispecs "github.com/opencontainers/image-spec/specs-go"
    33  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    34  	"github.com/pkg/errors"
    35  )
    36  
    37  type exportOptions struct {
    38  	manifests          []ocispec.Descriptor
    39  	platform           platforms.MatchComparer
    40  	allPlatforms       bool
    41  	skipDockerManifest bool
    42  }
    43  
    44  // ExportOpt defines options for configuring exported descriptors
    45  type ExportOpt func(context.Context, *exportOptions) error
    46  
    47  // WithPlatform defines the platform to require manifest lists have
    48  // not exporting all platforms.
    49  // Additionally, platform is used to resolve image configs for
    50  // Docker v1.1, v1.2 format compatibility.
    51  func WithPlatform(p platforms.MatchComparer) ExportOpt {
    52  	return func(ctx context.Context, o *exportOptions) error {
    53  		o.platform = p
    54  		return nil
    55  	}
    56  }
    57  
    58  // WithAllPlatforms exports all manifests from a manifest list.
    59  // Missing content will fail the export.
    60  func WithAllPlatforms() ExportOpt {
    61  	return func(ctx context.Context, o *exportOptions) error {
    62  		o.allPlatforms = true
    63  		return nil
    64  	}
    65  }
    66  
    67  // WithSkipDockerManifest skips creation of the Docker compatible
    68  // manifest.json file.
    69  func WithSkipDockerManifest() ExportOpt {
    70  	return func(ctx context.Context, o *exportOptions) error {
    71  		o.skipDockerManifest = true
    72  		return nil
    73  	}
    74  }
    75  
    76  // WithImage adds the provided images to the exported archive.
    77  func WithImage(is images.Store, name string) ExportOpt {
    78  	return func(ctx context.Context, o *exportOptions) error {
    79  		img, err := is.Get(ctx, name)
    80  		if err != nil {
    81  			return err
    82  		}
    83  
    84  		img.Target.Annotations = addNameAnnotation(name, img.Target.Annotations)
    85  		o.manifests = append(o.manifests, img.Target)
    86  
    87  		return nil
    88  	}
    89  }
    90  
    91  // WithManifest adds a manifest to the exported archive.
    92  // When names are given they will be set on the manifest in the
    93  // exported archive, creating an index record for each name.
    94  // When no names are provided, it is up to caller to put name annotation to
    95  // on the manifest descriptor if needed.
    96  func WithManifest(manifest ocispec.Descriptor, names ...string) ExportOpt {
    97  	return func(ctx context.Context, o *exportOptions) error {
    98  		if len(names) == 0 {
    99  			o.manifests = append(o.manifests, manifest)
   100  		}
   101  		for _, name := range names {
   102  			mc := manifest
   103  			mc.Annotations = addNameAnnotation(name, manifest.Annotations)
   104  			o.manifests = append(o.manifests, mc)
   105  		}
   106  
   107  		return nil
   108  	}
   109  }
   110  
   111  func addNameAnnotation(name string, base map[string]string) map[string]string {
   112  	annotations := map[string]string{}
   113  	for k, v := range base {
   114  		annotations[k] = v
   115  	}
   116  
   117  	annotations[images.AnnotationImageName] = name
   118  	annotations[ocispec.AnnotationRefName] = ociReferenceName(name)
   119  
   120  	return annotations
   121  }
   122  
   123  // Export implements Exporter.
   124  func Export(ctx context.Context, store content.Provider, writer io.Writer, opts ...ExportOpt) error {
   125  	var eo exportOptions
   126  	for _, opt := range opts {
   127  		if err := opt(ctx, &eo); err != nil {
   128  			return err
   129  		}
   130  	}
   131  
   132  	records := []tarRecord{
   133  		ociLayoutFile(""),
   134  		ociIndexRecord(eo.manifests),
   135  	}
   136  
   137  	algorithms := map[string]struct{}{}
   138  	dManifests := map[digest.Digest]*exportManifest{}
   139  	resolvedIndex := map[digest.Digest]digest.Digest{}
   140  	for _, desc := range eo.manifests {
   141  		switch desc.MediaType {
   142  		case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
   143  			mt, ok := dManifests[desc.Digest]
   144  			if !ok {
   145  				// TODO(containerd): Skip if already added
   146  				r, err := getRecords(ctx, store, desc, algorithms)
   147  				if err != nil {
   148  					return err
   149  				}
   150  				records = append(records, r...)
   151  
   152  				mt = &exportManifest{
   153  					manifest: desc,
   154  				}
   155  				dManifests[desc.Digest] = mt
   156  			}
   157  
   158  			name := desc.Annotations[images.AnnotationImageName]
   159  			if name != "" && !eo.skipDockerManifest {
   160  				mt.names = append(mt.names, name)
   161  			}
   162  		case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
   163  			d, ok := resolvedIndex[desc.Digest]
   164  			if !ok {
   165  				records = append(records, blobRecord(store, desc))
   166  
   167  				p, err := content.ReadBlob(ctx, store, desc)
   168  				if err != nil {
   169  					return err
   170  				}
   171  
   172  				var index ocispec.Index
   173  				if err := json.Unmarshal(p, &index); err != nil {
   174  					return err
   175  				}
   176  
   177  				var manifests []ocispec.Descriptor
   178  				for _, m := range index.Manifests {
   179  					if eo.platform != nil {
   180  						if m.Platform == nil || eo.platform.Match(*m.Platform) {
   181  							manifests = append(manifests, m)
   182  						} else if !eo.allPlatforms {
   183  							continue
   184  						}
   185  					}
   186  
   187  					r, err := getRecords(ctx, store, m, algorithms)
   188  					if err != nil {
   189  						return err
   190  					}
   191  
   192  					records = append(records, r...)
   193  				}
   194  
   195  				if !eo.skipDockerManifest {
   196  					if len(manifests) >= 1 {
   197  						if len(manifests) > 1 {
   198  							sort.SliceStable(manifests, func(i, j int) bool {
   199  								if manifests[i].Platform == nil {
   200  									return false
   201  								}
   202  								if manifests[j].Platform == nil {
   203  									return true
   204  								}
   205  								return eo.platform.Less(*manifests[i].Platform, *manifests[j].Platform)
   206  							})
   207  						}
   208  						d = manifests[0].Digest
   209  						dManifests[d] = &exportManifest{
   210  							manifest: manifests[0],
   211  						}
   212  					} else if eo.platform != nil {
   213  						return errors.Wrap(errdefs.ErrNotFound, "no manifest found for platform")
   214  					}
   215  				}
   216  				resolvedIndex[desc.Digest] = d
   217  			}
   218  			if d != "" {
   219  				if name := desc.Annotations[images.AnnotationImageName]; name != "" {
   220  					mt := dManifests[d]
   221  					mt.names = append(mt.names, name)
   222  				}
   223  
   224  			}
   225  		default:
   226  			return errors.Wrap(errdefs.ErrInvalidArgument, "only manifests may be exported")
   227  		}
   228  	}
   229  
   230  	if len(dManifests) > 0 {
   231  		tr, err := manifestsRecord(ctx, store, dManifests)
   232  		if err != nil {
   233  			return errors.Wrap(err, "unable to create manifests file")
   234  		}
   235  
   236  		records = append(records, tr)
   237  	}
   238  
   239  	if len(algorithms) > 0 {
   240  		records = append(records, directoryRecord("blobs/", 0755))
   241  		for alg := range algorithms {
   242  			records = append(records, directoryRecord("blobs/"+alg+"/", 0755))
   243  		}
   244  	}
   245  
   246  	tw := tar.NewWriter(writer)
   247  	defer tw.Close()
   248  	return writeTar(ctx, tw, records)
   249  }
   250  
   251  func getRecords(ctx context.Context, store content.Provider, desc ocispec.Descriptor, algorithms map[string]struct{}) ([]tarRecord, error) {
   252  	var records []tarRecord
   253  	exportHandler := func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   254  		records = append(records, blobRecord(store, desc))
   255  		algorithms[desc.Digest.Algorithm().String()] = struct{}{}
   256  		return nil, nil
   257  	}
   258  
   259  	childrenHandler := images.ChildrenHandler(store)
   260  
   261  	handlers := images.Handlers(
   262  		childrenHandler,
   263  		images.HandlerFunc(exportHandler),
   264  	)
   265  
   266  	// Walk sequentially since the number of fetchs is likely one and doing in
   267  	// parallel requires locking the export handler
   268  	if err := images.Walk(ctx, handlers, desc); err != nil {
   269  		return nil, err
   270  	}
   271  
   272  	return records, nil
   273  }
   274  
   275  type tarRecord struct {
   276  	Header *tar.Header
   277  	CopyTo func(context.Context, io.Writer) (int64, error)
   278  }
   279  
   280  func blobRecord(cs content.Provider, desc ocispec.Descriptor) tarRecord {
   281  	path := path.Join("blobs", desc.Digest.Algorithm().String(), desc.Digest.Encoded())
   282  	return tarRecord{
   283  		Header: &tar.Header{
   284  			Name:     path,
   285  			Mode:     0444,
   286  			Size:     desc.Size,
   287  			Typeflag: tar.TypeReg,
   288  		},
   289  		CopyTo: func(ctx context.Context, w io.Writer) (int64, error) {
   290  			r, err := cs.ReaderAt(ctx, desc)
   291  			if err != nil {
   292  				return 0, errors.Wrap(err, "failed to get reader")
   293  			}
   294  			defer r.Close()
   295  
   296  			// Verify digest
   297  			dgstr := desc.Digest.Algorithm().Digester()
   298  
   299  			n, err := io.Copy(io.MultiWriter(w, dgstr.Hash()), content.NewReader(r))
   300  			if err != nil {
   301  				return 0, errors.Wrap(err, "failed to copy to tar")
   302  			}
   303  			if dgstr.Digest() != desc.Digest {
   304  				return 0, errors.Errorf("unexpected digest %s copied", dgstr.Digest())
   305  			}
   306  			return n, nil
   307  		},
   308  	}
   309  }
   310  
   311  func directoryRecord(name string, mode int64) tarRecord {
   312  	return tarRecord{
   313  		Header: &tar.Header{
   314  			Name:     name,
   315  			Mode:     mode,
   316  			Typeflag: tar.TypeDir,
   317  		},
   318  	}
   319  }
   320  
   321  func ociLayoutFile(version string) tarRecord {
   322  	if version == "" {
   323  		version = ocispec.ImageLayoutVersion
   324  	}
   325  	layout := ocispec.ImageLayout{
   326  		Version: version,
   327  	}
   328  
   329  	b, err := json.Marshal(layout)
   330  	if err != nil {
   331  		panic(err)
   332  	}
   333  
   334  	return tarRecord{
   335  		Header: &tar.Header{
   336  			Name:     ocispec.ImageLayoutFile,
   337  			Mode:     0444,
   338  			Size:     int64(len(b)),
   339  			Typeflag: tar.TypeReg,
   340  		},
   341  		CopyTo: func(ctx context.Context, w io.Writer) (int64, error) {
   342  			n, err := w.Write(b)
   343  			return int64(n), err
   344  		},
   345  	}
   346  
   347  }
   348  
   349  func ociIndexRecord(manifests []ocispec.Descriptor) tarRecord {
   350  	index := ocispec.Index{
   351  		Versioned: ocispecs.Versioned{
   352  			SchemaVersion: 2,
   353  		},
   354  		Manifests: manifests,
   355  	}
   356  
   357  	b, err := json.Marshal(index)
   358  	if err != nil {
   359  		panic(err)
   360  	}
   361  
   362  	return tarRecord{
   363  		Header: &tar.Header{
   364  			Name:     "index.json",
   365  			Mode:     0644,
   366  			Size:     int64(len(b)),
   367  			Typeflag: tar.TypeReg,
   368  		},
   369  		CopyTo: func(ctx context.Context, w io.Writer) (int64, error) {
   370  			n, err := w.Write(b)
   371  			return int64(n), err
   372  		},
   373  	}
   374  }
   375  
   376  type exportManifest struct {
   377  	manifest ocispec.Descriptor
   378  	names    []string
   379  }
   380  
   381  func manifestsRecord(ctx context.Context, store content.Provider, manifests map[digest.Digest]*exportManifest) (tarRecord, error) {
   382  	mfsts := make([]struct {
   383  		Config   string
   384  		RepoTags []string
   385  		Layers   []string
   386  	}, len(manifests))
   387  
   388  	var i int
   389  	for _, m := range manifests {
   390  		p, err := content.ReadBlob(ctx, store, m.manifest)
   391  		if err != nil {
   392  			return tarRecord{}, err
   393  		}
   394  
   395  		var manifest ocispec.Manifest
   396  		if err := json.Unmarshal(p, &manifest); err != nil {
   397  			return tarRecord{}, err
   398  		}
   399  		if err := manifest.Config.Digest.Validate(); err != nil {
   400  			return tarRecord{}, errors.Wrapf(err, "invalid manifest %q", m.manifest.Digest)
   401  		}
   402  
   403  		dgst := manifest.Config.Digest
   404  		mfsts[i].Config = path.Join("blobs", dgst.Algorithm().String(), dgst.Encoded())
   405  		for _, l := range manifest.Layers {
   406  			path := path.Join("blobs", l.Digest.Algorithm().String(), l.Digest.Encoded())
   407  			mfsts[i].Layers = append(mfsts[i].Layers, path)
   408  		}
   409  
   410  		for _, name := range m.names {
   411  			nname, err := familiarizeReference(name)
   412  			if err != nil {
   413  				return tarRecord{}, err
   414  			}
   415  
   416  			mfsts[i].RepoTags = append(mfsts[i].RepoTags, nname)
   417  		}
   418  
   419  		i++
   420  	}
   421  
   422  	b, err := json.Marshal(mfsts)
   423  	if err != nil {
   424  		return tarRecord{}, err
   425  	}
   426  
   427  	return tarRecord{
   428  		Header: &tar.Header{
   429  			Name:     "manifest.json",
   430  			Mode:     0644,
   431  			Size:     int64(len(b)),
   432  			Typeflag: tar.TypeReg,
   433  		},
   434  		CopyTo: func(ctx context.Context, w io.Writer) (int64, error) {
   435  			n, err := w.Write(b)
   436  			return int64(n), err
   437  		},
   438  	}, nil
   439  }
   440  
   441  func writeTar(ctx context.Context, tw *tar.Writer, records []tarRecord) error {
   442  	sort.Slice(records, func(i, j int) bool {
   443  		return records[i].Header.Name < records[j].Header.Name
   444  	})
   445  
   446  	var last string
   447  	for _, record := range records {
   448  		if record.Header.Name == last {
   449  			continue
   450  		}
   451  		last = record.Header.Name
   452  		if err := tw.WriteHeader(record.Header); err != nil {
   453  			return err
   454  		}
   455  		if record.CopyTo != nil {
   456  			n, err := record.CopyTo(ctx, tw)
   457  			if err != nil {
   458  				return err
   459  			}
   460  			if n != record.Header.Size {
   461  				return errors.Errorf("unexpected copy size for %s", record.Header.Name)
   462  			}
   463  		} else if record.Header.Size > 0 {
   464  			return errors.Errorf("no content to write to record with non-zero size for %s", record.Header.Name)
   465  		}
   466  	}
   467  	return nil
   468  }