github.com/ali-iotechsys/cli@v20.10.0+incompatible/cli/command/manifest/push.go (about)

     1  package manifest
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  
     9  	"github.com/docker/cli/cli"
    10  	"github.com/docker/cli/cli/command"
    11  	"github.com/docker/cli/cli/manifest/types"
    12  	registryclient "github.com/docker/cli/cli/registry/client"
    13  	"github.com/docker/distribution"
    14  	"github.com/docker/distribution/manifest/manifestlist"
    15  	"github.com/docker/distribution/manifest/schema2"
    16  	"github.com/docker/distribution/reference"
    17  	"github.com/docker/docker/registry"
    18  	"github.com/pkg/errors"
    19  	"github.com/spf13/cobra"
    20  )
    21  
    22  type pushOpts struct {
    23  	insecure bool
    24  	purge    bool
    25  	target   string
    26  }
    27  
    28  type mountRequest struct {
    29  	ref      reference.Named
    30  	manifest types.ImageManifest
    31  }
    32  
    33  type manifestBlob struct {
    34  	canonical reference.Canonical
    35  	os        string
    36  }
    37  
    38  type pushRequest struct {
    39  	targetRef     reference.Named
    40  	list          *manifestlist.DeserializedManifestList
    41  	mountRequests []mountRequest
    42  	manifestBlobs []manifestBlob
    43  	insecure      bool
    44  }
    45  
    46  func newPushListCommand(dockerCli command.Cli) *cobra.Command {
    47  	opts := pushOpts{}
    48  
    49  	cmd := &cobra.Command{
    50  		Use:   "push [OPTIONS] MANIFEST_LIST",
    51  		Short: "Push a manifest list to a repository",
    52  		Args:  cli.ExactArgs(1),
    53  		RunE: func(cmd *cobra.Command, args []string) error {
    54  			opts.target = args[0]
    55  			return runPush(dockerCli, opts)
    56  		},
    57  	}
    58  
    59  	flags := cmd.Flags()
    60  	flags.BoolVarP(&opts.purge, "purge", "p", false, "Remove the local manifest list after push")
    61  	flags.BoolVar(&opts.insecure, "insecure", false, "Allow push to an insecure registry")
    62  	return cmd
    63  }
    64  
    65  func runPush(dockerCli command.Cli, opts pushOpts) error {
    66  
    67  	targetRef, err := normalizeReference(opts.target)
    68  	if err != nil {
    69  		return err
    70  	}
    71  
    72  	manifests, err := dockerCli.ManifestStore().GetList(targetRef)
    73  	if err != nil {
    74  		return err
    75  	}
    76  	if len(manifests) == 0 {
    77  		return errors.Errorf("%s not found", targetRef)
    78  	}
    79  
    80  	pushRequest, err := buildPushRequest(manifests, targetRef, opts.insecure)
    81  	if err != nil {
    82  		return err
    83  	}
    84  
    85  	ctx := context.Background()
    86  	if err := pushList(ctx, dockerCli, pushRequest); err != nil {
    87  		return err
    88  	}
    89  	if opts.purge {
    90  		return dockerCli.ManifestStore().Remove(targetRef)
    91  	}
    92  	return nil
    93  }
    94  
    95  func buildPushRequest(manifests []types.ImageManifest, targetRef reference.Named, insecure bool) (pushRequest, error) {
    96  	req := pushRequest{targetRef: targetRef, insecure: insecure}
    97  
    98  	var err error
    99  	req.list, err = buildManifestList(manifests, targetRef)
   100  	if err != nil {
   101  		return req, err
   102  	}
   103  
   104  	targetRepo, err := registry.ParseRepositoryInfo(targetRef)
   105  	if err != nil {
   106  		return req, err
   107  	}
   108  	targetRepoName, err := registryclient.RepoNameForReference(targetRepo.Name)
   109  	if err != nil {
   110  		return req, err
   111  	}
   112  
   113  	for _, imageManifest := range manifests {
   114  		manifestRepoName, err := registryclient.RepoNameForReference(imageManifest.Ref)
   115  		if err != nil {
   116  			return req, err
   117  		}
   118  
   119  		repoName, _ := reference.WithName(manifestRepoName)
   120  		if repoName.Name() != targetRepoName {
   121  			blobs, err := buildBlobRequestList(imageManifest, repoName)
   122  			if err != nil {
   123  				return req, err
   124  			}
   125  			req.manifestBlobs = append(req.manifestBlobs, blobs...)
   126  
   127  			manifestPush, err := buildPutManifestRequest(imageManifest, targetRef)
   128  			if err != nil {
   129  				return req, err
   130  			}
   131  			req.mountRequests = append(req.mountRequests, manifestPush)
   132  		}
   133  	}
   134  	return req, nil
   135  }
   136  
   137  func buildManifestList(manifests []types.ImageManifest, targetRef reference.Named) (*manifestlist.DeserializedManifestList, error) {
   138  	targetRepoInfo, err := registry.ParseRepositoryInfo(targetRef)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	descriptors := []manifestlist.ManifestDescriptor{}
   144  	for _, imageManifest := range manifests {
   145  		if imageManifest.Descriptor.Platform == nil ||
   146  			imageManifest.Descriptor.Platform.Architecture == "" ||
   147  			imageManifest.Descriptor.Platform.OS == "" {
   148  			return nil, errors.Errorf(
   149  				"manifest %s must have an OS and Architecture to be pushed to a registry", imageManifest.Ref)
   150  		}
   151  		descriptor, err := buildManifestDescriptor(targetRepoInfo, imageManifest)
   152  		if err != nil {
   153  			return nil, err
   154  		}
   155  		descriptors = append(descriptors, descriptor)
   156  	}
   157  
   158  	return manifestlist.FromDescriptors(descriptors)
   159  }
   160  
   161  func buildManifestDescriptor(targetRepo *registry.RepositoryInfo, imageManifest types.ImageManifest) (manifestlist.ManifestDescriptor, error) {
   162  	repoInfo, err := registry.ParseRepositoryInfo(imageManifest.Ref)
   163  	if err != nil {
   164  		return manifestlist.ManifestDescriptor{}, err
   165  	}
   166  
   167  	manifestRepoHostname := reference.Domain(repoInfo.Name)
   168  	targetRepoHostname := reference.Domain(targetRepo.Name)
   169  	if manifestRepoHostname != targetRepoHostname {
   170  		return manifestlist.ManifestDescriptor{}, errors.Errorf("cannot use source images from a different registry than the target image: %s != %s", manifestRepoHostname, targetRepoHostname)
   171  	}
   172  
   173  	manifest := manifestlist.ManifestDescriptor{
   174  		Descriptor: distribution.Descriptor{
   175  			Digest:    imageManifest.Descriptor.Digest,
   176  			Size:      imageManifest.Descriptor.Size,
   177  			MediaType: imageManifest.Descriptor.MediaType,
   178  		},
   179  	}
   180  
   181  	platform := types.PlatformSpecFromOCI(imageManifest.Descriptor.Platform)
   182  	if platform != nil {
   183  		manifest.Platform = *platform
   184  	}
   185  
   186  	if err = manifest.Descriptor.Digest.Validate(); err != nil {
   187  		return manifestlist.ManifestDescriptor{}, errors.Wrapf(err,
   188  			"digest parse of image %q failed", imageManifest.Ref)
   189  	}
   190  
   191  	return manifest, nil
   192  }
   193  
   194  func buildBlobRequestList(imageManifest types.ImageManifest, repoName reference.Named) ([]manifestBlob, error) {
   195  	var blobReqs []manifestBlob
   196  
   197  	for _, blobDigest := range imageManifest.Blobs() {
   198  		canonical, err := reference.WithDigest(repoName, blobDigest)
   199  		if err != nil {
   200  			return nil, err
   201  		}
   202  		var os string
   203  		if imageManifest.Descriptor.Platform != nil {
   204  			os = imageManifest.Descriptor.Platform.OS
   205  		}
   206  		blobReqs = append(blobReqs, manifestBlob{canonical: canonical, os: os})
   207  	}
   208  	return blobReqs, nil
   209  }
   210  
   211  // nolint: interfacer
   212  func buildPutManifestRequest(imageManifest types.ImageManifest, targetRef reference.Named) (mountRequest, error) {
   213  	refWithoutTag, err := reference.WithName(targetRef.Name())
   214  	if err != nil {
   215  		return mountRequest{}, err
   216  	}
   217  	mountRef, err := reference.WithDigest(refWithoutTag, imageManifest.Descriptor.Digest)
   218  	if err != nil {
   219  		return mountRequest{}, err
   220  	}
   221  
   222  	// This indentation has to be added to ensure sha parity with the registry
   223  	v2ManifestBytes, err := json.MarshalIndent(imageManifest.SchemaV2Manifest, "", "   ")
   224  	if err != nil {
   225  		return mountRequest{}, err
   226  	}
   227  	// indent only the DeserializedManifest portion of this, in order to maintain parity with the registry
   228  	// and not alter the sha
   229  	var v2Manifest schema2.DeserializedManifest
   230  	if err = v2Manifest.UnmarshalJSON(v2ManifestBytes); err != nil {
   231  		return mountRequest{}, err
   232  	}
   233  	imageManifest.SchemaV2Manifest = &v2Manifest
   234  
   235  	return mountRequest{ref: mountRef, manifest: imageManifest}, err
   236  }
   237  
   238  func pushList(ctx context.Context, dockerCli command.Cli, req pushRequest) error {
   239  	rclient := dockerCli.RegistryClient(req.insecure)
   240  
   241  	if err := mountBlobs(ctx, rclient, req.targetRef, req.manifestBlobs); err != nil {
   242  		return err
   243  	}
   244  	if err := pushReferences(ctx, dockerCli.Out(), rclient, req.mountRequests); err != nil {
   245  		return err
   246  	}
   247  	dgst, err := rclient.PutManifest(ctx, req.targetRef, req.list)
   248  	if err != nil {
   249  		return err
   250  	}
   251  
   252  	fmt.Fprintln(dockerCli.Out(), dgst.String())
   253  	return nil
   254  }
   255  
   256  func pushReferences(ctx context.Context, out io.Writer, client registryclient.RegistryClient, mounts []mountRequest) error {
   257  	for _, mount := range mounts {
   258  		newDigest, err := client.PutManifest(ctx, mount.ref, mount.manifest)
   259  		if err != nil {
   260  			return err
   261  		}
   262  		fmt.Fprintf(out, "Pushed ref %s with digest: %s\n", mount.ref, newDigest)
   263  	}
   264  	return nil
   265  }
   266  
   267  func mountBlobs(ctx context.Context, client registryclient.RegistryClient, ref reference.Named, blobs []manifestBlob) error {
   268  	for _, blob := range blobs {
   269  		err := client.MountBlob(ctx, blob.canonical, ref)
   270  		switch err.(type) {
   271  		case nil:
   272  		case registryclient.ErrBlobCreated:
   273  			if blob.os != "windows" {
   274  				return fmt.Errorf("error mounting %s to %s", blob.canonical, ref)
   275  			}
   276  		default:
   277  			return err
   278  		}
   279  	}
   280  	return nil
   281  }