@@ -2,18 +2,25 @@ package image
22
33import (
44 "context"
5+ "encoding/json"
56 "fmt"
67 "io"
8+ "os"
79
10+ "github.com/containerd/platforms"
811 "github.com/distribution/reference"
912 "github.com/docker/cli/cli"
1013 "github.com/docker/cli/cli/command"
1114 "github.com/docker/cli/cli/command/completion"
1215 "github.com/docker/cli/cli/streams"
16+ "github.com/docker/docker/api/types/auxprogress"
1317 "github.com/docker/docker/api/types/image"
1418 registrytypes "github.com/docker/docker/api/types/registry"
1519 "github.com/docker/docker/pkg/jsonmessage"
1620 "github.com/docker/docker/registry"
21+ "github.com/moby/term"
22+ "github.com/morikuni/aec"
23+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
1724 "github.com/pkg/errors"
1825 "github.com/spf13/cobra"
1926)
@@ -23,6 +30,7 @@ type pushOptions struct {
2330 remote string
2431 untrusted bool
2532 quiet bool
33+ platform string
2634}
2735
2836// NewPushCommand creates a new `docker push` command
@@ -48,12 +56,33 @@ func NewPushCommand(dockerCli command.Cli) *cobra.Command {
4856 flags .BoolVarP (& opts .all , "all-tags" , "a" , false , "Push all tags of an image to the repository" )
4957 flags .BoolVarP (& opts .quiet , "quiet" , "q" , false , "Suppress verbose output" )
5058 command .AddTrustSigningFlags (flags , & opts .untrusted , dockerCli .ContentTrustEnabled ())
59+ flags .StringVar (& opts .platform , "platform" , os .Getenv ("DOCKER_DEFAULT_PLATFORM" ),
60+ `Push a platform-specific manifest as a single-platform image to the registry.
61+ 'os[/arch[/variant]]': Explicit platform (eg. linux/amd64)` )
62+ flags .SetAnnotation ("platform" , "version" , []string {"1.46" })
5163
5264 return cmd
5365}
5466
5567// RunPush performs a push against the engine based on the specified options
68+ //
69+ //nolint:gocyclo
5670func RunPush (ctx context.Context , dockerCli command.Cli , opts pushOptions ) error {
71+ var platform * ocispec.Platform
72+ if opts .platform != "" {
73+ p , err := platforms .Parse (opts .platform )
74+ if err != nil {
75+ _ , _ = fmt .Fprintf (dockerCli .Err (), "Invalid platform %s" , opts .platform )
76+ return err
77+ }
78+ platform = & p
79+
80+ printNote (dockerCli , `Selecting a single platform will only push one matching image manifest from a multi-platform image index.
81+ This means that any other components attached to the multi-platform image index (like Buildkit attestations) won't be pushed.
82+ If you want to only push a single platform image while preserving the attestations, please use 'docker convert\n'
83+ ` )
84+ }
85+
5786 ref , err := reference .ParseNormalizedNamed (opts .remote )
5887 switch {
5988 case err != nil :
@@ -84,25 +113,73 @@ func RunPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error
84113 All : opts .all ,
85114 RegistryAuth : encodedAuth ,
86115 PrivilegeFunc : requestPrivilege ,
116+ Platform : platform ,
87117 }
88118
89119 responseBody , err := dockerCli .Client ().ImagePush (ctx , reference .FamiliarString (ref ), options )
90120 if err != nil {
91121 return err
92122 }
93123
124+ defer func () {
125+ for _ , note := range notes {
126+ fmt .Fprintln (dockerCli .Err (), "" )
127+ printNote (dockerCli , note )
128+ }
129+ }()
130+
94131 defer responseBody .Close ()
95132 if ! opts .untrusted {
96133 // TODO PushTrustedReference currently doesn't respect `--quiet`
97134 return PushTrustedReference (dockerCli , repoInfo , ref , authConfig , responseBody )
98135 }
99136
100137 if opts .quiet {
101- err = jsonmessage .DisplayJSONMessagesToStream (responseBody , streams .NewOut (io .Discard ), nil )
138+ err = jsonmessage .DisplayJSONMessagesToStream (responseBody , streams .NewOut (io .Discard ), handleAux ( dockerCli ) )
102139 if err == nil {
103140 fmt .Fprintln (dockerCli .Out (), ref .String ())
104141 }
105142 return err
106143 }
107- return jsonmessage .DisplayJSONMessagesToStream (responseBody , dockerCli .Out (), nil )
144+ return jsonmessage .DisplayJSONMessagesToStream (responseBody , dockerCli .Out (), handleAux (dockerCli ))
145+ }
146+
147+ var notes []string
148+
149+ func handleAux (dockerCli command.Cli ) func (jm jsonmessage.JSONMessage ) {
150+ return func (jm jsonmessage.JSONMessage ) {
151+ b := []byte (* jm .Aux )
152+
153+ var stripped auxprogress.ManifestPushedInsteadOfIndex
154+ err := json .Unmarshal (b , & stripped )
155+ if err == nil && stripped .ManifestPushedInsteadOfIndex {
156+ note := fmt .Sprintf ("Not all multiplatform-content is present and only the available single-platform image was pushed\n %s -> %s" ,
157+ aec .RedF .Apply (stripped .OriginalIndex .Digest .String ()),
158+ aec .GreenF .Apply (stripped .SelectedManifest .Digest .String ()),
159+ )
160+ notes = append (notes , note )
161+ }
162+
163+ var missing auxprogress.ContentMissing
164+ err = json .Unmarshal (b , & missing )
165+ if err == nil && missing .ContentMissing {
166+ note := `You're trying to push a manifest list/index which
167+ references multiple platform specific manifests, but not all of them are available locally
168+ or available to the remote repository.
169+
170+ Make sure you have all the referenced content and try again.
171+
172+ You can also push only a single platform specific manifest directly by specifying the platform you want to push with the --platform flag.`
173+ notes = append (notes , note )
174+ }
175+ }
176+ }
177+
178+ func printNote (dockerCli command.Cli , format string , args ... any ) {
179+ if _ , isTTY := term .GetFdInfo (dockerCli .Err ()); isTTY {
180+ _ , _ = fmt .Fprint (dockerCli .Err (), aec .WhiteF .Apply (aec .CyanB .Apply ("[ NOTE ]" ))+ " " )
181+ } else {
182+ _ , _ = fmt .Fprint (dockerCli .Err (), "[ NOTE ] " )
183+ }
184+ _ , _ = fmt .Fprintf (dockerCli .Err (), aec .Bold .Apply (format )+ "\n " , args ... )
108185}
0 commit comments