github.com/containerd/Containerd@v1.4.13/diff/walking/differ.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 walking
    18  
    19  import (
    20  	"context"
    21  	"encoding/base64"
    22  	"fmt"
    23  	"io"
    24  	"math/rand"
    25  	"time"
    26  
    27  	"github.com/containerd/containerd/archive"
    28  	"github.com/containerd/containerd/archive/compression"
    29  	"github.com/containerd/containerd/content"
    30  	"github.com/containerd/containerd/diff"
    31  	"github.com/containerd/containerd/errdefs"
    32  	"github.com/containerd/containerd/log"
    33  	"github.com/containerd/containerd/mount"
    34  	digest "github.com/opencontainers/go-digest"
    35  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    36  	"github.com/pkg/errors"
    37  )
    38  
    39  type walkingDiff struct {
    40  	store content.Store
    41  }
    42  
    43  var emptyDesc = ocispec.Descriptor{}
    44  var uncompressed = "containerd.io/uncompressed"
    45  
    46  // NewWalkingDiff is a generic implementation of diff.Comparer.  The diff is
    47  // calculated by mounting both the upper and lower mount sets and walking the
    48  // mounted directories concurrently. Changes are calculated by comparing files
    49  // against each other or by comparing file existence between directories.
    50  // NewWalkingDiff uses no special characteristics of the mount sets and is
    51  // expected to work with any filesystem.
    52  func NewWalkingDiff(store content.Store) diff.Comparer {
    53  	return &walkingDiff{
    54  		store: store,
    55  	}
    56  }
    57  
    58  // Compare creates a diff between the given mounts and uploads the result
    59  // to the content store.
    60  func (s *walkingDiff) Compare(ctx context.Context, lower, upper []mount.Mount, opts ...diff.Opt) (d ocispec.Descriptor, err error) {
    61  	var config diff.Config
    62  	for _, opt := range opts {
    63  		if err := opt(&config); err != nil {
    64  			return emptyDesc, err
    65  		}
    66  	}
    67  
    68  	if config.MediaType == "" {
    69  		config.MediaType = ocispec.MediaTypeImageLayerGzip
    70  	}
    71  
    72  	var isCompressed bool
    73  	switch config.MediaType {
    74  	case ocispec.MediaTypeImageLayer:
    75  	case ocispec.MediaTypeImageLayerGzip:
    76  		isCompressed = true
    77  	default:
    78  		return emptyDesc, errors.Wrapf(errdefs.ErrNotImplemented, "unsupported diff media type: %v", config.MediaType)
    79  	}
    80  
    81  	var ocidesc ocispec.Descriptor
    82  	if err := mount.WithTempMount(ctx, lower, func(lowerRoot string) error {
    83  		return mount.WithTempMount(ctx, upper, func(upperRoot string) error {
    84  			var newReference bool
    85  			if config.Reference == "" {
    86  				newReference = true
    87  				config.Reference = uniqueRef()
    88  			}
    89  
    90  			cw, err := s.store.Writer(ctx,
    91  				content.WithRef(config.Reference),
    92  				content.WithDescriptor(ocispec.Descriptor{
    93  					MediaType: config.MediaType, // most contentstore implementations just ignore this
    94  				}))
    95  			if err != nil {
    96  				return errors.Wrap(err, "failed to open writer")
    97  			}
    98  			defer func() {
    99  				if err != nil {
   100  					cw.Close()
   101  					if newReference {
   102  						if err := s.store.Abort(ctx, config.Reference); err != nil {
   103  							log.G(ctx).WithField("ref", config.Reference).Warnf("failed to delete diff upload")
   104  						}
   105  					}
   106  				}
   107  			}()
   108  			if !newReference {
   109  				if err = cw.Truncate(0); err != nil {
   110  					return err
   111  				}
   112  			}
   113  
   114  			if isCompressed {
   115  				dgstr := digest.SHA256.Digester()
   116  				var compressed io.WriteCloser
   117  				compressed, err = compression.CompressStream(cw, compression.Gzip)
   118  				if err != nil {
   119  					return errors.Wrap(err, "failed to get compressed stream")
   120  				}
   121  				err = archive.WriteDiff(ctx, io.MultiWriter(compressed, dgstr.Hash()), lowerRoot, upperRoot)
   122  				compressed.Close()
   123  				if err != nil {
   124  					return errors.Wrap(err, "failed to write compressed diff")
   125  				}
   126  
   127  				if config.Labels == nil {
   128  					config.Labels = map[string]string{}
   129  				}
   130  				config.Labels[uncompressed] = dgstr.Digest().String()
   131  			} else {
   132  				if err = archive.WriteDiff(ctx, cw, lowerRoot, upperRoot); err != nil {
   133  					return errors.Wrap(err, "failed to write diff")
   134  				}
   135  			}
   136  
   137  			var commitopts []content.Opt
   138  			if config.Labels != nil {
   139  				commitopts = append(commitopts, content.WithLabels(config.Labels))
   140  			}
   141  
   142  			dgst := cw.Digest()
   143  			if err := cw.Commit(ctx, 0, dgst, commitopts...); err != nil {
   144  				if !errdefs.IsAlreadyExists(err) {
   145  					return errors.Wrap(err, "failed to commit")
   146  				}
   147  			}
   148  
   149  			info, err := s.store.Info(ctx, dgst)
   150  			if err != nil {
   151  				return errors.Wrap(err, "failed to get info from content store")
   152  			}
   153  			if info.Labels == nil {
   154  				info.Labels = make(map[string]string)
   155  			}
   156  			// Set uncompressed label if digest already existed without label
   157  			if _, ok := info.Labels[uncompressed]; !ok {
   158  				info.Labels[uncompressed] = config.Labels[uncompressed]
   159  				if _, err := s.store.Update(ctx, info, "labels."+uncompressed); err != nil {
   160  					return errors.Wrap(err, "error setting uncompressed label")
   161  				}
   162  			}
   163  
   164  			ocidesc = ocispec.Descriptor{
   165  				MediaType: config.MediaType,
   166  				Size:      info.Size,
   167  				Digest:    info.Digest,
   168  			}
   169  			return nil
   170  		})
   171  	}); err != nil {
   172  		return emptyDesc, err
   173  	}
   174  
   175  	return ocidesc, nil
   176  }
   177  
   178  func uniqueRef() string {
   179  	t := time.Now()
   180  	var b [3]byte
   181  	// Ignore read failures, just decreases uniqueness
   182  	rand.Read(b[:])
   183  	return fmt.Sprintf("%d-%s", t.UnixNano(), base64.URLEncoding.EncodeToString(b[:]))
   184  }