github.1git.de/docker/cli@v26.1.3+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/distribution/reference"
    10  	"github.com/docker/cli/cli"
    11  	"github.com/docker/cli/cli/command"
    12  	"github.com/docker/cli/cli/manifest/types"
    13  	registryclient "github.com/docker/cli/cli/registry/client"
    14  	"github.com/docker/distribution"
    15  	"github.com/docker/distribution/manifest/manifestlist"
    16  	"github.com/docker/distribution/manifest/ocischema"
    17  	"github.com/docker/distribution/manifest/schema2"
    18  	"github.com/docker/docker/registry"
    19  	"github.com/pkg/errors"
    20  	"github.com/spf13/cobra"
    21  )
    22  
    23  type pushOpts struct {
    24  	insecure bool
    25  	purge    bool
    26  	target   string
    27  }
    28  
    29  type mountRequest struct {
    30  	ref      reference.Named
    31  	manifest types.ImageManifest
    32  }
    33  
    34  type manifestBlob struct {
    35  	canonical reference.Canonical
    36  	os        string
    37  }
    38  
    39  type pushRequest struct {
    40  	targetRef     reference.Named
    41  	list          *manifestlist.DeserializedManifestList
    42  	mountRequests []mountRequest
    43  	manifestBlobs []manifestBlob
    44  	insecure      bool
    45  }
    46  
    47  func newPushListCommand(dockerCli command.Cli) *cobra.Command {
    48  	opts := pushOpts{}
    49  
    50  	cmd := &cobra.Command{
    51  		Use:   "push [OPTIONS] MANIFEST_LIST",
    52  		Short: "Push a manifest list to a repository",
    53  		Args:  cli.ExactArgs(1),
    54  		RunE: func(cmd *cobra.Command, args []string) error {
    55  			opts.target = args[0]
    56  			return runPush(cmd.Context(), dockerCli, opts)
    57  		},
    58  	}
    59  
    60  	flags := cmd.Flags()
    61  	flags.BoolVarP(&opts.purge, "purge", "p", false, "Remove the local manifest list after push")
    62  	flags.BoolVar(&opts.insecure, "insecure", false, "Allow push to an insecure registry")
    63  	return cmd
    64  }
    65  
    66  func runPush(ctx context.Context, dockerCli command.Cli, opts pushOpts) error {
    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  	req, err := buildPushRequest(manifests, targetRef, opts.insecure)
    81  	if err != nil {
    82  		return err
    83  	}
    84  
    85  	if err := pushList(ctx, dockerCli, req); err != nil {
    86  		return err
    87  	}
    88  	if opts.purge {
    89  		return dockerCli.ManifestStore().Remove(targetRef)
    90  	}
    91  	return nil
    92  }
    93  
    94  func buildPushRequest(manifests []types.ImageManifest, targetRef reference.Named, insecure bool) (pushRequest, error) {
    95  	req := pushRequest{targetRef: targetRef, insecure: insecure}
    96  
    97  	var err error
    98  	req.list, err = buildManifestList(manifests, targetRef)
    99  	if err != nil {
   100  		return req, err
   101  	}
   102  
   103  	targetRepo, err := registry.ParseRepositoryInfo(targetRef)
   104  	if err != nil {
   105  		return req, err
   106  	}
   107  	targetRepoName, err := registryclient.RepoNameForReference(targetRepo.Name)
   108  	if err != nil {
   109  		return req, err
   110  	}
   111  
   112  	for _, imageManifest := range manifests {
   113  		manifestRepoName, err := registryclient.RepoNameForReference(imageManifest.Ref)
   114  		if err != nil {
   115  			return req, err
   116  		}
   117  
   118  		repoName, _ := reference.WithName(manifestRepoName)
   119  		if repoName.Name() != targetRepoName {
   120  			blobs, err := buildBlobRequestList(imageManifest, repoName)
   121  			if err != nil {
   122  				return req, err
   123  			}
   124  			req.manifestBlobs = append(req.manifestBlobs, blobs...)
   125  
   126  			manifestPush, err := buildPutManifestRequest(imageManifest, targetRef)
   127  			if err != nil {
   128  				return req, err
   129  			}
   130  			req.mountRequests = append(req.mountRequests, manifestPush)
   131  		}
   132  	}
   133  	return req, nil
   134  }
   135  
   136  func buildManifestList(manifests []types.ImageManifest, targetRef reference.Named) (*manifestlist.DeserializedManifestList, error) {
   137  	targetRepoInfo, err := registry.ParseRepositoryInfo(targetRef)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	descriptors := []manifestlist.ManifestDescriptor{}
   143  	for _, imageManifest := range manifests {
   144  		if imageManifest.Descriptor.Platform == nil ||
   145  			imageManifest.Descriptor.Platform.Architecture == "" ||
   146  			imageManifest.Descriptor.Platform.OS == "" {
   147  			return nil, errors.Errorf(
   148  				"manifest %s must have an OS and Architecture to be pushed to a registry", imageManifest.Ref)
   149  		}
   150  		descriptor, err := buildManifestDescriptor(targetRepoInfo, imageManifest)
   151  		if err != nil {
   152  			return nil, err
   153  		}
   154  		descriptors = append(descriptors, descriptor)
   155  	}
   156  
   157  	return manifestlist.FromDescriptors(descriptors)
   158  }
   159  
   160  func buildManifestDescriptor(targetRepo *registry.RepositoryInfo, imageManifest types.ImageManifest) (manifestlist.ManifestDescriptor, error) {
   161  	repoInfo, err := registry.ParseRepositoryInfo(imageManifest.Ref)
   162  	if err != nil {
   163  		return manifestlist.ManifestDescriptor{}, err
   164  	}
   165  
   166  	manifestRepoHostname := reference.Domain(repoInfo.Name)
   167  	targetRepoHostname := reference.Domain(targetRepo.Name)
   168  	if manifestRepoHostname != targetRepoHostname {
   169  		return manifestlist.ManifestDescriptor{}, errors.Errorf("cannot use source images from a different registry than the target image: %s != %s", manifestRepoHostname, targetRepoHostname)
   170  	}
   171  
   172  	manifest := manifestlist.ManifestDescriptor{
   173  		Descriptor: distribution.Descriptor{
   174  			Digest:    imageManifest.Descriptor.Digest,
   175  			Size:      imageManifest.Descriptor.Size,
   176  			MediaType: imageManifest.Descriptor.MediaType,
   177  		},
   178  	}
   179  
   180  	platform := types.PlatformSpecFromOCI(imageManifest.Descriptor.Platform)
   181  	if platform != nil {
   182  		manifest.Platform = *platform
   183  	}
   184  
   185  	if err = manifest.Descriptor.Digest.Validate(); err != nil {
   186  		return manifestlist.ManifestDescriptor{}, errors.Wrapf(err,
   187  			"digest parse of image %q failed", imageManifest.Ref)
   188  	}
   189  
   190  	return manifest, nil
   191  }
   192  
   193  func buildBlobRequestList(imageManifest types.ImageManifest, repoName reference.Named) ([]manifestBlob, error) {
   194  	blobs := imageManifest.Blobs()
   195  	blobReqs := make([]manifestBlob, 0, len(blobs))
   196  	for _, blobDigest := range blobs {
   197  		canonical, err := reference.WithDigest(repoName, blobDigest)
   198  		if err != nil {
   199  			return nil, err
   200  		}
   201  		var os string
   202  		if imageManifest.Descriptor.Platform != nil {
   203  			os = imageManifest.Descriptor.Platform.OS
   204  		}
   205  		blobReqs = append(blobReqs, manifestBlob{canonical: canonical, os: os})
   206  	}
   207  	return blobReqs, nil
   208  }
   209  
   210  func buildPutManifestRequest(imageManifest types.ImageManifest, targetRef reference.Named) (mountRequest, error) {
   211  	refWithoutTag, err := reference.WithName(targetRef.Name())
   212  	if err != nil {
   213  		return mountRequest{}, err
   214  	}
   215  	mountRef, err := reference.WithDigest(refWithoutTag, imageManifest.Descriptor.Digest)
   216  	if err != nil {
   217  		return mountRequest{}, err
   218  	}
   219  
   220  	// Attempt to reconstruct indentation of the manifest to ensure sha parity
   221  	// with the registry - if we haven't preserved the raw content.
   222  	//
   223  	// This is necessary because our previous internal storage format did not
   224  	// preserve whitespace. If we don't have the newer format present, we can
   225  	// attempt the reconstruction like before, but explicitly error if the
   226  	// reconstruction failed!
   227  	switch {
   228  	case imageManifest.SchemaV2Manifest != nil:
   229  		dt := imageManifest.Raw
   230  		if len(dt) == 0 {
   231  			dt, err = json.MarshalIndent(imageManifest.SchemaV2Manifest, "", "   ")
   232  			if err != nil {
   233  				return mountRequest{}, err
   234  			}
   235  		}
   236  
   237  		dig := imageManifest.Descriptor.Digest
   238  		if dig2 := dig.Algorithm().FromBytes(dt); dig != dig2 {
   239  			return mountRequest{}, errors.Errorf("internal digest mismatch for %s: expected %s, got %s", imageManifest.Ref, dig, dig2)
   240  		}
   241  
   242  		var manifest schema2.DeserializedManifest
   243  		if err = manifest.UnmarshalJSON(dt); err != nil {
   244  			return mountRequest{}, err
   245  		}
   246  		imageManifest.SchemaV2Manifest = &manifest
   247  	case imageManifest.OCIManifest != nil:
   248  		dt := imageManifest.Raw
   249  		if len(dt) == 0 {
   250  			dt, err = json.MarshalIndent(imageManifest.OCIManifest, "", "  ")
   251  			if err != nil {
   252  				return mountRequest{}, err
   253  			}
   254  		}
   255  
   256  		dig := imageManifest.Descriptor.Digest
   257  		if dig2 := dig.Algorithm().FromBytes(dt); dig != dig2 {
   258  			return mountRequest{}, errors.Errorf("internal digest mismatch for %s: expected %s, got %s", imageManifest.Ref, dig, dig2)
   259  		}
   260  
   261  		var manifest ocischema.DeserializedManifest
   262  		if err = manifest.UnmarshalJSON(dt); err != nil {
   263  			return mountRequest{}, err
   264  		}
   265  		imageManifest.OCIManifest = &manifest
   266  	}
   267  
   268  	return mountRequest{ref: mountRef, manifest: imageManifest}, err
   269  }
   270  
   271  func pushList(ctx context.Context, dockerCli command.Cli, req pushRequest) error {
   272  	rclient := dockerCli.RegistryClient(req.insecure)
   273  
   274  	if err := mountBlobs(ctx, rclient, req.targetRef, req.manifestBlobs); err != nil {
   275  		return err
   276  	}
   277  	if err := pushReferences(ctx, dockerCli.Out(), rclient, req.mountRequests); err != nil {
   278  		return err
   279  	}
   280  	dgst, err := rclient.PutManifest(ctx, req.targetRef, req.list)
   281  	if err != nil {
   282  		return err
   283  	}
   284  
   285  	fmt.Fprintln(dockerCli.Out(), dgst.String())
   286  	return nil
   287  }
   288  
   289  func pushReferences(ctx context.Context, out io.Writer, client registryclient.RegistryClient, mounts []mountRequest) error {
   290  	for _, mount := range mounts {
   291  		newDigest, err := client.PutManifest(ctx, mount.ref, mount.manifest)
   292  		if err != nil {
   293  			return err
   294  		}
   295  		fmt.Fprintf(out, "Pushed ref %s with digest: %s\n", mount.ref, newDigest)
   296  	}
   297  	return nil
   298  }
   299  
   300  func mountBlobs(ctx context.Context, client registryclient.RegistryClient, ref reference.Named, blobs []manifestBlob) error {
   301  	for _, blob := range blobs {
   302  		err := client.MountBlob(ctx, blob.canonical, ref)
   303  		switch err.(type) {
   304  		case nil:
   305  		case registryclient.ErrBlobCreated:
   306  			if blob.os != "windows" {
   307  				return fmt.Errorf("error mounting %s to %s", blob.canonical, ref)
   308  			}
   309  		default:
   310  			return err
   311  		}
   312  	}
   313  	return nil
   314  }