github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/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 }