Files
docker-cli/cli/command/image/push.go
Sebastiaan van Stijn c88268681e prevent login prompt on registry operations with no TTY attached
When pulling or pushing images, the CLI could prompt for a password
if the push/pull failed and the registry returned a 401 (Unauthorized)

Ironically, this feature did not work when using Docker Hub (and possibly
other registries using basic auth), due to some custom error handling added
in [moby@19a93a6e3d42], which also discards the registry's status code,
changing it to a 404;

    curl -v -XPOST --unix-socket /var/run/docker.sock 'http://localhost/v1.50/images/create?fromImage=docker.io%2Fexample%2Fprivate&tag=latest'
    ...
    < HTTP/1.1 404 Not Found
    < Content-Type: application/json
    ...
    {"message":"pull access denied for example/private, repository does not exist or may require 'docker login'"}

And due to a bug, other registries (not using basic auth) returned a generic
error, which resulted in a 500 Internal Server Error. That bug was fixed in
docker 28.2, now returning the upstream status code and trigger an interactive
prompt;

    docker pull icr.io/my-ns/my-image:latest
    Please login prior to pull:
    Username:

This prompt would be triggered unconditionally, also if the CLI was run
non-interactively and no TTY attached;

    docker pull icr.io/my-ns/my-image:latest < /dev/null
    Please login prior to pull:
    Username:

With this PR, no prompt is shown ;

    # without STDIN attached
    docker pull icr.io/my-ns/my-image:latest < /dev/null
    Error response from daemon: error from registry: Authorization required. See https://cloud.ibm.com/docs/Registry?topic=Registry-troubleshoot-auth-req - Authorization required. See https://cloud.ibm.com/docs/Registry?topic=Registry-troubleshoot-auth-req

For now, the prompt is still shown otherwise;

    docker pull icr.io/my-ns/my-image:latest

    Login prior to pull:
    Username: ^C

[moby@19a93a6e3d42]: 19a93a6e3d

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-06-19 10:25:27 +02:00

186 lines
6.1 KiB
Go

// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.23
package image
import (
"context"
"encoding/json"
"fmt"
"io"
"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/cli/internal/jsonstream"
"github.com/docker/cli/internal/tui"
"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/registry"
"github.com/morikuni/aec"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type pushOptions struct {
all bool
remote string
untrusted bool
quiet bool
platform string
}
// NewPushCommand creates a new `docker push` command
func NewPushCommand(dockerCli command.Cli) *cobra.Command {
var opts pushOptions
cmd := &cobra.Command{
Use: "push [OPTIONS] NAME[:TAG]",
Short: "Upload an image to a registry",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.remote = args[0]
return runPush(cmd.Context(), dockerCli, opts)
},
Annotations: map[string]string{
"category-top": "6",
"aliases": "docker image push, docker push",
},
ValidArgsFunction: completion.ImageNames(dockerCli, 1),
}
flags := cmd.Flags()
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())
// Don't default to DOCKER_DEFAULT_PLATFORM env variable, always default to
// pushing the image as-is. This also avoids forcing the platform selection
// on older APIs which don't support it.
flags.StringVar(&opts.platform, "platform", "",
`Push a platform-specific manifest as a single-platform image to the registry.
Image index won't be pushed, meaning that other manifests, including attestations won't be preserved.
'os[/arch[/variant]]': Explicit platform (eg. linux/amd64)`)
flags.SetAnnotation("platform", "version", []string{"1.46"})
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
return cmd
}
// runPush performs a push against the engine based on the specified options.
//
//nolint:gocyclo // ignore cyclomatic complexity 17 of func `runPush` is high (> 16) for now.
func runPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error {
var platform *ocispec.Platform
out := tui.NewOutput(dockerCli.Out())
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
out.PrintNote(`Using --platform pushes only the specified platform manifest of a multi-platform image index.
Other components, like attestations, will not be included.
To push the complete multi-platform image, remove the --platform flag.
`)
}
ref, err := reference.ParseNormalizedNamed(opts.remote)
switch {
case err != nil:
return err
case opts.all && !reference.IsNameOnly(ref):
return errors.New("tag can't be used with --all-tags/-a")
case !opts.all && reference.IsNameOnly(ref):
ref = reference.TagNameOnly(ref)
if tagged, ok := ref.(reference.Tagged); ok && !opts.quiet {
_, _ = fmt.Fprintln(dockerCli.Out(), "Using default tag:", tagged.Tag())
}
}
// Resolve the Repository name from fqn to RepositoryInfo
repoInfo, _ := registry.ParseRepositoryInfo(ref)
// Resolve the Auth config relevant for this server
authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index)
encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig)
if err != nil {
return err
}
var requestPrivilege registrytypes.RequestAuthConfig
if dockerCli.In().IsTerminal() {
requestPrivilege = command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "push")
}
options := image.PushOptions{
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 {
out.PrintNote(note)
}
}()
defer responseBody.Close()
if !opts.untrusted {
// TODO pushTrustedReference currently doesn't respect `--quiet`
return pushTrustedReference(ctx, dockerCli, repoInfo, ref, authConfig, responseBody)
}
if opts.quiet {
err = jsonstream.Display(ctx, responseBody, streams.NewOut(io.Discard), jsonstream.WithAuxCallback(handleAux()))
if err == nil {
fmt.Fprintln(dockerCli.Out(), ref.String())
}
return err
}
return jsonstream.Display(ctx, responseBody, dockerCli.Out(), jsonstream.WithAuxCallback(handleAux()))
}
var notes []string
func handleAux() func(jm jsonstream.JSONMessage) {
return func(jm jsonstream.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)
}
}
}