Merge pull request #4984 from vvoland/c8d-multiplatform-push

cli/push: Add `platform` switch
This commit is contained in:
Sebastiaan van Stijn 2024-06-11 17:23:27 +02:00 committed by GitHub
commit 52eddcf4e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 201 additions and 33 deletions

View File

@ -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...)
}

View File

@ -10,9 +10,10 @@ 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 |
| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.<br>'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) |
| `-q`, `--quiet` | | | Suppress verbose output |

View File

@ -10,9 +10,10 @@ 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 |
| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.<br>'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) |
| `-q`, `--quiet` | | | Suppress verbose output |

View File

@ -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

View File

@ -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=

View File

@ -1368,7 +1368,8 @@ definitions:
<p><br /></p>
> **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:
<p><br /></p>
> **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:
<p><br /></p>
> **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:
<p><br /></p>
> **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:
<p><br /></p>
> **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:
<p><br /></p>
> **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:
<p><br /></p>
> **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:
<p><br /></p>
> **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:
<p><br /></p>
> **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:
<p><br /></p>
> **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:
<p><br /></p>
> **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:
<p><br /></p>
> **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:

View File

@ -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"`
}

View File

@ -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 {

View File

@ -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)

View File

@ -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

3
vendor/modules.txt vendored
View File

@ -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