diff --git a/cli/command/image/push.go b/cli/command/image/push.go
index 18b4e9ecbb..ff5b688c41 100644
--- a/cli/command/image/push.go
+++ b/cli/command/image/push.go
@@ -2,18 +2,25 @@ package image
import (
"context"
+ "encoding/json"
"fmt"
"io"
+ "os"
+ "github.com/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/streams"
+ "github.com/docker/docker/api/types/auxprogress"
"github.com/docker/docker/api/types/image"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/registry"
+ "github.com/moby/term"
+ "github.com/morikuni/aec"
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@@ -23,6 +30,7 @@ type pushOptions struct {
remote string
untrusted bool
quiet bool
+ platform string
}
// NewPushCommand creates a new `docker push` command
@@ -48,12 +56,33 @@ func NewPushCommand(dockerCli command.Cli) *cobra.Command {
flags.BoolVarP(&opts.all, "all-tags", "a", false, "Push all tags of an image to the repository")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress verbose output")
command.AddTrustSigningFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled())
+ flags.StringVar(&opts.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"),
+ `Push a platform-specific manifest as a single-platform image to the registry.
+'os[/arch[/variant]]': Explicit platform (eg. linux/amd64)`)
+ flags.SetAnnotation("platform", "version", []string{"1.46"})
return cmd
}
// RunPush performs a push against the engine based on the specified options
+//
+//nolint:gocyclo
func RunPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error {
+ var platform *ocispec.Platform
+ if opts.platform != "" {
+ p, err := platforms.Parse(opts.platform)
+ if err != nil {
+ _, _ = fmt.Fprintf(dockerCli.Err(), "Invalid platform %s", opts.platform)
+ return err
+ }
+ platform = &p
+
+ printNote(dockerCli, `Selecting a single platform will only push one matching image manifest from a multi-platform image index.
+This means that any other components attached to the multi-platform image index (like Buildkit attestations) won't be pushed.
+If you want to only push a single platform image while preserving the attestations, please use 'docker convert\n'
+`)
+ }
+
ref, err := reference.ParseNormalizedNamed(opts.remote)
switch {
case err != nil:
@@ -84,6 +113,7 @@ func RunPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error
All: opts.all,
RegistryAuth: encodedAuth,
PrivilegeFunc: requestPrivilege,
+ Platform: platform,
}
responseBody, err := dockerCli.Client().ImagePush(ctx, reference.FamiliarString(ref), options)
@@ -91,6 +121,13 @@ func RunPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error
return err
}
+ defer func() {
+ for _, note := range notes {
+ fmt.Fprintln(dockerCli.Err(), "")
+ printNote(dockerCli, note)
+ }
+ }()
+
defer responseBody.Close()
if !opts.untrusted {
// TODO PushTrustedReference currently doesn't respect `--quiet`
@@ -98,11 +135,51 @@ func RunPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error
}
if opts.quiet {
- err = jsonmessage.DisplayJSONMessagesToStream(responseBody, streams.NewOut(io.Discard), nil)
+ err = jsonmessage.DisplayJSONMessagesToStream(responseBody, streams.NewOut(io.Discard), handleAux(dockerCli))
if err == nil {
fmt.Fprintln(dockerCli.Out(), ref.String())
}
return err
}
- return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil)
+ return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), handleAux(dockerCli))
+}
+
+var notes []string
+
+func handleAux(dockerCli command.Cli) func(jm jsonmessage.JSONMessage) {
+ return func(jm jsonmessage.JSONMessage) {
+ b := []byte(*jm.Aux)
+
+ var stripped auxprogress.ManifestPushedInsteadOfIndex
+ err := json.Unmarshal(b, &stripped)
+ if err == nil && stripped.ManifestPushedInsteadOfIndex {
+ note := fmt.Sprintf("Not all multiplatform-content is present and only the available single-platform image was pushed\n%s -> %s",
+ aec.RedF.Apply(stripped.OriginalIndex.Digest.String()),
+ aec.GreenF.Apply(stripped.SelectedManifest.Digest.String()),
+ )
+ notes = append(notes, note)
+ }
+
+ var missing auxprogress.ContentMissing
+ err = json.Unmarshal(b, &missing)
+ if err == nil && missing.ContentMissing {
+ note := `You're trying to push a manifest list/index which
+ references multiple platform specific manifests, but not all of them are available locally
+ or available to the remote repository.
+
+ Make sure you have all the referenced content and try again.
+
+ You can also push only a single platform specific manifest directly by specifying the platform you want to push with the --platform flag.`
+ notes = append(notes, note)
+ }
+ }
+}
+
+func printNote(dockerCli command.Cli, format string, args ...any) {
+ if _, isTTY := term.GetFdInfo(dockerCli.Err()); isTTY {
+ _, _ = fmt.Fprint(dockerCli.Err(), aec.WhiteF.Apply(aec.CyanB.Apply("[ NOTE ]"))+" ")
+ } else {
+ _, _ = fmt.Fprint(dockerCli.Err(), "[ NOTE ] ")
+ }
+ _, _ = fmt.Fprintf(dockerCli.Err(), aec.Bold.Apply(format)+"\n", args...)
}
diff --git a/docs/reference/commandline/image_push.md b/docs/reference/commandline/image_push.md
index 283694e73e..592d2c3bc7 100644
--- a/docs/reference/commandline/image_push.md
+++ b/docs/reference/commandline/image_push.md
@@ -9,11 +9,12 @@ Upload an image to a registry
### Options
-| Name | Type | Default | Description |
-|:---------------------------------------------|:-------|:--------|:--------------------------------------------|
-| [`-a`](#all-tags), [`--all-tags`](#all-tags) | | | Push all tags of an image to the repository |
-| `--disable-content-trust` | `bool` | `true` | Skip image signing |
-| `-q`, `--quiet` | | | Suppress verbose output |
+| Name | Type | Default | Description |
+|:---------------------------------------------|:---------|:--------|:--------------------------------------------------------------------------------------------------------------------------------------------|
+| [`-a`](#all-tags), [`--all-tags`](#all-tags) | | | Push all tags of an image to the repository |
+| `--disable-content-trust` | `bool` | `true` | Skip image signing |
+| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.
'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) |
+| `-q`, `--quiet` | | | Suppress verbose output |
diff --git a/docs/reference/commandline/push.md b/docs/reference/commandline/push.md
index dd93983d60..b49467e996 100644
--- a/docs/reference/commandline/push.md
+++ b/docs/reference/commandline/push.md
@@ -9,11 +9,12 @@ Upload an image to a registry
### Options
-| Name | Type | Default | Description |
-|:--------------------------|:-------|:--------|:--------------------------------------------|
-| `-a`, `--all-tags` | | | Push all tags of an image to the repository |
-| `--disable-content-trust` | `bool` | `true` | Skip image signing |
-| `-q`, `--quiet` | | | Suppress verbose output |
+| Name | Type | Default | Description |
+|:--------------------------|:---------|:--------|:--------------------------------------------------------------------------------------------------------------------------------------------|
+| `-a`, `--all-tags` | | | Push all tags of an image to the repository |
+| `--disable-content-trust` | `bool` | `true` | Skip image signing |
+| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.
'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) |
+| `-q`, `--quiet` | | | Suppress verbose output |
diff --git a/vendor.mod b/vendor.mod
index e4ece11962..2b01838d65 100644
--- a/vendor.mod
+++ b/vendor.mod
@@ -12,7 +12,7 @@ require (
github.com/creack/pty v1.1.21
github.com/distribution/reference v0.6.0
github.com/docker/distribution v2.8.3+incompatible
- github.com/docker/docker v26.1.1-0.20240610145149-a736d0701c41+incompatible // master (v27.0.0-dev)
+ github.com/docker/docker v26.1.1-0.20240610201418-9d9488468fe2+incompatible // master (v27.0.0-dev)
github.com/docker/docker-credential-helpers v0.8.2
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0
diff --git a/vendor.sum b/vendor.sum
index c058ae285c..b24afc8174 100644
--- a/vendor.sum
+++ b/vendor.sum
@@ -59,8 +59,8 @@ github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v26.1.1-0.20240610145149-a736d0701c41+incompatible h1:Kraon288jb3POkrmM5w6Xo979z2rrCtFzHycAjafRes=
-github.com/docker/docker v26.1.1-0.20240610145149-a736d0701c41+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v26.1.1-0.20240610201418-9d9488468fe2+incompatible h1:k63BdhjySkwvmdeofOsBElcuVrWaDBrI7FQgnyoVnnM=
+github.com/docker/docker v26.1.1-0.20240610201418-9d9488468fe2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
diff --git a/vendor/github.com/docker/docker/api/swagger.yaml b/vendor/github.com/docker/docker/api/swagger.yaml
index d845b1d737..f1e57756e3 100644
--- a/vendor/github.com/docker/docker/api/swagger.yaml
+++ b/vendor/github.com/docker/docker/api/swagger.yaml
@@ -1368,7 +1368,8 @@ definitions: