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:


- > **Note**: this field is always empty and must not be used. + > **Deprecated**: this field is not part of the image specification and is + > always empty. It must not be used, and will be removed in API v1.47. type: "string" example: "" Domainname: @@ -1377,7 +1378,8 @@ definitions:


- > **Note**: this field is always empty and must not be used. + > **Deprecated**: this field is not part of the image specification and is + > always empty. It must not be used, and will be removed in API v1.47. type: "string" example: "" User: @@ -1390,7 +1392,8 @@ definitions:


- > **Note**: this field is always false and must not be used. + > **Deprecated**: this field is not part of the image specification and is + > always false. It must not be used, and will be removed in API v1.47. type: "boolean" default: false example: false @@ -1400,7 +1403,8 @@ definitions:


- > **Note**: this field is always false and must not be used. + > **Deprecated**: this field is not part of the image specification and is + > always false. It must not be used, and will be removed in API v1.47. type: "boolean" default: false example: false @@ -1410,7 +1414,8 @@ definitions:


- > **Note**: this field is always false and must not be used. + > **Deprecated**: this field is not part of the image specification and is + > always false. It must not be used, and will be removed in API v1.47. type: "boolean" default: false example: false @@ -1436,7 +1441,8 @@ definitions:


- > **Note**: this field is always false and must not be used. + > **Deprecated**: this field is not part of the image specification and is + > always false. It must not be used, and will be removed in API v1.47. type: "boolean" default: false example: false @@ -1446,7 +1452,8 @@ definitions:


- > **Note**: this field is always false and must not be used. + > **Deprecated**: this field is not part of the image specification and is + > always false. It must not be used, and will be removed in API v1.47. type: "boolean" default: false example: false @@ -1456,7 +1463,8 @@ definitions:


- > **Note**: this field is always false and must not be used. + > **Deprecated**: this field is not part of the image specification and is + > always false. It must not be used, and will be removed in API v1.47. type: "boolean" default: false example: false @@ -1492,7 +1500,8 @@ definitions:


- > **Note**: this field is always empty and must not be used. + > **Deprecated**: this field is not part of the image specification and is + > always empty. It must not be used, and will be removed in API v1.47. type: "string" default: "" example: "" @@ -1530,7 +1539,8 @@ definitions:


- > **Note**: this field is always omitted and must not be used. + > **Deprecated**: this field is not part of the image specification and is + > always omitted. It must not be used, and will be removed in API v1.47. type: "boolean" default: false example: false @@ -1541,7 +1551,8 @@ definitions:


- > **Deprecated**: this field is deprecated in API v1.44 and up. It is always omitted. + > **Deprecated**: this field is not part of the image specification and is + > always omitted. It must not be used, and will be removed in API v1.47. type: "string" default: "" example: "" @@ -1574,7 +1585,8 @@ definitions:


