-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
cli/push: Add platform
switch
#4984
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' | ||
`) | ||
Comment on lines
+80
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's fine to be explicit here, I'd rather some users get annoyed by the extra message than having people not realize we're stripping attestations when they push a single-platform image. |
||
} | ||
|
||
ref, err := reference.ParseNormalizedNamed(opts.remote) | ||
switch { | ||
case err != nil: | ||
|
@@ -84,25 +113,73 @@ 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) | ||
if err != nil { | ||
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` | ||
return PushTrustedReference(dockerCli, repoInfo, ref, authConfig, responseBody) | ||
} | ||
|
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what happens if none of data could be unmarshalled? should it not error? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There might be other aux progress messages that are not (yet) supported by the CLI. I think it's best to just skip them instead. |
||
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...) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not for this PR, and not urgent, but if we decide to add more locations to pass
--platform
, and we want to consume those as aplatforms, we could consider implementing a platform _option_ that can be used for flags, performs the validation as part of that, and directly setting a
Platform`.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think we should, but left that to a second PR that will reuse this logic.
Btw, we already have it, but for the "string" platform:
cli/cli/command/utils.go
Line 158 in 0022fe7
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah! We should look where/how that's used. If the result is only "internal", it could still make sense to change it, and have the consumer convert it to a string (where needed 🤔).
In either case; all for separate work, just that I thought of it.