Compare commits

...

41 Commits

Author SHA1 Message Date
3ab4256958 Merge pull request #5374 from vvoland/vendor-docker
Some checks failed
build / plugins (push) Has been cancelled
codeql / codeql (push) Has been cancelled
e2e / e2e (alpine, 23, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 23, experimental) (push) Has been cancelled
e2e / e2e (alpine, 23, non-experimental) (push) Has been cancelled
e2e / e2e (alpine, 26.1, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 26.1, experimental) (push) Has been cancelled
e2e / e2e (alpine, 26.1, non-experimental) (push) Has been cancelled
e2e / e2e (alpine, 27, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 27, experimental) (push) Has been cancelled
e2e / e2e (alpine, 27, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 23, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 23, experimental) (push) Has been cancelled
e2e / e2e (debian, 23, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 26.1, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 26.1, experimental) (push) Has been cancelled
e2e / e2e (debian, 26.1, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 27, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 27, experimental) (push) Has been cancelled
e2e / e2e (debian, 27, non-experimental) (push) Has been cancelled
test / ctn (push) Has been cancelled
test / host (macos-13) (push) Has been cancelled
test / host (macos-14) (push) Has been cancelled
validate / validate (lint) (push) Has been cancelled
validate / validate (shellcheck) (push) Has been cancelled
validate / validate (update-authors) (push) Has been cancelled
validate / validate (validate-vendor) (push) Has been cancelled
validate / validate-md (push) Has been cancelled
validate / validate-make (manpages) (push) Has been cancelled
validate / validate-make (yamldocs) (push) Has been cancelled
[27.x backport] vendor: github.com/docker/docker 3ab5c7d0036c (v27.2.0-dev)
2024-08-27 16:08:11 +02:00
88a49df297 vendor: github.com/docker/docker 3ab5c7d0036c (v27.2.0-dev)
full diff: b27de4ef16...3ab5c7d003

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-27 16:02:26 +02:00
5d17c29eb2 Merge pull request #5372 from thaJeztah/27.x_backport_fix_linting_issues
[27.x backport] Fix linting issues in preparation of Go and GolangCI-lint update
2024-08-26 17:06:00 +02:00
64b9e4cd16 cli: rename args that collided with builtins (predeclard)
cli/required.go:33:22: param min has same name as predeclared identifier (predeclared)
    func RequiresMinArgs(min int) cobra.PositionalArgs {
                         ^
    cli/required.go:50:22: param max has same name as predeclared identifier (predeclared)
    func RequiresMaxArgs(max int) cobra.PositionalArgs {
                         ^
    cli/required.go:67:24: param min has same name as predeclared identifier (predeclared)
    func RequiresRangeArgs(min int, max int) cobra.PositionalArgs {
                           ^

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit c4a55df7c0)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-08-26 14:49:10 +02:00
4b71d0d1af e2e/global: fix n-constant format string in call (govet)
e2e/global/cli_test.go:217:28: printf: non-constant format string in call to gotest.tools/v3/poll.Continue (govet)
                            return poll.Continue(err.Error())
                                                 ^

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9c87891278)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-08-26 14:38:40 +02:00
002cfcde85 cli/command: fix n-constant format string in call (govet)
cli/command/utils.go:225:29: printf: non-constant format string in call to github.com/pkg/errors.Wrapf (govet)
                return errors.Wrapf(err, fmt.Sprintf("invalid output path: %q must be a directory or a regular file", path))
                                         ^
    cli/command/manifest/cmd.go:21:33: printf: non-constant format string in call to fmt.Fprintf (govet)
                fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
                                             ^
    cli/command/service/remove.go:45:24: printf: non-constant format string in call to github.com/pkg/errors.Errorf (govet)
            return errors.Errorf(strings.Join(errs, "\n"))
                                 ^
    cli/command/service/scale.go:93:23: printf: non-constant format string in call to github.com/pkg/errors.Errorf (govet)
        return errors.Errorf(strings.Join(errs, "\n"))
                             ^
    cli/command/stack/swarm/remove.go:74:24: printf: non-constant format string in call to github.com/pkg/errors.Errorf (govet)
            return errors.Errorf(strings.Join(errs, "\n"))
                                 ^

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit f101f07a7b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-08-26 14:38:40 +02:00
d8af7812b5 cli/command/system: remove redundant nil-check (gosimple)
cli/command/system/info.go:375:5: S1009: should omit nil check; len() for []github.com/docker/docker/api/types/system.NetworkAddressPool is defined as zero (gosimple)
        if info.DefaultAddressPools != nil && len(info.DefaultAddressPools) > 0 {
           ^

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit cc1d7b7ac9)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-08-26 14:38:37 +02:00
f042ddb5c9 Merge pull request #5371 from vvoland/vendor-docker
vendor: github.com/docker/docker b27de4ef1634 (v27.2.0-dev)
2024-08-26 14:33:00 +02:00
8e94ed15e6 vendor: github.com/docker/docker b27de4ef1634 (v27.2.0-dev)
full diff: 9942d656ba...b27de4ef16

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-26 14:02:54 +02:00
7a82aeeeba Merge pull request #5368 from dvdksn/27x_5360
[27.x backport] update link to engine api reference
2024-08-22 18:01:29 +02:00
24837f9260 chore: update link to docker engine api reference
Engine API reference page is moving to /reference/api/engine

Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit c974a83391)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-08-22 15:18:55 +02:00
5805df0205 Merge pull request #5365 from vvoland/5363-27.x
[27.x backport] cli/formatter: bracket IPv6 addrs prepended to ports
2024-08-22 14:23:10 +02:00
fb20f009f7 Merge pull request #5366 from dvdksn/27x_f1befabe9f1c979d94c39eeb7020e106b3c1e6a6
[27.x backport] use gh alert syntax for callouts
2024-08-21 15:22:02 +02:00
6ceb0aba82 cli/formatter: bracket IPv6 addrs prepended to ports
On `docker ps`, port bindings with an IPv6 HostIP should have their
addresses put into brackets when joining them to their ports.

RFC 3986 (Section 3.2.2) stipulates that IPv6 addresses should be
enclosed within square brackets. This RFC is only about URIs. However,
doing so here helps user identifier what's part of the IP address and
what's the port. It also makes it easier to copy/paste that
'[addr]:port' into other software (including browsers).

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
(cherry picked from commit 964155cd27)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-21 11:45:56 +02:00
2d7b8998c4 docs: use gh alert syntax for callouts
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit f1befabe9f)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-08-21 11:41:15 +02:00
cabd410a1a Merge pull request #5362 from laurazard/27.x-backport-oauth-escape-hatch
[27.x backport] login: add oauth escape hatch
2024-08-20 16:11:01 +02:00
a58af379e1 login: add e2e tests for oauth + escape hatch
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit a327476f7f)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-08-20 12:50:57 +01:00
1b3fa65759 login: add oauth escape hatch
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit 846ecf59ff)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-08-20 12:50:49 +01:00
cf01923519 Merge pull request #5348 from dvdksn/backport_update_build_context_link
[27.x backport] docs: update link to moved build context doc
2024-08-19 17:48:55 +02:00
a0d7f0dbd3 Merge pull request #5358 from vvoland/5356-27.x
[27.x backport] list/tree: No extra spacing for graphdriver
2024-08-19 13:47:05 +02:00
0c4e7478e2 list/tree: No extra spacing for graphdriver
Don't output the extra spacing around the images when none of the
top-level image entries has any children.

This makes the list look better when ran against the graphdrivers image
store.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 7b91647943)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-19 13:28:13 +02:00
60ce3fbc96 Merge pull request #5353 from vvoland/4982-27.x
Some checks failed
build / plugins (push) Has been cancelled
codeql / codeql (push) Has been cancelled
e2e / e2e (alpine, 23, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 23, experimental) (push) Has been cancelled
e2e / e2e (alpine, 23, non-experimental) (push) Has been cancelled
e2e / e2e (alpine, 26.1, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 26.1, experimental) (push) Has been cancelled
e2e / e2e (alpine, 26.1, non-experimental) (push) Has been cancelled
e2e / e2e (alpine, 27, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 27, experimental) (push) Has been cancelled
e2e / e2e (alpine, 27, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 23, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 23, experimental) (push) Has been cancelled
e2e / e2e (debian, 23, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 26.1, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 26.1, experimental) (push) Has been cancelled
e2e / e2e (debian, 26.1, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 27, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 27, experimental) (push) Has been cancelled
e2e / e2e (debian, 27, non-experimental) (push) Has been cancelled
test / ctn (push) Has been cancelled
test / host (macos-13) (push) Has been cancelled
test / host (macos-14) (push) Has been cancelled
validate / validate (lint) (push) Has been cancelled
validate / validate (shellcheck) (push) Has been cancelled
validate / validate (update-authors) (push) Has been cancelled
validate / validate (validate-vendor) (push) Has been cancelled
validate / validate-md (push) Has been cancelled
validate / validate-make (manpages) (push) Has been cancelled
validate / validate-make (yamldocs) (push) Has been cancelled
[27.x backport] image/list: Add `--tree` flag
2024-08-16 19:51:04 +02:00
7902b52714 list/tree: Print <untagged> as dangling image name
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 351249dce9)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:38 +02:00
7196200fc2 list/tree: Fix some escape codes included in nonTTY
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 6979ab073c)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:36 +02:00
f42fa0b8e1 list/tree: Add spacing before the content and first image
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit a9b78da546)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:34 +02:00
b719b10257 list/tree: Capitalize column headers
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 0242a1e3c6)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:33 +02:00
ab55d75cf5 list/tree: Add an experimental warning
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit d417d06682)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:32 +02:00
324cc5d30f list/tree: Sort by created date
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit b1a08f7841)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:30 +02:00
44a9ffa0ad list/tree: Align number right, text left
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 18ab78882c)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:29 +02:00
ba43ae0bd2 cli/tree: Add Content size column
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit ea8aafcd9e)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:28 +02:00
99b647cfca image/list: Add --tree flag
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit be11b74ee9)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:26 +02:00
f90dc28f1e Merge pull request #5354 from vvoland/vendor-docker
[27.x] vendor: github.com/docker/docker v27.2.0-dev (9942d656bade)
2024-08-16 19:39:57 +02:00
26536d1145 vendor: github.com/docker/docker v27.2.0-dev (9942d656bade)
full diff: f9522e5e96...9942d656ba

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 18:59:59 +02:00
c5e733becc Merge pull request #5349 from laurazard/27.x-backport-oauth-login
[27.x backport] auth: add support for oauth device-code login
2024-08-16 18:13:24 +02:00
7227402d94 Merge pull request #5351 from laurazard/backport-27.x-disable-pseudoterminal-ssh
[27.x backport] disable pseudoterminal creation
2024-08-16 18:12:10 +02:00
83f6ca4a73 disable pseudoterminal creation
avoided the join, also did manual iteration

added test, also added reflect for the DeepEqual comparison

Signed-off-by: Archimedes Trajano <developer@trajano.net>
(cherry picked from commit f3c2c26b10)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-08-16 14:11:10 +01:00
ad7912a846 fallback to regular login if oauth login fails to start
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit c3fe7bc336)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-08-16 10:09:41 +01:00
afb5e143b1 login: normalize registry-1.docker.io
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit e6624676e0)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-08-16 10:09:40 +01:00
b8a38fd22d Refactor cli/command/registry
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit 6e4818e7d6)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-08-16 10:09:39 +01:00
0c29d6bac1 auth: add support for oauth device-code login
This commit adds support for the oauth [device-code](https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow)
login flow when authenticating against the official registry.

This is achieved by adding `cli/internal/oauth`, which contains code to manage
interacting with the Docker OAuth tenant (`login.docker.com`), including launching
the device-code flow, refreshing access using the refresh-token, and logging out.

The `OAuthManager` introduced here is also made available through the `command.Cli`
interface method `OAuthManager()`.

In order to maintain compatibility with any clients manually accessing
the credentials through `~/.docker/config.json` or via credential
helpers, the added `OAuthManager` uses the retrieved access token to
automatically generate a PAT with Hub, and store that in the
credentials.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit fcfdd7b91f)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-08-16 10:09:38 +01:00
3eaf30278f docs: update link to moved build context doc
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit 2dd4eb06ae)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-08-13 12:17:58 +02:00
145 changed files with 12905 additions and 317 deletions

View File