- > **Note**: this field is always omitted and must not be used. + > **Deprecated**: this field is not part of the image specification and is + > always omitted. It must not be used, and will be removed in API v1.47. type: "integer" default: 10 x-nullable: true @@ -2115,6 +2127,7 @@ definitions: format: "dateTime" example: "2022-02-28T14:40:02.623929178Z" x-nullable: true + ImageSummary: type: "object" x-go-name: "Summary" @@ -9023,6 +9036,11 @@ paths: details. type: "string" required: true + - name: "platform" + in: "query" + description: "Select a platform-specific manifest to be pushed. OCI platform (JSON encoded)" + type: "string" + x-nullable: true tags: ["Image"] /images/{name}/tag: post: diff --git a/vendor/github.com/docker/docker/api/types/auxprogress/push.go b/vendor/github.com/docker/docker/api/types/auxprogress/push.go new file mode 100644 index 0000000000..9bddae8951 --- /dev/null +++ b/vendor/github.com/docker/docker/api/types/auxprogress/push.go @@ -0,0 +1,26 @@ +package auxprogress + +import ( + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// ManifestPushedInsteadOfIndex is a note that is sent when a manifest is pushed +// instead of an index. It is sent when the pushed image is an multi-platform +// index, but the whole index couldn't be pushed. +type ManifestPushedInsteadOfIndex struct { + ManifestPushedInsteadOfIndex bool `json:"manifestPushedInsteadOfIndex"` // Always true + + // OriginalIndex is the descriptor of the original image index. + OriginalIndex ocispec.Descriptor `json:"originalIndex"` + + // SelectedManifest is the descriptor of the manifest that was pushed instead. + SelectedManifest ocispec.Descriptor `json:"selectedManifest"` +} + +// ContentMissing is a note that is sent when push fails because the content is missing. +type ContentMissing struct { + ContentMissing bool `json:"contentMissing"` // Always true + + // Desc is the descriptor of the root object that was attempted to be pushed. + Desc ocispec.Descriptor `json:"desc"` +} diff --git a/vendor/github.com/docker/docker/api/types/image/opts.go b/vendor/github.com/docker/docker/api/types/image/opts.go index fe949b432c..8e32c9af86 100644 --- a/vendor/github.com/docker/docker/api/types/image/opts.go +++ b/vendor/github.com/docker/docker/api/types/image/opts.go @@ -5,6 +5,7 @@ import ( "io" "github.com/docker/docker/api/types/filters" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) // ImportSource holds source information for ImageImport @@ -43,7 +44,23 @@ type PullOptions struct { } // PushOptions holds information to push images. -type PushOptions PullOptions +type PushOptions struct { + All bool + RegistryAuth string // RegistryAuth is the base64 encoded credentials for the registry + + // PrivilegeFunc is a function that clients can supply to retry operations + // after getting an authorization error. This function returns the registry + // authentication header value in base64 encoded format, or an error if the + // privilege request fails. + // + // Also see [github.com/docker/docker/api/types.RequestPrivilegeFunc]. + PrivilegeFunc func(context.Context) (string, error) + + // Platform is an optional field that selects a specific platform to push + // when the image is a multi-platform image. + // Using this will only push a single platform-specific manifest. + Platform *ocispec.Platform `json:",omitempty"` +} // ListOptions holds parameters to list images with. type ListOptions struct { diff --git a/vendor/github.com/docker/docker/client/image_push.go b/vendor/github.com/docker/docker/client/image_push.go index 2b80f2e866..16f9c4651d 100644 --- a/vendor/github.com/docker/docker/client/image_push.go +++ b/vendor/github.com/docker/docker/client/image_push.go @@ -2,7 +2,9 @@ package client // import "github.com/docker/docker/client" import ( "context" + "encoding/json" "errors" + "fmt" "io" "net/http" "net/url" @@ -36,6 +38,20 @@ func (cli *Client) ImagePush(ctx context.Context, image string, options image.Pu } } + if options.Platform != nil { + if err := cli.NewVersionError(ctx, "1.46", "platform"); err != nil { + return nil, err + } + + p := *options.Platform + pJson, err := json.Marshal(p) + if err != nil { + return nil, fmt.Errorf("invalid platform: %v", err) + } + + query.Set("platform", string(pJson)) + } + resp, err := cli.tryImagePush(ctx, name, query, options.RegistryAuth) if errdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil { newAuthHeader, privilegeErr := options.PrivilegeFunc(ctx) diff --git a/vendor/github.com/docker/docker/pkg/archive/archive_linux.go b/vendor/github.com/docker/docker/pkg/archive/archive_linux.go index 2c3786cd50..1cecfb65ac 100644 --- a/vendor/github.com/docker/docker/pkg/archive/archive_linux.go +++ b/vendor/github.com/docker/docker/pkg/archive/archive_linux.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" + "github.com/containerd/containerd/pkg/userns" "github.com/docker/docker/pkg/system" "github.com/pkg/errors" "golang.org/x/sys/unix" @@ -35,13 +36,18 @@ func (overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os } if fi.Mode()&os.ModeDir != 0 { + opaqueXattrName := "trusted.overlay.opaque" + if userns.RunningInUserNS() { + opaqueXattrName = "user.overlay.opaque" + } + // convert opaque dirs to AUFS format by writing an empty file with the prefix - opaque, err := system.Lgetxattr(path, "trusted.overlay.opaque") + opaque, err := system.Lgetxattr(path, opaqueXattrName) if err != nil { return nil, err } if len(opaque) == 1 && opaque[0] == 'y' { - delete(hdr.PAXRecords, paxSchilyXattr+"trusted.overlay.opaque") + delete(hdr.PAXRecords, paxSchilyXattr+opaqueXattrName) // create a header for the whiteout file // it should inherit some properties from the parent, but be a regular file @@ -69,9 +75,14 @@ func (c overlayWhiteoutConverter) ConvertRead(hdr *tar.Header, path string) (boo // if a directory is marked as opaque by the AUFS special file, we need to translate that to overlay if base == WhiteoutOpaqueDir { - err := unix.Setxattr(dir, "trusted.overlay.opaque", []byte{'y'}, 0) + opaqueXattrName := "trusted.overlay.opaque" + if userns.RunningInUserNS() { + opaqueXattrName = "user.overlay.opaque" + } + + err := unix.Setxattr(dir, opaqueXattrName, []byte{'y'}, 0) if err != nil { - return false, errors.Wrapf(err, "setxattr(%q, trusted.overlay.opaque=y)", dir) + return false, errors.Wrapf(err, "setxattr(%q, %s=y)", dir, opaqueXattrName) } // don't write the file itself return false, err diff --git a/vendor/modules.txt b/vendor/modules.txt index b276b6932d..fd14bb0684 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -56,10 +56,11 @@ github.com/docker/distribution/registry/client/transport github.com/docker/distribution/registry/storage/cache github.com/docker/distribution/registry/storage/cache/memory github.com/docker/distribution/uuid -# github.com/docker/docker v26.1.1-0.20240610145149-a736d0701c41+incompatible +# github.com/docker/docker v26.1.1-0.20240610201418-9d9488468fe2+incompatible ## explicit github.com/docker/docker/api github.com/docker/docker/api/types +github.com/docker/docker/api/types/auxprogress github.com/docker/docker/api/types/blkiodev github.com/docker/docker/api/types/checkpoint github.com/docker/docker/api/types/container