Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce1223035a | |||
| 263ba959c4 | |||
| be9b9f3d70 | |||
| a4149b0c3d | |||
| 4aac4154b0 | |||
| 854695884a | |||
| f05200311d | |||
| 460f1becc5 | |||
| e85edf8556 | |||
| ca62759b0f | |||
| e4dc9d2ac7 | |||
| ddf8f23403 | |||
| c6dde25470 | |||
| fcf6dd05df | |||
| 4b5a196fee | |||
| e2eb069aed | |||
| 58a14cc379 | |||
| 81ca58bd20 | |||
| 6b5e4be0cb | |||
| b5c6541769 | |||
| 6fdd11b012 | |||
| 6bf64a88af | |||
| d896559894 | |||
| f350724e21 | |||
| 84d4a5ef11 | |||
| 7c72283b37 | |||
| 245f79ff57 | |||
| db7a014d37 | |||
| bc87ef962a | |||
| 7811c442d6 | |||
| 48a2cdff97 | |||
| b48720e65e | |||
| c85c3df6b5 | |||
| 74bebe2719 | |||
| 9748bef1c3 | |||
| 2fc18b9874 | |||
| 1a199b63da | |||
| 968341cc7d | |||
| fbb0cfd86a | |||
| 49e33a03df | |||
| 295b75e5ff | |||
| 090d1ff555 | |||
| 5dde3d8570 | |||
| d64740d347 | |||
| f68936ef2e | |||
| 4607c883c5 | |||
| 7ff3daa446 | |||
| 1fe06dd0e7 | |||
| b381ab1d8d | |||
| 05fbfc6995 | |||
| fba240c5b4 | |||
| 965699ba0f | |||
| c65ac2d90d | |||
| 05fb576772 | |||
| 4be6b1f3d7 | |||
| 65decb5731 | |||
| 90559a6143 | |||
| dbde5b3681 | |||
| b1e50eea92 | |||
| 9e34c9bb39 | |||
| 324cdbca40 | |||
| b5290d4e0b | |||
| 3db9538748 | |||
| 1ab89e71fa | |||
| 667d9fd4df | |||
| 41e61c45d9 | |||
| 869df10064 | |||
| 6feee4ab35 | |||
| d0c1a80617 | |||
| 383c428451 | |||
| 5f8416e541 | |||
| 5bf5cb9ff6 | |||
| 3a15d5a640 | |||
| 1dfd11acc0 | |||
| de2b49b074 | |||
| c5d846735c | |||
| 6274754e66 | |||
| 7a50cd0f01 | |||
| 074dfc0f88 | |||
| 92423287cc | |||
| 1a0b6a7a44 | |||
| 8fcfc0b803 | |||
| 28d2fed463 | |||
| 83072c0232 | |||
| 40109aa45f | |||
| 32aadc9902 | |||
| 3ab4256958 | |||
| 88a49df297 | |||
| 5d17c29eb2 | |||
| 64b9e4cd16 | |||
| 4b71d0d1af | |||
| 002cfcde85 | |||
| d8af7812b5 | |||
| f042ddb5c9 | |||
| 8e94ed15e6 | |||
| 7a82aeeeba | |||
| 24837f9260 | |||
| 5805df0205 | |||
| fb20f009f7 | |||
| 6ceb0aba82 | |||
| 2d7b8998c4 | |||
| cabd410a1a | |||
| a58af379e1 | |||
| 1b3fa65759 | |||
| cf01923519 | |||
| a0d7f0dbd3 | |||
| 0c4e7478e2 | |||
| 60ce3fbc96 | |||
| 7902b52714 | |||
| 7196200fc2 | |||
| f42fa0b8e1 | |||
| b719b10257 | |||
| ab55d75cf5 | |||
| 324cc5d30f | |||
| 44a9ffa0ad | |||
| ba43ae0bd2 | |||
| 99b647cfca | |||
| f90dc28f1e | |||
| 26536d1145 | |||
| c5e733becc | |||
| 7227402d94 | |||
| 83f6ca4a73 | |||
| ad7912a846 | |||
| afb5e143b1 | |||
| b8a38fd22d | |||
| 0c29d6bac1 | |||
| 3eaf30278f |
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@ -67,7 +67,7 @@ jobs:
|
||||
name: Update Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: 1.22.7
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -74,7 +74,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.13
|
||||
go-version: 1.22.7
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
|
||||
@ -4,12 +4,12 @@ ARG BASE_VARIANT=alpine
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.21.13
|
||||
ARG XX_VERSION=1.4.0
|
||||
ARG GO_VERSION=1.22.7
|
||||
ARG XX_VERSION=1.5.0
|
||||
ARG GOVERSIONINFO_VERSION=v1.3.0
|
||||
ARG GOTESTSUM_VERSION=v1.10.0
|
||||
ARG BUILDX_VERSION=0.16.1
|
||||
ARG COMPOSE_VERSION=v2.29.0
|
||||
ARG BUILDX_VERSION=0.17.1
|
||||
ARG COMPOSE_VERSION=v2.29.7
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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{
|
||||
{
|
||||
|
||||
@ -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
393
cli/command/image/tree.go
Normal 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
|
||||
}
|
||||
@ -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": ""},
|
||||
}
|
||||
|
||||
@ -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:
|
||||
@ -100,20 +124,10 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
|
||||
cli.SetIn(streams.NewIn(os.Stdin))
|
||||
}
|
||||
|
||||
// Some links documenting this:
|
||||
// - https://code.google.com/archive/p/mintty/issues/56
|
||||
// - https://github.com/docker/docker/issues/15272
|
||||
// - https://mintty.github.io/ (compatibility)
|
||||
// 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")
|
||||
}
|
||||
isDefaultRegistry := serverAddress == registry.IndexServer
|
||||
defaultUsername = strings.TrimSpace(defaultUsername)
|
||||
|
||||
authconfig.Username = strings.TrimSpace(authconfig.Username)
|
||||
|
||||
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 +138,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
|
||||
|
||||
@ -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"
|
||||
@ -36,8 +39,8 @@ func NewLoginCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "login [OPTIONS] [SERVER]",
|
||||
Short: "Log in to a registry",
|
||||
Long: "Log in to a registry.\nIf no server is specified, the default is defined by the daemon.",
|
||||
Short: "Authenticate to a registry",
|
||||
Long: "Authenticate to a registry.\nDefaults to Docker Hub if no server is specified.",
|
||||
Args: cli.RequiresMaxArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
@ -100,80 +103,176 @@ 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 {
|
||||
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, authConfig.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) {
|
||||
// Some links documenting this:
|
||||
// - https://code.google.com/archive/p/mintty/issues/56
|
||||
// - https://github.com/docker/docker/issues/15272
|
||||
// - https://mintty.github.io/ (compatibility)
|
||||
// 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 (opts.user == "" || opts.password == "") && !dockerCli.In().IsTerminal() {
|
||||
return nil, errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
@ -16,6 +16,7 @@ import (
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/fs"
|
||||
@ -74,7 +75,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()
|
||||
@ -83,88 +84,207 @@ func TestLoginWithCredStoreCreds(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunLogin(t *testing.T) {
|
||||
const (
|
||||
storedServerAddress = "reg1"
|
||||
validUsername = "u1"
|
||||
validPassword = "p1"
|
||||
validPassword2 = "p2"
|
||||
)
|
||||
|
||||
validAuthConfig := configtypes.AuthConfig{
|
||||
ServerAddress: storedServerAddress,
|
||||
Username: validUsername,
|
||||
Password: validPassword,
|
||||
}
|
||||
expiredAuthConfig := configtypes.AuthConfig{
|
||||
ServerAddress: storedServerAddress,
|
||||
Username: validUsername,
|
||||
Password: expiredPassword,
|
||||
}
|
||||
validIdentityToken := configtypes.AuthConfig{
|
||||
ServerAddress: storedServerAddress,
|
||||
Username: validUsername,
|
||||
IdentityToken: useToken,
|
||||
}
|
||||
testCases := []struct {
|
||||
doc string
|
||||
inputLoginOption loginOptions
|
||||
inputStoredCred *configtypes.AuthConfig
|
||||
expectedErr string
|
||||
expectedSavedCred configtypes.AuthConfig
|
||||
doc string
|
||||
priorCredentials map[string]configtypes.AuthConfig
|
||||
input loginOptions
|
||||
expectedCredentials map[string]configtypes.AuthConfig
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "valid auth from store",
|
||||
inputLoginOption: loginOptions{
|
||||
serverAddress: storedServerAddress,
|
||||
priorCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: "a-password",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
inputStoredCred: &validAuthConfig,
|
||||
expectedSavedCred: validAuthConfig,
|
||||
},
|
||||
{
|
||||
doc: "expired auth",
|
||||
inputLoginOption: loginOptions{
|
||||
serverAddress: storedServerAddress,
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
},
|
||||
inputStoredCred: &expiredAuthConfig,
|
||||
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
|
||||
},
|
||||
{
|
||||
doc: "valid username and password",
|
||||
inputLoginOption: loginOptions{
|
||||
serverAddress: storedServerAddress,
|
||||
user: validUsername,
|
||||
password: validPassword2,
|
||||
},
|
||||
inputStoredCred: &validAuthConfig,
|
||||
expectedSavedCred: configtypes.AuthConfig{
|
||||
ServerAddress: storedServerAddress,
|
||||
Username: validUsername,
|
||||
Password: validPassword2,
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: "a-password",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "unknown user",
|
||||
inputLoginOption: loginOptions{
|
||||
serverAddress: storedServerAddress,
|
||||
doc: "expired auth from store",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: expiredPassword,
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
},
|
||||
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
|
||||
},
|
||||
{
|
||||
doc: "store valid username and password",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: "my-username",
|
||||
password: "p2",
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: "p2",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "unknown user w/ prior credentials",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: "a-password",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: unknownUser,
|
||||
password: validPassword,
|
||||
password: "a-password",
|
||||
},
|
||||
expectedErr: errUnknownUser,
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "a-password",
|
||||
Password: "a-password",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
inputStoredCred: &validAuthConfig,
|
||||
expectedErr: errUnknownUser,
|
||||
},
|
||||
{
|
||||
doc: "valid token",
|
||||
inputLoginOption: loginOptions{
|
||||
serverAddress: storedServerAddress,
|
||||
user: validUsername,
|
||||
doc: "unknown user w/o prior credentials",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: unknownUser,
|
||||
password: "a-password",
|
||||
},
|
||||
expectedErr: errUnknownUser,
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{},
|
||||
},
|
||||
{
|
||||
doc: "store valid token",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: "my-username",
|
||||
password: useToken,
|
||||
},
|
||||
inputStoredCred: &validIdentityToken,
|
||||
expectedSavedCred: validIdentityToken,
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
IdentityToken: useToken,
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "valid token from store",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: useToken,
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
IdentityToken: useToken,
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "no registry specified defaults to index server",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
input: loginOptions{
|
||||
user: "my-username",
|
||||
password: "my-password",
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
registry.IndexServer: {
|
||||
Username: "my-username",
|
||||
Password: "my-password",
|
||||
ServerAddress: registry.IndexServer,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "registry-1.docker.io",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
input: loginOptions{
|
||||
serverAddress: "registry-1.docker.io",
|
||||
user: "my-username",
|
||||
password: "my-password",
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"registry-1.docker.io": {
|
||||
Username: "my-username",
|
||||
Password: "my-password",
|
||||
ServerAddress: "registry-1.docker.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Regression test for https://github.com/docker/cli/issues/5382
|
||||
{
|
||||
doc: "sanitizes server address to remove repo",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
input: loginOptions{
|
||||
serverAddress: "registry-1.docker.io/bork/test",
|
||||
user: "my-username",
|
||||
password: "a-password",
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"registry-1.docker.io": {
|
||||
Username: "my-username",
|
||||
Password: "a-password",
|
||||
ServerAddress: "registry-1.docker.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Regression test for https://github.com/docker/cli/issues/5382
|
||||
{
|
||||
doc: "updates credential if server address includes repo",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{
|
||||
"registry-1.docker.io": {
|
||||
Username: "my-username",
|
||||
Password: "a-password",
|
||||
ServerAddress: "registry-1.docker.io",
|
||||
},
|
||||
},
|
||||
input: loginOptions{
|
||||
serverAddress: "registry-1.docker.io/bork/test",
|
||||
user: "my-username",
|
||||
password: "new-password",
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"registry-1.docker.io": {
|
||||
Username: "my-username",
|
||||
Password: "new-password",
|
||||
ServerAddress: "registry-1.docker.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
tmpFile := fs.NewFile(t, "test-run-login")
|
||||
defer tmpFile.Remove()
|
||||
@ -172,23 +292,166 @@ func TestRunLogin(t *testing.T) {
|
||||
configfile := cli.ConfigFile()
|
||||
configfile.Filename = tmpFile.Path()
|
||||
|
||||
if tc.inputStoredCred != nil {
|
||||
cred := *tc.inputStoredCred
|
||||
assert.NilError(t, configfile.GetCredentialsStore(cred.ServerAddress).Store(cred))
|
||||
for _, priorCred := range tc.priorCredentials {
|
||||
assert.NilError(t, configfile.GetCredentialsStore(priorCred.ServerAddress).Store(priorCred))
|
||||
}
|
||||
loginErr := runLogin(context.Background(), cli, tc.inputLoginOption)
|
||||
storedCreds, err := configfile.GetAllCredentials()
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, storedCreds, tc.priorCredentials)
|
||||
|
||||
loginErr := runLogin(context.Background(), cli, tc.input)
|
||||
if tc.expectedErr != "" {
|
||||
assert.Error(t, loginErr, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
assert.NilError(t, loginErr)
|
||||
savedCred, credStoreErr := configfile.GetCredentialsStore(tc.inputStoredCred.ServerAddress).Get(tc.inputStoredCred.ServerAddress)
|
||||
assert.Check(t, credStoreErr)
|
||||
assert.DeepEqual(t, tc.expectedSavedCred, savedCred)
|
||||
|
||||
outputCreds, err := configfile.GetAllCredentials()
|
||||
assert.Check(t, err)
|
||||
assert.DeepEqual(t, outputCreds, tc.expectedCredentials)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginNonInteractive(t *testing.T) {
|
||||
t.Run("no prior credentials", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
doc string
|
||||
username bool
|
||||
password bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "success - w/ user w/ password",
|
||||
username: true,
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
doc: "error - w/o user w/o pass ",
|
||||
username: false,
|
||||
password: false,
|
||||
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
|
||||
},
|
||||
{
|
||||
doc: "error - w/ user w/o pass",
|
||||
username: true,
|
||||
password: false,
|
||||
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
|
||||
},
|
||||
{
|
||||
doc: "error - w/o user w/ pass",
|
||||
username: false,
|
||||
password: true,
|
||||
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
|
||||
},
|
||||
}
|
||||
|
||||
// "" meaning default registry
|
||||
registries := []string{"", "my-registry.com"}
|
||||
|
||||
for _, registry := range registries {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
tmpFile := fs.NewFile(t, "test-run-login")
|
||||
defer tmpFile.Remove()
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
configfile := cli.ConfigFile()
|
||||
configfile.Filename = tmpFile.Path()
|
||||
options := loginOptions{
|
||||
serverAddress: registry,
|
||||
}
|
||||
if tc.username {
|
||||
options.user = "my-username"
|
||||
}
|
||||
if tc.password {
|
||||
options.password = "my-password"
|
||||
}
|
||||
|
||||
loginErr := runLogin(context.Background(), cli, options)
|
||||
if tc.expectedErr != "" {
|
||||
assert.Error(t, loginErr, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
assert.NilError(t, loginErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("w/ prior credentials", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
doc string
|
||||
username bool
|
||||
password bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "success - w/ user w/ password",
|
||||
username: true,
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
doc: "success - w/o user w/o pass ",
|
||||
username: false,
|
||||
password: false,
|
||||
},
|
||||
{
|
||||
doc: "error - w/ user w/o pass",
|
||||
username: true,
|
||||
password: false,
|
||||
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
|
||||
},
|
||||
{
|
||||
doc: "error - w/o user w/ pass",
|
||||
username: false,
|
||||
password: true,
|
||||
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
|
||||
},
|
||||
}
|
||||
|
||||
// "" meaning default registry
|
||||
registries := []string{"", "my-registry.com"}
|
||||
|
||||
for _, registry := range registries {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
tmpFile := fs.NewFile(t, "test-run-login")
|
||||
defer tmpFile.Remove()
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
configfile := cli.ConfigFile()
|
||||
configfile.Filename = tmpFile.Path()
|
||||
serverAddress := registry
|
||||
if serverAddress == "" {
|
||||
serverAddress = "https://index.docker.io/v1/"
|
||||
}
|
||||
assert.NilError(t, configfile.GetCredentialsStore(serverAddress).Store(configtypes.AuthConfig{
|
||||
Username: "my-username",
|
||||
Password: "my-password",
|
||||
ServerAddress: serverAddress,
|
||||
}))
|
||||
|
||||
options := loginOptions{
|
||||
serverAddress: registry,
|
||||
}
|
||||
if tc.username {
|
||||
options.user = "my-username"
|
||||
}
|
||||
if tc.password {
|
||||
options.password = "my-password"
|
||||
}
|
||||
|
||||
loginErr := runLogin(context.Background(), cli, options)
|
||||
if tc.expectedErr != "" {
|
||||
assert.Error(t, loginErr, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
assert.NilError(t, loginErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoginTermination(t *testing.T) {
|
||||
p, tty, err := pty.Open()
|
||||
assert.NilError(t, err)
|
||||
@ -213,7 +476,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 +491,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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -273,21 +273,9 @@ func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) []error {
|
||||
|
||||
if info.OSType == "linux" {
|
||||
fprintln(output, " Init Binary:", info.InitBinary)
|
||||
|
||||
for _, ci := range []struct {
|
||||
Name string
|
||||
Commit system.Commit
|
||||
}{
|
||||
{"containerd", info.ContainerdCommit},
|
||||
{"runc", info.RuncCommit},
|
||||
{"init", info.InitCommit},
|
||||
} {
|
||||
fprintf(output, " %s version: %s", ci.Name, ci.Commit.ID)
|
||||
if ci.Commit.ID != ci.Commit.Expected {
|
||||
fprintf(output, " (expected: %s)", ci.Commit.Expected)
|
||||
}
|
||||
fprintln(output)
|
||||
}
|
||||
fprintln(output, " containerd version:", info.ContainerdCommit.ID)
|
||||
fprintln(output, " runc version:", info.RuncCommit.ID)
|
||||
fprintln(output, " init version:", info.InitCommit.ID)
|
||||
if len(info.SecurityOptions) != 0 {
|
||||
if kvs, err := system.DecodeSecurityOptions(info.SecurityOptions); err != nil {
|
||||
errs = append(errs, err)
|
||||
@ -372,7 +360,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)
|
||||
|
||||
@ -5,9 +5,14 @@ package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.opentelemetry.io/otel"
|
||||
@ -77,14 +82,7 @@ func dockerExporterOTLPEndpoint(cli Cli) (endpoint string, secure bool) {
|
||||
|
||||
switch u.Scheme {
|
||||
case "unix":
|
||||
// Unix sockets are a bit weird. OTEL seems to imply they
|
||||
// can be used as an environment variable and are handled properly,
|
||||
// but they don't seem to be as the behavior of the environment variable
|
||||
// is to strip the scheme from the endpoint, but the underlying implementation
|
||||
// needs the scheme to use the correct resolver.
|
||||
//
|
||||
// We'll just handle this in a special way and add the unix:// back to the endpoint.
|
||||
endpoint = "unix://" + path.Join(u.Host, u.Path)
|
||||
endpoint = unixSocketEndpoint(u)
|
||||
case "https":
|
||||
secure = true
|
||||
fallthrough
|
||||
@ -135,3 +133,109 @@ func dockerMetricExporter(ctx context.Context, cli Cli) []sdkmetric.Option {
|
||||
}
|
||||
return []sdkmetric.Option{sdkmetric.WithReader(newCLIReader(exp))}
|
||||
}
|
||||
|
||||
// unixSocketEndpoint converts the unix scheme from URL to
|
||||
// an OTEL endpoint that can be used with the OTLP exporter.
|
||||
//
|
||||
// The OTLP exporter handles unix sockets in a strange way.
|
||||
// It seems to imply they can be used as an environment variable
|
||||
// and are handled properly, but they don't seem to be as the behavior
|
||||
// of the environment variable is to strip the scheme from the endpoint
|
||||
// while the underlying implementation needs the scheme to use the
|
||||
// correct resolver.
|
||||
func unixSocketEndpoint(u *url.URL) string {
|
||||
// GRPC does not allow host to be used.
|
||||
socketPath := u.Path
|
||||
|
||||
// If we are on windows and we have an absolute path
|
||||
// that references a letter drive, check to see if the
|
||||
// WSL equivalent path exists and we should use that instead.
|
||||
if isWsl() {
|
||||
if p := wslSocketPath(socketPath, os.DirFS("/")); p != "" {
|
||||
socketPath = p
|
||||
}
|
||||
}
|
||||
// Enforce that we are using forward slashes.
|
||||
return "unix://" + filepath.ToSlash(socketPath)
|
||||
}
|
||||
|
||||
// wslSocketPath will convert the referenced URL to a WSL-compatible
|
||||
// path and check if that path exists. If the path exists, it will
|
||||
// be returned.
|
||||
func wslSocketPath(s string, f fs.FS) string {
|
||||
if p := toWslPath(s); p != "" {
|
||||
if _, err := stat(p, f); err == nil {
|
||||
return "/" + p
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// toWslPath converts the referenced URL to a WSL-compatible
|
||||
// path if this looks like a Windows absolute path.
|
||||
//
|
||||
// If no drive is in the URL, defaults to the C drive.
|
||||
func toWslPath(s string) string {
|
||||
drive, p, ok := parseUNCPath(s)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("mnt/%s%s", strings.ToLower(drive), p)
|
||||
}
|
||||
|
||||
func parseUNCPath(s string) (drive, p string, ok bool) {
|
||||
// UNC paths use backslashes but we're using forward slashes
|
||||
// so also enforce that here.
|
||||
//
|
||||
// In reality, this should have been enforced much earlier
|
||||
// than here since backslashes aren't allowed in URLs, but
|
||||
// we're going to code defensively here.
|
||||
s = filepath.ToSlash(s)
|
||||
|
||||
const uncPrefix = "//./"
|
||||
if !strings.HasPrefix(s, uncPrefix) {
|
||||
// Not a UNC path.
|
||||
return "", "", false
|
||||
}
|
||||
s = s[len(uncPrefix):]
|
||||
|
||||
parts := strings.SplitN(s, "/", 2)
|
||||
if len(parts) != 2 {
|
||||
// Not enough components.
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
drive, ok = splitWindowsDrive(parts[0])
|
||||
if !ok {
|
||||
// Not a windows drive.
|
||||
return "", "", false
|
||||
}
|
||||
return drive, "/" + parts[1], true
|
||||
}
|
||||
|
||||
// splitWindowsDrive checks if the string references a windows
|
||||
// drive (such as c:) and returns the drive letter if it is.
|
||||
func splitWindowsDrive(s string) (string, bool) {
|
||||
if b := []rune(s); len(b) == 2 && unicode.IsLetter(b[0]) && b[1] == ':' {
|
||||
return string(b[0]), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func stat(p string, f fs.FS) (fs.FileInfo, error) {
|
||||
if f, ok := f.(fs.StatFS); ok {
|
||||
return f.Stat(p)
|
||||
}
|
||||
|
||||
file, err := f.Open(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
return file.Stat()
|
||||
}
|
||||
|
||||
func isWsl() bool {
|
||||
return os.Getenv("WSL_DISTRO_NAME") != ""
|
||||
}
|
||||
|
||||
57
cli/command/telemetry_docker_test.go
Normal file
57
cli/command/telemetry_docker_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestWslSocketPath(t *testing.T) {
|
||||
testCases := []struct {
|
||||
doc string
|
||||
fs fs.FS
|
||||
url string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
doc: "filesystem where WSL path does not exist",
|
||||
fs: fstest.MapFS{
|
||||
"my/file/path": {},
|
||||
},
|
||||
url: "unix:////./c:/my/file/path",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
doc: "filesystem where WSL path exists",
|
||||
fs: fstest.MapFS{
|
||||
"mnt/c/my/file/path": {},
|
||||
},
|
||||
url: "unix:////./c:/my/file/path",
|
||||
expected: "/mnt/c/my/file/path",
|
||||
},
|
||||
{
|
||||
doc: "filesystem where WSL path exists uppercase URL",
|
||||
fs: fstest.MapFS{
|
||||
"mnt/c/my/file/path": {},
|
||||
},
|
||||
url: "unix:////./C:/my/file/path",
|
||||
expected: "/mnt/c/my/file/path",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
u, err := url.Parse(tc.url)
|
||||
assert.NilError(t, err)
|
||||
// Ensure host is empty.
|
||||
assert.Equal(t, u.Host, "")
|
||||
|
||||
result := wslSocketPath(u.Path, tc.fs)
|
||||
|
||||
assert.Equal(t, result, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"github.com/docker/cli/cli/version"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
@ -94,7 +95,9 @@ func startCobraCommandTimer(mp metric.MeterProvider, attrs []attribute.KeyValue)
|
||||
metric.WithAttributes(cmdStatusAttrs...),
|
||||
)
|
||||
if mp, ok := mp.(MeterProvider); ok {
|
||||
mp.ForceFlush(ctx)
|
||||
if err := mp.ForceFlush(ctx); err != nil {
|
||||
otel.Handle(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -18,7 +18,7 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "update [OPTIONS] [VOLUME]",
|
||||
Short: "Update a volume (cluster volumes only)",
|
||||
Args: cli.RequiresMaxArgs(1),
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runUpdate(cmd.Context(), dockerCli, args[0], availability, cmd.Flags())
|
||||
},
|
||||
|
||||
22
cli/command/volume/update_test.go
Normal file
22
cli/command/volume/update_test.go
Normal file
@ -0,0 +1,22 @@
|
||||
package volume
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestUpdateCmd(t *testing.T) {
|
||||
cmd := newUpdateCommand(
|
||||
test.NewFakeCli(&fakeClient{}),
|
||||
)
|
||||
cmd.SetArgs([]string{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
err := cmd.Execute()
|
||||
|
||||
assert.ErrorContains(t, err, "requires exactly 1 argument")
|
||||
}
|
||||
@ -45,13 +45,14 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*ConnectionHelper
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "ssh host connection is not valid")
|
||||
}
|
||||
sshFlags = addSSHTimeout(sshFlags)
|
||||
sshFlags = disablePseudoTerminalAllocation(sshFlags)
|
||||
return &ConnectionHelper{
|
||||
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
args := []string{"docker"}
|
||||
if sp.Path != "" {
|
||||
args = append(args, "--host", "unix://"+sp.Path)
|
||||
}
|
||||
sshFlags = addSSHTimeout(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")
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
261
cli/internal/oauth/api/api.go
Normal file
261
cli/internal/oauth/api/api.go
Normal file
@ -0,0 +1,261 @@
|
||||
// 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 for polling tenant for login – based on the interval
|
||||
// specified by the tenant response.
|
||||
ticker := time.NewTimer(state.IntervalDuration())
|
||||
defer ticker.Stop()
|
||||
// The tenant tells us for as long as we can poll it for credentials
|
||||
// while the user logs in through their browser. Timeout if we don't get
|
||||
// credentials within this period.
|
||||
timeout := time.NewTimer(state.ExpiryDuration())
|
||||
defer timeout.Stop()
|
||||
|
||||
for {
|
||||
resetTimer(ticker, state.IntervalDuration())
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// user canceled login
|
||||
return TokenResponse{}, ctx.Err()
|
||||
case <-ticker.C:
|
||||
// tick, check for user login
|
||||
res, err := a.getDeviceToken(ctx, state)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
// if the caller canceled the context, continue
|
||||
// and let the select hit the ctx.Done() branch
|
||||
continue
|
||||
}
|
||||
return TokenResponse{}, err
|
||||
}
|
||||
|
||||
if res.Error != nil {
|
||||
if *res.Error == "authorization_pending" {
|
||||
continue
|
||||
}
|
||||
|
||||
return res, errors.New(res.ErrorDescription)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
case <-timeout.C:
|
||||
// login timed out
|
||||
return TokenResponse{}, ErrTimeout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// resetTimer is a helper function thatstops, drains and resets the timer.
|
||||
// This is necessary in go versions <1.23, since the timer isn't stopped +
|
||||
// the timer's channel isn't drained on timer.Reset.
|
||||
// See: https://go-review.googlesource.com/c/go/+/568341
|
||||
// FIXME: remove/simplify this after we update to go1.23
|
||||
func resetTimer(t *time.Timer, d time.Duration) {
|
||||
if !t.Stop() {
|
||||
select {
|
||||
case <-t.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
t.Reset(d)
|
||||
}
|
||||
|
||||
// getToken calls the token endpoint of Auth0 and returns the response.
|
||||
func (a API) getDeviceToken(ctx context.Context, state State) (TokenResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
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"`
|
||||
}
|
||||
}
|
||||
428
cli/internal/oauth/api/api_test.go
Normal file
428
cli/internal/oauth/api/api_test.go
Normal 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: 5,
|
||||
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)
|
||||
})
|
||||
}
|
||||
26
cli/internal/oauth/api/state.go
Normal file
26
cli/internal/oauth/api/state.go
Normal 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
73
cli/internal/oauth/jwt.go
Normal 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)
|
||||
}
|
||||
204
cli/internal/oauth/manager/manager.go
Normal file
204
cli/internal/oauth/manager/manager.go
Normal 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),
|
||||
)
|
||||
}
|
||||
363
cli/internal/oauth/manager/manager_test.go
Normal file
363
cli/internal/oauth/manager/manager_test.go
Normal 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}
|
||||
}
|
||||
28
cli/internal/oauth/manager/util.go
Normal file
28
cli/internal/oauth/manager/util.go
Normal 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)
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -360,7 +360,11 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
|
||||
|
||||
mp := dockerCli.MeterProvider()
|
||||
if mp, ok := mp.(command.MeterProvider); ok {
|
||||
defer mp.Shutdown(ctx)
|
||||
defer func() {
|
||||
if err := mp.Shutdown(ctx); err != nil {
|
||||
otel.Handle(err)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
fmt.Fprint(dockerCli.Err(), "Warning: Unexpected OTEL error, metrics may not be flushed")
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
variable "GO_VERSION" {
|
||||
default = "1.21.13"
|
||||
default = "1.22.7"
|
||||
}
|
||||
variable "VERSION" {
|
||||
default = ""
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.21.13
|
||||
ARG GO_VERSION=1.22.7
|
||||
ARG ALPINE_VERSION=3.20
|
||||
|
||||
ARG BUILDX_VERSION=0.16.1
|
||||
ARG BUILDX_VERSION=0.17.1
|
||||
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.21.13
|
||||
ARG GO_VERSION=1.22.7
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG GOLANGCI_LINT_VERSION=v1.59.1
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.21.13
|
||||
ARG GO_VERSION=1.22.7
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG MODOUTDATED_VERSION=v0.8.0
|
||||
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
---
|
||||
title: Deprecated Docker Engine features
|
||||
linkTitle: Deprecated features
|
||||
aliases: ["/engine/misc/deprecated/"]
|
||||
description: "Deprecated Features."
|
||||
keywords: "docker, documentation, about, technology, deprecate"
|
||||
@ -13,14 +15,12 @@ keywords: "docker, documentation, about, technology, deprecate"
|
||||
will be rejected.
|
||||
-->
|
||||
|
||||
# Deprecated Engine Features
|
||||
|
||||
This page provides an overview of features that are deprecated in Engine. Changes
|
||||
in packaging, and supported (Linux) distributions are not included. To learn
|
||||
about end of support for Linux distributions, refer to the
|
||||
[release notes](https://docs.docker.com/engine/release-notes/).
|
||||
|
||||
## Feature Deprecation Policy
|
||||
## Feature deprecation policy
|
||||
|
||||
As changes are made to Docker there may be times when existing features need to
|
||||
be removed or replaced with newer features. Before an existing feature is removed
|
||||
@ -32,21 +32,24 @@ Users are expected to take note of the list of deprecated features each release
|
||||
and plan their migration away from those features, and (if applicable) towards
|
||||
the replacement features as soon as possible.
|
||||
|
||||
## Deprecated Engine Features
|
||||
## Deprecated engine features
|
||||
|
||||
The table below provides an overview of the current status of deprecated features:
|
||||
The following table provides an overview of the current status of deprecated features:
|
||||
|
||||
- **Deprecated**: the feature is marked "deprecated" and should no longer be used.
|
||||
|
||||
The feature may be removed, disabled, or change behavior in a future release.
|
||||
The _"Deprecated"_ column contains the release in which the feature was marked
|
||||
The _"Deprecated"_ column contains the release in which the feature was marked
|
||||
deprecated, whereas the _"Remove"_ column contains a tentative release in which
|
||||
the feature is to be removed. If no release is included in the _"Remove"_ column,
|
||||
the release is yet to be decided on.
|
||||
- **Removed**: the feature was removed, disabled, or hidden. Refer to the linked
|
||||
section for details. Some features are "soft" deprecated, which means that they
|
||||
remain functional for backward compatibility, and to allow users to migrate to
|
||||
alternatives. In such cases, a warning may be printed, and users should not rely
|
||||
on this feature.
|
||||
|
||||
- **Removed**: the feature was removed, disabled, or hidden.
|
||||
|
||||
Refer to the linked section for details. Some features are "soft" deprecated,
|
||||
which means that they remain functional for backward compatibility, and to
|
||||
allow users to migrate to alternatives. In such cases, a warning may be
|
||||
printed, and users should not rely on this feature.
|
||||
|
||||
| Status | Feature | Deprecated | Remove |
|
||||
|------------|------------------------------------------------------------------------------------------------------------------------------------|------------|--------|
|
||||
@ -57,10 +60,10 @@ The table below provides an overview of the current status of deprecated feature
|
||||
| Deprecated | [`Container` and `ContainerConfig` fields in Image inspect](#container-and-containerconfig-fields-in-image-inspect) | v25.0 | v26.0 |
|
||||
| Deprecated | [Deprecate legacy API versions](#deprecate-legacy-api-versions) | v25.0 | v26.0 |
|
||||
| Removed | [Container short ID in network Aliases field](#container-short-id-in-network-aliases-field) | v25.0 | v26.0 |
|
||||
| Deprecated | [IsAutomated field, and "is-automated" filter on docker search](#isautomated-field-and-is-automated-filter-on-docker-search) | v25.0 | v26.0 |
|
||||
| Deprecated | [IsAutomated field, and `is-automated` filter on `docker search`](#isautomated-field-and-is-automated-filter-on-docker-search) | v25.0 | v26.0 |
|
||||
| Removed | [logentries logging driver](#logentries-logging-driver) | v24.0 | v25.0 |
|
||||
| Removed | [OOM-score adjust for the daemon](#oom-score-adjust-for-the-daemon) | v24.0 | v25.0 |
|
||||
| Removed | [Buildkit build information](#buildkit-build-information) | v23.0 | v24.0 |
|
||||
| Removed | [BuildKit build information](#buildkit-build-information) | v23.0 | v24.0 |
|
||||
| Deprecated | [Legacy builder for Linux images](#legacy-builder-for-linux-images) | v23.0 | - |
|
||||
| Deprecated | [Legacy builder fallback](#legacy-builder-fallback) | v23.0 | - |
|
||||
| Removed | [Btrfs storage driver on CentOS 7 and RHEL 7](#btrfs-storage-driver-on-centos-7-and-rhel-7) | v20.10 | v23.0 |
|
||||
@ -91,7 +94,7 @@ The table below provides an overview of the current status of deprecated feature
|
||||
| Removed | [Asynchronous `service create` and `service update` as default](#asynchronous-service-create-and-service-update-as-default) | v17.05 | v17.10 |
|
||||
| Removed | [`-g` and `--graph` flags on `dockerd`](#-g-and---graph-flags-on-dockerd) | v17.05 | v23.0 |
|
||||
| Deprecated | [Top-level network properties in NetworkSettings](#top-level-network-properties-in-networksettings) | v1.13 | v17.12 |
|
||||
| Removed | [`filter` param for `/images/json` endpoint](#filter-param-for-imagesjson-endpoint) | v1.13 | v20.10 |
|
||||
| Removed | [`filter` option for `/images/json` endpoint](#filter-option-for-imagesjson-endpoint) | v1.13 | v20.10 |
|
||||
| Removed | [`repository:shortid` image references](#repositoryshortid-image-references) | v1.13 | v17.12 |
|
||||
| Removed | [`docker daemon` subcommand](#docker-daemon-subcommand) | v1.13 | v17.12 |
|
||||
| Removed | [Duplicate keys with conflicting values in engine labels](#duplicate-keys-with-conflicting-values-in-engine-labels) | v1.13 | v17.12 |
|
||||
@ -122,8 +125,8 @@ The table below provides an overview of the current status of deprecated feature
|
||||
|
||||
The `Config` field returned shown in `docker image inspect` (and as returned by
|
||||
the `GET /images/{name}/json` API endpoint) returns additional fields that are
|
||||
not part of the image's configuration and not part of the [Docker Image Spec]
|
||||
and [OCI Image Specification].
|
||||
not part of the image's configuration and not part of the [Docker image specification]
|
||||
and [OCI image specification].
|
||||
|
||||
These fields are never set (and always return the default value for the type),
|
||||
but are not omitted in the response when left empty. As these fields were not
|
||||
@ -131,7 +134,7 @@ intended to be part of the image configuration response, they are deprecated,
|
||||
and will be removed from the API in thee next release.
|
||||
|
||||
The following fields are currently included in the API response, but are not
|
||||
part of the underlying image's Config, and deprecated:
|
||||
part of the underlying image's `Config` field, and deprecated:
|
||||
|
||||
- `Hostname`
|
||||
- `Domainname`
|
||||
@ -146,8 +149,8 @@ part of the underlying image's Config, and deprecated:
|
||||
- `MacAddress` (already omitted unless set)
|
||||
- `StopTimeout` (already omitted unless set)
|
||||
|
||||
[Docker image spec]: https://github.com/moby/docker-image-spec/blob/v1.3.1/specs-go/v1/image.go#L19-L32
|
||||
[OCI Image Spec]: https://github.com/opencontainers/image-spec/blob/v1.1.0/specs-go/v1/config.go#L24-L62
|
||||
[Docker image specification]: https://github.com/moby/docker-image-spec/blob/v1.3.1/specs-go/v1/image.go#L19-L32
|
||||
[OCI image specification]: https://github.com/opencontainers/image-spec/blob/v1.1.0/specs-go/v1/config.go#L24-L62
|
||||
|
||||
### Graphdriver plugins (experimental)
|
||||
|
||||
@ -211,7 +214,7 @@ transit and providing a mechanism for mutual authentication.
|
||||
|
||||
For environments remote daemon access isn't required,
|
||||
we recommend binding the Docker daemon to a Unix socket.
|
||||
For daemon's where remote access is required and where TLS encryption is not feasible,
|
||||
For daemons where remote access is required and where TLS encryption is not feasible,
|
||||
you may want to consider using SSH as an alternative solution.
|
||||
|
||||
For further information, assistance, and step-by-step instructions on
|
||||
@ -255,19 +258,19 @@ daemon may be updated to the latest release, but not all clients may be up-to-da
|
||||
or vice versa). Support for API versions before that (API versions provided by
|
||||
EOL versions of the Docker Daemon) is provided on a "best effort" basis.
|
||||
|
||||
Use of old API versions is very rare, and support for legacy API versions
|
||||
Use of old API versions is rare, and support for legacy API versions
|
||||
involves significant complexity (Docker 1.0.0 having been released 10 years ago).
|
||||
Because of this, we'll start deprecating support for legacy API versions.
|
||||
|
||||
Docker Engine v25.0 by default disables API version older than 1.24 (aligning
|
||||
the minimum supported API version between Linux and Windows daemons). When
|
||||
connecting with a client that uses an API version version older than 1.24,
|
||||
the daemon returns an error. The following example configures the docker
|
||||
connecting with a client that uses an API version older than 1.24,
|
||||
the daemon returns an error. The following example configures the Docker
|
||||
CLI to use API version 1.23, which produces an error:
|
||||
|
||||
```console
|
||||
DOCKER_API_VERSION=1.23 docker version
|
||||
Error response from daemon: client version 1.23 is too old. Minimum supported API version is 1.24,
|
||||
Error response from daemon: client version 1.23 is too old. Minimum supported API version is 1.24,
|
||||
upgrade your client to a newer version
|
||||
```
|
||||
|
||||
@ -301,12 +304,12 @@ A new field `DNSNames` containing the container name (if one was specified),
|
||||
the hostname, the network aliases, as well as the container short ID, has been
|
||||
introduced in v25.0 and should be used instead of the `Aliases` field.
|
||||
|
||||
### IsAutomated field, and "is-automated" filter on docker search
|
||||
### IsAutomated field, and `is-automated` filter on `docker search`
|
||||
|
||||
**Deprecated in Release: v25.0**
|
||||
**Target For Removal In Release: v26.0**
|
||||
|
||||
The "is_automated" field has been deprecated by Docker Hub's search API.
|
||||
The `is_automated` field has been deprecated by Docker Hub's search API.
|
||||
Consequently, the `IsAutomated` field in image search will always be set
|
||||
to `false` in future, and searching for "is-automated=true" will yield no
|
||||
results.
|
||||
@ -346,7 +349,7 @@ Users currently depending on this feature are recommended to adjust the
|
||||
daemon's OOM score using systemd or through other means, when starting
|
||||
the daemon.
|
||||
|
||||
### Buildkit build information
|
||||
### BuildKit build information
|
||||
|
||||
**Deprecated in Release: v23.0**
|
||||
**Removed in Release: v24.0**
|
||||
@ -354,7 +357,7 @@ the daemon.
|
||||
[Build information](https://github.com/moby/buildkit/blob/v0.11/docs/buildinfo.md)
|
||||
structures have been introduced in [BuildKit v0.10.0](https://github.com/moby/buildkit/releases/tag/v0.10.0)
|
||||
and are generated with build metadata that allows you to see all the sources
|
||||
(images, git repositories) that were used by the build with their exact
|
||||
(images, Git repositories) that were used by the build with their exact
|
||||
versions and also the configuration that was passed to the build. This
|
||||
information is also embedded into the image configuration if one is generated.
|
||||
|
||||
@ -389,7 +392,7 @@ you to report issues in the [BuildKit issue tracker on GitHub](https://github.co
|
||||
> `docker build` continues to use the classic builder to build native Windows
|
||||
> images on Windows daemons.
|
||||
|
||||
### Legacy builder fallback
|
||||
### Legacy builder fallback
|
||||
|
||||
**Deprecated in Release: v23.0**
|
||||
|
||||
@ -402,11 +405,11 @@ To provide a smooth transition to BuildKit as the default builder, Docker v23.0
|
||||
has an automatic fallback for some situations, or produces an error to assist
|
||||
users to resolve the problem.
|
||||
|
||||
In situations where the user did not explicitly opt-in to use BuildKit (i.e.,
|
||||
In situations where the user did not explicitly opt-in to use BuildKit (i.e.,
|
||||
`DOCKER_BUILDKIT=1` is not set), the CLI automatically falls back to the classic
|
||||
builder, but prints a deprecation warning:
|
||||
|
||||
```
|
||||
```text
|
||||
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
|
||||
Install the buildx component to build images with BuildKit:
|
||||
https://docs.docker.com/go/buildx/
|
||||
@ -420,7 +423,7 @@ and use BuildKit for your builds, or opt-out of using BuildKit with `DOCKER_BUIL
|
||||
If you opted-in to use BuildKit (`DOCKER_BUILDKIT=1`), but the Buildx component
|
||||
is missing, an error is printed instead, and the `docker build` command fails:
|
||||
|
||||
```
|
||||
```text
|
||||
ERROR: BuildKit is enabled but the buildx component is missing or broken.
|
||||
Install the buildx component to build images with BuildKit:
|
||||
https://docs.docker.com/go/buildx/
|
||||
@ -470,27 +473,27 @@ to decrypt the private key, and store it un-encrypted to continue using it.
|
||||
|
||||
Following the deprecation of [Compose on Kubernetes](https://github.com/docker/compose-on-kubernetes),
|
||||
support for Kubernetes in the `stack` and `context` commands has been removed from
|
||||
the cli, and options related to this functionality are now either ignored, or may
|
||||
the CLI, and options related to this functionality are now either ignored, or may
|
||||
produce an error.
|
||||
|
||||
The following command-line flags are removed from the `docker context` subcommands:
|
||||
|
||||
- `--default-stack-orchestrator` - swarm is now the only (and default) orchestrator for stacks.
|
||||
- `--kubernetes` - the kubernetes endpoint can no longer be stored in `docker context`.
|
||||
- `--kubernetes` - the Kubernetes endpoint can no longer be stored in `docker context`.
|
||||
- `--kubeconfig` - exporting a context as a kubeconfig file is no longer supported.
|
||||
|
||||
The output produced by the `docker context inspect` subcommand no longer contains
|
||||
information about `StackOrchestrator` and `Kubernetes` endpoints for new contexts.
|
||||
|
||||
The following command-line flags are removed from the `docker stack` subcommands:
|
||||
|
||||
|
||||
- `--kubeconfig` - using a kubeconfig file as context is no longer supported.
|
||||
- `--namespace` - configuring the kubernetes namespace for stacks is no longer supported.
|
||||
- `--namespace` - configuring the Kubernetes namespace for stacks is no longer supported.
|
||||
- `--orchestrator` - swarm is now the only (and default) orchestrator for stacks.
|
||||
|
||||
The `DOCKER_STACK_ORCHESTRATOR`, `DOCKER_ORCHESTRATOR`, and `KUBECONFIG` environment
|
||||
variables, as well as the `stackOrchestrator` option in the `~/.docker/config.json`
|
||||
cli configuration file are no longer used, and ignored.
|
||||
CLI configuration file are no longer used, and ignored.
|
||||
|
||||
### Pulling images from non-compliant image registries
|
||||
|
||||
@ -532,7 +535,7 @@ major release.
|
||||
The experimental feature to run Linux containers on Windows (LCOW) was introduced
|
||||
as a technical preview in Docker 17.09. While many enhancements were made after
|
||||
its introduction, the feature never reached completeness, and development has
|
||||
now stopped in favor of running docker natively on Linux in WSL2.
|
||||
now stopped in favor of running Docker natively on Linux in WSL2.
|
||||
|
||||
Developers who want to run Linux workloads on a Windows host are encouraged to use
|
||||
[Docker Desktop with WSL2](https://docs.docker.com/docker-for-windows/wsl/) instead.
|
||||
@ -562,15 +565,14 @@ 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
|
||||
> runtime, this option takes no effect. The Linux kernel did not explicitly
|
||||
> deprecate this feature, and there is a tracking ticket in the `runc` issue
|
||||
> tracker to determine if this option should be reinstated or if this was an
|
||||
> oversight of the Linux kernel maintainers (see [opencontainers/runc#3174](https://github.com/opencontainers/runc/issues/3174)).
|
||||
>
|
||||
>
|
||||
> The `memory.kmem.tcp.limit_in_bytes` option is only supported with cgroups v1,
|
||||
> and not available on installations running with cgroups v2. This option is
|
||||
> only supported by the API, and not exposed on the `docker` command-line.
|
||||
@ -589,7 +591,7 @@ networks using an external key/value store. The corresponding`--cluster-advertis
|
||||
**Deprecated in Release: v20.10**
|
||||
**Removed in Release: v23.0**
|
||||
|
||||
The docker CLI up until v1.7.0 used the `~/.dockercfg` file to store credentials
|
||||
The Docker CLI up until v1.7.0 used the `~/.dockercfg` file to store credentials
|
||||
after authenticating to a registry (`docker login`). Docker v1.7.0 replaced this
|
||||
file with a new CLI configuration file, located in `~/.docker/config.json`. When
|
||||
implementing the new configuration file, the old file (and file-format) was kept
|
||||
@ -723,8 +725,7 @@ to Docker Enterprise, using an image-based distribution of the Docker Engine.
|
||||
This feature was only available on Linux, and only when executed on a local node.
|
||||
Given the limitations of this feature, and the feature not getting widely adopted,
|
||||
the `docker engine` subcommands will be removed, in favor of installation through
|
||||
standard package managers.
|
||||
|
||||
standard package managers.
|
||||
|
||||
### Top-level `docker deploy` subcommand (experimental)
|
||||
|
||||
@ -737,7 +738,6 @@ The top-level `docker deploy` command (using the "Docker Application Bundle"
|
||||
17.03, but superseded by support for Docker Compose files using the `docker stack deploy`
|
||||
subcommand.
|
||||
|
||||
|
||||
### `docker stack deploy` using "dab" files (experimental)
|
||||
|
||||
**Deprecated in Release: v19.03**
|
||||
@ -745,7 +745,7 @@ subcommand.
|
||||
**Removed in Release: v20.10**
|
||||
|
||||
With no development being done on this feature, and no active use of the file
|
||||
format, support for the DAB file format and the top-level docker deploy command
|
||||
format, support for the DAB file format and the top-level `docker deploy` command
|
||||
(hidden by default in 19.03), will be removed, in favour of `docker stack deploy`
|
||||
using compose files.
|
||||
|
||||
@ -786,12 +786,12 @@ maintenance of the `aufs` storage driver.
|
||||
|
||||
The `overlay` storage driver is deprecated in favor of the `overlay2` storage
|
||||
driver, which has all the benefits of `overlay`, without its limitations (excessive
|
||||
inode consumption). The legacy `overlay` storage driver has been removed in
|
||||
inode consumption). The legacy `overlay` storage driver has been removed in
|
||||
Docker Engine v24.0. Users of the `overlay` storage driver should migrate to the
|
||||
`overlay2` storage driver before upgrading to Docker Engine v24.0.
|
||||
|
||||
The legacy `overlay` storage driver allowed using overlayFS-backed filesystems
|
||||
on pre 4.x kernels. Now that all supported distributions are able to run `overlay2`
|
||||
on kernels older than v4.x. Now that all supported distributions are able to run `overlay2`
|
||||
(as they are either on kernel 4.x, or have support for multiple lowerdirs
|
||||
backported), there is no reason to keep maintaining the `overlay` storage driver.
|
||||
|
||||
@ -825,7 +825,6 @@ were always documented to be reserved, but there was never any enforcement.
|
||||
Usage of these namespaces will now cause a warning in the engine logs to discourage their
|
||||
use, and will error instead in v20.10 and above.
|
||||
|
||||
|
||||
### `--disable-legacy-registry` override daemon option
|
||||
|
||||
**Disabled In Release: v17.12**
|
||||
@ -836,7 +835,6 @@ The `--disable-legacy-registry` flag was disabled in Docker 17.12 and will print
|
||||
an error when used. For this error to be printed, the flag itself is still present,
|
||||
but hidden. The flag has been removed in Docker 19.03.
|
||||
|
||||
|
||||
### Interacting with V1 registries
|
||||
|
||||
**Disabled By Default In Release: v17.06**
|
||||
@ -844,7 +842,7 @@ but hidden. The flag has been removed in Docker 19.03.
|
||||
**Removed In Release: v17.12**
|
||||
|
||||
Version 1.8.3 added a flag (`--disable-legacy-registry=false`) which prevents the
|
||||
docker daemon from `pull`, `push`, and `login` operations against v1
|
||||
Docker daemon from `pull`, `push`, and `login` operations against v1
|
||||
registries. Though enabled by default, this signals the intent to deprecate
|
||||
the v1 protocol.
|
||||
|
||||
@ -856,7 +854,6 @@ Starting with Docker 17.12, support for V1 registries has been removed, and the
|
||||
`--disable-legacy-registry` flag can no longer be used, and `dockerd` will fail to
|
||||
start when set.
|
||||
|
||||
|
||||
### Asynchronous `service create` and `service update` as default
|
||||
|
||||
**Deprecated In Release: v17.05**
|
||||
@ -896,20 +893,22 @@ about the default ("bridge") network;
|
||||
|
||||
These properties are deprecated in favor of per-network properties in
|
||||
`NetworkSettings.Networks`. These properties were already "deprecated" in
|
||||
docker 1.9, but kept around for backward compatibility.
|
||||
Docker 1.9, but kept around for backward compatibility.
|
||||
|
||||
Refer to [#17538](https://github.com/docker/docker/pull/17538) for further
|
||||
information.
|
||||
|
||||
### `filter` param for `/images/json` endpoint
|
||||
### `filter` option for `/images/json` endpoint
|
||||
|
||||
**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)**
|
||||
|
||||
**Removed In Release: v20.10**
|
||||
|
||||
The `filter` param to filter the list of image by reference (name or name:tag)
|
||||
The `filter` option to filter the list of image by reference (name or name:tag)
|
||||
is now implemented as a regular filter, named `reference`.
|
||||
|
||||
### `repository:shortid` image references
|
||||
|
||||
**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)**
|
||||
|
||||
**Removed In Release: v17.12**
|
||||
@ -921,6 +920,7 @@ Support for the `repository:shortid` notation to reference images was removed
|
||||
in Docker 17.12.
|
||||
|
||||
### `docker daemon` subcommand
|
||||
|
||||
**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)**
|
||||
|
||||
**Removed In Release: v17.12**
|
||||
@ -928,6 +928,7 @@ in Docker 17.12.
|
||||
The daemon is moved to a separate binary (`dockerd`), and should be used instead.
|
||||
|
||||
### Duplicate keys with conflicting values in engine labels
|
||||
|
||||
**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)**
|
||||
|
||||
**Removed In Release: v17.12**
|
||||
@ -936,11 +937,13 @@ When setting duplicate keys with conflicting values, an error will be produced,
|
||||
will fail to start.
|
||||
|
||||
### `MAINTAINER` in Dockerfile
|
||||
|
||||
**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)**
|
||||
|
||||
`MAINTAINER` was an early very limited form of `LABEL` which should be used instead.
|
||||
|
||||
### API calls without a version
|
||||
|
||||
**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)**
|
||||
|
||||
**Target For Removal In Release: v17.12**
|
||||
@ -950,6 +953,7 @@ future Engine versions. Instead of just requesting, for example, the URL
|
||||
`/containers/json`, you must now request `/v1.25/containers/json`.
|
||||
|
||||
### Backing filesystem without `d_type` support for overlay/overlay2
|
||||
|
||||
**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)**
|
||||
|
||||
**Removed In Release: v17.12**
|
||||
@ -964,7 +968,6 @@ backing filesystem without `d_type` support.
|
||||
|
||||
Refer to [#27358](https://github.com/docker/docker/issues/27358) for details.
|
||||
|
||||
|
||||
### `--automated` and `--stars` flags on `docker search`
|
||||
|
||||
**Deprecated in Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)**
|
||||
@ -974,7 +977,6 @@ Refer to [#27358](https://github.com/docker/docker/issues/27358) for details.
|
||||
The `docker search --automated` and `docker search --stars` options are deprecated.
|
||||
Use `docker search --filter=is-automated=<true|false>` and `docker search --filter=stars=...` instead.
|
||||
|
||||
|
||||
### `-h` shorthand for `--help`
|
||||
|
||||
**Deprecated In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)**
|
||||
@ -987,13 +989,15 @@ on all subcommands (due to it conflicting with, e.g. `-h` / `--hostname` on
|
||||
"usage" output of subcommands, nor documented, and is now marked "deprecated".
|
||||
|
||||
### `-e` and `--email` flags on `docker login`
|
||||
|
||||
**Deprecated In Release: [v1.11.0](https://github.com/docker/docker/releases/tag/v1.11.0)**
|
||||
|
||||
**Removed In Release: [v17.06](https://github.com/docker/docker-ce/releases/tag/v17.06.0-ce)**
|
||||
|
||||
The docker login command is removing the ability to automatically register for an account with the target registry if the given username doesn't exist. Due to this change, the email flag is no longer required, and will be deprecated.
|
||||
The `docker login` no longer automatically registers an account with the target registry if the given username doesn't exist. Due to this change, the email flag is no longer required, and will be deprecated.
|
||||
|
||||
### Separator (`:`) of `--security-opt` flag on `docker run`
|
||||
|
||||
**Deprecated In Release: [v1.11.0](https://github.com/docker/docker/releases/tag/v1.11.0)**
|
||||
|
||||
**Target For Removal In Release: v17.06**
|
||||
@ -1001,12 +1005,14 @@ The docker login command is removing the ability to automatically register for a
|
||||
The flag `--security-opt` doesn't use the colon separator (`:`) anymore to divide keys and values, it uses the equal symbol (`=`) for consistency with other similar flags, like `--storage-opt`.
|
||||
|
||||
### Ambiguous event fields in API
|
||||
|
||||
**Deprecated In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)**
|
||||
|
||||
The fields `ID`, `Status` and `From` in the events API have been deprecated in favor of a more rich structure.
|
||||
See the events API documentation for the new format.
|
||||
|
||||
### `-f` flag on `docker tag`
|
||||
|
||||
**Deprecated In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)**
|
||||
|
||||
**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)**
|
||||
@ -1014,6 +1020,7 @@ See the events API documentation for the new format.
|
||||
To make tagging consistent across the various `docker` commands, the `-f` flag on the `docker tag` command is deprecated. It is no longer necessary to specify `-f` to move a tag from one image to another. Nor will `docker` generate an error if the `-f` flag is missing and the specified tag is already in use.
|
||||
|
||||
### HostConfig at API container start
|
||||
|
||||
**Deprecated In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)**
|
||||
|
||||
**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)**
|
||||
@ -1030,8 +1037,8 @@ defining it at container creation (`POST /containers/create`).
|
||||
The `docker ps --before` and `docker ps --since` options are deprecated.
|
||||
Use `docker ps --filter=before=...` and `docker ps --filter=since=...` instead.
|
||||
|
||||
|
||||
### Driver-specific log tags
|
||||
|
||||
**Deprecated In Release: [v1.9.0](https://github.com/docker/docker/releases/tag/v1.9.0)**
|
||||
|
||||
**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)**
|
||||
@ -1044,8 +1051,8 @@ Because of which, the driver specific log tag options `syslog-tag`, `gelf-tag` a
|
||||
$ docker --log-driver=syslog --log-opt tag="{{.ImageName}}/{{.Name}}/{{.ID}}"
|
||||
```
|
||||
|
||||
|
||||
### Docker Content Trust ENV passphrase variables name change
|
||||
|
||||
**Deprecated In Release: [v1.9.0](https://github.com/docker/docker/releases/tag/v1.9.0)**
|
||||
|
||||
**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)**
|
||||
@ -1055,7 +1062,6 @@ Since 1.9, Docker Content Trust Offline key has been renamed to Root key and the
|
||||
- DOCKER_CONTENT_TRUST_OFFLINE_PASSPHRASE is now named DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE
|
||||
- DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE is now named DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE
|
||||
|
||||
|
||||
### `/containers/(id or name)/copy` endpoint
|
||||
|
||||
**Deprecated In Release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)**
|
||||
@ -1064,63 +1070,61 @@ Since 1.9, Docker Content Trust Offline key has been renamed to Root key and the
|
||||
|
||||
The endpoint `/containers/(id or name)/copy` is deprecated in favor of `/containers/(id or name)/archive`.
|
||||
|
||||
|
||||
### LXC built-in exec driver
|
||||
|
||||
**Deprecated In Release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)**
|
||||
|
||||
**Removed In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)**
|
||||
|
||||
The built-in LXC execution driver, the lxc-conf flag, and API fields have been removed.
|
||||
|
||||
|
||||
### Old Command Line Options
|
||||
|
||||
**Deprecated In Release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)**
|
||||
|
||||
**Removed In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)**
|
||||
|
||||
The flags `-d` and `--daemon` are deprecated in favor of the `daemon` subcommand:
|
||||
|
||||
docker daemon -H ...
|
||||
The flags `-d` and `--daemon` are deprecated. Use the separate `dockerd` binary instead.
|
||||
|
||||
The following single-dash (`-opt`) variant of certain command line options
|
||||
are deprecated and replaced with double-dash options (`--opt`):
|
||||
|
||||
docker attach -nostdin
|
||||
docker attach -sig-proxy
|
||||
docker build -no-cache
|
||||
docker build -rm
|
||||
docker commit -author
|
||||
docker commit -run
|
||||
docker events -since
|
||||
docker history -notrunc
|
||||
docker images -notrunc
|
||||
docker inspect -format
|
||||
docker ps -beforeId
|
||||
docker ps -notrunc
|
||||
docker ps -sinceId
|
||||
docker rm -link
|
||||
docker run -cidfile
|
||||
docker run -dns
|
||||
docker run -entrypoint
|
||||
docker run -expose
|
||||
docker run -link
|
||||
docker run -lxc-conf
|
||||
docker run -n
|
||||
docker run -privileged
|
||||
docker run -volumes-from
|
||||
docker search -notrunc
|
||||
docker search -stars
|
||||
docker search -t
|
||||
docker search -trusted
|
||||
docker tag -force
|
||||
- `docker attach -nostdin`
|
||||
- `docker attach -sig-proxy`
|
||||
- `docker build -no-cache`
|
||||
- `docker build -rm`
|
||||
- `docker commit -author`
|
||||
- `docker commit -run`
|
||||
- `docker events -since`
|
||||
- `docker history -notrunc`
|
||||
- `docker images -notrunc`
|
||||
- `docker inspect -format`
|
||||
- `docker ps -beforeId`
|
||||
- `docker ps -notrunc`
|
||||
- `docker ps -sinceId`
|
||||
- `docker rm -link`
|
||||
- `docker run -cidfile`
|
||||
- `docker run -dns`
|
||||
- `docker run -entrypoint`
|
||||
- `docker run -expose`
|
||||
- `docker run -link`
|
||||
- `docker run -lxc-conf`
|
||||
- `docker run -n`
|
||||
- `docker run -privileged`
|
||||
- `docker run -volumes-from`
|
||||
- `docker search -notrunc`
|
||||
- `docker search -stars`
|
||||
- `docker search -t`
|
||||
- `docker search -trusted`
|
||||
- `docker tag -force`
|
||||
|
||||
The following double-dash options are deprecated and have no replacement:
|
||||
|
||||
docker run --cpuset
|
||||
docker run --networking
|
||||
docker ps --since-id
|
||||
docker ps --before-id
|
||||
docker search --trusted
|
||||
- `docker run --cpuset`
|
||||
- `docker run --networking`
|
||||
- `docker ps --since-id`
|
||||
- `docker ps --before-id`
|
||||
- `docker search --trusted`
|
||||
|
||||
**Deprecated In Release: [v1.5.0](https://github.com/docker/docker/releases/tag/v1.5.0)**
|
||||
|
||||
@ -1128,11 +1132,7 @@ The following double-dash options are deprecated and have no replacement:
|
||||
|
||||
The single-dash (`-help`) was removed, in favor of the double-dash `--help`
|
||||
|
||||
docker -help
|
||||
docker [COMMAND] -help
|
||||
|
||||
|
||||
### `--api-enable-cors` flag on dockerd
|
||||
### `--api-enable-cors` flag on `dockerd`
|
||||
|
||||
**Deprecated In Release: [v1.6.0](https://github.com/docker/docker/releases/tag/v1.6.0)**
|
||||
|
||||
@ -1141,19 +1141,19 @@ The single-dash (`-help`) was removed, in favor of the double-dash `--help`
|
||||
The flag `--api-enable-cors` is deprecated since v1.6.0. Use the flag
|
||||
`--api-cors-header` instead.
|
||||
|
||||
### `--run` flag on docker commit
|
||||
### `--run` flag on `docker commit`
|
||||
|
||||
**Deprecated In Release: [v0.10.0](https://github.com/docker/docker/releases/tag/v0.10.0)**
|
||||
|
||||
**Removed In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)**
|
||||
|
||||
The flag `--run` of the docker commit (and its short version `-run`) were deprecated in favor
|
||||
The flag `--run` of the `docker commit` command (and its short version `-run`) were deprecated in favor
|
||||
of the `--changes` flag that allows to pass `Dockerfile` commands.
|
||||
|
||||
|
||||
### Three arguments form in `docker import`
|
||||
|
||||
**Deprecated In Release: [v0.6.7](https://github.com/docker/docker/releases/tag/v0.6.7)**
|
||||
|
||||
**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)**
|
||||
|
||||
The `docker import` command format `file|URL|- [REPOSITORY [TAG]]` is deprecated since November 2013. It's no more supported.
|
||||
The `docker import` command format `file|URL|- [REPOSITORY [TAG]]` is deprecated since November 2013. It's no longer supported.
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Docker Engine managed plugin system
|
||||
linkTitle: Docker Engine plugins
|
||||
description: Develop and use a plugin with the managed plugin system
|
||||
keywords: "API, Usage, plugins, documentation, developer"
|
||||
aliases:
|
||||
@ -16,8 +17,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 +38,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 +125,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.
|
||||
|
||||
@ -8,7 +8,7 @@ keywords: "Examples, Usage, plugins, docker, documentation, user guide"
|
||||
|
||||
This document describes the Docker Engine plugins generally available in Docker
|
||||
Engine. To view information on plugins managed by Docker,
|
||||
refer to [Docker Engine plugin system](index.md).
|
||||
refer to [Docker Engine plugin system](_index.md).
|
||||
|
||||
You can extend the capabilities of the Docker Engine by loading third-party
|
||||
plugins. This page explains the types of plugins and provides links to several
|
||||
@ -16,7 +16,7 @@ volume and network plugins for Docker.
|
||||
|
||||
## Types of plugins
|
||||
|
||||
Plugins extend Docker's functionality. They come in specific types. For
|
||||
Plugins extend Docker's functionality. They come in specific types. For
|
||||
example, a [volume plugin](plugins_volume.md) might enable Docker
|
||||
volumes to persist across multiple Docker hosts and a
|
||||
[network plugin](plugins_network.md) might provide network plumbing.
|
||||
|
||||
@ -8,7 +8,7 @@ Docker plugins are out-of-process extensions which add capabilities to the
|
||||
Docker Engine.
|
||||
|
||||
This document describes the Docker Engine plugin API. To view information on
|
||||
plugins managed by Docker Engine, refer to [Docker Engine plugin system](index.md).
|
||||
plugins managed by Docker Engine, refer to [Docker Engine plugin system](_index.md).
|
||||
|
||||
This page is intended for people who want to develop their own Docker plugin.
|
||||
If you just want to learn about or use Docker plugins, look
|
||||
|
||||
@ -8,7 +8,7 @@ aliases:
|
||||
|
||||
This document describes the Docker Engine plugins available in Docker
|
||||
Engine. To view information on plugins managed by Docker Engine,
|
||||
refer to [Docker Engine plugin system](index.md).
|
||||
refer to [Docker Engine plugin system](_index.md).
|
||||
|
||||
Docker's out-of-the-box authorization model is all or nothing. Any user with
|
||||
permission to access the Docker daemon can run any Docker client command. The
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -6,12 +6,12 @@ keywords: "Examples, Usage, plugins, docker, documentation, user guide"
|
||||
|
||||
This document describes Docker Engine network driver plugins generally
|
||||
available in Docker Engine. To view information on plugins
|
||||
managed by Docker Engine, refer to [Docker Engine plugin system](index.md).
|
||||
managed by Docker Engine, refer to [Docker Engine plugin system](_index.md).
|
||||
|
||||
Docker Engine network plugins enable Engine deployments to be extended to
|
||||
support a wide range of networking technologies, such as VXLAN, IPVLAN, MACVLAN
|
||||
or something completely different. Network driver plugins are supported via the
|
||||
LibNetwork project. Each plugin is implemented as a "remote driver" for
|
||||
LibNetwork project. Each plugin is implemented as a "remote driver" for
|
||||
LibNetwork, which shares plugin infrastructure with Engine. Effectively, network
|
||||
driver plugins are activated in the same way as other plugins, and use the same
|
||||
kind of protocol.
|
||||
@ -19,7 +19,7 @@ kind of protocol.
|
||||
## Network plugins and Swarm mode
|
||||
|
||||
[Legacy plugins](legacy_plugins.md) do not work in Swarm mode. However,
|
||||
plugins written using the [v2 plugin system](index.md) do work in Swarm mode, as
|
||||
plugins written using the [v2 plugin system](_index.md) do work in Swarm mode, as
|
||||
long as they are installed on each Swarm worker node.
|
||||
|
||||
## Use network driver plugins
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 }
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 container’s PID 1 and does not receive Unix signals.
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -1243,11 +1234,10 @@ the container and remove the file system when the container exits, use the
|
||||
`--rm` flag:
|
||||
|
||||
```text
|
||||
--rm=false: Automatically remove the container when it exits
|
||||
--rm: 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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ The base command for the Docker CLI.
|
||||
| [`inspect`](inspect.md) | Return low-level information on Docker objects |
|
||||
| [`kill`](kill.md) | Kill one or more running containers |
|
||||
| [`load`](load.md) | Load an image from a tar archive or STDIN |
|
||||
| [`login`](login.md) | Log in to a registry |
|
||||
| [`login`](login.md) | Authenticate to a registry |
|
||||
| [`logout`](logout.md) | Log out from a registry |
|
||||
| [`logs`](logs.md) | Fetch the logs of a container |
|
||||
| [`manifest`](manifest.md) | Manage Docker image manifests and manifest lists |
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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-->
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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-->
|
||||
|
||||
@ -1,60 +1,51 @@
|
||||
# login
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Log in to a registry.
|
||||
If no server is specified, the default is defined by the daemon.
|
||||
Authenticate to a registry.
|
||||
Defaults to Docker Hub if no server is specified.
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:--------------------------------------|:---------|:--------|:-----------------------------|
|
||||
| `-p`, `--password` | `string` | | Password |
|
||||
| [`--password-stdin`](#password-stdin) | `bool` | | Take the password from stdin |
|
||||
| `-u`, `--username` | `string` | | Username |
|
||||
| Name | Type | Default | Description |
|
||||
|:---------------------------------------------|:---------|:--------|:-----------------------------|
|
||||
| `-p`, `--password` | `string` | | Password |
|
||||
| [`--password-stdin`](#password-stdin) | `bool` | | Take the password from stdin |
|
||||
| [`-u`](#username), [`--username`](#username) | `string` | | Username |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
## Description
|
||||
|
||||
Log in to a registry.
|
||||
Authenticate to a registry.
|
||||
|
||||
## Examples
|
||||
You can authenticate to any public or private registry for which you have
|
||||
credentials. Authentication may be required for pulling and pushing images.
|
||||
Other commands, such as `docker scout` and `docker build`, may also require
|
||||
authentication to access subscription-only features or data related to your
|
||||
Docker organization.
|
||||
|
||||
### Login to a self-hosted registry
|
||||
Authentication credentials are stored in the configured [credential
|
||||
store](#credential-stores). If you use Docker Desktop, credentials are
|
||||
automatically saved to the native keychain of your operating system. If you're
|
||||
not using Docker Desktop, you can configure the credential store in the Docker
|
||||
configuration file, which is located at `$HOME/.docker/config.json` on Linux or
|
||||
`%USERPROFILE%/.docker/config.json` on Windows. If you don't configure a
|
||||
credential store, Docker stores credentials in the `config.json` file in a
|
||||
base64-encoded format. This method is less secure than configuring and using a
|
||||
credential store.
|
||||
|
||||
If you want to log in to a self-hosted registry you can specify this by
|
||||
adding the server name.
|
||||
`docker login` also supports [credential helpers](#credential-helpers) to help
|
||||
you handle credentials for specific registries.
|
||||
|
||||
```console
|
||||
$ docker login localhost:8080
|
||||
```
|
||||
### Authentication methods
|
||||
|
||||
### <a name="password-stdin"></a> Provide a password using STDIN (--password-stdin)
|
||||
|
||||
To run the `docker login` command non-interactively, you can set the
|
||||
`--password-stdin` flag to provide a password through `STDIN`. Using
|
||||
`STDIN` prevents the password from ending up in the shell's history,
|
||||
or log-files.
|
||||
|
||||
The following example reads a password from a file, and passes it to the
|
||||
`docker login` command using `STDIN`:
|
||||
|
||||
```console
|
||||
$ cat ~/my_password.txt | docker login --username foo --password-stdin
|
||||
```
|
||||
|
||||
### Privileged user requirement
|
||||
|
||||
`docker login` requires you to use `sudo` or be `root`, except when:
|
||||
|
||||
- Connecting to a remote daemon, such as a `docker-machine` provisioned `docker engine`.
|
||||
- The user is added to the `docker` group. This will impact the security of your system; the `docker` group is `root` equivalent. See [Docker Daemon Attack Surface](https://docs.docker.com/engine/security/#docker-daemon-attack-surface) for details.
|
||||
|
||||
You can log in to any public or private repository for which you have
|
||||
credentials. When you log in, the command stores credentials in
|
||||
`$HOME/.docker/config.json` on Linux or `%USERPROFILE%/.docker/config.json` on
|
||||
Windows, via the procedure described below.
|
||||
You can authenticate to a registry using a username and access token or
|
||||
password. Docker Hub also supports a web-based sign-in flow, which signs you in
|
||||
to your Docker account without entering your password. For Docker Hub, the
|
||||
`docker login` command uses a device code flow by default, unless the
|
||||
`--username` flag is specified. The device code flow is a secure way to sign
|
||||
in. See [Authenticate to Docker Hub using device code](#authenticate-to-docker-hub-using-device-code).
|
||||
|
||||
### Credential stores
|
||||
|
||||
@ -75,6 +66,10 @@ Helpers are available for the following credential stores:
|
||||
- Microsoft Windows Credential Manager
|
||||
- [pass](https://www.passwordstore.org/)
|
||||
|
||||
With Docker Desktop, the credential store is already installed and configured
|
||||
for you. Unless you want to change the credential store used by Docker Desktop,
|
||||
you can skip the following steps.
|
||||
|
||||
#### Configure the credential store
|
||||
|
||||
You need to specify the credential store in `$HOME/.docker/config.json`
|
||||
@ -94,22 +89,22 @@ the credentials from the file and run `docker login` again.
|
||||
#### Default behavior
|
||||
|
||||
By default, Docker looks for the native binary on each of the platforms, i.e.
|
||||
"osxkeychain" on macOS, "wincred" on windows, and "pass" on Linux. A special
|
||||
case is that on Linux, Docker will fall back to the "secretservice" binary if
|
||||
it cannot find the "pass" binary. If none of these binaries are present, it
|
||||
stores the credentials (i.e. password) in base64 encoding in the config files
|
||||
described above.
|
||||
`osxkeychain` on macOS, `wincred` on Windows, and `pass` on Linux. A special
|
||||
case is that on Linux, Docker will fall back to the `secretservice` binary if
|
||||
it cannot find the `pass` binary. If none of these binaries are present, it
|
||||
stores the base64-encoded credentials in the `config.json` configuration file.
|
||||
|
||||
#### Credential helper protocol
|
||||
|
||||
Credential helpers can be any program or script that follows a very simple protocol.
|
||||
This protocol is heavily inspired by Git, but it differs in the information shared.
|
||||
Credential helpers can be any program or script that implements the credential
|
||||
helper protocol. This protocol is inspired by Git, but differs in the
|
||||
information shared.
|
||||
|
||||
The helpers always use the first argument in the command to identify the action.
|
||||
There are only three possible values for that argument: `store`, `get`, and `erase`.
|
||||
|
||||
The `store` command takes a JSON payload from the standard input. That payload carries
|
||||
the server address, to identify the credential, the user name, and either a password
|
||||
the server address, to identify the credential, the username, and either a password
|
||||
or an identity token.
|
||||
|
||||
```json
|
||||
@ -149,10 +144,10 @@ will show if there was an issue.
|
||||
|
||||
### Credential helpers
|
||||
|
||||
Credential helpers are similar to the credential store above, but act as the
|
||||
designated programs to handle credentials for specific registries. The default
|
||||
credential store (`credsStore` or the config file itself) will not be used for
|
||||
operations concerning credentials of the specified registries.
|
||||
Credential helpers are similar to [credential stores](#credential-stores), but
|
||||
act as the designated programs to handle credentials for specific registries.
|
||||
The default credential store will not be used for operations concerning
|
||||
credentials of the specified registries.
|
||||
|
||||
#### Configure credential helpers
|
||||
|
||||
@ -162,19 +157,93 @@ the credentials from the default store.
|
||||
Credential helpers are specified in a similar way to `credsStore`, but
|
||||
allow for multiple helpers to be configured at a time. Keys specify the
|
||||
registry domain, and values specify the suffix of the program to use
|
||||
(i.e. everything after `docker-credential-`).
|
||||
For example:
|
||||
(i.e. everything after `docker-credential-`). For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"credHelpers": {
|
||||
"registry.example.com": "registryhelper",
|
||||
"awesomereg.example.org": "hip-star",
|
||||
"unicorn.example.io": "vcbait"
|
||||
"myregistry.example.com": "secretservice",
|
||||
"docker.internal.example": "pass",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Authenticate to Docker Hub with web-based login
|
||||
|
||||
By default, the `docker login` command authenticates to Docker Hub, using a
|
||||
device code flow. This flow lets you authenticate to Docker Hub without
|
||||
entering your password. Instead, you visit a URL in your web browser, enter a
|
||||
code, and authenticate.
|
||||
|
||||
```console
|
||||
$ docker login
|
||||
|
||||
USING WEB-BASED LOGIN
|
||||
To sign in with credentials on the command line, use 'docker login -u <username>'
|
||||
|
||||
Your one-time device confirmation code is: LNFR-PGCJ
|
||||
Press ENTER to open your browser or submit your device code here: https://login.docker.com/activate
|
||||
|
||||
Waiting for authentication in the browser…
|
||||
```
|
||||
|
||||
After entering the code in your browser, you are authenticated to Docker Hub
|
||||
using the account you're currently signed in with on the Docker Hub website or
|
||||
in Docker Desktop. If you aren't signed in, you are prompted to sign in after
|
||||
entering the device code.
|
||||
|
||||
### Authenticate to a self-hosted registry
|
||||
|
||||
If you want to authenticate to a self-hosted registry you can specify this by
|
||||
adding the server name.
|
||||
|
||||
```console
|
||||
$ docker login registry.example.com
|
||||
```
|
||||
|
||||
By default, the `docker login` command assumes that the registry listens on
|
||||
port 443 or 80. If the registry listens on a different port, you can specify it
|
||||
by adding the port number to the server name.
|
||||
|
||||
```console
|
||||
$ docker login registry.example.com:1337
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Registry addresses should not include URL path components, only the hostname
|
||||
> and (optionally) the port. Registry addresses with URL path components may
|
||||
> result in an error. For example, `docker login registry.example.com/foo/`
|
||||
> is incorrect, while `docker login registry.example.com` is correct.
|
||||
>
|
||||
> The exception to this rule is the Docker Hub registry, which may use the
|
||||
> `/v1/` path component in the address for historical reasons.
|
||||
|
||||
### <a name="username"></a> Authenticate to a registry with a username and password
|
||||
|
||||
To authenticate to a registry with a username and password, you can use the
|
||||
`--username` or `-u` flag. The following example authenticates to Docker Hub
|
||||
with the username `moby`. The password is entered interactively.
|
||||
|
||||
```console
|
||||
$ docker login -u moby
|
||||
```
|
||||
|
||||
### <a name="password-stdin"></a> Provide a password using STDIN (--password-stdin)
|
||||
|
||||
To run the `docker login` command non-interactively, you can set the
|
||||
`--password-stdin` flag to provide a password through `STDIN`. Using
|
||||
`STDIN` prevents the password from ending up in the shell's history,
|
||||
or log-files.
|
||||
|
||||
The following example reads a password from a file, and passes it to the
|
||||
`docker login` command using `STDIN`:
|
||||
|
||||
```console
|
||||
$ cat ~/my_password.txt | docker login --username foo --password-stdin
|
||||
```
|
||||
|
||||
## Related commands
|
||||
|
||||
* [logout](logout.md)
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -57,6 +57,7 @@ Options:
|
||||
--exec-opt list Runtime execution options
|
||||
--exec-root string Root directory for execution state files (default "/var/run/docker")
|
||||
--experimental Enable experimental features
|
||||
--feature map Enable feature in the daemon
|
||||
--fixed-cidr string IPv4 subnet for fixed IPs
|
||||
--fixed-cidr-v6 string IPv6 subnet for fixed IPs
|
||||
-G, --group string Group for the unix socket (default "docker")
|
||||
@ -79,6 +80,7 @@ Options:
|
||||
--label list Set key=value labels to the daemon
|
||||
--live-restore Enable live restore of docker when containers are still running
|
||||
--log-driver string Default driver for container logs (default "json-file")
|
||||
--log-format string Set the logging format ("text"|"json") (default "text")
|
||||
-l, --log-level string Set the logging level ("debug"|"info"|"warn"|"error"|"fatal") (default "info")
|
||||
--log-opt map Default log driver options for containers (default map[])
|
||||
--max-concurrent-downloads int Set the max concurrent downloads (default 3)
|
||||
@ -124,6 +126,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 +155,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 +193,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 +260,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 +709,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 +857,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
|
||||
@ -894,6 +892,33 @@ Alternatively, you can set custom locations for CDI specifications using the
|
||||
When CDI is enabled for a daemon, you can view the configured CDI specification
|
||||
directories using the `docker info` command.
|
||||
|
||||
#### <a name="log-format"></a> Daemon logging format
|
||||
|
||||
The `--log-format` option or "log-format" option in the [daemon configuration file](#daemon-configuration-file)
|
||||
lets you set the format for logs produced by the daemon. The logging format should
|
||||
only be configured either through the `--log-format` command line option or
|
||||
through the "log-format" field in the configuration file; using both
|
||||
the command-line option and the "log-format" field in the configuration
|
||||
file produces an error. If this option is not set, the default is "text".
|
||||
|
||||
The following example configures the daemon through the `--log-format` command
|
||||
line option to use `json` formatted logs;
|
||||
|
||||
```console
|
||||
$ dockerd --log-format=json
|
||||
# ...
|
||||
{"level":"info","msg":"API listen on /var/run/docker.sock","time":"2024-09-16T11:06:08.558145428Z"}
|
||||
```
|
||||
|
||||
The following example shows a `daemon.json` configuration file with the
|
||||
"log-format" set;
|
||||
|
||||
```json
|
||||
{
|
||||
"log-format": "json"
|
||||
}
|
||||
```
|
||||
|
||||
### Miscellaneous options
|
||||
|
||||
IP masquerading uses address translation to allow containers without a public
|
||||
@ -975,6 +1000,36 @@ Example of usage:
|
||||
}
|
||||
```
|
||||
|
||||
### <a name="feature"></a> Enable feature in the daemon (--feature)
|
||||
|
||||
The `--feature` option lets you enable or disable a feature in the daemon.
|
||||
This option corresponds with the "features" field in the [daemon.json configuration file](#daemon-configuration-file).
|
||||
Features should only be configured either through the `--feature` command line
|
||||
option or through the "features" field in the configuration file; using both
|
||||
the command-line option and the "features" field in the configuration
|
||||
file produces an error. The feature option can be specified multiple times
|
||||
to configure multiple features. The `--feature` option accepts a name and
|
||||
optional boolean value. When omitting the value, the default is `true`.
|
||||
|
||||
The following example runs the daemon with the `cdi` and `containerd-snapshotter`
|
||||
features enabled. The `cdi` option is provided with a value;
|
||||
|
||||
```console
|
||||
$ dockerd --feature cdi=true --feature containerd-snapshotter
|
||||
```
|
||||
|
||||
The following example is the equivalent using the `daemon.json` configuration
|
||||
file;
|
||||
|
||||
```json
|
||||
{
|
||||
"features": {
|
||||
"cdi": true,
|
||||
"containerd-snapshotter": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Daemon configuration file
|
||||
|
||||
The `--config-file` option allows you to set any configuration option
|
||||
@ -1069,7 +1124,10 @@ The following is a full example of the allowed configuration options on Linux:
|
||||
"exec-opts": [],
|
||||
"exec-root": "",
|
||||
"experimental": false,
|
||||
"features": {},
|
||||
"features": {
|
||||
"cdi": true,
|
||||
"containerd-snapshotter": true
|
||||
},
|
||||
"fixed-cidr": "",
|
||||
"fixed-cidr-v6": "",
|
||||
"group": "",
|
||||
@ -1093,6 +1151,7 @@ The following is a full example of the allowed configuration options on Linux:
|
||||
"labels": [],
|
||||
"live-restore": true,
|
||||
"log-driver": "json-file",
|
||||
"log-format": "text",
|
||||
"log-level": "",
|
||||
"log-opts": {
|
||||
"cache-disabled": "false",
|
||||
@ -1145,8 +1204,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
|
||||
@ -1188,6 +1246,7 @@ The following is a full example of the allowed configuration options on Windows:
|
||||
"insecure-registries": [],
|
||||
"labels": [],
|
||||
"log-driver": "",
|
||||
"log-format": "text",
|
||||
"log-level": "",
|
||||
"max-concurrent-downloads": 3,
|
||||
"max-concurrent-uploads": 5,
|
||||
@ -1242,7 +1301,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 +1334,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.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user