This replaces the visitAll recursive function with a test that verifies that the option is set for all commands and subcommands, so that it doesn't have to be modified at runtime. We currently still have to loop over all functions for the setValidateArgs call, but that can be looked at separately. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
292 lines
8.7 KiB
Go
292 lines
8.7 KiB
Go
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"
|
|
"github.com/docker/cli/internal/registryclient"
|
|
"github.com/docker/distribution"
|
|
"github.com/docker/distribution/manifest/manifestlist"
|
|
"github.com/docker/distribution/manifest/ocischema"
|
|
"github.com/docker/distribution/manifest/schema2"
|
|
"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)
|
|
},
|
|
DisableFlagsInUseLine: true,
|
|
}
|
|
|
|
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 := newManifestStore(dockerCli).GetList(targetRef)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(manifests) == 0 {
|
|
return fmt.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 newManifestStore(dockerCli).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
|
|
}
|
|
|
|
targetRepoName := reference.Path(reference.TrimNamed(targetRef))
|
|
|
|
for _, imageManifest := range manifests {
|
|
manifestRepoName := reference.Path(reference.TrimNamed(imageManifest.Ref))
|
|
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) {
|
|
targetRepo := reference.TrimNamed(targetRef)
|
|
descriptors := make([]manifestlist.ManifestDescriptor, 0, len(manifests))
|
|
for _, imageManifest := range manifests {
|
|
if imageManifest.Descriptor.Platform == nil ||
|
|
imageManifest.Descriptor.Platform.Architecture == "" ||
|
|
imageManifest.Descriptor.Platform.OS == "" {
|
|
return nil, fmt.Errorf("manifest %s must have an OS and Architecture to be pushed to a registry", imageManifest.Ref)
|
|
}
|
|
descriptor, err := buildManifestDescriptor(targetRepo, imageManifest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
descriptors = append(descriptors, descriptor)
|
|
}
|
|
|
|
return manifestlist.FromDescriptors(descriptors)
|
|
}
|
|
|
|
func buildManifestDescriptor(targetRepo reference.Named, imageManifest types.ImageManifest) (manifestlist.ManifestDescriptor, error) {
|
|
manifestRepoHostname := reference.Domain(reference.TrimNamed(imageManifest.Ref))
|
|
targetRepoHostname := reference.Domain(reference.TrimNamed(targetRepo))
|
|
if manifestRepoHostname != targetRepoHostname {
|
|
return manifestlist.ManifestDescriptor{}, fmt.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{}, fmt.Errorf("digest parse of image %q failed: %w", imageManifest.Ref, err)
|
|
}
|
|
|
|
return manifest, nil
|
|
}
|
|
|
|
func buildBlobRequestList(imageManifest types.ImageManifest, repoName reference.Named) ([]manifestBlob, error) {
|
|
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{}, fmt.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{}, fmt.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 {
|
|
registryClient := newRegistryClient(dockerCLI, req.insecure)
|
|
|
|
if err := mountBlobs(ctx, registryClient, req.targetRef, req.manifestBlobs); err != nil {
|
|
return err
|
|
}
|
|
if err := pushReferences(ctx, dockerCLI.Out(), registryClient, req.mountRequests); err != nil {
|
|
return err
|
|
}
|
|
dgst, err := registryClient.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
|
|
}
|