DockerCLI/cli/command/manifest/push.go

315 lines
9.1 KiB
Go
Raw Permalink Normal View History

package manifest
import (
"context"
"encoding/json"
"fmt"
"io"
"github.com/distribution/reference"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/manifest/types"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/ocischema"
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/docker/registry"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type pushOpts struct {
insecure bool
purge bool
target string
}
type mountRequest struct {
ref reference.Named
manifest types.ImageManifest
}
type manifestBlob struct {
canonical reference.Canonical
os string
}
type pushRequest struct {
targetRef reference.Named
list *manifestlist.DeserializedManifestList
mountRequests []mountRequest
manifestBlobs []manifestBlob
insecure bool
}
func newPushListCommand(dockerCli command.Cli) *cobra.Command {
opts := pushOpts{}
cmd := &cobra.Command{
Use: "push [OPTIONS] MANIFEST_LIST",
Short: "Push a manifest list to a repository",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.target = args[0]
return runPush(cmd.Context(), dockerCli, opts)
},
}
flags := cmd.Flags()
flags.BoolVarP(&opts.purge, "purge", "p", false, "Remove the local manifest list after push")
flags.BoolVar(&opts.insecure, "insecure", false, "Allow push to an insecure registry")
return cmd
}
func runPush(ctx context.Context, dockerCli command.Cli, opts pushOpts) error {
targetRef, err := normalizeReference(opts.target)
if err != nil {
return err
}
manifests, err := dockerCli.ManifestStore().GetList(targetRef)
if err != nil {
return err
}
if len(manifests) == 0 {
return errors.Errorf("%s not found", targetRef)
}
req, err := buildPushRequest(manifests, targetRef, opts.insecure)
if err != nil {
return err
}
if err := pushList(ctx, dockerCli, req); err != nil {
return err
}
if opts.purge {
return dockerCli.ManifestStore().Remove(targetRef)
}
return nil
}
func buildPushRequest(manifests []types.ImageManifest, targetRef reference.Named, insecure bool) (pushRequest, error) {
req := pushRequest{targetRef: targetRef, insecure: insecure}
var err error
req.list, err = buildManifestList(manifests, targetRef)
if err != nil {
return req, err
}
targetRepo, err := registry.ParseRepositoryInfo(targetRef)
if err != nil {
return req, err
}
targetRepoName, err := registryclient.RepoNameForReference(targetRepo.Name)
if err != nil {
return req, err
}
for _, imageManifest := range manifests {
manifestRepoName, err := registryclient.RepoNameForReference(imageManifest.Ref)
if err != nil {
return req, err
}
repoName, _ := reference.WithName(manifestRepoName)
if repoName.Name() != targetRepoName {
blobs, err := buildBlobRequestList(imageManifest, repoName)
if err != nil {
return req, err
}
req.manifestBlobs = append(req.manifestBlobs, blobs...)
manifestPush, err := buildPutManifestRequest(imageManifest, targetRef)
if err != nil {
return req, err
}
req.mountRequests = append(req.mountRequests, manifestPush)
}
}
return req, nil
}
func buildManifestList(manifests []types.ImageManifest, targetRef reference.Named) (*manifestlist.DeserializedManifestList, error) {
targetRepoInfo, err := registry.ParseRepositoryInfo(targetRef)
if err != nil {
return nil, err
}
descriptors := []manifestlist.ManifestDescriptor{}
for _, imageManifest := range manifests {
if imageManifest.Descriptor.Platform == nil ||
imageManifest.Descriptor.Platform.Architecture == "" ||
imageManifest.Descriptor.Platform.OS == "" {
return nil, errors.Errorf(
"manifest %s must have an OS and Architecture to be pushed to a registry", imageManifest.Ref)
}
descriptor, err := buildManifestDescriptor(targetRepoInfo, imageManifest)
if err != nil {
return nil, err
}
descriptors = append(descriptors, descriptor)
}
return manifestlist.FromDescriptors(descriptors)
}
func buildManifestDescriptor(targetRepo *registry.RepositoryInfo, imageManifest types.ImageManifest) (manifestlist.ManifestDescriptor, error) {
repoInfo, err := registry.ParseRepositoryInfo(imageManifest.Ref)
if err != nil {
return manifestlist.ManifestDescriptor{}, err
}
manifestRepoHostname := reference.Domain(repoInfo.Name)
targetRepoHostname := reference.Domain(targetRepo.Name)
if manifestRepoHostname != targetRepoHostname {
return manifestlist.ManifestDescriptor{}, errors.Errorf("cannot use source images from a different registry than the target image: %s != %s", manifestRepoHostname, targetRepoHostname)
}
manifest := manifestlist.ManifestDescriptor{
Descriptor: distribution.Descriptor{
Digest: imageManifest.Descriptor.Digest,
Size: imageManifest.Descriptor.Size,
MediaType: imageManifest.Descriptor.MediaType,
},
}
platform := types.PlatformSpecFromOCI(imageManifest.Descriptor.Platform)
if platform != nil {
manifest.Platform = *platform
}
if err = manifest.Descriptor.Digest.Validate(); err != nil {
return manifestlist.ManifestDescriptor{}, errors.Wrapf(err,
"digest parse of image %q failed", imageManifest.Ref)
}
return manifest, nil
}
func buildBlobRequestList(imageManifest types.ImageManifest, repoName reference.Named) ([]manifestBlob, error) {
linting: Consider pre-allocating sliceVar (prealloc) While updating, also addressed some redundant fmt.Sprintf() opts/throttledevice.go:86:2: Consider pre-allocating `out` (prealloc) var out []string ^ opts/ulimit.go:37:2: Consider pre-allocating `out` (prealloc) var out []string ^ opts/ulimit.go:47:2: Consider pre-allocating `ulimits` (prealloc) var ulimits []*units.Ulimit ^ opts/weightdevice.go:68:2: Consider pre-allocating `out` (prealloc) var out []string ^ cli/context/store/metadatastore.go:96:2: Consider pre-allocating `res` (prealloc) var res []Metadata ^ cli/context/store/store.go:127:2: Consider pre-allocating `names` (prealloc) var names []string ^ cli/compose/loader/loader.go:223:2: Consider pre-allocating `keys` (prealloc) var keys []string ^ cli/compose/loader/loader.go:397:2: Consider pre-allocating `services` (prealloc) var services []types.ServiceConfig ^ cli/command/stack/loader/loader.go:63:2: Consider pre-allocating `msgs` (prealloc) var msgs []string ^ cli/command/stack/loader/loader.go:118:2: Consider pre-allocating `configFiles` (prealloc) var configFiles []composetypes.ConfigFile ^ cli/command/formatter/container.go:245:2: Consider pre-allocating `joinLabels` (prealloc) var joinLabels []string ^ cli/command/formatter/container.go:265:2: Consider pre-allocating `mounts` (prealloc) var mounts []string ^ cli/command/formatter/container.go:316:2: Consider pre-allocating `result` (prealloc) var result []string ^ cli/command/formatter/displayutils.go:43:2: Consider pre-allocating `display` (prealloc) var ( ^ cli/command/formatter/volume.go:103:2: Consider pre-allocating `joinLabels` (prealloc) var joinLabels []string ^ cli-plugins/manager/manager_test.go:49:2: Consider pre-allocating `dirs` (prealloc) var dirs []string ^ cli/command/swarm/init.go:69:2: Consider pre-allocating `defaultAddrPool` (prealloc) var defaultAddrPool []string ^ cli/command/manifest/push.go:195:2: Consider pre-allocating `blobReqs` (prealloc) var blobReqs []manifestBlob ^ cli/command/secret/formatter.go:111:2: Consider pre-allocating `joinLabels` (prealloc) var joinLabels []string ^ cli/command/network/formatter.go:104:2: Consider pre-allocating `joinLabels` (prealloc) var joinLabels []string ^ cli/command/context/list.go:52:2: Consider pre-allocating `contexts` (prealloc) var contexts []*formatter.ClientContext ^ cli/command/config/formatter.go:104:2: Consider pre-allocating `joinLabels` (prealloc) var joinLabels []string ^ cli/command/trust/common_test.go:23:2: Consider pre-allocating `targetNames` (prealloc) var targetNames []string ^ cli/command/service/generic_resource_opts.go:55:2: Consider pre-allocating `generic` (prealloc) var generic []swarm.GenericResource ^ cli/command/service/generic_resource_opts.go:98:2: Consider pre-allocating `l` (prealloc) var l []swarm.GenericResource ^ cli/command/service/opts.go:378:2: Consider pre-allocating `netAttach` (prealloc) var netAttach []swarm.NetworkAttachmentConfig ^ cli/command/service/update.go:731:2: Consider pre-allocating `limits` (prealloc) var limits []*units.Ulimit ^ cli/command/service/update.go:1315:2: Consider pre-allocating `newNetworks` (prealloc) var newNetworks []swarm.NetworkAttachmentConfig ^ cli/command/service/update.go:1514:2: Consider pre-allocating `out` (prealloc) var out []string ^ cli/compose/convert/service.go:713:2: Consider pre-allocating `ulimits` (prealloc) var ulimits []*units.Ulimit ^ cli/compose/convert/volume.go:13:2: Consider pre-allocating `mounts` (prealloc) var mounts []mount.Mount ^ cli/command/stack/swarm/list.go:39:2: Consider pre-allocating `stacks` (prealloc) var stacks []*formatter.Stack ^ Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2022-09-03 14:07:29 -04:00
blobs := imageManifest.Blobs()
blobReqs := make([]manifestBlob, 0, len(blobs))
for _, blobDigest := range blobs {
canonical, err := reference.WithDigest(repoName, blobDigest)
if err != nil {
return nil, err
}
var os string
if imageManifest.Descriptor.Platform != nil {
os = imageManifest.Descriptor.Platform.OS
}
blobReqs = append(blobReqs, manifestBlob{canonical: canonical, os: os})
}
return blobReqs, nil
}
func buildPutManifestRequest(imageManifest types.ImageManifest, targetRef reference.Named) (mountRequest, error) {
refWithoutTag, err := reference.WithName(targetRef.Name())
if err != nil {
return mountRequest{}, err
}
mountRef, err := reference.WithDigest(refWithoutTag, imageManifest.Descriptor.Digest)
if err != nil {
return mountRequest{}, err
}
// Attempt to reconstruct indentation of the manifest to ensure sha parity
// with the registry - if we haven't preserved the raw content.
//
// This is necessary because our previous internal storage format did not
// preserve whitespace. If we don't have the newer format present, we can
// attempt the reconstruction like before, but explicitly error if the
// reconstruction failed!
switch {
case imageManifest.SchemaV2Manifest != nil:
dt := imageManifest.Raw
if len(dt) == 0 {
dt, err = json.MarshalIndent(imageManifest.SchemaV2Manifest, "", " ")
if err != nil {
return mountRequest{}, err
}
}
dig := imageManifest.Descriptor.Digest
if dig2 := dig.Algorithm().FromBytes(dt); dig != dig2 {
return mountRequest{}, errors.Errorf("internal digest mismatch for %s: expected %s, got %s", imageManifest.Ref, dig, dig2)
}
var manifest schema2.DeserializedManifest
if err = manifest.UnmarshalJSON(dt); err != nil {
return mountRequest{}, err
}
imageManifest.SchemaV2Manifest = &manifest
case imageManifest.OCIManifest != nil:
dt := imageManifest.Raw
if len(dt) == 0 {
dt, err = json.MarshalIndent(imageManifest.OCIManifest, "", " ")
if err != nil {
return mountRequest{}, err
}
}
dig := imageManifest.Descriptor.Digest
if dig2 := dig.Algorithm().FromBytes(dt); dig != dig2 {
return mountRequest{}, errors.Errorf("internal digest mismatch for %s: expected %s, got %s", imageManifest.Ref, dig, dig2)
}
var manifest ocischema.DeserializedManifest
if err = manifest.UnmarshalJSON(dt); err != nil {
return mountRequest{}, err
}
imageManifest.OCIManifest = &manifest
}
return mountRequest{ref: mountRef, manifest: imageManifest}, err
}
func pushList(ctx context.Context, dockerCli command.Cli, req pushRequest) error {
rclient := dockerCli.RegistryClient(req.insecure)
if err := mountBlobs(ctx, rclient, req.targetRef, req.manifestBlobs); err != nil {
return err
}
if err := pushReferences(ctx, dockerCli.Out(), rclient, req.mountRequests); err != nil {
return err
}
dgst, err := rclient.PutManifest(ctx, req.targetRef, req.list)
if err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), dgst.String())
return nil
}
func pushReferences(ctx context.Context, out io.Writer, client registryclient.RegistryClient, mounts []mountRequest) error {
for _, mount := range mounts {
newDigest, err := client.PutManifest(ctx, mount.ref, mount.manifest)
if err != nil {
return err
}
fmt.Fprintf(out, "Pushed ref %s with digest: %s\n", mount.ref, newDigest)
}
return nil
}
func mountBlobs(ctx context.Context, client registryclient.RegistryClient, ref reference.Named, blobs []manifestBlob) error {
for _, blob := range blobs {
err := client.MountBlob(ctx, blob.canonical, ref)
switch err.(type) {
case nil:
case registryclient.ErrBlobCreated:
if blob.os != "windows" {
return fmt.Errorf("error mounting %s to %s", blob.canonical, ref)
}
default:
return err
}
}
return nil
}