github.com/alibaba/sealer@v0.8.6-0.20220430115802-37a2bdaa8173/pkg/image/distributionutil/pull.go (about) 1 // Copyright © 2021 Alibaba Group Holding Ltd. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package distributionutil 16 17 import ( 18 "context" 19 "encoding/json" 20 "fmt" 21 "io" 22 "io/ioutil" 23 24 distribution "github.com/distribution/distribution/v3" 25 "github.com/distribution/distribution/v3/manifest/schema2" 26 "github.com/docker/docker/pkg/progress" 27 "github.com/opencontainers/go-digest" 28 "golang.org/x/sync/errgroup" 29 30 "github.com/alibaba/sealer/pkg/image/reference" 31 "github.com/alibaba/sealer/pkg/image/store" 32 v1 "github.com/alibaba/sealer/types/api/v1" 33 "github.com/alibaba/sealer/utils/archive" 34 ) 35 36 type Puller interface { 37 Pull(ctx context.Context, named reference.Named, manifest schema2.Manifest) (*v1.Image, error) 38 } 39 40 type ImagePuller struct { 41 config Config 42 repository distribution.Repository 43 } 44 45 func (puller *ImagePuller) Pull(ctx context.Context, named reference.Named, manifest schema2.Manifest) (*v1.Image, error) { 46 var ( 47 layerStore = puller.config.LayerStore 48 layers = []v1.Layer{} 49 eg *errgroup.Group 50 ) 51 52 v1Image, err := puller.getRemoteImageMetadata(ctx, manifest.Config.Digest) 53 if err != nil { 54 return nil, err 55 } 56 57 eg, _ = errgroup.WithContext(ctx) 58 for _, l := range v1Image.Spec.Layers { 59 if l.ID == "" { 60 continue 61 } 62 layers = append(layers, l) 63 } 64 // number of non-empty layer and layer in distribution should be equal 65 if len(layers) != len(manifest.Layers) { 66 return nil, fmt.Errorf("the number layerIDs %d and LayerDescriptor %d are mismatch", len(layers), len(manifest.Layers)) 67 } 68 69 for i, l := range manifest.Layers { 70 // local value to current scope, safe to pass into goroutine 71 var ( 72 descriptor = l 73 layer = layers[i] 74 ) 75 // we take hash of layer as real layer id, hash of descriptor is just 76 // an identifier for remote data 77 eg.Go(func() error { 78 // roLayer now does not exist, new one 79 // descriptor.Size is temp size for this layer 80 // real size will be set within downloadLayer 81 roLayer, layerErr := store.NewROLayer(layer.ID, 82 descriptor.Size, 83 map[string]digest.Digest{named.Domain() + "/" + named.Repo(): descriptor.Digest}) 84 if layerErr != nil { 85 return layerErr 86 } 87 88 layerErr = puller.downloadLayer(ctx, roLayer, descriptor) 89 if layerErr != nil { 90 return layerErr 91 } 92 93 return layerStore.RegisterLayerIfNotPresent(roLayer) 94 }) 95 } 96 err = eg.Wait() 97 if err != nil { 98 return nil, fmt.Errorf("failed to pull image %s, err: %s", named.Raw(), err) 99 } 100 101 return &v1Image, nil 102 } 103 104 func (puller *ImagePuller) downloadLayer(ctx context.Context, layer store.Layer, descriptor distribution.Descriptor) error { 105 var ( 106 layerStore = puller.config.LayerStore 107 progressOut = puller.config.ProgressOutput 108 repo = puller.repository 109 ) 110 backend, err := store.NewFSStoreBackend() 111 if err != nil { 112 return err 113 } 114 // descriptor is remote layer data, but its hash may not be the layer id, so we 115 // use layer.ID(hash of layer from v1.Image) to check layer existence. 116 roLayer := layerStore.Get(layer.ID()) 117 if roLayer != nil { 118 progress.Message(progressOut, layer.SimpleID(), "already exists") 119 return nil 120 } 121 122 bs := repo.Blobs(ctx) 123 layerReader, err := bs.Open(ctx, descriptor.Digest) 124 if err != nil { 125 return err 126 } 127 defer layerReader.Close() 128 129 digester := digest.Canonical.Digester() 130 layerDownloadReader := ioutil.NopCloser(io.TeeReader(layerReader, digester.Hash())) 131 progressReader := progress.NewProgressReader(layerDownloadReader, progressOut, descriptor.Size, layer.SimpleID(), "pulling") 132 size, err := archive.Decompress(progressReader, backend.LayerDataDir(layer.ID().ToDigest()), archive.Options{Compress: true}) 133 if err != nil { 134 progress.Update(progressOut, layer.SimpleID(), err.Error()) 135 return err 136 } 137 // update rolayer size for storing the info under layerdb 138 layer.SetSize(size) 139 if digester.Digest() != descriptor.Digest { 140 return fmt.Errorf("digest verified failed for %s", layer.ID()) 141 } 142 progress.Update(progressOut, layer.SimpleID(), "pull completed") 143 return nil 144 } 145 146 // not docker image, get sealer image metadata 147 func (puller *ImagePuller) getRemoteImageMetadata(context context.Context, digest digest.Digest) (v1.Image, error) { 148 repo := puller.repository 149 bs := repo.Blobs(context) 150 manifestImageBytes, err := bs.Get(context, digest) 151 if err != nil { 152 return v1.Image{}, err 153 } 154 155 img := v1.Image{} 156 return img, json.Unmarshal(manifestImageBytes, &img) 157 } 158 159 func NewPuller(repo distribution.Repository, config Config) (Puller, error) { 160 return &ImagePuller{ 161 repository: repo, 162 config: config, 163 }, nil 164 }