@ -5,6 +5,7 @@ package formatter
import (
"fmt"
"net"
"sort"
"strconv"
"strings"
@ -331,7 +332,8 @@ func DisplayablePorts(ports []types.Port) string {
portKey := port.Type
if port.IP != "" {
if port.PublicPort != current {
hostMappings = append(hostMappings, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type))
hAddrPort := net.JoinHostPort(port.IP, strconv.Itoa(int(port.PublicPort)))
hostMappings = append(hostMappings, fmt.Sprintf("%s->%d/%s", hAddrPort, port.PrivatePort, port.Type))
continue
}
portKey = port.IP + "/" + port.Type

View File

@ -471,6 +471,16 @@ func TestDisplayablePorts(t *testing.T) {
},
"0.0.0.0:0->9988/tcp",
},
{
[]types.Port{
{
IP: "::",
PrivatePort: 9988,
Type: "tcp",
},
},
"[::]:0->9988/tcp",
},
{
[]types.Port{
{

View File

@ -2,6 +2,7 @@ package image
import (
"context"
"errors"
"fmt"
"io"
@ -24,6 +25,7 @@ type imagesOptions struct {
format string
filter opts.FilterOpt
calledAs string
tree bool
}
// NewImagesCommand creates a new `docker images` command
@ -59,6 +61,10 @@ func NewImagesCommand(dockerCLI command.Cli) *cobra.Command {
flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
flags.BoolVar(&options.tree, "tree", false, "List multi-platform images as a tree (EXPERIMENTAL)")
flags.SetAnnotation("tree", "version", []string{"1.47"})
flags.SetAnnotation("tree", "experimentalCLI", nil)
return cmd
}
@ -75,6 +81,26 @@ func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions
filters.Add("reference", options.matchName)
}
if options.tree {
if options.quiet {
return errors.New("--quiet is not yet supported with --tree")
}
if options.noTrunc {
return errors.New("--no-trunc is not yet supported with --tree")
}
if options.showDigests {
return errors.New("--show-digest is not yet supported with --tree")
}
if options.format != "" {
return errors.New("--format is not yet supported with --tree")
}
return runTree(ctx, dockerCLI, treeOptions{
all: options.all,
filters: filters,
})
}
images, err := dockerCLI.Client().ImageList(ctx, image.ListOptions{
All: options.all,
Filters: filters,

393
cli/command/image/tree.go Normal file
View File

@ -0,0 +1,393 @@
package image
import (
"context"
"fmt"
"sort"
"strings"
"unicode/utf8"
"github.com/containerd/platforms"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/streams"
"github.com/docker/docker/api/types/filters"
imagetypes "github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"github.com/morikuni/aec"
)
type treeOptions struct {
all bool
filters filters.Args
}
type treeView struct {
images []topImage
// imageSpacing indicates whether there should be extra spacing between images.
imageSpacing bool
}
func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error {
images, err := dockerCLI.Client().ImageList(ctx, imagetypes.ListOptions{
All: opts.all,
Filters: opts.filters,
Manifests: true,
})
if err != nil {
return err
}
view := treeView{
images: make([]topImage, 0, len(images)),
}
for _, img := range images {
details := imageDetails{
ID: img.ID,
DiskUsage: units.HumanSizeWithPrecision(float64(img.Size), 3),
Used: img.Containers > 0,
}
var totalContent int64
children := make([]subImage, 0, len(img.Manifests))
for _, im := range img.Manifests {
if im.Kind != imagetypes.ManifestKindImage {
continue
}
im := im
sub := subImage{
Platform: platforms.Format(im.ImageData.Platform),
Available: im.Available,
Details: imageDetails{
ID: im.ID,
DiskUsage: units.HumanSizeWithPrecision(float64(im.Size.Total), 3),
Used: len(im.ImageData.Containers) > 0,
ContentSize: units.HumanSizeWithPrecision(float64(im.Size.Content), 3),
},
}
if sub.Details.Used {
// Mark top-level parent image as used if any of its subimages are used.
details.Used = true
}
totalContent += im.Size.Content
children = append(children, sub)
// Add extra spacing between images if there's at least one entry with children.
view.imageSpacing = true
}
details.ContentSize = units.HumanSizeWithPrecision(float64(totalContent), 3)
view.images = append(view.images, topImage{
Names: img.RepoTags,
Details: details,
Children: children,
created: img.Created,
})
}
sort.Slice(view.images, func(i, j int) bool {
return view.images[i].created > view.images[j].created
})
return printImageTree(dockerCLI, view)
}
type imageDetails struct {
ID string
DiskUsage string
Used bool
ContentSize string
}
type topImage struct {
Names []string
Details imageDetails
Children []subImage
created int64
}
type subImage struct {
Platform string
Available bool
Details imageDetails
}
const columnSpacing = 3
func printImageTree(dockerCLI command.Cli, view treeView) error {
out := dockerCLI.Out()
_, width := out.GetTtySize()
if width == 0 {
width = 80
}
if width < 20 {
width = 20
}
warningColor := aec.LightYellowF
headerColor := aec.NewBuilder(aec.DefaultF, aec.Bold).ANSI
topNameColor := aec.NewBuilder(aec.BlueF, aec.Underline, aec.Bold).ANSI
normalColor := aec.NewBuilder(aec.DefaultF).ANSI
greenColor := aec.NewBuilder(aec.GreenF).ANSI
untaggedColor := aec.NewBuilder(aec.Faint).ANSI
if !out.IsTerminal() {
headerColor = noColor{}
topNameColor = noColor{}
normalColor = noColor{}
greenColor = noColor{}
warningColor = noColor{}
untaggedColor = noColor{}
}
_, _ = fmt.Fprintln(out, warningColor.Apply("WARNING: This is an experimental feature. The output may change and shouldn't be depended on."))
_, _ = fmt.Fprintln(out, "")
columns := []imgColumn{
{
Title: "Image",
Align: alignLeft,
Width: 0,
},
{
Title: "ID",
Align: alignLeft,
Width: 12,
DetailsValue: func(d *imageDetails) string {
return stringid.TruncateID(d.ID)
},
},
{
Title: "Disk usage",
Align: alignRight,
Width: 10,
DetailsValue: func(d *imageDetails) string {
return d.DiskUsage
},
},
{
Title: "Content size",
Align: alignRight,
Width: 12,
DetailsValue: func(d *imageDetails) string {
return d.ContentSize
},
},
{
Title: "Used",
Align: alignCenter,
Width: 4,
Color: &greenColor,
DetailsValue: func(d *imageDetails) string {
if d.Used {
return "✔"
}
return " "
},
},
}
nameWidth := int(width)
for idx, h := range columns {
if h.Width == 0 {
continue
}
d := h.Width
if idx > 0 {
d += columnSpacing
}
// If the first column gets too short, remove remaining columns
if nameWidth-d < 12 {
columns = columns[:idx]
break
}
nameWidth -= d
}
images := view.images
// Try to make the first column as narrow as possible
widest := widestFirstColumnValue(columns, images)
if nameWidth > widest {
nameWidth = widest
}
columns[0].Width = nameWidth
// Print columns
for i, h := range columns {
if i > 0 {
_, _ = fmt.Fprint(out, strings.Repeat(" ", columnSpacing))
}
_, _ = fmt.Fprint(out, h.Print(headerColor, strings.ToUpper(h.Title)))
}
_, _ = fmt.Fprintln(out)
// Print images
for _, img := range images {
printNames(out, columns, img, topNameColor, untaggedColor)
printDetails(out, columns, normalColor, img.Details)
if len(img.Children) > 0 || view.imageSpacing {
_, _ = fmt.Fprintln(out)
}
printChildren(out, columns, img, normalColor)
_, _ = fmt.Fprintln(out)
}
return nil
}
func printDetails(out *streams.Out, headers []imgColumn, defaultColor aec.ANSI, details imageDetails) {
for _, h := range headers {
if h.DetailsValue == nil {
continue
}
_, _ = fmt.Fprint(out, strings.Repeat(" ", columnSpacing))
clr := defaultColor
if h.Color != nil {
clr = *h.Color
}
val := h.DetailsValue(&details)
_, _ = fmt.Fprint(out, h.Print(clr, val))
}
}
func printChildren(out *streams.Out, headers []imgColumn, img topImage, normalColor aec.ANSI) {
for idx, sub := range img.Children {
clr := normalColor
if !sub.Available {
clr = normalColor.With(aec.Faint)
}
if idx != len(img.Children)-1 {
_, _ = fmt.Fprint(out, headers[0].Print(clr, "├─ "+sub.Platform))
} else {
_, _ = fmt.Fprint(out, headers[0].Print(clr, "└─ "+sub.Platform))
}
printDetails(out, headers, clr, sub.Details)
_, _ = fmt.Fprintln(out, "")
}
}
func printNames(out *streams.Out, headers []imgColumn, img topImage, color, untaggedColor aec.ANSI) {
if len(img.Names) == 0 {
_, _ = fmt.Fprint(out, headers[0].Print(untaggedColor, "<untagged>"))
}
for nameIdx, name := range img.Names {
if nameIdx != 0 {
_, _ = fmt.Fprintln(out, "")
}
_, _ = fmt.Fprint(out, headers[0].Print(color, name))
}
}
type alignment int
const (
alignLeft alignment = iota
alignCenter
alignRight
)
type imgColumn struct {
Title string
Width int
Align alignment
DetailsValue func(*imageDetails) string
Color *aec.ANSI
}
func truncateRunes(s string, length int) string {
runes := []rune(s)
if len(runes) > length {
return string(runes[:length-3]) + "..."
}
return s
}
func (h imgColumn) Print(clr aec.ANSI, s string) string {
switch h.Align {
case alignCenter:
return h.PrintC(clr, s)
case alignRight:
return h.PrintR(clr, s)
case alignLeft:
}
return h.PrintL(clr, s)
}
func (h imgColumn) PrintC(clr aec.ANSI, s string) string {
ln := utf8.RuneCountInString(s)
if ln > h.Width {
return clr.Apply(truncateRunes(s, h.Width))
}
fill := h.Width - ln
l := fill / 2
r := fill - l
return strings.Repeat(" ", l) + clr.Apply(s) + strings.Repeat(" ", r)
}
func (h imgColumn) PrintL(clr aec.ANSI, s string) string {
ln := utf8.RuneCountInString(s)
if ln > h.Width {
return clr.Apply(truncateRunes(s, h.Width))
}
return clr.Apply(s) + strings.Repeat(" ", h.Width-ln)
}
func (h imgColumn) PrintR(clr aec.ANSI, s string) string {
ln := utf8.RuneCountInString(s)
if ln > h.Width {
return clr.Apply(truncateRunes(s, h.Width))
}
return strings.Repeat(" ", h.Width-ln) + clr.Apply(s)
}
type noColor struct{}
func (a noColor) With(_ ...aec.ANSI) aec.ANSI {
return a
}
func (a noColor) Apply(s string) string {
return s
}
func (a noColor) String() string {
return ""
}
// widestFirstColumnValue calculates the width needed to fully display the image names and platforms.
func widestFirstColumnValue(headers []imgColumn, images []topImage) int {
width := len(headers[0].Title)
for _, img := range images {
for _, name := range img.Names {
if len(name) > width {
width = len(name)
}
}
for _, sub := range img.Children {
pl := len(sub.Platform) + len("└─ ")
if pl > width {
width = pl
}
}
}
return width
}

View File

@ -18,7 +18,7 @@ func NewManifestCommand(dockerCli command.Cli) *cobra.Command {
Long: manifestDescription,
Args: cli.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
_, _ = fmt.Fprint(dockerCli.Err(), "\n"+cmd.UsageString())
},
Annotations: map[string]string{"experimentalCLI": ""},
}

View File

@ -41,7 +41,7 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
default:
}
err = ConfigureAuth(ctx, cli, "", "", &authConfig, isDefaultRegistry)
authConfig, err = PromptUserForCredentials(ctx, cli, "", "", authConfig.Username, indexServer)
if err != nil {
return "", err
}
@ -86,8 +86,32 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
return registrytypes.AuthConfig(authconfig), nil
}
// ConfigureAuth handles prompting of user's username and password if needed
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authconfig *registrytypes.AuthConfig, isDefaultRegistry bool) error {
// ConfigureAuth handles prompting of user's username and password if needed.
// Deprecated: use PromptUserForCredentials instead.
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authConfig *registrytypes.AuthConfig, _ bool) error {
defaultUsername := authConfig.Username
serverAddress := authConfig.ServerAddress
newAuthConfig, err := PromptUserForCredentials(ctx, cli, flUser, flPassword, defaultUsername, serverAddress)
if err != nil {
return err
}
authConfig.Username = newAuthConfig.Username
authConfig.Password = newAuthConfig.Password
return nil
}
// PromptUserForCredentials handles the CLI prompt for the user to input
// credentials.
// If argUser is not empty, then the user is only prompted for their password.
// If argPassword is not empty, then the user is only prompted for their username
// If neither argUser nor argPassword are empty, then the user is not prompted and
// an AuthConfig is returned with those values.
// If defaultUsername is not empty, the username prompt includes that username
// and the user can hit enter without inputting a username to use that default
// username.
func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (authConfig registrytypes.AuthConfig, err error) {
// On Windows, force the use of the regular OS stdin stream.
//
// See:
@ -107,13 +131,14 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
// Linux will hit this if you attempt `cat | docker login`, and Windows
// will hit this if you attempt docker login from mintty where stdin
// is a pipe, not a character based console.
if flPassword == "" && !cli.In().IsTerminal() {
return errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
if argPassword == "" && !cli.In().IsTerminal() {
return authConfig, errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
}
authconfig.Username = strings.TrimSpace(authconfig.Username)
isDefaultRegistry := serverAddress == registry.IndexServer
defaultUsername = strings.TrimSpace(defaultUsername)
if flUser = strings.TrimSpace(flUser); flUser == "" {
if argUser = strings.TrimSpace(argUser); argUser == "" {
if isDefaultRegistry {
// if this is a default registry (docker hub), then display the following message.
fmt.Fprintln(cli.Out(), "Log in with your Docker ID or email address to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com/ to create one.")
@ -124,44 +149,43 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
}
var prompt string
if authconfig.Username == "" {
if defaultUsername == "" {
prompt = "Username: "
} else {
prompt = fmt.Sprintf("Username (%s): ", authconfig.Username)
prompt = fmt.Sprintf("Username (%s): ", defaultUsername)
}
var err error
flUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
if err != nil {
return err
return authConfig, err
}
if flUser == "" {
flUser = authconfig.Username
if argUser == "" {
argUser = defaultUsername
}
}
if flUser == "" {
return errors.Errorf("Error: Non-null Username Required")
if argUser == "" {
return authConfig, errors.Errorf("Error: Non-null Username Required")
}
if flPassword == "" {
if argPassword == "" {
restoreInput, err := DisableInputEcho(cli.In())
if err != nil {
return err
return authConfig, err
}
defer restoreInput()
flPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
if err != nil {
return err
return authConfig, err
}
fmt.Fprint(cli.Out(), "\n")
if flPassword == "" {
return errors.Errorf("Error: Password Required")
if argPassword == "" {
return authConfig, errors.Errorf("Error: Password Required")
}
}
authconfig.Username = flUser
authconfig.Password = flPassword
return nil
authConfig.Username = argUser
authConfig.Password = argPassword
authConfig.ServerAddress = serverAddress
return authConfig, nil
}
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete

View File

@ -4,12 +4,15 @@ import (
"context"
"fmt"
"io"
"os"
"strconv"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
configtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/internal/oauth/manager"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
@ -100,80 +103,167 @@ func verifyloginOptions(dockerCli command.Cli, opts *loginOptions) error {
return nil
}
func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error { //nolint:gocyclo
clnt := dockerCli.Client()
func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error {
if err := verifyloginOptions(dockerCli, &opts); err != nil {
return err
}
var (
serverAddress string
response registrytypes.AuthenticateOKBody
response *registrytypes.AuthenticateOKBody
)
if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace {
if opts.serverAddress != "" &&
opts.serverAddress != registry.DefaultNamespace &&
opts.serverAddress != registry.DefaultRegistryHost {
serverAddress = opts.serverAddress
} else {
serverAddress = registry.IndexServer
}
isDefaultRegistry := serverAddress == registry.IndexServer
// attempt login with current (stored) credentials
authConfig, err := command.GetDefaultAuthConfig(dockerCli.ConfigFile(), opts.user == "" && opts.password == "", serverAddress, isDefaultRegistry)
if err == nil && authConfig.Username != "" && authConfig.Password != "" {
response, err = loginWithCredStoreCreds(ctx, dockerCli, &authConfig)
response, err = loginWithStoredCredentials(ctx, dockerCli, authConfig)
}
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
err = command.ConfigureAuth(ctx, dockerCli, opts.user, opts.password, &authConfig, isDefaultRegistry)
if err != nil {
return err
}
response, err = clnt.RegistryLogin(ctx, authConfig)
if err != nil && client.IsErrConnectionFailed(err) {
// If the server isn't responding (yet) attempt to login purely client side
response, err = loginClientSide(ctx, authConfig)
}
// If we (still) have an error, give up
// if we failed to authenticate with stored credentials (or didn't have stored credentials),
// prompt the user for new credentials
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
response, err = loginUser(ctx, dockerCli, opts, authConfig.Username, serverAddress)
if err != nil {
return err
}
}
if response != nil && response.Status != "" {
_, _ = fmt.Fprintln(dockerCli.Out(), response.Status)
}
return nil
}
func loginWithStoredCredentials(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (*registrytypes.AuthenticateOKBody, error) {
_, _ = fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n")
response, err := dockerCli.Client().RegistryLogin(ctx, authConfig)
if err != nil {
if errdefs.IsUnauthorized(err) {
_, _ = fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n")
} else {
_, _ = fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err)
}
}
if response.IdentityToken != "" {
authConfig.Password = ""
authConfig.IdentityToken = response.IdentityToken
}
creds := dockerCli.ConfigFile().GetCredentialsStore(serverAddress)
if err := storeCredentials(dockerCli, authConfig); err != nil {
return nil, err
}
return &response, err
}
const OauthLoginEscapeHatchEnvVar = "DOCKER_CLI_DISABLE_OAUTH_LOGIN"
func isOauthLoginDisabled() bool {
if v := os.Getenv(OauthLoginEscapeHatchEnvVar); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
}
return enabled
}
return false
}
func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) {
// If we're logging into the index server and the user didn't provide a username or password, use the device flow
if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" && !isOauthLoginDisabled() {
response, err := loginWithDeviceCodeFlow(ctx, dockerCli)
// if the error represents a failure to initiate the device-code flow,
// then we fallback to regular cli credentials login
if !errors.Is(err, manager.ErrDeviceLoginStartFail) {
return response, err
}
fmt.Fprint(dockerCli.Err(), "Failed to start web-based login - falling back to command line login...\n\n")
}
return loginWithUsernameAndPassword(ctx, dockerCli, opts, defaultUsername, serverAddress)
}
func loginWithUsernameAndPassword(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) {
// Prompt user for credentials
authConfig, err := command.PromptUserForCredentials(ctx, dockerCli, opts.user, opts.password, defaultUsername, serverAddress)
if err != nil {
return nil, err
}
response, err := loginWithRegistry(ctx, dockerCli, authConfig)
if err != nil {
return nil, err
}
if response.IdentityToken != "" {
authConfig.Password = ""
authConfig.IdentityToken = response.IdentityToken
}
if err = storeCredentials(dockerCli, authConfig); err != nil {
return nil, err
}
return &response, nil
}
func loginWithDeviceCodeFlow(ctx context.Context, dockerCli command.Cli) (*registrytypes.AuthenticateOKBody, error) {
store := dockerCli.ConfigFile().GetCredentialsStore(registry.IndexServer)
authConfig, err := manager.NewManager(store).LoginDevice(ctx, dockerCli.Err())
if err != nil {
return nil, err
}
response, err := loginWithRegistry(ctx, dockerCli, registrytypes.AuthConfig(*authConfig))
if err != nil {
return nil, err
}
if err = storeCredentials(dockerCli, registrytypes.AuthConfig(*authConfig)); err != nil {
return nil, err
}
return &response, nil
}
func storeCredentials(dockerCli command.Cli, authConfig registrytypes.AuthConfig) error {
creds := dockerCli.ConfigFile().GetCredentialsStore(authConfig.ServerAddress)
store, isDefault := creds.(isFileStore)
// Display a warning if we're storing the users password (not a token)
if isDefault && authConfig.Password != "" {
err = displayUnencryptedWarning(dockerCli, store.GetFilename())
err := displayUnencryptedWarning(dockerCli, store.GetFilename())
if err != nil {
return err
}
}
if err := creds.Store(configtypes.AuthConfig(authConfig)); err != nil {
return errors.Errorf("Error saving credentials: %v", err)
}
if response.Status != "" {
fmt.Fprintln(dockerCli.Out(), response.Status)
}
return nil
}
func loginWithCredStoreCreds(ctx context.Context, dockerCli command.Cli, authConfig *registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n")
cliClient := dockerCli.Client()
response, err := cliClient.RegistryLogin(ctx, *authConfig)
if err != nil {
if errdefs.IsUnauthorized(err) {
fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n")
} else {
fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err)
}
func loginWithRegistry(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
response, err := dockerCli.Client().RegistryLogin(ctx, authConfig)
if err != nil && client.IsErrConnectionFailed(err) {
// If the server isn't responding (yet) attempt to login purely client side
response, err = loginClientSide(ctx, authConfig)
}
return response, err
// If we (still) have an error, give up
if err != nil {
return registrytypes.AuthenticateOKBody{}, err
}
return response, nil
}
func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {

View File

@ -74,7 +74,7 @@ func TestLoginWithCredStoreCreds(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
errBuf := new(bytes.Buffer)
cli.SetErr(streams.NewOut(errBuf))
loginWithCredStoreCreds(ctx, cli, &tc.inputAuthConfig)
loginWithStoredCredentials(ctx, cli, tc.inputAuthConfig)
outputString := cli.OutBuffer().String()
assert.Check(t, is.Equal(tc.expectedMsg, outputString))
errorString := errBuf.String()
@ -213,7 +213,9 @@ func TestLoginTermination(t *testing.T) {
runErr := make(chan error)
go func() {
runErr <- runLogin(ctx, cli, loginOptions{})
runErr <- runLogin(ctx, cli, loginOptions{
user: "test-user",
})
}()
// Let the prompt get canceled by the context
@ -226,3 +228,47 @@ func TestLoginTermination(t *testing.T) {
assert.ErrorIs(t, err, command.ErrPromptTerminated)
}
}
func TestIsOauthLoginDisabled(t *testing.T) {
testCases := []struct {
envVar string
disabled bool
}{
{
envVar: "",
disabled: false,
},
{
envVar: "bork",
disabled: false,
},
{
envVar: "0",
disabled: false,
},
{
envVar: "false",
disabled: false,
},
{
envVar: "true",
disabled: true,
},
{
envVar: "TRUE",
disabled: true,
},
{
envVar: "1",
disabled: true,
},
}
for _, tc := range testCases {
t.Setenv(OauthLoginEscapeHatchEnvVar, tc.envVar)
disabled := isOauthLoginDisabled()
assert.Equal(t, disabled, tc.disabled)
}
}

View File

@ -7,6 +7,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/internal/oauth/manager"
"github.com/docker/docker/registry"
"github.com/spf13/cobra"
)
@ -34,7 +35,7 @@ func NewLogoutCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
func runLogout(_ context.Context, dockerCli command.Cli, serverAddress string) error {
func runLogout(ctx context.Context, dockerCli command.Cli, serverAddress string) error {
var isDefaultRegistry bool
if serverAddress == "" {
@ -53,6 +54,13 @@ func runLogout(_ context.Context, dockerCli command.Cli, serverAddress string) e
regsToLogout = append(regsToLogout, hostnameAddress, "http://"+hostnameAddress, "https://"+hostnameAddress)
}
if isDefaultRegistry {
store := dockerCli.ConfigFile().GetCredentialsStore(registry.IndexServer)
if err := manager.NewManager(store).Logout(ctx); err != nil {
fmt.Fprintf(dockerCli.Err(), "WARNING: %v\n", err)
}
}
fmt.Fprintf(dockerCli.Out(), "Removing login credentials for %s\n", hostnameAddress)
errs := make(map[string]error)
for _, r := range regsToLogout {

View File

@ -39,10 +39,10 @@ func runRemove(ctx context.Context, dockerCli command.Cli, sids []string) error
errs = append(errs, err.Error())
continue
}
fmt.Fprintf(dockerCli.Out(), "%s\n", sid)
_, _ = fmt.Fprintf(dockerCli.Out(), "%s\n", sid)
}
if len(errs) > 0 {
return errors.Errorf(strings.Join(errs, "\n"))
return errors.New(strings.Join(errs, "\n"))
}
return nil
}

View File

@ -90,7 +90,7 @@ func runScale(ctx context.Context, dockerCli command.Cli, options *scaleOptions,
if len(errs) == 0 {
return nil
}
return errors.Errorf(strings.Join(errs, "\n"))
return errors.New(strings.Join(errs, "\n"))
}
func runServiceScale(ctx context.Context, dockerCli command.Cli, serviceID string, scale uint64) error {

View File

@ -48,7 +48,7 @@ func RunRemove(ctx context.Context, dockerCli command.Cli, opts options.Remove)
}
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", namespace)
_, _ = fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", namespace)
continue
}
@ -71,7 +71,7 @@ func RunRemove(ctx context.Context, dockerCli command.Cli, opts options.Remove)
}
if len(errs) > 0 {
return errors.Errorf(strings.Join(errs, "\n"))
return errors.New(strings.Join(errs, "\n"))
}
return nil
}

View File

@ -372,7 +372,7 @@ func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) []error {
fprintln(output, " Product License:", info.ProductLicense)
}
if info.DefaultAddressPools != nil && len(info.DefaultAddressPools) > 0 {
if len(info.DefaultAddressPools) > 0 {
fprintln(output, " Default Address Pools:")
for _, pool := range info.DefaultAddressPools {
fprintf(output, " Base: %s, Size: %d\n", pool.Base, pool.Size)

View File

@ -222,7 +222,7 @@ func ValidateOutputPath(path string) error {
}
if err := ValidateOutputPathFileMode(fileInfo.Mode()); err != nil {
return errors.Wrapf(err, fmt.Sprintf("invalid output path: %q must be a directory or a regular file", path))
return errors.Wrapf(err, "invalid output path: %q must be a directory or a regular file", path)
}
}
return nil

View File

@ -52,6 +52,7 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*ConnectionHelper
args = append(args, "--host", "unix://"+sp.Path)
}
sshFlags = addSSHTimeout(sshFlags)
sshFlags = disablePseudoTerminalAllocation(sshFlags)
args = append(args, "system", "dial-stdio")
return commandconn.New(ctx, "ssh", append(sshFlags, sp.Args(args...)...)...)
},
@ -79,3 +80,14 @@ func addSSHTimeout(sshFlags []string) []string {
}
return sshFlags
}
// disablePseudoTerminalAllocation disables pseudo-terminal allocation to
// prevent SSH from executing as a login shell
func disablePseudoTerminalAllocation(sshFlags []string) []string {
for _, flag := range sshFlags {
if flag == "-T" {
return sshFlags
}
}
return append(sshFlags, "-T")
}

View File

@ -1,6 +1,7 @@
package connhelper
import (
"reflect"
"testing"
"gotest.tools/v3/assert"
@ -29,3 +30,36 @@ func TestSSHFlags(t *testing.T) {
assert.DeepEqual(t, addSSHTimeout(tc.in), tc.out)
}
}
func TestDisablePseudoTerminalAllocation(t *testing.T) {
testCases := []struct {
name string
sshFlags []string
expected []string
}{
{
name: "No -T flag present",
sshFlags: []string{"-v", "-oStrictHostKeyChecking=no"},
expected: []string{"-v", "-oStrictHostKeyChecking=no", "-T"},
},
{
name: "Already contains -T flag",
sshFlags: []string{"-v", "-T", "-oStrictHostKeyChecking=no"},
expected: []string{"-v", "-T", "-oStrictHostKeyChecking=no"},
},
{
name: "Empty sshFlags",
sshFlags: []string{},
expected: []string{"-T"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := disablePseudoTerminalAllocation(tc.sshFlags)
if !reflect.DeepEqual(result, tc.expected) {
t.Errorf("expected %v, got %v", tc.expected, result)
}
})
}
}

View File

@ -0,0 +1,228 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.21
package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"runtime"
"strings"
"time"
"github.com/docker/cli/cli/version"
)
type OAuthAPI interface {
GetDeviceCode(ctx context.Context, audience string) (State, error)
WaitForDeviceToken(ctx context.Context, state State) (TokenResponse, error)
RevokeToken(ctx context.Context, refreshToken string) error
GetAutoPAT(ctx context.Context, audience string, res TokenResponse) (string, error)
}
// API represents API interactions with Auth0.
type API struct {
// TenantURL is the base used for each request to Auth0.
TenantURL string
// ClientID is the client ID for the application to auth with the tenant.
ClientID string
// Scopes are the scopes that are requested during the device auth flow.
Scopes []string
}
// TokenResponse represents the response of the /oauth/token route.
type TokenResponse struct {
AccessToken string `json:"access_token"`
IDToken string `json:"id_token"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
Error *string `json:"error,omitempty"`
ErrorDescription string `json:"error_description,omitempty"`
}
var ErrTimeout = errors.New("timed out waiting for device token")
// GetDeviceCode initiates the device-code auth flow with the tenant.
// The state returned contains the device code that the user must use to
// authenticate, as well as the URL to visit, etc.
func (a API) GetDeviceCode(ctx context.Context, audience string) (State, error) {
data := url.Values{
"client_id": {a.ClientID},
"audience": {audience},
"scope": {strings.Join(a.Scopes, " ")},
}
deviceCodeURL := a.TenantURL + "/oauth/device/code"
resp, err := postForm(ctx, deviceCodeURL, strings.NewReader(data.Encode()))
if err != nil {
return State{}, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return State{}, tryDecodeOAuthError(resp)
}
var state State
err = json.NewDecoder(resp.Body).Decode(&state)
if err != nil {
return state, fmt.Errorf("failed to get device code: %w", err)
}
return state, nil
}
func tryDecodeOAuthError(resp *http.Response) error {
var body map[string]any
if err := json.NewDecoder(resp.Body).Decode(&body); err == nil {
if errorDescription, ok := body["error_description"].(string); ok {
return errors.New(errorDescription)
}
}
return errors.New("unexpected response from tenant: " + resp.Status)
}
// WaitForDeviceToken polls the tenant to get access/refresh tokens for the user.
// This should be called after GetDeviceCode, and will block until the user has
// authenticated or we have reached the time limit for authenticating (based on
// the response from GetDeviceCode).
func (a API) WaitForDeviceToken(ctx context.Context, state State) (TokenResponse, error) {
ticker := time.NewTicker(state.IntervalDuration())
defer ticker.Stop()
timeout := time.After(state.ExpiryDuration())
for {
select {
case <-ctx.Done():
return TokenResponse{}, ctx.Err()
case <-ticker.C:
res, err := a.getDeviceToken(ctx, state)
if err != nil {
return res, err
}
if res.Error != nil {
if *res.Error == "authorization_pending" {
continue
}
return res, errors.New(res.ErrorDescription)
}
return res, nil
case <-timeout:
return TokenResponse{}, ErrTimeout
}
}
}
// getToken calls the token endpoint of Auth0 and returns the response.
func (a API) getDeviceToken(ctx context.Context, state State) (TokenResponse, error) {
data := url.Values{
"client_id": {a.ClientID},
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
"device_code": {state.DeviceCode},
}
oauthTokenURL := a.TenantURL + "/oauth/token"
resp, err := postForm(ctx, oauthTokenURL, strings.NewReader(data.Encode()))
if err != nil {
return TokenResponse{}, fmt.Errorf("failed to get tokens: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()
// this endpoint returns a 403 with an `authorization_pending` error until the
// user has authenticated, so we don't check the status code here and instead
// decode the response and check for the error.
var res TokenResponse
err = json.NewDecoder(resp.Body).Decode(&res)
if err != nil {
return res, fmt.Errorf("failed to decode response: %w", err)
}
return res, nil
}
// RevokeToken revokes a refresh token with the tenant so that it can no longer
// be used to get new tokens.
func (a API) RevokeToken(ctx context.Context, refreshToken string) error {
data := url.Values{
"client_id": {a.ClientID},
"token": {refreshToken},
}
revokeURL := a.TenantURL + "/oauth/revoke"
resp, err := postForm(ctx, revokeURL, strings.NewReader(data.Encode()))
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return tryDecodeOAuthError(resp)
}
return nil
}
func postForm(ctx context.Context, reqURL string, data io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, data)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
cliVersion := strings.ReplaceAll(version.Version, ".", "_")
req.Header.Set("User-Agent", fmt.Sprintf("docker-cli:%s:%s-%s", cliVersion, runtime.GOOS, runtime.GOARCH))
return http.DefaultClient.Do(req)
}
func (a API) GetAutoPAT(ctx context.Context, audience string, res TokenResponse) (string, error) {
patURL := audience + "/v2/access-tokens/desktop-generate"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, patURL, nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+res.AccessToken)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("unexpected response from Hub: %s", resp.Status)
}
var response patGenerateResponse
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return "", err
}
return response.Data.Token, nil
}
type patGenerateResponse struct {
Data struct {
Token string `json:"token"`
}
}

View File

@ -0,0 +1,428 @@
package api
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"gotest.tools/v3/assert"
)
func TestGetDeviceCode(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) {
t.Parallel()
var clientID, audience, scope, path string
expectedState := State{
DeviceCode: "aDeviceCode",
UserCode: "aUserCode",
VerificationURI: "aVerificationURI",
ExpiresIn: 60,
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
clientID = r.FormValue("client_id")
audience = r.FormValue("audience")
scope = r.FormValue("scope")
path = r.URL.Path
jsonState, err := json.Marshal(expectedState)
assert.NilError(t, err)
_, _ = w.Write(jsonState)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
state, err := api.GetDeviceCode(context.Background(), "anAudience")
assert.NilError(t, err)
assert.DeepEqual(t, expectedState, state)
assert.Equal(t, clientID, "aClientID")
assert.Equal(t, audience, "anAudience")
assert.Equal(t, scope, "bork meow")
assert.Equal(t, path, "/oauth/device/code")
})
t.Run("error w/ description", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
jsonState, err := json.Marshal(TokenResponse{
ErrorDescription: "invalid audience",
})
assert.NilError(t, err)
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write(jsonState)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
_, err := api.GetDeviceCode(context.Background(), "bad_audience")
assert.ErrorContains(t, err, "invalid audience")
})
t.Run("general error", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "an error", http.StatusInternalServerError)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
_, err := api.GetDeviceCode(context.Background(), "anAudience")
assert.ErrorContains(t, err, "unexpected response from tenant: 500 Internal Server Error")
})
t.Run("canceled context", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
time.Sleep(2 * time.Second)
http.Error(w, "an error", http.StatusInternalServerError)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(1 * time.Second)
cancel()
}()
_, err := api.GetDeviceCode(ctx, "anAudience")
assert.ErrorContains(t, err, "context canceled")
})
}
func TestWaitForDeviceToken(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) {
t.Parallel()
expectedToken := TokenResponse{
AccessToken: "a-real-token",
IDToken: "",
RefreshToken: "the-refresh-token",
Scope: "",
ExpiresIn: 3600,
TokenType: "",
}
var respond atomic.Bool
go func() {
time.Sleep(5 * time.Second)
respond.Store(true)
}()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/oauth/token", r.URL.Path)
assert.Equal(t, r.FormValue("client_id"), "aClientID")
assert.Equal(t, r.FormValue("grant_type"), "urn:ietf:params:oauth:grant-type:device_code")
assert.Equal(t, r.FormValue("device_code"), "aDeviceCode")
if respond.Load() {
jsonState, err := json.Marshal(expectedToken)
assert.NilError(t, err)
w.Write(jsonState)
} else {
pendingError := "authorization_pending"
jsonResponse, err := json.Marshal(TokenResponse{
Error: &pendingError,
})
assert.NilError(t, err)
w.Write(jsonResponse)
}
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
state := State{
DeviceCode: "aDeviceCode",
UserCode: "aUserCode",
Interval: 1,
ExpiresIn: 30,
}
token, err := api.WaitForDeviceToken(context.Background(), state)
assert.NilError(t, err)
assert.DeepEqual(t, token, expectedToken)
})
t.Run("timeout", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/oauth/token", r.URL.Path)
assert.Equal(t, r.FormValue("client_id"), "aClientID")
assert.Equal(t, r.FormValue("grant_type"), "urn:ietf:params:oauth:grant-type:device_code")
assert.Equal(t, r.FormValue("device_code"), "aDeviceCode")
pendingError := "authorization_pending"
jsonResponse, err := json.Marshal(TokenResponse{
Error: &pendingError,
})
assert.NilError(t, err)
w.Write(jsonResponse)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
state := State{
DeviceCode: "aDeviceCode",
UserCode: "aUserCode",
Interval: 1,
ExpiresIn: 1,
}
_, err := api.WaitForDeviceToken(context.Background(), state)
assert.ErrorIs(t, err, ErrTimeout)
})
t.Run("canceled context", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
pendingError := "authorization_pending"
jsonResponse, err := json.Marshal(TokenResponse{
Error: &pendingError,
})
assert.NilError(t, err)
w.Write(jsonResponse)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
state := State{
DeviceCode: "aDeviceCode",
UserCode: "aUserCode",
Interval: 1,
ExpiresIn: 5,
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(1 * time.Second)
cancel()
}()
_, err := api.WaitForDeviceToken(ctx, state)
assert.ErrorContains(t, err, "context canceled")
})
}
func TestRevoke(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/oauth/revoke", r.URL.Path)
assert.Equal(t, r.FormValue("client_id"), "aClientID")
assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token")
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
err := api.RevokeToken(context.Background(), "v1.a-refresh-token")
assert.NilError(t, err)
})
t.Run("unexpected response", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/oauth/revoke", r.URL.Path)
assert.Equal(t, r.FormValue("client_id"), "aClientID")
assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token")
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
err := api.RevokeToken(context.Background(), "v1.a-refresh-token")
assert.ErrorContains(t, err, "unexpected response from tenant: 404 Not Found")
})
t.Run("error w/ description", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jsonState, err := json.Marshal(TokenResponse{
ErrorDescription: "invalid client id",
})
assert.NilError(t, err)
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write(jsonState)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
err := api.RevokeToken(context.Background(), "v1.a-refresh-token")
assert.ErrorContains(t, err, "invalid client id")
})
t.Run("canceled context", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/oauth/revoke", r.URL.Path)
assert.Equal(t, r.FormValue("client_id"), "aClientID")
assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token")
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
err := api.RevokeToken(ctx, "v1.a-refresh-token")
assert.ErrorContains(t, err, "context canceled")
})
}
func TestGetAutoPAT(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/v2/access-tokens/desktop-generate", r.URL.Path)
assert.Equal(t, "Bearer bork", r.Header.Get("Authorization"))
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
marshalledResponse, err := json.Marshal(patGenerateResponse{
Data: struct {
Token string `json:"token"`
}{
Token: "a-docker-pat",
},
})
assert.NilError(t, err)
w.WriteHeader(http.StatusCreated)
w.Write(marshalledResponse)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
pat, err := api.GetAutoPAT(context.Background(), ts.URL, TokenResponse{
AccessToken: "bork",
})
assert.NilError(t, err)
assert.Equal(t, "a-docker-pat", pat)
})
t.Run("general error", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
_, err := api.GetAutoPAT(context.Background(), ts.URL, TokenResponse{
AccessToken: "bork",
})
assert.ErrorContains(t, err, "unexpected response from Hub: 500 Internal Server Error")
})
t.Run("context canceled", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/v2/access-tokens/desktop-generate", r.URL.Path)
assert.Equal(t, "Bearer bork", r.Header.Get("Authorization"))
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
marshalledResponse, err := json.Marshal(patGenerateResponse{
Data: struct {
Token string `json:"token"`
}{
Token: "a-docker-pat",
},
})
assert.NilError(t, err)
time.Sleep(500 * time.Millisecond)
w.WriteHeader(http.StatusCreated)
w.Write(marshalledResponse)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
pat, err := api.GetAutoPAT(ctx, ts.URL, TokenResponse{
AccessToken: "bork",
})
assert.ErrorContains(t, err, "context canceled")
assert.Equal(t, "", pat)
})
}

View File

@ -0,0 +1,26 @@
package api
import (
"time"
)
// State represents the state of exchange after submitting.
type State struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri_complete"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
// IntervalDuration returns the duration that should be waited between each auth
// polling event.
func (s State) IntervalDuration() time.Duration {
return time.Second * time.Duration(s.Interval)
}
// ExpiryDuration returns the total duration for which the client should keep
// polling.
func (s State) ExpiryDuration() time.Duration {
return time.Second * time.Duration(s.ExpiresIn)
}

73
cli/internal/oauth/jwt.go Normal file
View File

@ -0,0 +1,73 @@
package oauth
import (
"github.com/go-jose/go-jose/v3/jwt"
)
// Claims represents standard claims along with some custom ones.
type Claims struct {
jwt.Claims
// Domain is the domain claims for the token.
Domain DomainClaims `json:"https://hub.docker.com"`
// Scope is the scopes for the claims as a string that is space delimited.
Scope string `json:"scope,omitempty"`
}
// DomainClaims represents a custom claim data set that doesn't change the spec
// payload. This is primarily introduced by Auth0 and is defined by a fully
// specified URL as it's key. e.g. "https://hub.docker.com"
type DomainClaims struct {
// UUID is the user, machine client, or organization's UUID in our database.
UUID string `json:"uuid"`
// Email is the user's email address.
Email string `json:"email"`
// Username is the user's username.
Username string `json:"username"`
// Source is the source of the JWT. This should look like
// `docker_{type}|{id}`.
Source string `json:"source"`
// SessionID is the unique ID of the token.
SessionID string `json:"session_id"`
// ClientID is the client_id that generated the token. This is filled if
// M2M.
ClientID string `json:"client_id,omitempty"`
// ClientName is the name of the client that generated the token. This is
// filled if M2M.
ClientName string `json:"client_name,omitempty"`
}
// Source represents a source of a JWT.
type Source struct {
// Type is the type of source. This could be "pat" etc.
Type string `json:"type"`
// ID is the identifier to the source type. If "pat" then this will be the
// ID of the PAT.
ID string `json:"id"`
}
// GetClaims returns claims from an access token without verification.
func GetClaims(accessToken string) (claims Claims, err error) {
token, err := parseSigned(accessToken)
if err != nil {
return
}
err = token.UnsafeClaimsWithoutVerification(&claims)
return
}
// parseSigned parses a JWT and returns the signature object or error. This does
// not verify the validity of the JWT.
func parseSigned(token string) (*jwt.JSONWebToken, error) {
return jwt.ParseSigned(token)
}

View File

@ -0,0 +1,204 @@
package manager
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/internal/oauth"
"github.com/docker/cli/cli/internal/oauth/api"
"github.com/docker/docker/registry"
"github.com/morikuni/aec"
"github.com/sirupsen/logrus"
"github.com/pkg/browser"
)
// OAuthManager is the manager responsible for handling authentication
// flows with the oauth tenant.
type OAuthManager struct {
store credentials.Store
tenant string
audience string
clientID string
api api.OAuthAPI
openBrowser func(string) error
}
// OAuthManagerOptions are the options used for New to create a new auth manager.
type OAuthManagerOptions struct {
Store credentials.Store
Audience string
ClientID string
Scopes []string
Tenant string
DeviceName string
OpenBrowser func(string) error
}
func New(options OAuthManagerOptions) *OAuthManager {
scopes := []string{"openid", "offline_access"}
if len(options.Scopes) > 0 {
scopes = options.Scopes
}
openBrowser := options.OpenBrowser
if openBrowser == nil {
// Prevent errors from missing binaries (like xdg-open) from
// cluttering the output. We can handle errors ourselves.
browser.Stdout = io.Discard
browser.Stderr = io.Discard
openBrowser = browser.OpenURL
}
return &OAuthManager{
clientID: options.ClientID,
audience: options.Audience,
tenant: options.Tenant,
store: options.Store,
api: api.API{
TenantURL: "https://" + options.Tenant,
ClientID: options.ClientID,
Scopes: scopes,
},
openBrowser: openBrowser,
}
}
var ErrDeviceLoginStartFail = errors.New("failed to start device code flow login")
// LoginDevice launches the device authentication flow with the tenant,
// printing instructions to the provided writer and attempting to open the
// browser for the user to authenticate.
// After the user completes the browser login, LoginDevice uses the retrieved
// tokens to create a Hub PAT which is returned to the caller.
// The retrieved tokens are stored in the credentials store (under a separate
// key), and the refresh token is concatenated with the client ID.
func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.AuthConfig, error) {
state, err := m.api.GetDeviceCode(ctx, m.audience)
if err != nil {
logrus.Debugf("failed to start device code login: %v", err)
return nil, ErrDeviceLoginStartFail
}
if state.UserCode == "" {
logrus.Debugf("failed to start device code login: missing user code")
return nil, ErrDeviceLoginStartFail
}
_, _ = fmt.Fprintln(w, aec.Bold.Apply("\nUSING WEB BASED LOGIN"))
_, _ = fmt.Fprintln(w, "To sign in with credentials on the command line, use 'docker login -u <username>'")
_, _ = fmt.Fprintf(w, "\nYour one-time device confirmation code is: "+aec.Bold.Apply("%s\n"), state.UserCode)
_, _ = fmt.Fprintf(w, aec.Bold.Apply("Press ENTER")+" to open your browser or submit your device code here: "+aec.Underline.Apply("%s\n"), strings.Split(state.VerificationURI, "?")[0])
tokenResChan := make(chan api.TokenResponse)
waitForTokenErrChan := make(chan error)
go func() {
tokenRes, err := m.api.WaitForDeviceToken(ctx, state)
if err != nil {
waitForTokenErrChan <- err
return
}
tokenResChan <- tokenRes
}()
go func() {
reader := bufio.NewReader(os.Stdin)
_, _ = reader.ReadString('\n')
_ = m.openBrowser(state.VerificationURI)
}()
_, _ = fmt.Fprint(w, "\nWaiting for authentication in the browser…\n")
var tokenRes api.TokenResponse
select {
case <-ctx.Done():
return nil, errors.New("login canceled")
case err := <-waitForTokenErrChan:
return nil, fmt.Errorf("failed waiting for authentication: %w", err)
case tokenRes = <-tokenResChan:
}
claims, err := oauth.GetClaims(tokenRes.AccessToken)
if err != nil {
return nil, fmt.Errorf("failed to parse token claims: %w", err)
}
err = m.storeTokensInStore(tokenRes, claims.Domain.Username)
if err != nil {
return nil, fmt.Errorf("failed to store tokens: %w", err)
}
pat, err := m.api.GetAutoPAT(ctx, m.audience, tokenRes)
if err != nil {
return nil, err
}
return &types.AuthConfig{
Username: claims.Domain.Username,
Password: pat,
ServerAddress: registry.IndexServer,
}, nil
}
// Logout fetches the refresh token from the store and revokes it
// with the configured oauth tenant. The stored access and refresh
// tokens are then erased from the store.
// If the refresh token is not found in the store, an error is not
// returned.
func (m *OAuthManager) Logout(ctx context.Context) error {
refreshConfig, err := m.store.Get(refreshTokenKey)
if err != nil {
return err
}
if refreshConfig.Password == "" {
return nil
}
parts := strings.Split(refreshConfig.Password, "..")
if len(parts) != 2 {
// the token wasn't stored by the CLI, so don't revoke it
// or erase it from the store/error
return nil
}
// erase the token from the store first, that way
// if the revoke fails, the user can try to logout again
if err := m.eraseTokensFromStore(); err != nil {
return fmt.Errorf("failed to erase tokens: %w", err)
}
if err := m.api.RevokeToken(ctx, parts[0]); err != nil {
return fmt.Errorf("credentials erased successfully, but there was a failure to revoke the OAuth refresh token with the tenant: %w", err)
}
return nil
}
const (
accessTokenKey = registry.IndexServer + "access-token"
refreshTokenKey = registry.IndexServer + "refresh-token"
)
func (m *OAuthManager) storeTokensInStore(tokens api.TokenResponse, username string) error {
return errors.Join(
m.store.Store(types.AuthConfig{
Username: username,
Password: tokens.AccessToken,
ServerAddress: accessTokenKey,
}),
m.store.Store(types.AuthConfig{
Username: username,
Password: tokens.RefreshToken + ".." + m.clientID,
ServerAddress: refreshTokenKey,
}),
)
}
func (m *OAuthManager) eraseTokensFromStore() error {
return errors.Join(
m.store.Erase(accessTokenKey),
m.store.Erase(refreshTokenKey),
)
}

View File

@ -0,0 +1,363 @@
package manager
import (
"context"
"errors"
"os"
"testing"
"time"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/internal/oauth/api"
"gotest.tools/v3/assert"
)
const (
//nolint:lll
validToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InhYa3BCdDNyV3MyRy11YjlscEpncSJ9.eyJodHRwczovL2h1Yi5kb2NrZXIuY29tIjp7ImVtYWlsIjoiYm9ya0Bkb2NrZXIuY29tIiwic2Vzc2lvbl9pZCI6ImEtc2Vzc2lvbi1pZCIsInNvdXJjZSI6InNhbWxwIiwidXNlcm5hbWUiOiJib3JrISIsInV1aWQiOiIwMTIzLTQ1Njc4OSJ9LCJpc3MiOiJodHRwczovL2xvZ2luLmRvY2tlci5jb20vIiwic3ViIjoic2FtbHB8c2FtbHAtZG9ja2VyfGJvcmtAZG9ja2VyLmNvbSIsImF1ZCI6WyJodHRwczovL2F1ZGllbmNlLmNvbSJdLCJpYXQiOjE3MTk1MDI5MzksImV4cCI6MTcxOTUwNjUzOSwic2NvcGUiOiJvcGVuaWQgb2ZmbGluZV9hY2Nlc3MifQ.VUSp-9_SOvMPWJPRrSh7p4kSPoye4DA3kyd2I0TW0QtxYSRq7xCzNj0NC_ywlPlKBFBeXKm4mh93d1vBSh79I9Heq5tj0Fr4KH77U5xJRMEpjHqoT5jxMEU1hYXX92xctnagBMXxDvzUfu3Yf0tvYSA0RRoGbGTHfdYYRwOrGbwQ75Qg1dyIxUkwsG053eYX2XkmLGxymEMgIq_gWksgAamOc40_0OCdGr-MmDeD2HyGUa309aGltzQUw7Z0zG1AKSXy3WwfMHdWNFioTAvQphwEyY3US8ybSJi78upSFTjwUcryMeHUwQ3uV9PxwPMyPoYxo1izVB-OUJxM8RqEbg"
)
// parsed token:
// {
// "https://hub.docker.com": {
// "email": "bork@docker.com",
// "session_id": "a-session-id",
// "source": "samlp",
// "username": "bork!",
// "uuid": "0123-456789"
// },
// "iss": "https://login.docker.com/",
// "sub": "samlp|samlp-docker|bork@docker.com",
// "aud": [
// "https://audience.com"
// ],
// "iat": 1719502939,
// "exp": 1719506539,
// "scope": "openid offline_access"
// }
func TestLoginDevice(t *testing.T) {
t.Run("valid token", func(t *testing.T) {
expectedState := api.State{
DeviceCode: "device-code",
UserCode: "0123-4567",
VerificationURI: "an-url",
ExpiresIn: 300,
}
var receivedAudience string
getDeviceToken := func(audience string) (api.State, error) {
receivedAudience = audience
return expectedState, nil
}
var receivedState api.State
waitForDeviceToken := func(state api.State) (api.TokenResponse, error) {
receivedState = state
return api.TokenResponse{
AccessToken: validToken,
RefreshToken: "refresh-token",
}, nil
}
var receivedAccessToken, getPatReceivedAudience string
getAutoPat := func(audience string, res api.TokenResponse) (string, error) {
receivedAccessToken = res.AccessToken
getPatReceivedAudience = audience
return "a-pat", nil
}
api := &testAPI{
getDeviceToken: getDeviceToken,
waitForDeviceToken: waitForDeviceToken,
getAutoPAT: getAutoPat,
}
store := newStore(map[string]types.AuthConfig{})
manager := OAuthManager{
store: credentials.NewFileStore(store),
audience: "https://hub.docker.com",
api: api,
openBrowser: func(url string) error {
return nil
},
}
authConfig, err := manager.LoginDevice(context.Background(), os.Stderr)
assert.NilError(t, err)
assert.Equal(t, receivedAudience, "https://hub.docker.com")
assert.Equal(t, receivedState, expectedState)
assert.DeepEqual(t, authConfig, &types.AuthConfig{
Username: "bork!",
Password: "a-pat",
ServerAddress: "https://index.docker.io/v1/",
})
assert.Equal(t, receivedAccessToken, validToken)
assert.Equal(t, getPatReceivedAudience, "https://hub.docker.com")
})
t.Run("stores in cred store", func(t *testing.T) {
getDeviceToken := func(audience string) (api.State, error) {
return api.State{
DeviceCode: "device-code",
UserCode: "0123-4567",
}, nil
}
waitForDeviceToken := func(state api.State) (api.TokenResponse, error) {
return api.TokenResponse{
AccessToken: validToken,
RefreshToken: "refresh-token",
}, nil
}
getAutoPAT := func(audience string, res api.TokenResponse) (string, error) {
return "a-pat", nil
}
a := &testAPI{
getDeviceToken: getDeviceToken,
waitForDeviceToken: waitForDeviceToken,
getAutoPAT: getAutoPAT,
}
store := newStore(map[string]types.AuthConfig{})
manager := OAuthManager{
clientID: "client-id",
store: credentials.NewFileStore(store),
api: a,
openBrowser: func(url string) error {
return nil
},
}
authConfig, err := manager.LoginDevice(context.Background(), os.Stderr)
assert.NilError(t, err)
assert.Equal(t, authConfig.Password, "a-pat")
assert.Equal(t, authConfig.Username, "bork!")
assert.Equal(t, len(store.configs), 2)
assert.Equal(t, store.configs["https://index.docker.io/v1/access-token"].Password, validToken)
assert.Equal(t, store.configs["https://index.docker.io/v1/refresh-token"].Password, "refresh-token..client-id")
})
t.Run("timeout", func(t *testing.T) {
getDeviceToken := func(audience string) (api.State, error) {
return api.State{
DeviceCode: "device-code",
UserCode: "0123-4567",
VerificationURI: "an-url",
ExpiresIn: 300,
}, nil
}
waitForDeviceToken := func(state api.State) (api.TokenResponse, error) {
return api.TokenResponse{}, api.ErrTimeout
}
a := &testAPI{
getDeviceToken: getDeviceToken,
waitForDeviceToken: waitForDeviceToken,
}
manager := OAuthManager{
api: a,
openBrowser: func(url string) error {
return nil
},
}
_, err := manager.LoginDevice(context.Background(), os.Stderr)
assert.ErrorContains(t, err, "failed waiting for authentication: timed out waiting for device token")
})
t.Run("canceled context", func(t *testing.T) {
getDeviceToken := func(audience string) (api.State, error) {
return api.State{
DeviceCode: "device-code",
UserCode: "0123-4567",
}, nil
}
waitForDeviceToken := func(state api.State) (api.TokenResponse, error) {
// make sure that the context is cancelled before this returns
time.Sleep(500 * time.Millisecond)
return api.TokenResponse{
AccessToken: validToken,
RefreshToken: "refresh-token",
}, nil
}
a := &testAPI{
getDeviceToken: getDeviceToken,
waitForDeviceToken: waitForDeviceToken,
}
manager := OAuthManager{
api: a,
openBrowser: func(url string) error {
return nil
},
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := manager.LoginDevice(ctx, os.Stderr)
assert.ErrorContains(t, err, "login canceled")
})
}
func TestLogout(t *testing.T) {
t.Run("successfully revokes token", func(t *testing.T) {
var receivedToken string
a := &testAPI{
revokeToken: func(token string) error {
receivedToken = token
return nil
},
}
store := newStore(map[string]types.AuthConfig{
"https://index.docker.io/v1/access-token": {
Password: validToken,
},
"https://index.docker.io/v1/refresh-token": {
Password: "a-refresh-token..client-id",
},
})
manager := OAuthManager{
store: credentials.NewFileStore(store),
api: a,
}
err := manager.Logout(context.Background())
assert.NilError(t, err)
assert.Equal(t, receivedToken, "a-refresh-token")
assert.Equal(t, len(store.configs), 0)
})
t.Run("error revoking token", func(t *testing.T) {
a := &testAPI{
revokeToken: func(token string) error {
return errors.New("couldn't reach tenant")
},
}
store := newStore(map[string]types.AuthConfig{
"https://index.docker.io/v1/access-token": {
Password: validToken,
},
"https://index.docker.io/v1/refresh-token": {
Password: "a-refresh-token..client-id",
},
})
manager := OAuthManager{
store: credentials.NewFileStore(store),
api: a,
}
err := manager.Logout(context.Background())
assert.ErrorContains(t, err, "credentials erased successfully, but there was a failure to revoke the OAuth refresh token with the tenant: couldn't reach tenant")
assert.Equal(t, len(store.configs), 0)
})
t.Run("invalid refresh token", func(t *testing.T) {
var triedRevoke bool
a := &testAPI{
revokeToken: func(token string) error {
triedRevoke = true
return nil
},
}
store := newStore(map[string]types.AuthConfig{
"https://index.docker.io/v1/access-token": {
Password: validToken,
},
"https://index.docker.io/v1/refresh-token": {
Password: "a-refresh-token-without-client-id",
},
})
manager := OAuthManager{
store: credentials.NewFileStore(store),
api: a,
}
err := manager.Logout(context.Background())
assert.NilError(t, err)
assert.Check(t, !triedRevoke)
})
t.Run("no refresh token", func(t *testing.T) {
a := &testAPI{}
var triedRevoke bool
revokeToken := func(token string) error {
triedRevoke = true
return nil
}
a.revokeToken = revokeToken
store := newStore(map[string]types.AuthConfig{})
manager := OAuthManager{
store: credentials.NewFileStore(store),
api: a,
}
err := manager.Logout(context.Background())
assert.NilError(t, err)
assert.Check(t, !triedRevoke)
})
}
var _ api.OAuthAPI = &testAPI{}
type testAPI struct {
getDeviceToken func(audience string) (api.State, error)
waitForDeviceToken func(state api.State) (api.TokenResponse, error)
refresh func(token string) (api.TokenResponse, error)
revokeToken func(token string) error
getAutoPAT func(audience string, res api.TokenResponse) (string, error)
}
func (t *testAPI) GetDeviceCode(_ context.Context, audience string) (api.State, error) {
if t.getDeviceToken != nil {
return t.getDeviceToken(audience)
}
return api.State{}, nil
}
func (t *testAPI) WaitForDeviceToken(_ context.Context, state api.State) (api.TokenResponse, error) {
if t.waitForDeviceToken != nil {
return t.waitForDeviceToken(state)
}
return api.TokenResponse{}, nil
}
func (t *testAPI) Refresh(_ context.Context, token string) (api.TokenResponse, error) {
if t.refresh != nil {
return t.refresh(token)
}
return api.TokenResponse{}, nil
}
func (t *testAPI) RevokeToken(_ context.Context, token string) error {
if t.revokeToken != nil {
return t.revokeToken(token)
}
return nil
}
func (t *testAPI) GetAutoPAT(_ context.Context, audience string, res api.TokenResponse) (string, error) {
if t.getAutoPAT != nil {
return t.getAutoPAT(audience, res)
}
return "", nil
}
type fakeStore struct {
configs map[string]types.AuthConfig
}
func (f *fakeStore) Save() error {
return nil
}
func (f *fakeStore) GetAuthConfigs() map[string]types.AuthConfig {
return f.configs
}
func (f *fakeStore) GetFilename() string {
return "/tmp/docker-fakestore"
}
func newStore(auths map[string]types.AuthConfig) *fakeStore {
return &fakeStore{configs: auths}
}

View File

@ -0,0 +1,28 @@
package manager
import (
"fmt"
"runtime"
"strings"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/version"
)
const (
audience = "https://hub.docker.com"
tenant = "login.docker.com"
clientID = "L4v0dmlNBpYUjGGab0C2JtgTgXr1Qz4d"
)
func NewManager(store credentials.Store) *OAuthManager {
cliVersion := strings.ReplaceAll(version.Version, ".", "_")
options := OAuthManagerOptions{
Store: store,
Audience: audience,
ClientID: clientID,
Tenant: tenant,
DeviceName: fmt.Sprintf("docker-cli:%s:%s-%s", cliVersion, runtime.GOOS, runtime.GOARCH),
}
return New(options)
}

View File

@ -27,16 +27,16 @@ func NoArgs(cmd *cobra.Command, args []string) error {
}
// RequiresMinArgs returns an error if there is not at least min args
func RequiresMinArgs(min int) cobra.PositionalArgs {
func RequiresMinArgs(minArgs int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) >= min {
if len(args) >= minArgs {
return nil
}
return errors.Errorf(
"%q requires at least %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s",
cmd.CommandPath(),
min,
pluralize("argument", min),
minArgs,
pluralize("argument", minArgs),
cmd.CommandPath(),
cmd.UseLine(),
cmd.Short,
@ -45,16 +45,16 @@ func RequiresMinArgs(min int) cobra.PositionalArgs {
}
// RequiresMaxArgs returns an error if there is not at most max args
func RequiresMaxArgs(max int) cobra.PositionalArgs {
func RequiresMaxArgs(maxArgs int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) <= max {
if len(args) <= maxArgs {
return nil
}
return errors.Errorf(
"%q requires at most %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s",
cmd.CommandPath(),
max,
pluralize("argument", max),
maxArgs,
pluralize("argument", maxArgs),
cmd.CommandPath(),
cmd.UseLine(),
cmd.Short,
@ -63,17 +63,17 @@ func RequiresMaxArgs(max int) cobra.PositionalArgs {
}
// RequiresRangeArgs returns an error if there is not at least min args and at most max args
func RequiresRangeArgs(min int, max int) cobra.PositionalArgs {
func RequiresRangeArgs(minArgs int, maxArgs int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) >= min && len(args) <= max {
if len(args) >= minArgs && len(args) <= maxArgs {
return nil
}
return errors.Errorf(
"%q requires at least %d and at most %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s",
cmd.CommandPath(),
min,
max,
pluralize("argument", max),
minArgs,
maxArgs,
pluralize("argument", maxArgs),
cmd.CommandPath(),
cmd.UseLine(),
cmd.Short,

View File

@ -562,8 +562,7 @@ Docker API v1.42 and up now ignores this option when set. Older versions of the
API continue to accept the option, but depending on the OCI runtime used, may
take no effect.
> **Note**
>
> [!NOTE]
> While not deprecated (yet) in Docker, the OCI runtime specification also
> deprecated the `memory.kmem.tcp.limit_in_bytes` option. When using `runc` as
> runtime, this option takes no effect. The linux kernel did not explicitly

View File

@ -16,8 +16,7 @@ plugins using Docker Engine.
For information about legacy (non-managed) plugins, refer to
[Understand legacy Docker Engine plugins](legacy_plugins.md).
> **Note**
>
> [!NOTE]
> Docker Engine managed plugins are currently not supported on Windows daemons.
## Installing and using a plugin
@ -38,8 +37,7 @@ operation, such as creating a volume.
In the following example, you install the `sshfs` plugin, verify that it is
enabled, and use it to create a volume.
> **Note**
>
> [!NOTE]
> This example is intended for instructional purposes only. Once the volume is
> created, your SSH password to the remote host is exposed as plaintext when
> inspecting the volume. Delete the volume as soon as you are done with the
@ -126,8 +124,7 @@ commands and options, see the
The `rootfs` directory represents the root filesystem of the plugin. In this
example, it was created from a Dockerfile:
> **Note**
>
> [!NOTE]
> The `/run/docker/plugins` directory is mandatory inside of the
> plugin's filesystem for Docker to communicate with the plugin.

View File

@ -43,8 +43,7 @@ Authorization plugins must follow the rules described in [Docker Plugin API](plu
Each plugin must reside within directories described under the
[Plugin discovery](plugin_api.md#plugin-discovery) section.
> **Note**
>
> [!NOTE]
> The abbreviations `AuthZ` and `AuthN` mean authorization and authentication
> respectively.

View File

@ -8,8 +8,7 @@ Docker exposes internal metrics based on the Prometheus format. Metrics plugins
enable accessing these metrics in a consistent way by providing a Unix
socket at a predefined path where the plugin can scrape the metrics.
> **Note**
>
> [!NOTE]
> While the plugin interface for metrics is non-experimental, the naming of the
> metrics and metric labels is still considered experimental and may change in a
> future version.

View File

@ -80,8 +80,7 @@ provide the Docker Daemon with writeable paths on the host filesystem. The Docke
daemon provides these paths to containers to consume. The Docker daemon makes
the volumes available by bind-mounting the provided paths into the containers.
> **Note**
>
> [!NOTE]
> Volume plugins should *not* write data to the `/var/lib/docker/` directory,
> including `/var/lib/docker/volumes`. The `/var/lib/docker/` directory is
> reserved for Docker.

View File

@ -19,8 +19,7 @@ Creates a config using standard input or from a file for the config content.
For detailed information about using configs, refer to [store configuration data using Docker Configs](https://docs.docker.com/engine/swarm/configs/).
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a Swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -25,8 +25,7 @@ describes all the details of the format.
For detailed information about using configs, refer to [store configuration data using Docker Configs](https://docs.docker.com/engine/swarm/configs/).
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a Swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -24,8 +24,7 @@ Run this command on a manager node to list the configs in the Swarm.
For detailed information about using configs, refer to [store configuration data using Docker Configs](https://docs.docker.com/engine/swarm/configs/).
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a Swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -16,8 +16,7 @@ Removes the specified configs from the Swarm.
For detailed information about using configs, refer to [store configuration data using Docker Configs](https://docs.docker.com/engine/swarm/configs/).
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a Swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the
@ -32,8 +31,7 @@ $ docker config rm my_config
sapth4csdo5b6wz2p5uimh5xg
```
> **Warning**
>
> [!WARNING]
> This command doesn't ask for confirmation before removing a config.
{ .warning }

View File

@ -25,8 +25,7 @@ Use `docker attach` to attach your terminal's standard input, output, and error
ID or name. This lets you view its output or control it interactively, as
though the commands were running directly in your terminal.
> **Note**
>
> [!NOTE]
> The `attach` command displays the output of the container's `ENTRYPOINT` and
> `CMD` process. This can appear as if the attach command is hung when in fact
> the process may simply not be writing any output at that time.
@ -39,8 +38,7 @@ container. If `--sig-proxy` is true (the default),`CTRL-c` sends a `SIGINT` to
the container. If the container was run with `-i` and `-t`, you can detach from
a container and leave it running using the `CTRL-p CTRL-q` key sequence.
> **Note**
>
> [!NOTE]
> A process running as PID 1 inside a container is treated specially by
> Linux: it ignores any signal with the default action. So, the process
> doesn't terminate on `SIGINT` or `SIGTERM` unless it's coded to do so.

View File

@ -33,8 +33,7 @@ set through `--signal` may be non-terminal, depending on the container's main
process. For example, the `SIGHUP` signal in most cases will be non-terminal,
and the container will continue running after receiving the signal.
> **Note**
>
> [!NOTE]
> `ENTRYPOINT` and `CMD` in the *shell* form run as a child process of
> `/bin/sh -c`, which does not pass signals. This means that the executable is
> not the containers PID 1 and does not receive Unix signals.

View File

@ -291,8 +291,7 @@ running processes in that namespace. By default, all containers, including
those with `--network=host`, have their own UTS namespace. Setting `--uts` to
`host` results in the container using the same UTS namespace as the host.
> **Note**
>
> [!NOTE]
> Docker disallows combining the `--hostname` and `--domainname` flags with
> `--uts=host`. This is to prevent containers running in the host's UTS
> namespace from attempting to change the hosts' configuration.
@ -350,8 +349,7 @@ In other words, the container can then do almost everything that the host can
do. This flag exists to allow special use-cases, like running Docker within
Docker.
> **Warning**
>
> [!WARNING]
> Use the `--privileged` flag with caution.
> A container with `--privileged` is not a securely sandboxed process.
> Containers in this mode can get a root shell on the host
@ -533,8 +531,7 @@ host. You can also specify `udp` and `sctp` ports. The [Networking overview
page](https://docs.docker.com/network/) explains in detail how to publish ports
with Docker.
> **Note**
>
> [!NOTE]
> If you don't specify an IP address (i.e., `-p 80:80` instead of `-p
> 127.0.0.1:80:80`) when publishing a container's ports, Docker publishes the
> port on all interfaces (address `0.0.0.0`) by default. These ports are
@ -715,8 +712,7 @@ or name. For `overlay` networks or custom plugins that support multi-host
connectivity, containers connected to the same multi-host network but launched
from different Engines can also communicate in this way.
> **Note**
>
> [!NOTE]
> The default bridge network only allows containers to communicate with each other using
> internal IP addresses. User-created bridge networks provide DNS resolution between
> containers using container names.
@ -784,8 +780,7 @@ $ docker network create --subnet 192.0.2.0/24 my-net
$ docker run -itd --network=name=my-net,\"driver-opt=com.docker.network.endpoint.sysctls=net.ipv4.conf.IFNAME.log_martians=1,net.ipv4.conf.IFNAME.forwarding=0\",ip=192.0.2.42 busybox
```
> **Note**
>
> [!NOTE]
> Network drivers may restrict the sysctl settings that can be modified and, to protect
> the operation of the network, new restrictions may be added in the future.
@ -912,8 +907,7 @@ $ docker run --device=/dev/sda:/dev/xvdc:m --rm -it ubuntu fdisk /dev/xvdc
fdisk: unable to open /dev/xvdc: Operation not permitted
```
> **Note**
>
> [!NOTE]
> The `--device` option cannot be safely used with ephemeral devices. You shouldn't
> add block devices that may be removed to untrusted containers with `--device`.
@ -935,15 +929,13 @@ ports on the host visible in the container.
PS C:\> docker run --device=class/86E0D1E0-8089-11D0-9CE4-08003E301F73 mcr.microsoft.com/windows/servercore:ltsc2019
```
> **Note**
>
> [!NOTE]
> The `--device` option is only supported on process-isolated Windows containers,
> and produces an error if the container isolation is `hyperv`.
#### CDI devices
> **Note**
>
> [!NOTE]
> The CDI feature is experimental, and potentially subject to change.
> CDI is currently only supported for Linux containers.
@ -1010,8 +1002,7 @@ ID once the container has finished running.
$ cat somefile | docker run -i -a stdin mybuilder dobuild
```
> **Note**
>
> [!NOTE]
> A process running as PID 1 inside a container is treated specially by
> Linux: it ignores any signal with the default action. So, the process
> doesn't terminate on `SIGINT` or `SIGTERM` unless it's coded to do so.
@ -1124,7 +1115,8 @@ $ docker run -d --device-cgroup-rule='c 42:* rmw' --name my-container my-image
Then, a user could ask `udev` to execute a script that would `docker exec my-container mknod newDevX c 42 <minor>`
the required device when it is added.
> **Note**: You still need to explicitly add initially present devices to the
> [!NOTE]
> You still need to explicitly add initially present devices to the
> `docker run` / `docker create` command.
### <a name="gpus"></a> Access an NVIDIA GPU
@ -1132,8 +1124,7 @@ the required device when it is added.
The `--gpus` flag allows you to access NVIDIA GPU resources. First you need to
install the [nvidia-container-runtime](https://nvidia.github.io/nvidia-container-runtime/).
> **Note**
>
> [!NOTE]
> You can also specify a GPU as a CDI device with the `--device` flag, see
> [CDI devices](#cdi-devices).
@ -1246,8 +1237,7 @@ the container and remove the file system when the container exits, use the
--rm=false: Automatically remove the container when it exits
```
> **Note**
>
> [!NOTE]
> If you set the `--rm` flag, Docker also removes the anonymous volumes
> associated with the container when the container is removed. This is similar
> to running `docker rm -v my-container`. Only volumes that are specified
@ -1345,14 +1335,12 @@ $ docker run --ulimit nofile=1024:1024 --rm debian sh -c "ulimit -n"
1024
```
> **Note**
>
> [!NOTE]
> If you don't provide a hard limit value, Docker uses the soft limit value
> for both values. If you don't provide any values, they are inherited from
> the default `ulimits` set on the daemon.
> **Note**
>
> [!NOTE]
> The `as` option is deprecated.
> In other words, the following script is not supported:
>
@ -1417,8 +1405,7 @@ the same content between containers.
$ docker run --security-opt label=level:s0:c100,c200 -it fedora bash
```
> **Note**
>
> [!NOTE]
> Automatic translation of MLS labels isn't supported.
To disable the security labeling for a container entirely, you can use
@ -1436,8 +1423,7 @@ that's only allowed to listen on Apache ports:
$ docker run --security-opt label=type:svirt_apache_t -it ubuntu bash
```
> **Note**
>
> [!NOTE]
> You would have to write policy defining a `svirt_apache_t` type.
To prevent your container processes from gaining additional privileges, you can
@ -1558,8 +1544,7 @@ network namespace, run this command:
$ docker run --sysctl net.ipv4.ip_forward=1 someimage
```
> **Note**
>
> [!NOTE]
> Not all sysctls are namespaced. Docker does not support changing sysctls
> inside of a container that also modify the host system. As the kernel
> evolves we expect to see more sysctls become namespaced.

View File

@ -29,8 +29,7 @@ containers do not return any data.
If you need more detailed information about a container's resource usage, use
the `/containers/(id)/stats` API endpoint.
> **Note**
>
> [!NOTE]
> On Linux, the Docker CLI reports memory usage by subtracting cache usage from
> the total memory usage. The API does not perform such a calculation but rather
> provides the total memory usage and the amount from the cache so that clients
@ -41,8 +40,7 @@ the `/containers/(id)/stats` API endpoint.
> field. On cgroup v2 hosts, the cache usage is defined as the value of
> `inactive_file` field.
> **Note**
>
> [!NOTE]
> The `PIDS` column contains the number of processes and kernel threads created
> by that container. Threads is the term used by Linux kernel. Other equivalent
> terms are "lightweight process" or "kernel task", etc. A large number in the

View File

@ -42,8 +42,7 @@ options on a running or a stopped container. On kernel version older than
4.6, you can only update `--kernel-memory` on a stopped container or on
a running container with kernel memory initialized.
> **Warning**
>
> [!WARNING]
> The `docker update` and `docker container update` commands are not supported
> for Windows containers.
{ .warning }
@ -78,8 +77,7 @@ running container only if the container was started with `--kernel-memory`.
If the container was started without `--kernel-memory` you need to stop
the container before updating kernel memory.
> **Note**
>
> [!NOTE]
> The `--kernel-memory` option has been deprecated since Docker 20.10.
For example, if you started a container with this command:

View File

@ -10,8 +10,7 @@ Block until one or more containers stop, then print their exit codes
<!---MARKER_GEN_END-->
> **Note**
>
> [!NOTE]
> `docker wait` returns `0` when run against a container which had already
> exited before the `docker wait` command was run.

View File

@ -186,8 +186,7 @@ Sometimes, multiple options can call for a more complex value string as for
$ docker run -v /host:/container example/mysql
```
> **Note**
>
> [!NOTE]
> Do not use the `-t` and `-a stderr` options together due to
> limitations in the `pty` implementation. All `stderr` in `pty` mode
> simply goes to `stdout`.
@ -247,8 +246,7 @@ By default, configuration file is stored in `~/.docker/config.json`. Refer to th
[change the `.docker` directory](#change-the-docker-directory) section to use a
different location.
> **Warning**
>
> [!WARNING]
> The configuration file and other files inside the `~/.docker` configuration
> directory may contain sensitive information, such as authentication information
> for proxies or, depending on your credential store, credentials for your image
@ -324,8 +322,7 @@ used as proxy settings for the `docker` CLI or the `dockerd` daemon. Refer to th
[environment variables](#environment-variables) and [HTTP/HTTPS proxy](https://docs.docker.com/engine/daemon/proxy/#httphttps-proxy)
sections for configuring proxy settings for the CLI and daemon.
> **Warning**
>
> [!WARNING]
> Proxy settings may contain sensitive information (for example, if the proxy
> requires authentication). Environment variables are stored as plain text in
> the container's configuration, and as such can be inspected through the remote
@ -464,8 +461,7 @@ daemon with IP address `174.17.0.1`, listening on port `2376`:
$ docker -H tcp://174.17.0.1:2376 ps
```
> **Note**
>
> [!NOTE]
> By convention, the Docker daemon uses port `2376` for secure TLS connections,
> and port `2375` for insecure, non-TLS connections.

View File

@ -47,8 +47,7 @@ Build an image from a Dockerfile
## Description
> **Note**
>
> [!NOTE]
> This page refers to the **legacy implementation** of `docker build`,
> using the legacy (pre-BuildKit) build backend.
> This configuration is only relevant if you're building Windows containers.
@ -93,7 +92,7 @@ in the context.
When using the legacy builder, it's therefore extra important that you
carefully consider what files you include in the context you specify. Use a
[`.dockerignore`](https://docs.docker.com/build/building/context/#dockerignore-files)
[`.dockerignore`](https://docs.docker.com/build/concepts/context/#dockerignore-files)
file to exclude files and directories that you don't require in your build from
being sent as part of the build context.
@ -146,7 +145,7 @@ the `credentialspec` option. The `credentialspec` must be in the format
#### Overview
> **Note**
> [!NOTE]
> The `--squash` option is an experimental feature, and should not be considered
> stable.

View File

@ -17,6 +17,7 @@ List images
| [`--format`](#format) | `string` | | Format output using a custom template:<br>'table': Print output in table format with column headers (default)<br>'table TEMPLATE': Print output in table format using the given Go template<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
| [`--no-trunc`](#no-trunc) | `bool` | | Don't truncate output |
| `-q`, `--quiet` | `bool` | | Only show image IDs |
| `--tree` | `bool` | | List multi-platform images as a tree (EXPERIMENTAL) |
<!---MARKER_GEN_END-->

View File

@ -82,6 +82,7 @@ which removes images with the specified labels. The other
format is the `label!=...` (`label!=<key>` or `label!=<key>=<value>`), which removes
images without the specified labels.
> [!NOTE]
> **Predicting what will be removed**
>
> If you are using positive filtering (testing for the existence of a label or
@ -186,8 +187,7 @@ This example removes images which have a maintainer label not set to `john`:
$ docker image prune --filter="label!=maintainer=john"
```
> **Note**
>
> [!NOTE]
> You are prompted for confirmation before the `prune` removes
> anything, but you are not shown a list of what will potentially be removed.
> In addition, `docker image ls` doesn't support negative filtering, so it

View File

@ -158,8 +158,7 @@ FROM ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217
LABEL org.opencontainers.image.authors="some maintainer <maintainer@example.com>"
```
> **Note**
>
> [!NOTE]
> Using this feature "pins" an image to a specific version in time.
> Docker does therefore not pull updated versions of an image, which may include
> security updates. If you want to pull an updated image, you need to change the

View File

@ -17,6 +17,7 @@ List images
| `--format` | `string` | | Format output using a custom template:<br>'table': Print output in table format with column headers (default)<br>'table TEMPLATE': Print output in table format using the given Go template<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
| `--no-trunc` | `bool` | | Don't truncate output |
| `-q`, `--quiet` | `bool` | | Only show image IDs |
| `--tree` | `bool` | | List multi-platform images as a tree (EXPERIMENTAL) |
<!---MARKER_GEN_END-->

View File

@ -284,8 +284,7 @@ $ docker manifest create --insecure myprivateregistry.mycompany.com/repo/image:1
$ docker manifest push --insecure myprivateregistry.mycompany.com/repo/image:tag
```
> **Note**
>
> [!NOTE]
> The `--insecure` flag is not required to annotate a manifest list,
> since annotations are to a locally-stored copy of a manifest list. You may also
> skip the `--insecure` flag if you are performing a `docker manifest inspect`

View File

@ -80,8 +80,7 @@ sets `net.ipv4.conf.eth3.log_martians=1` and `net.ipv4.conf.eth3.forwarding=0`.
$ docker network connect --driver-opt=\"com.docker.network.endpoint.sysctls=net.ipv4.conf.IFNAME.log_martians=1,net.ipv4.conf.IFNAME.forwarding=0\" multi-host-network container2
```
> **Note**
>
> [!NOTE]
> Network drivers may restrict the sysctl settings that can be modified and, to protect
> the operation of the network, new restrictions may be added in the future.

View File

@ -10,8 +10,7 @@ Demote one or more nodes from manager in the swarm
Demotes an existing manager so that it is no longer a manager.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the [Swarm mode
> section](https://docs.docker.com/engine/swarm/) in the documentation.

View File

@ -21,8 +21,7 @@ given template for each result. Go's
[text/template](https://pkg.go.dev/text/template) package describes all the
details of the format.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -24,8 +24,7 @@ Lists all the nodes that the Docker Swarm manager knows about. You can filter
using the `-f` or `--filter` flag. Refer to the [filtering](#filter) section
for more information about available filter options.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the
@ -42,8 +41,7 @@ ID HOSTNAME STATUS AVAILABILITY MANAGER STATU
e216jshn25ckzbvmwlnh5jr3g * swarm-manager1 Ready Active Leader
```
> **Note**
>
> [!NOTE]
> In the above example output, there is a hidden column of `.Self` that indicates
> if the node is the same node as the current docker daemon. A `*` (e.g.,
> `e216jshn25ckzbvmwlnh5jr3g *`) means this node is the current docker daemon.

View File

@ -10,8 +10,7 @@ Promote one or more nodes to manager in the swarm
Promotes a node to manager. This command can only be executed on a manager node.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -22,8 +22,7 @@ Lists all the tasks on a Node that Docker knows about. You can filter using the
`-f` or `--filter` flag. Refer to the [filtering](#filter) section for more
information about available filter options.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -20,8 +20,7 @@ Remove one or more nodes from the swarm
Removes the specified nodes from a swarm.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -19,8 +19,7 @@ Update a node
Update metadata about a node, such as its availability, labels, or roles.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -100,8 +100,7 @@ $ docker plugin inspect -f '{{with $mount := index .Settings.Mounts 0}}{{$mount.
/bar
```
> **Note**
>
> [!NOTE]
> Since only `source` is settable in `mymount`,
> `docker plugins set mymount=/bar myplugin` would work too.
@ -122,8 +121,7 @@ $ docker plugin inspect -f '{{with $device := index .Settings.Devices 0}}{{$devi
/dev/bar
```
> **Note**
>
> [!NOTE]
> Since only `path` is settable in `mydevice`,
> `docker plugins set mydevice=/dev/bar myplugin` would work too.

View File

@ -20,8 +20,7 @@ Creates a secret using standard input or from a file for the secret content.
For detailed information about using secrets, refer to [manage sensitive data with Docker secrets](https://docs.docker.com/engine/swarm/secrets/).
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -25,8 +25,7 @@ describes all the details of the format.
For detailed information about using secrets, refer to [manage sensitive data with Docker secrets](https://docs.docker.com/engine/swarm/secrets/).
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -24,8 +24,7 @@ Run this command on a manager node to list the secrets in the swarm.
For detailed information about using secrets, refer to [manage sensitive data with Docker secrets](https://docs.docker.com/engine/swarm/secrets/).
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -16,8 +16,7 @@ Removes the specified secrets from the swarm.
For detailed information about using secrets, refer to [manage sensitive data with Docker secrets](https://docs.docker.com/engine/swarm/secrets/).
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the
@ -32,8 +31,7 @@ $ docker secret rm secret.json
sapth4csdo5b6wz2p5uimh5xg
```
> **Warning**
>
> [!WARNING]
> Unlike `docker rm`, this command does not ask for confirmation before removing
> a secret.
{ .warning }

View File

@ -25,8 +25,7 @@ Manage Swarm services
Manage Swarm services.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -88,8 +88,7 @@ Create a new service
Creates a service as described by the specified parameters.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -23,8 +23,7 @@ the given template will be executed for each result.
Go's [text/template](https://pkg.go.dev/text/template) package
describes all the details of the format.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -24,8 +24,7 @@ Fetch the logs of a service or task
The `docker service logs` command batch-retrieves logs present at the time of execution.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the
@ -36,8 +35,7 @@ service, or with the ID of a task. If a service is passed, it will display logs
for all of the containers in that service. If a task is passed, it will only
display logs from that particular task.
> **Note**
>
> [!NOTE]
> This command is only functional for services that are started with
> the `json-file` or `journald` logging driver.

View File

@ -22,8 +22,7 @@ List services
This command lists services that are running in the swarm.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -20,8 +20,7 @@ List the tasks of one or more services
Lists the tasks that are running as part of the specified services.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -14,8 +14,7 @@ Remove one or more services
Removes the specified services from the swarm.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the
@ -35,8 +34,7 @@ $ docker service ls
ID NAME MODE REPLICAS IMAGE
```
> **Warning**
>
> [!WARNING]
> Unlike `docker rm`, this command does not ask for confirmation before removing
> a running service.

View File

@ -17,8 +17,7 @@ Revert changes to a service's configuration
Roll back a specified service to its previous version from the swarm.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -20,8 +20,7 @@ services which are global mode. The command will return immediately, but the
actual scaling of the service may take some time. To stop all replicas of a
service while keeping the service active in the swarm you can set the scale to 0.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -115,8 +115,7 @@ service requires recreating the tasks for it to take effect. For example, only c
setting. However, the `--force` flag will cause the tasks to be recreated anyway. This can be used to perform a
rolling restart without any changes to the service parameters.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -25,8 +25,7 @@ Deploy a new stack or update an existing stack
Create and update a stack from a `compose` file on the swarm.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -20,8 +20,7 @@ List stacks
Lists the stacks.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -20,8 +20,7 @@ List the tasks in the stack
Lists the tasks that are running as part of the specified stack.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -20,8 +20,7 @@ Remove one or more stacks
Remove the stack from the swarm.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -18,8 +18,7 @@ List the services in the stack
Lists the services that are running as part of the specified stack.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -22,8 +22,7 @@ Display and rotate the root CA
View or rotate the current swarm CA certificate.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the
@ -81,8 +80,7 @@ gyg5u9Iliel99l7SuMhNeLkrU7fXs+Of1nTyyM73ig==
### <a name="rotate"></a> Root CA rotation (--rotate)
> **Note**
>
> [!NOTE]
> Mirantis Kubernetes Engine (MKE), formerly known as Docker UCP, provides an external
> certificate manager service for the swarm. If you run swarm on MKE, you shouldn't
> rotate the CA certificates manually. Instead, contact Mirantis support if you need

View File

@ -21,8 +21,7 @@ role. You pass the token using the `--token` flag when you run
[swarm join](swarm_join.md). Nodes use the join token only when they join the
swarm.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -22,8 +22,7 @@ swarm.
You can view or rotate the unlock key using `swarm unlock-key`. To view the key,
run the `docker swarm unlock-key` command without any arguments:
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -13,8 +13,7 @@ used to reactivate a manager after its Docker daemon restarts if the autolock
setting is turned on. The unlock key is printed at the time when autolock is
enabled, and is also available from the `docker swarm unlock-key` command.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -22,8 +22,7 @@ Update the swarm
Updates a swarm with new parameter values.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -62,8 +62,7 @@ my-named-vol 0
* `UNIQUE SIZE` is the amount of space that's only used by a given image
* `SIZE` is the virtual size of the image, it's the sum of `SHARED SIZE` and `UNIQUE SIZE`
> **Note**
>
> [!NOTE]
> Network information isn't shown, because it doesn't consume disk space.
## Performance

View File

@ -118,7 +118,7 @@ and Docker Engine perform API version negotiation, and select the highest API
version that is supported by both the Docker CLI and the Docker Engine.
For example, if the CLI is connecting with Docker Engine version 19.03, it downgrades
to API version 1.40 (refer to the [API version matrix](https://docs.docker.com/engine/api/#api-version-matrix)
to API version 1.40 (refer to the [API version matrix](https://docs.docker.com/reference/api/engine/#api-version-matrix)
to learn about the supported API versions for Docker Engine):
```console

View File

@ -124,6 +124,7 @@ type `dockerd`.
To run the daemon with debug output, use `dockerd --debug` or add `"debug": true`
to [the `daemon.json` file](#daemon-configuration-file).
> [!NOTE]
> **Enabling experimental features**
>
> Enable experimental features by starting `dockerd` with the `--experimental`
@ -152,8 +153,7 @@ to learn about environment variables supported by the `docker` CLI.
### Proxy configuration
> **Note**
>
> [!NOTE]
> Refer to the [Docker Desktop manual](https://docs.docker.com/desktop/networking/#httphttps-proxy-support)
> if you are running [Docker Desktop](https://docs.docker.com/desktop/).
@ -191,8 +191,7 @@ interface using its IP address: `-H tcp://192.168.59.103:2375`. It is
conventional to use port `2375` for un-encrypted, and port `2376` for encrypted
communication with the daemon.
> **Note**
>
> [!NOTE]
> If you're using an HTTPS encrypted socket, keep in mind that only
> TLS version 1.0 and higher is supported. Protocols SSLv3 and below are not
> supported for security reasons.
@ -259,8 +258,7 @@ supported. If your key is protected with passphrase, you need to set up
#### Bind Docker to another host/port or a Unix socket
> **Warning**
>
> [!WARNING]
> Changing the default `docker` daemon binding to a TCP port or Unix `docker`
> user group introduces security risks, as it may allow non-root users to gain
> root access on the host. Make sure you control access to `docker`. If you are
@ -709,8 +707,7 @@ This option is useful when pushing images containing non-distributable artifacts
to a registry on an air-gapped network so hosts on that network can pull the
images without connecting to another server.
> **Warning**
>
> [!WARNING]
> Non-distributable artifacts typically have restrictions on how
> and where they can be distributed and shared. Only use this feature to push
> artifacts to private registries and ensure that you are in compliance with
@ -858,8 +855,7 @@ PING host.docker.internal (192.0.2.0): 56 data bytes
### Enable CDI devices
> **Note**
>
> [!NOTE]
> This is experimental feature and as such doesn't represent a stable API.
>
> This feature isn't enabled by default. To this feature, set `features.cdi` to
@ -1145,8 +1141,7 @@ The following is a full example of the allowed configuration options on Linux:
}
```
> **Note**
>
> [!NOTE]
> You can't set options in `daemon.json` that have already been set on
> daemon startup as a flag.
> On systems that use systemd to start the Docker daemon, `-H` is already set, so
@ -1242,7 +1237,7 @@ The list of feature options include:
external names. The current default is `false`, it will change to `true` in
a future release. This option is only allowed on Windows.
> **Warning**
> [!WARNING]
> The `windows-dns-proxy` feature flag will be removed in a future release.
#### Configuration reload behavior
@ -1275,8 +1270,7 @@ The list of currently supported options that can be reconfigured is this:
### Run multiple daemons
> **Note**
>
> [!NOTE]
> Running multiple daemons on a single host is considered experimental.
> You may encounter unsolved problems, and things may not work as expected in some cases.

View File

@ -69,8 +69,7 @@ to start an interactive shell in the container (if the image you select has an
$ docker run -it IMAGE sh
```
> **Note**
>
> [!NOTE]
> Depending on your Docker system configuration, you may be
> required to preface the `docker run` command with `sudo`. To avoid
> having to use `sudo` with the `docker` command, your system
@ -696,8 +695,7 @@ By default, all containers get the same proportion of block IO bandwidth
container's blkio weight relative to the weighting of all other running
containers using the `--blkio-weight` flag.
> **Note:**
>
> [!NOTE]
> The blkio weight setting is only available for direct IO. Buffered IO is not
> currently supported.
@ -1039,8 +1037,7 @@ You can reset a containers entrypoint by passing an empty string, for example:
$ docker run -it --entrypoint="" mysql bash
```
> **Note**
>
> [!NOTE]
> Passing `--entrypoint` clears out any default command set on the image. That
> is, any `CMD` instruction in the Dockerfile used to build it.
@ -1223,8 +1220,7 @@ The followings examples are all valid:
--user=[ user | user:group | uid | uid:gid | user:gid | uid:group ]
```
> **Note**
>
> [!NOTE]
> If you pass a numeric user ID, it must be in the range of 0-2147483647. If
> you pass a username, the user must exist in the container.

View File

@ -214,7 +214,7 @@ func TestPromptExitCode(t *testing.T) {
default:
if err := bufioWriter.Flush(); err != nil {
return poll.Continue(err.Error())
return poll.Continue("%v", err)
}
if strings.Contains(buf.String(), "[y/N]") {
return poll.Success()

View File

@ -0,0 +1,56 @@
package registry
import (
"io"
"os/exec"
"strings"
"syscall"
"testing"
"time"
"github.com/creack/pty"
"gotest.tools/v3/assert"
)
func TestOauthLogin(t *testing.T) {
t.Parallel()
loginCmd := exec.Command("docker", "login")
p, err := pty.Start(loginCmd)
assert.NilError(t, err)
defer func() {
_ = loginCmd.Wait()
_ = p.Close()
}()
time.Sleep(1 * time.Second)
pid := loginCmd.Process.Pid
t.Logf("terminating PID %d", pid)
err = syscall.Kill(pid, syscall.SIGTERM)
assert.NilError(t, err)
output, _ := io.ReadAll(p)
assert.Check(t, strings.Contains(string(output), "USING WEB BASED LOGIN"), string(output))
}
func TestLoginWithEscapeHatch(t *testing.T) {
t.Parallel()
loginCmd := exec.Command("docker", "login")
loginCmd.Env = append(loginCmd.Env, "DOCKER_CLI_DISABLE_OAUTH_LOGIN=1")
p, err := pty.Start(loginCmd)
assert.NilError(t, err)
defer func() {
_ = loginCmd.Wait()
_ = p.Close()
}()
time.Sleep(1 * time.Second)
pid := loginCmd.Process.Pid
t.Logf("terminating PID %d", pid)
err = syscall.Kill(pid, syscall.SIGTERM)
assert.NilError(t, err)
output, _ := io.ReadAll(p)
assert.Check(t, strings.Contains(string(output), "Username:"), string(output))
}

17
e2e/registry/main_test.go Normal file
View File

@ -0,0 +1,17 @@
package registry
import (
"fmt"
"os"
"testing"
"github.com/docker/cli/internal/test/environment"
)
func TestMain(m *testing.M) {
if err := environment.Setup(); err != nil {
fmt.Println(err.Error())
os.Exit(3)
}
os.Exit(m.Run())
}

View File

@ -13,11 +13,12 @@ require (
github.com/distribution/reference v0.6.0
github.com/docker/cli-docs-tool v0.8.0
github.com/docker/distribution v2.8.3+incompatible
github.com/docker/docker v27.1.2-0.20240810135946-f9522e5e96c3+incompatible // 27.x branch (v27.1.2-dev)
github.com/docker/docker v27.2.0-rc.1.0.20240827140014-3ab5c7d0036c+incompatible // 27.x branch (v27.2.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
github.com/fvbommel/sortorder v1.0.2
github.com/fvbommel/sortorder v1.1.0
github.com/go-jose/go-jose/v3 v3.0.3
github.com/go-viper/mapstructure/v2 v2.0.0
github.com/gogo/protobuf v1.3.2
github.com/google/go-cmp v0.6.0
@ -79,11 +80,13 @@ require (
github.com/moby/sys/symlink v0.2.0 // indirect
github.com/moby/sys/user v0.3.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/prometheus/client_golang v1.17.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
go.etcd.io/etcd/raft/v3 v3.5.6 // indirect

View File

@ -57,8 +57,8 @@ github.com/docker/cli-docs-tool v0.8.0/go.mod h1:8TQQ3E7mOXoYUs811LiPdUnAhXrcVsB
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 v27.1.2-0.20240810135946-f9522e5e96c3+incompatible h1:cDD1nJea6JwvYwLjAZG+6CFlzSTSi9swKUz0qaHwTYA=
github.com/docker/docker v27.1.2-0.20240810135946-f9522e5e96c3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.2.0-rc.1.0.20240827140014-3ab5c7d0036c+incompatible h1:EuHKrI999zfPX8J2mF6AcHVAMQvSTkawrSGh81s5ncU=
github.com/docker/docker v27.2.0-rc.1.0.20240827140014-3ab5c7d0036c+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=
@ -80,9 +80,11 @@ github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo=
github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
@ -118,6 +120,7 @@ github.com/google/certificate-transparency-go v1.1.4 h1:hCyXHDbtqlr/lMXU0D4Wgbal
github.com/google/certificate-transparency-go v1.1.4/go.mod h1:D6lvbfwckhNrbM9WVl1EVeMOyzC19mpIjMOI4nxBHtQ=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -210,6 +213,8 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -268,8 +273,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a h1:tlJ7tGUHvcvL1v3yR6NcCc9nOqh2L+CG6HWrYQtwzQ0=
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a/go.mod h1:Y94A6rPp2OwNfP/7vmf8O2xx2IykP8pPXQ1DLouGnEw=
github.com/tonistiigi/go-rosetta v0.0.0-20200727161949-f79598599c5d h1:wvQZpqy8p0D/FUia6ipKDhXrzPzBVJE4PZyPc5+5Ay0=
@ -285,6 +290,7 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc h1:zkGwegkOW709y0oiAraH/3D8njopUR/pARHv4tZZ6pw=
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc/go.mod h1:FM4U1E3NzlNMRnSUTU3P1UdukWhYGifqEsjk9fn7BCk=
github.com/zmap/zlint/v3 v3.1.0 h1:WjVytZo79m/L1+/Mlphl09WBob6YTGljN5IGWZFpAv0=
@ -326,10 +332,14 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -337,6 +347,10 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -345,6 +359,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -356,17 +372,34 @@ golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
@ -376,6 +409,8 @@ golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -3,7 +3,7 @@ package api // import "github.com/docker/docker/api"
// Common constants for daemon and client.
const (
// DefaultVersion of the current REST API.
DefaultVersion = "1.46"
DefaultVersion = "1.47"
// MinSupportedAPIVersion is the minimum API version that can be supported
// by the API server, specified as "major.minor". Note that the daemon

View File

@ -19,10 +19,10 @@ produces:
consumes:
- "application/json"
- "text/plain"
basePath: "/v1.46"
basePath: "/v1.47"
info:
title: "Docker Engine API"
version: "1.46"
version: "1.47"
x-logo:
url: "https://docs.docker.com/assets/images/logo-docker-main.png"
description: |
@ -55,8 +55,8 @@ info:
the URL is not supported by the daemon, a HTTP `400 Bad Request` error message
is returned.
If you omit the version-prefix, the current version of the API (v1.46) is used.
For example, calling `/info` is the same as calling `/v1.46/info`. Using the
If you omit the version-prefix, the current version of the API (v1.47) is used.
For example, calling `/info` is the same as calling `/v1.47/info`. Using the
API without a version-prefix is deprecated and will be removed in a future release.
Engine releases in the near future should support this version of the API,
@ -2265,6 +2265,19 @@ definitions:
x-nullable: false
type: "integer"
example: 2
Manifests:
description: |
Manifests is a list of manifests available in this image.
It provides a more detailed view of the platform-specific image manifests
or other image-attached data like build attestations.
WARNING: This is experimental and may change at any time without any backward
compatibility.
type: "array"
x-nullable: false
x-omitempty: true
items:
$ref: "#/definitions/ImageManifestSummary"
AuthConfig:
type: "object"
@ -5318,7 +5331,7 @@ definitions:
description: |
The default (and highest) API version that is supported by the daemon
type: "string"
example: "1.46"
example: "1.47"
MinAPIVersion:
description: |
The minimum API version that is supported by the daemon
@ -6644,6 +6657,120 @@ definitions:
additionalProperties:
type: "string"
ImageManifestSummary:
x-go-name: "ManifestSummary"
description: |
ImageManifestSummary represents a summary of an image manifest.
type: "object"
required: ["ID", "Descriptor", "Available", "Size", "Kind"]
properties:
ID:
description: |
ID is the content-addressable ID of an image and is the same as the
digest of the image manifest.
type: "string"
example: "sha256:95869fbcf224d947ace8d61d0e931d49e31bb7fc67fffbbe9c3198c33aa8e93f"
Descriptor:
$ref: "#/definitions/OCIDescriptor"
Available:
description: Indicates whether all the child content (image config, layers) is fully available locally.
type: "boolean"
example: true
Size:
type: "object"
x-nullable: false
required: ["Content", "Total"]
properties:
Total:
type: "integer"
format: "int64"
example: 8213251
description: |
Total is the total size (in bytes) of all the locally present
data (both distributable and non-distributable) that's related to
this manifest and its children.
This equal to the sum of [Content] size AND all the sizes in the
[Size] struct present in the Kind-specific data struct.
For example, for an image kind (Kind == "image")
this would include the size of the image content and unpacked
image snapshots ([Size.Content] + [ImageData.Size.Unpacked]).
Content:
description: |
Content is the size (in bytes) of all the locally present
content in the content store (e.g. image config, layers)
referenced by this manifest and its children.
This only includes blobs in the content store.
type: "integer"
format: "int64"
example: 3987495
Kind:
type: "string"
example: "image"
enum:
- "image"
- "attestation"
- "unknown"
description: |
The kind of the manifest.
kind | description
-------------|-----------------------------------------------------------
image | Image manifest that can be used to start a container.
attestation | Attestation manifest produced by the Buildkit builder for a specific image manifest.
ImageData:
description: |
The image data for the image manifest.
This field is only populated when Kind is "image".
type: "object"
x-nullable: true
x-omitempty: true
required: ["Platform", "Containers", "Size", "UnpackedSize"]
properties:
Platform:
$ref: "#/definitions/OCIPlatform"
description: |
OCI platform of the image. This will be the platform specified in the
manifest descriptor from the index/manifest list.
If it's not available, it will be obtained from the image config.
Containers:
description: |
The IDs of the containers that are using this image.
type: "array"
items:
type: "string"
example: ["ede54ee1fda366ab42f824e8a5ffd195155d853ceaec74a927f249ea270c7430", "abadbce344c096744d8d6071a90d474d28af8f1034b5ea9fb03c3f4bfc6d005e"]
Size:
type: "object"
x-nullable: false
required: ["Unpacked"]
properties:
Unpacked:
type: "integer"
format: "int64"
example: 3987495
description: |
Unpacked is the size (in bytes) of the locally unpacked
(uncompressed) image content that's directly usable by the containers
running this image.
It's independent of the distributable content - e.g.
the image might still have an unpacked data that's still used by
some container even when the distributable/compressed content is
already gone.
AttestationData:
description: |
The image data for the attestation manifest.
This field is only populated when Kind is "attestation".
type: "object"
x-nullable: true
x-omitempty: true
required: ["For"]
properties:
For:
description: |
The digest of the image manifest that this attestation is for.
type: "string"
example: "sha256:95869fbcf224d947ace8d61d0e931d49e31bb7fc67fffbbe9c3198c33aa8e93f"
paths:
/containers/json:
get:
@ -8622,6 +8749,11 @@ paths:
description: "Show digest information as a `RepoDigests` field on each image."
type: "boolean"
default: false
- name: "manifests"
in: "query"
description: "Include `Manifests` in the image summary."
type: "boolean"
default: false
tags: ["Image"]
/build:
post:

View File

@ -0,0 +1,99 @@
package image
import (
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
type ManifestKind string
const (
ManifestKindImage ManifestKind = "image"
ManifestKindAttestation ManifestKind = "attestation"
ManifestKindUnknown ManifestKind = "unknown"
)
type ManifestSummary struct {
// ID is the content-addressable ID of an image and is the same as the
// digest of the image manifest.
//
// Required: true
ID string `json:"ID"`
// Descriptor is the OCI descriptor of the image.
//
// Required: true
Descriptor ocispec.Descriptor `json:"Descriptor"`
// Indicates whether all the child content (image config, layers) is
// fully available locally
//
// Required: true
Available bool `json:"Available"`
// Size is the size information of the content related to this manifest.
// Note: These sizes only take the locally available content into account.
//
// Required: true
Size struct {
// Content is the size (in bytes) of all the locally present
// content in the content store (e.g. image config, layers)
// referenced by this manifest and its children.
// This only includes blobs in the content store.
Content int64 `json:"Content"`
// Total is the total size (in bytes) of all the locally present
// data (both distributable and non-distributable) that's related to
// this manifest and its children.
// This equal to the sum of [Content] size AND all the sizes in the
// [Size] struct present in the Kind-specific data struct.
// For example, for an image kind (Kind == ManifestKindImage),
// this would include the size of the image content and unpacked
// image snapshots ([Size.Content] + [ImageData.Size.Unpacked]).
Total int64 `json:"Total"`
} `json:"Size"`
// Kind is the kind of the image manifest.
//
// Required: true
Kind ManifestKind `json:"Kind"`
// Fields below are specific to the kind of the image manifest.
// Present only if Kind == ManifestKindImage.
ImageData *ImageProperties `json:"ImageData,omitempty"`
// Present only if Kind == ManifestKindAttestation.
AttestationData *AttestationProperties `json:"AttestationData,omitempty"`
}
type ImageProperties struct {
// Platform is the OCI platform object describing the platform of the image.
//
// Required: true
Platform ocispec.Platform `json:"Platform"`
Size struct {
// Unpacked is the size (in bytes) of the locally unpacked
// (uncompressed) image content that's directly usable by the containers
// running this image.
// It's independent of the distributable content - e.g.
// the image might still have an unpacked data that's still used by
// some container even when the distributable/compressed content is
// already gone.
//
// Required: true
Unpacked int64 `json:"Unpacked"`
}
// Containers is an array containing the IDs of the containers that are
// using this image.
//
// Required: true
Containers []string `json:"Containers"`
}
type AttestationProperties struct {
// For is the digest of the image manifest that this attestation is for.
For digest.Digest `json:"For"`
}

View File

@ -76,6 +76,9 @@ type ListOptions struct {
// ContainerCount indicates whether container count should be computed.
ContainerCount bool
// Manifests indicates whether the image manifests should be returned.
Manifests bool
}
// RemoveOptions holds parameters to remove images.

View File

@ -1,10 +1,5 @@
package image
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// Summary summary
// swagger:model Summary
type Summary struct {
// Number of containers using this image. Includes both stopped and running
@ -47,6 +42,14 @@ type Summary struct {
// Required: true
ParentID string `json:"ParentId"`
// Manifests is a list of image manifests available in this image. It
// provides a more detailed view of the platform-specific image manifests or
// other image-attached data like build attestations.
//
// WARNING: This is experimental and may change at any time without any backward
// compatibility.
Manifests []ManifestSummary `json:"Manifests,omitempty"`
// List of content-addressable digests of locally available image manifests
// that the image is referenced from. Multiple manifests can refer to the
// same image.

View File

@ -34,10 +34,9 @@ type AuthConfig struct {
}
// EncodeAuthConfig serializes the auth configuration as a base64url encoded
// RFC4648, section 5) JSON string for sending through the X-Registry-Auth header.
// ([RFC4648, section 5]) JSON string for sending through the X-Registry-Auth header.
//
// For details on base64url encoding, see:
// - RFC4648, section 5: https://tools.ietf.org/html/rfc4648#section-5
// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5
func EncodeAuthConfig(authConfig AuthConfig) (string, error) {
buf, err := json.Marshal(authConfig)
if err != nil {
@ -46,15 +45,14 @@ func EncodeAuthConfig(authConfig AuthConfig) (string, error) {
return base64.URLEncoding.EncodeToString(buf), nil
}
// DecodeAuthConfig decodes base64url encoded (RFC4648, section 5) JSON
// DecodeAuthConfig decodes base64url encoded ([RFC4648, section 5]) JSON
// authentication information as sent through the X-Registry-Auth header.
//
// This function always returns an AuthConfig, even if an error occurs. It is up
// This function always returns an [AuthConfig], even if an error occurs. It is up
// to the caller to decide if authentication is required, and if the error can
// be ignored.
//
// For details on base64url encoding, see:
// - RFC4648, section 5: https://tools.ietf.org/html/rfc4648#section-5
// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5
func DecodeAuthConfig(authEncoded string) (*AuthConfig, error) {
if authEncoded == "" {
return &AuthConfig{}, nil
@ -69,7 +67,7 @@ func DecodeAuthConfig(authEncoded string) (*AuthConfig, error) {
// clients and API versions. Current clients and API versions expect authentication
// to be provided through the X-Registry-Auth header.
//
// Like DecodeAuthConfig, this function always returns an AuthConfig, even if an
// Like [DecodeAuthConfig], this function always returns an [AuthConfig], even if an
// error occurs. It is up to the caller to decide if authentication is required,
// and if the error can be ignored.
func DecodeAuthConfigBody(rdr io.ReadCloser) (*AuthConfig, error) {

View File

@ -11,6 +11,11 @@ import (
)
// ImageList returns a list of images in the docker host.
//
// Experimental: Setting the [options.Manifest] will populate
// [image.Summary.Manifests] with information about image manifests.
// This is experimental and might change in the future without any backward
// compatibility.
func (cli *Client) ImageList(ctx context.Context, options image.ListOptions) ([]image.Summary, error) {
var images []image.Summary
@ -47,6 +52,9 @@ func (cli *Client) ImageList(ctx context.Context, options image.ListOptions) ([]
if options.SharedSize && versions.GreaterThanOrEqualTo(cli.version, "1.42") {
query.Set("shared-size", "1")
}
if options.Manifests && versions.GreaterThanOrEqualTo(cli.version, "1.47") {
query.Set("manifests", "1")
}
serverResp, err := cli.get(ctx, "/images/json", query, nil)
defer ensureReaderClosed(serverResp)

View File

@ -3,3 +3,7 @@
import "github.com/fvbommel/sortorder"
Sort orders and comparison functions.
Case-insensitive sort orders are in the `casefolded` sub-package
because it pulls in the Unicode tables in the standard library,
which can add significantly to the size of binaries.

View File

@ -4,7 +4,7 @@ package sortorder
// means that e.g. "abc2" < "abc12".
//
// Non-digit sequences and numbers are compared separately. The former are
// compared bytewise, while the latter are compared numerically (except that
// compared bytewise, while digits are compared numerically (except that
// the number of leading zeros is used as a tie-breaker, so e.g. "2" < "02")
//
// Limitation: only ASCII digits (0-9) are considered.
@ -14,13 +14,13 @@ func (n Natural) Len() int { return len(n) }
func (n Natural) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
func (n Natural) Less(i, j int) bool { return NaturalLess(n[i], n[j]) }
func isdigit(b byte) bool { return '0' <= b && b <= '9' }
func isDigit(b byte) bool { return '0' <= b && b <= '9' }
// NaturalLess compares two strings using natural ordering. This means that e.g.
// "abc2" < "abc12".
//
// Non-digit sequences and numbers are compared separately. The former are
// compared bytewise, while the latter are compared numerically (except that
// compared bytewise, while digits are compared numerically (except that
// the number of leading zeros is used as a tie-breaker, so e.g. "2" < "02")
//
// Limitation: only ASCII digits (0-9) are considered.
@ -28,7 +28,7 @@ func NaturalLess(str1, str2 string) bool {
idx1, idx2 := 0, 0
for idx1 < len(str1) && idx2 < len(str2) {
c1, c2 := str1[idx1], str2[idx2]
dig1, dig2 := isdigit(c1), isdigit(c2)
dig1, dig2 := isDigit(c1), isDigit(c2)
switch {
case dig1 != dig2: // Digits before other characters.
return dig1 // True if LHS is a digit, false if the RHS is one.
@ -48,16 +48,16 @@ func NaturalLess(str1, str2 string) bool {
}
// Eat all digits.
nonZero1, nonZero2 := idx1, idx2
for ; idx1 < len(str1) && isdigit(str1[idx1]); idx1++ {
for ; idx1 < len(str1) && isDigit(str1[idx1]); idx1++ {
}
for ; idx2 < len(str2) && isdigit(str2[idx2]); idx2++ {
for ; idx2 < len(str2) && isDigit(str2[idx2]); idx2++ {
}
// If lengths of numbers with non-zero prefix differ, the shorter
// one is less.
if len1, len2 := idx1-nonZero1, idx2-nonZero2; len1 != len2 {
return len1 < len2
}
// If they're equal, string comparison is correct.
// If they're equally long, string comparison is correct.
if nr1, nr2 := str1[nonZero1:idx1], str2[nonZero2:idx2]; nr1 != nr2 {
return nr1 < nr2
}

2
vendor/github.com/go-jose/go-jose/v3/.gitignore generated vendored Normal file
View File

@ -0,0 +1,2 @@
jose-util/jose-util
jose-util.t.err

53
vendor/github.com/go-jose/go-jose/v3/.golangci.yml generated vendored Normal file
View File

@ -0,0 +1,53 @@
# https://github.com/golangci/golangci-lint
run:
skip-files:
- doc_test.go
modules-download-mode: readonly
linters:
enable-all: true
disable:
- gochecknoglobals
- goconst
- lll
- maligned
- nakedret
- scopelint
- unparam
- funlen # added in 1.18 (requires go-jose changes before it can be enabled)
linters-settings:
gocyclo:
min-complexity: 35
issues:
exclude-rules:
- text: "don't use ALL_CAPS in Go names"
linters:
- golint
- text: "hardcoded credentials"
linters:
- gosec
- text: "weak cryptographic primitive"
linters:
- gosec
- path: json/
linters:
- dupl
- errcheck
- gocritic
- gocyclo
- golint
- govet
- ineffassign
- staticcheck
- structcheck
- stylecheck
- unused
- path: _test\.go
linters:
- scopelint
- path: jwk.go
linters:
- gocyclo

33
vendor/github.com/go-jose/go-jose/v3/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,33 @@
language: go
matrix:
fast_finish: true
allow_failures:
- go: tip
go:
- "1.13.x"
- "1.14.x"
- tip
before_script:
- export PATH=$HOME/.local/bin:$PATH
before_install:
- go get -u github.com/mattn/goveralls github.com/wadey/gocovmerge
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.18.0
- pip install cram --user
script:
- go test -v -covermode=count -coverprofile=profile.cov .
- go test -v -covermode=count -coverprofile=cryptosigner/profile.cov ./cryptosigner
- go test -v -covermode=count -coverprofile=cipher/profile.cov ./cipher
- go test -v -covermode=count -coverprofile=jwt/profile.cov ./jwt
- go test -v ./json # no coverage for forked encoding/json package
- golangci-lint run
- cd jose-util && go build && PATH=$PWD:$PATH cram -v jose-util.t # cram tests jose-util
- cd ..
after_success:
- gocovmerge *.cov */*.cov > merged.coverprofile
- goveralls -coverprofile merged.coverprofile -service=travis-ci

Some files were not shown because too many files have changed in this diff Show More