github.com/alibaba/sealer@v0.8.6-0.20220430115802-37a2bdaa8173/pkg/image/distributionutil/push.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 "sync" 23 24 distribution "github.com/distribution/distribution/v3" 25 "github.com/distribution/distribution/v3/manifest/manifestlist" 26 27 "github.com/alibaba/sealer/pkg/image/reference" 28 "github.com/alibaba/sealer/pkg/image/store" 29 v1 "github.com/alibaba/sealer/types/api/v1" 30 "github.com/alibaba/sealer/utils/archive" 31 "github.com/distribution/distribution/v3/manifest/schema2" 32 "github.com/docker/docker/pkg/progress" 33 "github.com/opencontainers/go-digest" 34 "github.com/pkg/errors" 35 "golang.org/x/sync/errgroup" 36 ) 37 38 const ( 39 manifestV2 = "application/vnd.docker.distribution.manifest.v2+json" 40 ) 41 42 type Pusher interface { 43 Push(ctx context.Context, named reference.Named) error 44 } 45 46 type ImagePusher struct { 47 config Config 48 repository distribution.Repository 49 imageStore store.ImageStore 50 } 51 52 func (pusher *ImagePusher) Push(ctx context.Context, named reference.Named) error { 53 descriptors := []manifestlist.ManifestDescriptor{} 54 imageMetadata, err := pusher.imageStore.GetImageMetadataMap() 55 if err != nil { 56 return err 57 } 58 59 manifestList, ok := imageMetadata[named.CompleteName()] 60 if !ok { 61 return fmt.Errorf("image: %s not found", named.Raw()) 62 } 63 64 for _, m := range manifestList.Manifests { 65 image, err := pusher.imageStore.GetByID(m.ID) 66 if err != nil { 67 return err 68 } 69 70 dgst, err := pusher.push(ctx, image, named) 71 if err != nil { 72 return err 73 } 74 75 descriptor, err := buildManifestDescriptor(dgst, m) 76 if err != nil { 77 return err 78 } 79 descriptors = append(descriptors, descriptor) 80 } 81 82 // push manifestList 83 ml, err := manifestlist.FromDescriptors(descriptors) 84 if err != nil { 85 return err 86 } 87 88 _, err = pusher.putManifestList(ctx, named, ml) 89 if err != nil { 90 return err 91 } 92 return nil 93 } 94 95 func (pusher *ImagePusher) push(ctx context.Context, image *v1.Image, named reference.Named) (digest.Digest, error) { 96 var ( 97 layerStore = pusher.config.LayerStore 98 pushedLayers = map[string]distribution.Descriptor{} 99 pushMux sync.Mutex 100 eg *errgroup.Group 101 ) 102 103 eg, _ = errgroup.WithContext(context.Background()) 104 for _, l := range image.Spec.Layers { 105 if l.ID == "" { 106 continue 107 } 108 err := l.ID.Validate() 109 if err != nil { 110 return "", fmt.Errorf("layer hash %s validate failed, err: %s", l.ID, err) 111 } 112 113 // this scope value, safe to pass into eg.Go 114 roLayer := layerStore.Get(store.LayerID(l.ID)) 115 if roLayer == nil { 116 return "", fmt.Errorf("failed to put image %s, layer %s not exists locally", named.Raw(), l.ID.String()) 117 } 118 119 eg.Go(func() error { 120 layerDescriptor, layerErr := pusher.uploadLayer(ctx, roLayer) 121 if layerErr != nil { 122 return layerErr 123 } 124 125 pushMux.Lock() 126 pushedLayers[roLayer.ID().String()] = layerDescriptor 127 pushMux.Unlock() 128 // add distribution digest metadata to disk 129 return layerStore.AddDistributionMetadata(roLayer.ID(), named, layerDescriptor.Digest) 130 }) 131 } 132 133 if err := eg.Wait(); err != nil { 134 return "", fmt.Errorf("failed to push layers of %s, err: %s", named.Raw(), err) 135 } 136 137 // for making descriptors have same order with image layers 138 // descriptor and image yaml are both saved in registry 139 // but they are different, layer digest in layer yaml is layerid. 140 // And digest in descriptor indicate the hash of layer content. 141 var layerDescriptors []distribution.Descriptor 142 for _, l := range image.Spec.Layers { 143 if l.ID == "" { 144 continue 145 } 146 // l.Hash.String() is same as layer.ID().String() above 147 layerDescriptor, ok := pushedLayers[l.ID.String()] 148 if !ok { 149 continue 150 } 151 layerDescriptors = append(layerDescriptors, layerDescriptor) 152 } 153 if len(layerDescriptors) != len(pushedLayers) { 154 return "", errors.New("failed to push image, the number of layerDescriptors and pushedLayers mismatch") 155 } 156 // push sealer image metadata to registry 157 configJSON, err := pusher.putManifestConfig(ctx, *image) 158 if err != nil { 159 return "", err 160 } 161 162 return pusher.putManifest(ctx, configJSON, named, layerDescriptors) 163 } 164 165 func (pusher *ImagePusher) uploadLayer(ctx context.Context, roLayer store.Layer) (distribution.Descriptor, error) { 166 var ( 167 err error 168 layerContentStream io.ReadCloser 169 repo = pusher.repository 170 progressChanOut = pusher.config.ProgressOutput 171 layerDistributionDigests = roLayer.DistributionMetadata() 172 ) 173 174 bs := repo.Blobs(ctx) 175 // if layerDistributionDigests is empty, we take the layer inexistence in the registry 176 // check all candidates 177 if len(layerDistributionDigests) > 0 { 178 // check if layer exists remotely. 179 for _, cand := range layerDistributionDigests { 180 remoteLayerDescriptor, err := bs.Stat(ctx, cand) 181 if err == nil { 182 progress.Message(progressChanOut, roLayer.SimpleID(), "already exists") 183 return remoteLayerDescriptor, nil 184 } 185 } 186 } 187 188 // pack layer files into tar.gz 189 progress.Update(progressChanOut, roLayer.SimpleID(), "preparing") 190 layerContentStream, err = roLayer.TarStream() 191 if err != nil { 192 return distribution.Descriptor{}, errors.Errorf("failed to get tar stream for layer %s, err: %s", roLayer.ID(), err) 193 } 194 //progress.NewProgressReader will close layerContentStream 195 progressReader := progress.NewProgressReader(layerContentStream, progressChanOut, roLayer.Size(), roLayer.SimpleID(), "pushing") 196 uploadStream, _ := archive.GzipCompress(progressReader) 197 defer func() { 198 err := layerContentStream.Close() 199 if err != nil { 200 return 201 } 202 err = uploadStream.Close() 203 if err != nil { 204 return 205 } 206 }() 207 208 layerUploader, err := bs.Create(ctx) 209 if err != nil { 210 progress.Update(progressChanOut, roLayer.SimpleID(), "push failed") 211 return distribution.Descriptor{}, err 212 } 213 defer layerUploader.Close() 214 215 // calculate hash of layer content stream 216 digester := digest.Canonical.Digester() 217 tee := io.TeeReader(uploadStream, digester.Hash()) 218 realSize, err := layerUploader.ReadFrom(tee) 219 if err != nil { 220 return distribution.Descriptor{}, fmt.Errorf("failed to upload layer %s, err: %s", roLayer.ID(), err) 221 } 222 223 layerContentDigest := digester.Digest() 224 if _, err = layerUploader.Commit(ctx, distribution.Descriptor{Digest: layerContentDigest}); err != nil { 225 return distribution.Descriptor{}, fmt.Errorf("failed to commit layer to registry, err: %s", err) 226 } 227 228 progress.Update(progressChanOut, roLayer.SimpleID(), "push completed") 229 return buildBlobs(layerContentDigest, realSize, roLayer.MediaType()), nil 230 } 231 232 func (pusher *ImagePusher) putManifest(ctx context.Context, configJSON []byte, named reference.Named, layerDescriptors []distribution.Descriptor) (digest.Digest, error) { 233 var ( 234 bs = &blobService{descriptors: map[digest.Digest]distribution.Descriptor{}} 235 repo = pusher.repository 236 ) 237 238 manifestBuilder := schema2.NewManifestBuilder( 239 bs, 240 // use schema2.MediaTypeImageConfig by default 241 //TODO plan to support more types to support more registry 242 schema2.MediaTypeImageConfig, 243 configJSON) 244 245 for _, d := range layerDescriptors { 246 err := manifestBuilder.AppendReference(d) 247 if err != nil { 248 return "", err 249 } 250 } 251 252 manifest, err := manifestBuilder.Build(ctx) 253 if err != nil { 254 return "", err 255 } 256 257 ms, err := repo.Manifests(ctx) 258 if err != nil { 259 return "", err 260 } 261 262 putOptions := []distribution.ManifestServiceOption{distribution.WithTag(named.Tag())} 263 dgst, err := ms.Put(ctx, manifest, putOptions...) 264 if err != nil { 265 return "", err 266 } 267 268 return dgst, nil 269 } 270 271 func (pusher *ImagePusher) putManifestList(ctx context.Context, named reference.Named, manifest distribution.Manifest) (digest.Digest, error) { 272 repo := pusher.repository 273 manifestService, err := repo.Manifests(ctx) 274 if err != nil { 275 return digest.Digest(""), err 276 } 277 278 putOptions := []distribution.ManifestServiceOption{distribution.WithTag(named.Tag())} 279 dgst, err := manifestService.Put(ctx, manifest, putOptions...) 280 if err != nil { 281 return "", errors.Wrapf(err, "failed to put manifest") 282 } 283 284 return dgst, nil 285 } 286 287 func (pusher *ImagePusher) putManifestConfig(ctx context.Context, image v1.Image) ([]byte, error) { 288 repo := pusher.repository 289 290 dockerImageConfig, err := addDockerManifestConfig(image) 291 if err != nil { 292 return nil, fmt.Errorf("add docker manifest config error: %s", err) 293 } 294 295 configJSON, err := json.Marshal(dockerImageConfig) 296 if err != nil { 297 return nil, err 298 } 299 300 bs := repo.Blobs(ctx) 301 _, err = bs.Put(ctx, schema2.MediaTypeImageConfig, configJSON) 302 if err != nil { 303 return nil, err 304 } 305 306 return configJSON, err 307 } 308 309 type dockerImageLayerInfo struct { 310 Created string `json:"created,omitempty"` 311 CreatedBy string `json:"created_by,omitempty"` 312 EmptyLayer bool `json:"empty_layer,omitempty"` 313 } 314 315 //wrap v1.Image with docker image config fields 316 type dockerManifestConfig struct { 317 v1.Image 318 Architecture string `json:"architecture,omitempty"` 319 OS string `json:"os,omitempty"` 320 History []dockerImageLayerInfo `json:"history,omitempty"` 321 } 322 323 // add docker image config fields to display some metadata on docker hub 324 // os, architecture and each layer command 325 func addDockerManifestConfig(image v1.Image) (*dockerManifestConfig, error) { 326 var dockerImage = &dockerManifestConfig{} 327 config, err := json.Marshal(image) 328 if err != nil { 329 return nil, err 330 } 331 err = json.Unmarshal(config, dockerImage) 332 if err != nil { 333 return nil, err 334 } 335 336 dockerImage.OS = image.Spec.Platform.OS 337 dockerImage.Architecture = image.Spec.Platform.Architecture 338 339 for _, layer := range image.Spec.Layers { 340 var tmpLayerInfo = dockerImageLayerInfo{CreatedBy: layer.Type + " " + layer.Value} 341 if layer.ID == "" { 342 tmpLayerInfo.EmptyLayer = true 343 } 344 dockerImage.History = append(dockerImage.History, tmpLayerInfo) 345 } 346 return dockerImage, nil 347 } 348 349 func buildBlobs(dig digest.Digest, size int64, mediaType string) distribution.Descriptor { 350 return distribution.Descriptor{ 351 Digest: dig, 352 Size: size, 353 MediaType: mediaType, 354 } 355 } 356 357 func NewPusher(named reference.Named, config Config) (Pusher, error) { 358 repo, err := NewV2Repository(named, "push", "pull") 359 if err != nil { 360 return nil, err 361 } 362 363 is, err := store.NewDefaultImageStore() 364 if err != nil { 365 return nil, err 366 } 367 368 return &ImagePusher{ 369 repository: repo, 370 config: config, 371 imageStore: is, 372 }, nil 373 }