github.com/containerd/Containerd@v1.4.13/remotes/docker/schema1/converter.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 schema1
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/base64"
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	"io/ioutil"
    27  	"strconv"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  
    32  	"golang.org/x/sync/errgroup"
    33  
    34  	"github.com/containerd/containerd/archive/compression"
    35  	"github.com/containerd/containerd/content"
    36  	"github.com/containerd/containerd/errdefs"
    37  	"github.com/containerd/containerd/images"
    38  	"github.com/containerd/containerd/log"
    39  	"github.com/containerd/containerd/remotes"
    40  	digest "github.com/opencontainers/go-digest"
    41  	specs "github.com/opencontainers/image-spec/specs-go"
    42  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    43  	"github.com/pkg/errors"
    44  )
    45  
    46  const (
    47  	manifestSizeLimit            = 8e6 // 8MB
    48  	labelDockerSchema1EmptyLayer = "containerd.io/docker.schema1.empty-layer"
    49  )
    50  
    51  type blobState struct {
    52  	diffID digest.Digest
    53  	empty  bool
    54  }
    55  
    56  // Converter converts schema1 manifests to schema2 on fetch
    57  type Converter struct {
    58  	contentStore content.Store
    59  	fetcher      remotes.Fetcher
    60  
    61  	pulledManifest *manifest
    62  
    63  	mu         sync.Mutex
    64  	blobMap    map[digest.Digest]blobState
    65  	layerBlobs map[digest.Digest]ocispec.Descriptor
    66  }
    67  
    68  // NewConverter returns a new converter
    69  func NewConverter(contentStore content.Store, fetcher remotes.Fetcher) *Converter {
    70  	return &Converter{
    71  		contentStore: contentStore,
    72  		fetcher:      fetcher,
    73  		blobMap:      map[digest.Digest]blobState{},
    74  		layerBlobs:   map[digest.Digest]ocispec.Descriptor{},
    75  	}
    76  }
    77  
    78  // Handle fetching descriptors for a docker media type
    79  func (c *Converter) Handle(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
    80  	switch desc.MediaType {
    81  	case images.MediaTypeDockerSchema1Manifest:
    82  		if err := c.fetchManifest(ctx, desc); err != nil {
    83  			return nil, err
    84  		}
    85  
    86  		m := c.pulledManifest
    87  		if len(m.FSLayers) != len(m.History) {
    88  			return nil, errors.New("invalid schema 1 manifest, history and layer mismatch")
    89  		}
    90  		descs := make([]ocispec.Descriptor, 0, len(c.pulledManifest.FSLayers))
    91  
    92  		for i := range m.FSLayers {
    93  			if _, ok := c.blobMap[c.pulledManifest.FSLayers[i].BlobSum]; !ok {
    94  				empty, err := isEmptyLayer([]byte(m.History[i].V1Compatibility))
    95  				if err != nil {
    96  					return nil, err
    97  				}
    98  
    99  				// Do no attempt to download a known empty blob
   100  				if !empty {
   101  					descs = append([]ocispec.Descriptor{
   102  						{
   103  							MediaType: images.MediaTypeDockerSchema2LayerGzip,
   104  							Digest:    c.pulledManifest.FSLayers[i].BlobSum,
   105  							Size:      -1,
   106  						},
   107  					}, descs...)
   108  				}
   109  				c.blobMap[c.pulledManifest.FSLayers[i].BlobSum] = blobState{
   110  					empty: empty,
   111  				}
   112  			}
   113  		}
   114  		return descs, nil
   115  	case images.MediaTypeDockerSchema2LayerGzip:
   116  		if c.pulledManifest == nil {
   117  			return nil, errors.New("manifest required for schema 1 blob pull")
   118  		}
   119  		return nil, c.fetchBlob(ctx, desc)
   120  	default:
   121  		return nil, fmt.Errorf("%v not support for schema 1 manifests", desc.MediaType)
   122  	}
   123  }
   124  
   125  // ConvertOptions provides options on converting a docker schema1 manifest.
   126  type ConvertOptions struct {
   127  	// ManifestMediaType specifies the media type of the manifest OCI descriptor.
   128  	ManifestMediaType string
   129  
   130  	// ConfigMediaType specifies the media type of the manifest config OCI
   131  	// descriptor.
   132  	ConfigMediaType string
   133  }
   134  
   135  // ConvertOpt allows configuring a convert operation.
   136  type ConvertOpt func(context.Context, *ConvertOptions) error
   137  
   138  // UseDockerSchema2 is used to indicate that a schema1 manifest should be
   139  // converted into the media types for a docker schema2 manifest.
   140  func UseDockerSchema2() ConvertOpt {
   141  	return func(ctx context.Context, o *ConvertOptions) error {
   142  		o.ManifestMediaType = images.MediaTypeDockerSchema2Manifest
   143  		o.ConfigMediaType = images.MediaTypeDockerSchema2Config
   144  		return nil
   145  	}
   146  }
   147  
   148  // Convert a docker manifest to an OCI descriptor
   149  func (c *Converter) Convert(ctx context.Context, opts ...ConvertOpt) (ocispec.Descriptor, error) {
   150  	co := ConvertOptions{
   151  		ManifestMediaType: ocispec.MediaTypeImageManifest,
   152  		ConfigMediaType:   ocispec.MediaTypeImageConfig,
   153  	}
   154  	for _, opt := range opts {
   155  		if err := opt(ctx, &co); err != nil {
   156  			return ocispec.Descriptor{}, err
   157  		}
   158  	}
   159  
   160  	history, diffIDs, err := c.schema1ManifestHistory()
   161  	if err != nil {
   162  		return ocispec.Descriptor{}, errors.Wrap(err, "schema 1 conversion failed")
   163  	}
   164  
   165  	var img ocispec.Image
   166  	if err := json.Unmarshal([]byte(c.pulledManifest.History[0].V1Compatibility), &img); err != nil {
   167  		return ocispec.Descriptor{}, errors.Wrap(err, "failed to unmarshal image from schema 1 history")
   168  	}
   169  
   170  	img.History = history
   171  	img.RootFS = ocispec.RootFS{
   172  		Type:    "layers",
   173  		DiffIDs: diffIDs,
   174  	}
   175  
   176  	b, err := json.MarshalIndent(img, "", "   ")
   177  	if err != nil {
   178  		return ocispec.Descriptor{}, errors.Wrap(err, "failed to marshal image")
   179  	}
   180  
   181  	config := ocispec.Descriptor{
   182  		MediaType: co.ConfigMediaType,
   183  		Digest:    digest.Canonical.FromBytes(b),
   184  		Size:      int64(len(b)),
   185  	}
   186  
   187  	layers := make([]ocispec.Descriptor, len(diffIDs))
   188  	for i, diffID := range diffIDs {
   189  		layers[i] = c.layerBlobs[diffID]
   190  	}
   191  
   192  	manifest := ocispec.Manifest{
   193  		Versioned: specs.Versioned{
   194  			SchemaVersion: 2,
   195  		},
   196  		Config: config,
   197  		Layers: layers,
   198  	}
   199  
   200  	mb, err := json.MarshalIndent(manifest, "", "   ")
   201  	if err != nil {
   202  		return ocispec.Descriptor{}, errors.Wrap(err, "failed to marshal image")
   203  	}
   204  
   205  	desc := ocispec.Descriptor{
   206  		MediaType: co.ManifestMediaType,
   207  		Digest:    digest.Canonical.FromBytes(mb),
   208  		Size:      int64(len(mb)),
   209  	}
   210  
   211  	labels := map[string]string{}
   212  	labels["containerd.io/gc.ref.content.0"] = manifest.Config.Digest.String()
   213  	for i, ch := range manifest.Layers {
   214  		labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i+1)] = ch.Digest.String()
   215  	}
   216  
   217  	ref := remotes.MakeRefKey(ctx, desc)
   218  	if err := content.WriteBlob(ctx, c.contentStore, ref, bytes.NewReader(mb), desc, content.WithLabels(labels)); err != nil {
   219  		return ocispec.Descriptor{}, errors.Wrap(err, "failed to write image manifest")
   220  	}
   221  
   222  	ref = remotes.MakeRefKey(ctx, config)
   223  	if err := content.WriteBlob(ctx, c.contentStore, ref, bytes.NewReader(b), config); err != nil {
   224  		return ocispec.Descriptor{}, errors.Wrap(err, "failed to write image config")
   225  	}
   226  
   227  	return desc, nil
   228  }
   229  
   230  // ReadStripSignature reads in a schema1 manifest and returns a byte array
   231  // with the "signatures" field stripped
   232  func ReadStripSignature(schema1Blob io.Reader) ([]byte, error) {
   233  	b, err := ioutil.ReadAll(io.LimitReader(schema1Blob, manifestSizeLimit)) // limit to 8MB
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  
   238  	return stripSignature(b)
   239  }
   240  
   241  func (c *Converter) fetchManifest(ctx context.Context, desc ocispec.Descriptor) error {
   242  	log.G(ctx).Debug("fetch schema 1")
   243  
   244  	rc, err := c.fetcher.Fetch(ctx, desc)
   245  	if err != nil {
   246  		return err
   247  	}
   248  
   249  	b, err := ReadStripSignature(rc)
   250  	rc.Close()
   251  	if err != nil {
   252  		return err
   253  	}
   254  
   255  	var m manifest
   256  	if err := json.Unmarshal(b, &m); err != nil {
   257  		return err
   258  	}
   259  	if len(m.Manifests) != 0 || len(m.Layers) != 0 {
   260  		return errors.New("converter: expected schema1 document but found extra keys")
   261  	}
   262  	c.pulledManifest = &m
   263  
   264  	return nil
   265  }
   266  
   267  func (c *Converter) fetchBlob(ctx context.Context, desc ocispec.Descriptor) error {
   268  	log.G(ctx).Debug("fetch blob")
   269  
   270  	var (
   271  		ref            = remotes.MakeRefKey(ctx, desc)
   272  		calc           = newBlobStateCalculator()
   273  		compressMethod = compression.Gzip
   274  	)
   275  
   276  	// size may be unknown, set to zero for content ingest
   277  	ingestDesc := desc
   278  	if ingestDesc.Size == -1 {
   279  		ingestDesc.Size = 0
   280  	}
   281  
   282  	cw, err := content.OpenWriter(ctx, c.contentStore, content.WithRef(ref), content.WithDescriptor(ingestDesc))
   283  	if err != nil {
   284  		if !errdefs.IsAlreadyExists(err) {
   285  			return err
   286  		}
   287  
   288  		reuse, err := c.reuseLabelBlobState(ctx, desc)
   289  		if err != nil {
   290  			return err
   291  		}
   292  
   293  		if reuse {
   294  			return nil
   295  		}
   296  
   297  		ra, err := c.contentStore.ReaderAt(ctx, desc)
   298  		if err != nil {
   299  			return err
   300  		}
   301  		defer ra.Close()
   302  
   303  		r, err := compression.DecompressStream(content.NewReader(ra))
   304  		if err != nil {
   305  			return err
   306  		}
   307  
   308  		compressMethod = r.GetCompression()
   309  		_, err = io.Copy(calc, r)
   310  		r.Close()
   311  		if err != nil {
   312  			return err
   313  		}
   314  	} else {
   315  		defer cw.Close()
   316  
   317  		rc, err := c.fetcher.Fetch(ctx, desc)
   318  		if err != nil {
   319  			return err
   320  		}
   321  		defer rc.Close()
   322  
   323  		eg, _ := errgroup.WithContext(ctx)
   324  		pr, pw := io.Pipe()
   325  
   326  		eg.Go(func() error {
   327  			r, err := compression.DecompressStream(pr)
   328  			if err != nil {
   329  				return err
   330  			}
   331  
   332  			compressMethod = r.GetCompression()
   333  			_, err = io.Copy(calc, r)
   334  			r.Close()
   335  			pr.CloseWithError(err)
   336  			return err
   337  		})
   338  
   339  		eg.Go(func() error {
   340  			defer pw.Close()
   341  
   342  			return content.Copy(ctx, cw, io.TeeReader(rc, pw), ingestDesc.Size, ingestDesc.Digest)
   343  		})
   344  
   345  		if err := eg.Wait(); err != nil {
   346  			return err
   347  		}
   348  	}
   349  
   350  	if desc.Size == -1 {
   351  		info, err := c.contentStore.Info(ctx, desc.Digest)
   352  		if err != nil {
   353  			return errors.Wrap(err, "failed to get blob info")
   354  		}
   355  		desc.Size = info.Size
   356  	}
   357  
   358  	if compressMethod == compression.Uncompressed {
   359  		log.G(ctx).WithField("id", desc.Digest).Debugf("changed media type for uncompressed schema1 layer blob")
   360  		desc.MediaType = images.MediaTypeDockerSchema2Layer
   361  	}
   362  
   363  	state := calc.State()
   364  
   365  	cinfo := content.Info{
   366  		Digest: desc.Digest,
   367  		Labels: map[string]string{
   368  			"containerd.io/uncompressed": state.diffID.String(),
   369  			labelDockerSchema1EmptyLayer: strconv.FormatBool(state.empty),
   370  		},
   371  	}
   372  
   373  	if _, err := c.contentStore.Update(ctx, cinfo, "labels.containerd.io/uncompressed", fmt.Sprintf("labels.%s", labelDockerSchema1EmptyLayer)); err != nil {
   374  		return errors.Wrap(err, "failed to update uncompressed label")
   375  	}
   376  
   377  	c.mu.Lock()
   378  	c.blobMap[desc.Digest] = state
   379  	c.layerBlobs[state.diffID] = desc
   380  	c.mu.Unlock()
   381  
   382  	return nil
   383  }
   384  
   385  func (c *Converter) reuseLabelBlobState(ctx context.Context, desc ocispec.Descriptor) (bool, error) {
   386  	cinfo, err := c.contentStore.Info(ctx, desc.Digest)
   387  	if err != nil {
   388  		return false, errors.Wrap(err, "failed to get blob info")
   389  	}
   390  	desc.Size = cinfo.Size
   391  
   392  	diffID, ok := cinfo.Labels["containerd.io/uncompressed"]
   393  	if !ok {
   394  		return false, nil
   395  	}
   396  
   397  	emptyVal, ok := cinfo.Labels[labelDockerSchema1EmptyLayer]
   398  	if !ok {
   399  		return false, nil
   400  	}
   401  
   402  	isEmpty, err := strconv.ParseBool(emptyVal)
   403  	if err != nil {
   404  		log.G(ctx).WithField("id", desc.Digest).Warnf("failed to parse bool from label %s: %v", labelDockerSchema1EmptyLayer, isEmpty)
   405  		return false, nil
   406  	}
   407  
   408  	bState := blobState{empty: isEmpty}
   409  
   410  	if bState.diffID, err = digest.Parse(diffID); err != nil {
   411  		log.G(ctx).WithField("id", desc.Digest).Warnf("failed to parse digest from label containerd.io/uncompressed: %v", diffID)
   412  		return false, nil
   413  	}
   414  
   415  	// NOTE: there is no need to read header to get compression method
   416  	// because there are only two kinds of methods.
   417  	if bState.diffID == desc.Digest {
   418  		desc.MediaType = images.MediaTypeDockerSchema2Layer
   419  	} else {
   420  		desc.MediaType = images.MediaTypeDockerSchema2LayerGzip
   421  	}
   422  
   423  	c.mu.Lock()
   424  	c.blobMap[desc.Digest] = bState
   425  	c.layerBlobs[bState.diffID] = desc
   426  	c.mu.Unlock()
   427  	return true, nil
   428  }
   429  
   430  func (c *Converter) schema1ManifestHistory() ([]ocispec.History, []digest.Digest, error) {
   431  	if c.pulledManifest == nil {
   432  		return nil, nil, errors.New("missing schema 1 manifest for conversion")
   433  	}
   434  	m := *c.pulledManifest
   435  
   436  	if len(m.History) == 0 {
   437  		return nil, nil, errors.New("no history")
   438  	}
   439  
   440  	history := make([]ocispec.History, len(m.History))
   441  	diffIDs := []digest.Digest{}
   442  	for i := range m.History {
   443  		var h v1History
   444  		if err := json.Unmarshal([]byte(m.History[i].V1Compatibility), &h); err != nil {
   445  			return nil, nil, errors.Wrap(err, "failed to unmarshal history")
   446  		}
   447  
   448  		blobSum := m.FSLayers[i].BlobSum
   449  
   450  		state := c.blobMap[blobSum]
   451  
   452  		history[len(history)-i-1] = ocispec.History{
   453  			Author:     h.Author,
   454  			Comment:    h.Comment,
   455  			Created:    &h.Created,
   456  			CreatedBy:  strings.Join(h.ContainerConfig.Cmd, " "),
   457  			EmptyLayer: state.empty,
   458  		}
   459  
   460  		if !state.empty {
   461  			diffIDs = append([]digest.Digest{state.diffID}, diffIDs...)
   462  
   463  		}
   464  	}
   465  
   466  	return history, diffIDs, nil
   467  }
   468  
   469  type fsLayer struct {
   470  	BlobSum digest.Digest `json:"blobSum"`
   471  }
   472  
   473  type history struct {
   474  	V1Compatibility string `json:"v1Compatibility"`
   475  }
   476  
   477  type manifest struct {
   478  	FSLayers  []fsLayer       `json:"fsLayers"`
   479  	History   []history       `json:"history"`
   480  	Layers    json.RawMessage `json:"layers,omitempty"`    // OCI manifest
   481  	Manifests json.RawMessage `json:"manifests,omitempty"` // OCI index
   482  }
   483  
   484  type v1History struct {
   485  	Author          string    `json:"author,omitempty"`
   486  	Created         time.Time `json:"created"`
   487  	Comment         string    `json:"comment,omitempty"`
   488  	ThrowAway       *bool     `json:"throwaway,omitempty"`
   489  	Size            *int      `json:"Size,omitempty"` // used before ThrowAway field
   490  	ContainerConfig struct {
   491  		Cmd []string `json:"Cmd,omitempty"`
   492  	} `json:"container_config,omitempty"`
   493  }
   494  
   495  // isEmptyLayer returns whether the v1 compatibility history describes an
   496  // empty layer. A return value of true indicates the layer is empty,
   497  // however false does not indicate non-empty.
   498  func isEmptyLayer(compatHistory []byte) (bool, error) {
   499  	var h v1History
   500  	if err := json.Unmarshal(compatHistory, &h); err != nil {
   501  		return false, err
   502  	}
   503  
   504  	if h.ThrowAway != nil {
   505  		return *h.ThrowAway, nil
   506  	}
   507  	if h.Size != nil {
   508  		return *h.Size == 0, nil
   509  	}
   510  
   511  	// If no `Size` or `throwaway` field is given, then
   512  	// it cannot be determined whether the layer is empty
   513  	// from the history, return false
   514  	return false, nil
   515  }
   516  
   517  type signature struct {
   518  	Signatures []jsParsedSignature `json:"signatures"`
   519  }
   520  
   521  type jsParsedSignature struct {
   522  	Protected string `json:"protected"`
   523  }
   524  
   525  type protectedBlock struct {
   526  	Length int    `json:"formatLength"`
   527  	Tail   string `json:"formatTail"`
   528  }
   529  
   530  // joseBase64UrlDecode decodes the given string using the standard base64 url
   531  // decoder but first adds the appropriate number of trailing '=' characters in
   532  // accordance with the jose specification.
   533  // http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2
   534  func joseBase64UrlDecode(s string) ([]byte, error) {
   535  	switch len(s) % 4 {
   536  	case 0:
   537  	case 2:
   538  		s += "=="
   539  	case 3:
   540  		s += "="
   541  	default:
   542  		return nil, errors.New("illegal base64url string")
   543  	}
   544  	return base64.URLEncoding.DecodeString(s)
   545  }
   546  
   547  func stripSignature(b []byte) ([]byte, error) {
   548  	var sig signature
   549  	if err := json.Unmarshal(b, &sig); err != nil {
   550  		return nil, err
   551  	}
   552  	if len(sig.Signatures) == 0 {
   553  		return nil, errors.New("no signatures")
   554  	}
   555  	pb, err := joseBase64UrlDecode(sig.Signatures[0].Protected)
   556  	if err != nil {
   557  		return nil, errors.Wrapf(err, "could not decode %s", sig.Signatures[0].Protected)
   558  	}
   559  
   560  	var protected protectedBlock
   561  	if err := json.Unmarshal(pb, &protected); err != nil {
   562  		return nil, err
   563  	}
   564  
   565  	if protected.Length > len(b) {
   566  		return nil, errors.New("invalid protected length block")
   567  	}
   568  
   569  	tail, err := joseBase64UrlDecode(protected.Tail)
   570  	if err != nil {
   571  		return nil, errors.Wrap(err, "invalid tail base 64 value")
   572  	}
   573  
   574  	return append(b[:protected.Length], tail...), nil
   575  }
   576  
   577  type blobStateCalculator struct {
   578  	empty    bool
   579  	digester digest.Digester
   580  }
   581  
   582  func newBlobStateCalculator() *blobStateCalculator {
   583  	return &blobStateCalculator{
   584  		empty:    true,
   585  		digester: digest.Canonical.Digester(),
   586  	}
   587  }
   588  
   589  func (c *blobStateCalculator) Write(p []byte) (int, error) {
   590  	if c.empty {
   591  		for _, b := range p {
   592  			if b != 0x00 {
   593  				c.empty = false
   594  				break
   595  			}
   596  		}
   597  	}
   598  	return c.digester.Hash().Write(p)
   599  }
   600  
   601  func (c *blobStateCalculator) State() blobState {
   602  	return blobState{
   603  		empty:  c.empty,
   604  		diffID: c.digester.Digest(),
   605  	}
   606  }