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  }