Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@ -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:
|
||||
@ -107,13 +131,14 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
|
||||
// Linux will hit this if you attempt `cat | docker login`, and Windows
|
||||
// will hit this if you attempt docker login from mintty where stdin
|
||||
// is a pipe, not a character based console.
|
||||
if flPassword == "" && !cli.In().IsTerminal() {
|
||||
return errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
|
||||
if argPassword == "" && !cli.In().IsTerminal() {
|
||||
return authConfig, errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
|
||||
}
|
||||
|
||||
authconfig.Username = strings.TrimSpace(authconfig.Username)
|
||||
isDefaultRegistry := serverAddress == registry.IndexServer
|
||||
defaultUsername = strings.TrimSpace(defaultUsername)
|
||||
|
||||
if flUser = strings.TrimSpace(flUser); flUser == "" {
|
||||
if argUser = strings.TrimSpace(argUser); argUser == "" {
|
||||
if isDefaultRegistry {
|
||||
// if this is a default registry (docker hub), then display the following message.
|
||||
fmt.Fprintln(cli.Out(), "Log in with your Docker ID or email address to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com/ to create one.")
|
||||
@ -124,44 +149,43 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
|
||||
}
|
||||
|
||||
var prompt string
|
||||
if authconfig.Username == "" {
|
||||
if defaultUsername == "" {
|
||||
prompt = "Username: "
|
||||
} else {
|
||||
prompt = fmt.Sprintf("Username (%s): ", authconfig.Username)
|
||||
prompt = fmt.Sprintf("Username (%s): ", defaultUsername)
|
||||
}
|
||||
var err error
|
||||
flUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
|
||||
argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
|
||||
if err != nil {
|
||||
return err
|
||||
return authConfig, err
|
||||
}
|
||||
if flUser == "" {
|
||||
flUser = authconfig.Username
|
||||
if argUser == "" {
|
||||
argUser = defaultUsername
|
||||
}
|
||||
}
|
||||
if flUser == "" {
|
||||
return errors.Errorf("Error: Non-null Username Required")
|
||||
if argUser == "" {
|
||||
return authConfig, errors.Errorf("Error: Non-null Username Required")
|
||||
}
|
||||
if flPassword == "" {
|
||||
if argPassword == "" {
|
||||
restoreInput, err := DisableInputEcho(cli.In())
|
||||
if err != nil {
|
||||
return err
|
||||
return authConfig, err
|
||||
}
|
||||
defer restoreInput()
|
||||
|
||||
flPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
|
||||
argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
|
||||
if err != nil {
|
||||
return err
|
||||
return authConfig, err
|
||||
}
|
||||
fmt.Fprint(cli.Out(), "\n")
|
||||
if flPassword == "" {
|
||||
return errors.Errorf("Error: Password Required")
|
||||
if argPassword == "" {
|
||||
return authConfig, errors.Errorf("Error: Password Required")
|
||||
}
|
||||
}
|
||||
|
||||
authconfig.Username = flUser
|
||||
authconfig.Password = flPassword
|
||||
|
||||
return nil
|
||||
authConfig.Username = argUser
|
||||
authConfig.Password = argPassword
|
||||
authConfig.ServerAddress = serverAddress
|
||||
return authConfig, nil
|
||||
}
|
||||
|
||||
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete
|
||||
|
||||
@ -4,12 +4,15 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
configtypes "github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/cli/internal/oauth/manager"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/errdefs"
|
||||
@ -100,80 +103,167 @@ func verifyloginOptions(dockerCli command.Cli, opts *loginOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error { //nolint:gocyclo
|
||||
clnt := dockerCli.Client()
|
||||
func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error {
|
||||
if err := verifyloginOptions(dockerCli, &opts); err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
serverAddress string
|
||||
response registrytypes.AuthenticateOKBody
|
||||
response *registrytypes.AuthenticateOKBody
|
||||
)
|
||||
if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace {
|
||||
if opts.serverAddress != "" &&
|
||||
opts.serverAddress != registry.DefaultNamespace &&
|
||||
opts.serverAddress != registry.DefaultRegistryHost {
|
||||
serverAddress = opts.serverAddress
|
||||
} else {
|
||||
serverAddress = registry.IndexServer
|
||||
}
|
||||
|
||||
isDefaultRegistry := serverAddress == registry.IndexServer
|
||||
|
||||
// attempt login with current (stored) credentials
|
||||
authConfig, err := command.GetDefaultAuthConfig(dockerCli.ConfigFile(), opts.user == "" && opts.password == "", serverAddress, isDefaultRegistry)
|
||||
if err == nil && authConfig.Username != "" && authConfig.Password != "" {
|
||||
response, err = loginWithCredStoreCreds(ctx, dockerCli, &authConfig)
|
||||
response, err = loginWithStoredCredentials(ctx, dockerCli, authConfig)
|
||||
}
|
||||
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
|
||||
err = command.ConfigureAuth(ctx, dockerCli, opts.user, opts.password, &authConfig, isDefaultRegistry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err = clnt.RegistryLogin(ctx, authConfig)
|
||||
if err != nil && client.IsErrConnectionFailed(err) {
|
||||
// If the server isn't responding (yet) attempt to login purely client side
|
||||
response, err = loginClientSide(ctx, authConfig)
|
||||
}
|
||||
// If we (still) have an error, give up
|
||||
// if we failed to authenticate with stored credentials (or didn't have stored credentials),
|
||||
// prompt the user for new credentials
|
||||
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
|
||||
response, err = loginUser(ctx, dockerCli, opts, authConfig.Username, serverAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if response != nil && response.Status != "" {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), response.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loginWithStoredCredentials(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (*registrytypes.AuthenticateOKBody, error) {
|
||||
_, _ = fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n")
|
||||
response, err := dockerCli.Client().RegistryLogin(ctx, authConfig)
|
||||
if err != nil {
|
||||
if errdefs.IsUnauthorized(err) {
|
||||
_, _ = fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n")
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
if response.IdentityToken != "" {
|
||||
authConfig.Password = ""
|
||||
authConfig.IdentityToken = response.IdentityToken
|
||||
}
|
||||
|
||||
creds := dockerCli.ConfigFile().GetCredentialsStore(serverAddress)
|
||||
if err := storeCredentials(dockerCli, authConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, err
|
||||
}
|
||||
|
||||
const OauthLoginEscapeHatchEnvVar = "DOCKER_CLI_DISABLE_OAUTH_LOGIN"
|
||||
|
||||
func isOauthLoginDisabled() bool {
|
||||
if v := os.Getenv(OauthLoginEscapeHatchEnvVar); v != "" {
|
||||
enabled, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) {
|
||||
// If we're logging into the index server and the user didn't provide a username or password, use the device flow
|
||||
if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" && !isOauthLoginDisabled() {
|
||||
response, err := loginWithDeviceCodeFlow(ctx, dockerCli)
|
||||
// if the error represents a failure to initiate the device-code flow,
|
||||
// then we fallback to regular cli credentials login
|
||||
if !errors.Is(err, manager.ErrDeviceLoginStartFail) {
|
||||
return response, err
|
||||
}
|
||||
fmt.Fprint(dockerCli.Err(), "Failed to start web-based login - falling back to command line login...\n\n")
|
||||
}
|
||||
|
||||
return loginWithUsernameAndPassword(ctx, dockerCli, opts, defaultUsername, serverAddress)
|
||||
}
|
||||
|
||||
func loginWithUsernameAndPassword(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) {
|
||||
// Prompt user for credentials
|
||||
authConfig, err := command.PromptUserForCredentials(ctx, dockerCli, opts.user, opts.password, defaultUsername, serverAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := loginWithRegistry(ctx, dockerCli, authConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.IdentityToken != "" {
|
||||
authConfig.Password = ""
|
||||
authConfig.IdentityToken = response.IdentityToken
|
||||
}
|
||||
if err = storeCredentials(dockerCli, authConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func loginWithDeviceCodeFlow(ctx context.Context, dockerCli command.Cli) (*registrytypes.AuthenticateOKBody, error) {
|
||||
store := dockerCli.ConfigFile().GetCredentialsStore(registry.IndexServer)
|
||||
authConfig, err := manager.NewManager(store).LoginDevice(ctx, dockerCli.Err())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := loginWithRegistry(ctx, dockerCli, registrytypes.AuthConfig(*authConfig))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = storeCredentials(dockerCli, registrytypes.AuthConfig(*authConfig)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func storeCredentials(dockerCli command.Cli, authConfig registrytypes.AuthConfig) error {
|
||||
creds := dockerCli.ConfigFile().GetCredentialsStore(authConfig.ServerAddress)
|
||||
|
||||
store, isDefault := creds.(isFileStore)
|
||||
// Display a warning if we're storing the users password (not a token)
|
||||
if isDefault && authConfig.Password != "" {
|
||||
err = displayUnencryptedWarning(dockerCli, store.GetFilename())
|
||||
err := displayUnencryptedWarning(dockerCli, store.GetFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := creds.Store(configtypes.AuthConfig(authConfig)); err != nil {
|
||||
return errors.Errorf("Error saving credentials: %v", err)
|
||||
}
|
||||
|
||||
if response.Status != "" {
|
||||
fmt.Fprintln(dockerCli.Out(), response.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loginWithCredStoreCreds(ctx context.Context, dockerCli command.Cli, authConfig *registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
|
||||
fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n")
|
||||
cliClient := dockerCli.Client()
|
||||
response, err := cliClient.RegistryLogin(ctx, *authConfig)
|
||||
if err != nil {
|
||||
if errdefs.IsUnauthorized(err) {
|
||||
fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n")
|
||||
} else {
|
||||
fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err)
|
||||
}
|
||||
func loginWithRegistry(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
|
||||
response, err := dockerCli.Client().RegistryLogin(ctx, authConfig)
|
||||
if err != nil && client.IsErrConnectionFailed(err) {
|
||||
// If the server isn't responding (yet) attempt to login purely client side
|
||||
response, err = loginClientSide(ctx, authConfig)
|
||||
}
|
||||
return response, err
|
||||
// If we (still) have an error, give up
|
||||
if err != nil {
|
||||
return registrytypes.AuthenticateOKBody{}, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
|
||||
|
||||
@ -74,7 +74,7 @@ func TestLoginWithCredStoreCreds(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
errBuf := new(bytes.Buffer)
|
||||
cli.SetErr(streams.NewOut(errBuf))
|
||||
loginWithCredStoreCreds(ctx, cli, &tc.inputAuthConfig)
|
||||
loginWithStoredCredentials(ctx, cli, tc.inputAuthConfig)
|
||||
outputString := cli.OutBuffer().String()
|
||||
assert.Check(t, is.Equal(tc.expectedMsg, outputString))
|
||||
errorString := errBuf.String()
|
||||
@ -213,7 +213,9 @@ func TestLoginTermination(t *testing.T) {
|
||||
|
||||
runErr := make(chan error)
|
||||
go func() {
|
||||
runErr <- runLogin(ctx, cli, loginOptions{})
|
||||
runErr <- runLogin(ctx, cli, loginOptions{
|
||||
user: "test-user",
|
||||
})
|
||||
}()
|
||||
|
||||
// Let the prompt get canceled by the context
|
||||
@ -226,3 +228,47 @@ func TestLoginTermination(t *testing.T) {
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsOauthLoginDisabled(t *testing.T) {
|
||||
testCases := []struct {
|
||||
envVar string
|
||||
disabled bool
|
||||
}{
|
||||
{
|
||||
envVar: "",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
envVar: "bork",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
envVar: "0",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
envVar: "false",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
envVar: "true",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
envVar: "TRUE",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
envVar: "1",
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Setenv(OauthLoginEscapeHatchEnvVar, tc.envVar)
|
||||
|
||||
disabled := isOauthLoginDisabled()
|
||||
|
||||
assert.Equal(t, disabled, tc.disabled)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -372,7 +372,7 @@ func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) []error {
|
||||
fprintln(output, " Product License:", info.ProductLicense)
|
||||
}
|
||||
|
||||
if info.DefaultAddressPools != nil && len(info.DefaultAddressPools) > 0 {
|
||||
if len(info.DefaultAddressPools) > 0 {
|
||||
fprintln(output, " Default Address Pools:")
|
||||
for _, pool := range info.DefaultAddressPools {
|
||||
fprintf(output, " Base: %s, Size: %d\n", pool.Base, pool.Size)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -52,6 +52,7 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*ConnectionHelper
|
||||
args = append(args, "--host", "unix://"+sp.Path)
|
||||
}
|
||||
sshFlags = addSSHTimeout(sshFlags)
|
||||
sshFlags = disablePseudoTerminalAllocation(sshFlags)
|
||||
args = append(args, "system", "dial-stdio")
|
||||
return commandconn.New(ctx, "ssh", append(sshFlags, sp.Args(args...)...)...)
|
||||
},
|
||||
@ -79,3 +80,14 @@ func addSSHTimeout(sshFlags []string) []string {
|
||||
}
|
||||
return sshFlags
|
||||
}
|
||||
|
||||
// disablePseudoTerminalAllocation disables pseudo-terminal allocation to
|
||||
// prevent SSH from executing as a login shell
|
||||
func disablePseudoTerminalAllocation(sshFlags []string) []string {
|
||||
for _, flag := range sshFlags {
|
||||
if flag == "-T" {
|
||||
return sshFlags
|
||||
}
|
||||
}
|
||||
return append(sshFlags, "-T")
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
228
cli/internal/oauth/api/api.go
Normal file
228
cli/internal/oauth/api/api.go
Normal file
@ -0,0 +1,228 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.21
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/version"
|
||||
)
|
||||
|
||||
type OAuthAPI interface {
|
||||
GetDeviceCode(ctx context.Context, audience string) (State, error)
|
||||
WaitForDeviceToken(ctx context.Context, state State) (TokenResponse, error)
|
||||
RevokeToken(ctx context.Context, refreshToken string) error
|
||||
GetAutoPAT(ctx context.Context, audience string, res TokenResponse) (string, error)
|
||||
}
|
||||
|
||||
// API represents API interactions with Auth0.
|
||||
type API struct {
|
||||
// TenantURL is the base used for each request to Auth0.
|
||||
TenantURL string
|
||||
// ClientID is the client ID for the application to auth with the tenant.
|
||||
ClientID string
|
||||
// Scopes are the scopes that are requested during the device auth flow.
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
// TokenResponse represents the response of the /oauth/token route.
|
||||
type TokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
IDToken string `json:"id_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Scope string `json:"scope"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
var ErrTimeout = errors.New("timed out waiting for device token")
|
||||
|
||||
// GetDeviceCode initiates the device-code auth flow with the tenant.
|
||||
// The state returned contains the device code that the user must use to
|
||||
// authenticate, as well as the URL to visit, etc.
|
||||
func (a API) GetDeviceCode(ctx context.Context, audience string) (State, error) {
|
||||
data := url.Values{
|
||||
"client_id": {a.ClientID},
|
||||
"audience": {audience},
|
||||
"scope": {strings.Join(a.Scopes, " ")},
|
||||
}
|
||||
|
||||
deviceCodeURL := a.TenantURL + "/oauth/device/code"
|
||||
resp, err := postForm(ctx, deviceCodeURL, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return State{}, err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return State{}, tryDecodeOAuthError(resp)
|
||||
}
|
||||
|
||||
var state State
|
||||
err = json.NewDecoder(resp.Body).Decode(&state)
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("failed to get device code: %w", err)
|
||||
}
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func tryDecodeOAuthError(resp *http.Response) error {
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err == nil {
|
||||
if errorDescription, ok := body["error_description"].(string); ok {
|
||||
return errors.New(errorDescription)
|
||||
}
|
||||
}
|
||||
return errors.New("unexpected response from tenant: " + resp.Status)
|
||||
}
|
||||
|
||||
// WaitForDeviceToken polls the tenant to get access/refresh tokens for the user.
|
||||
// This should be called after GetDeviceCode, and will block until the user has
|
||||
// authenticated or we have reached the time limit for authenticating (based on
|
||||
// the response from GetDeviceCode).
|
||||
func (a API) WaitForDeviceToken(ctx context.Context, state State) (TokenResponse, error) {
|
||||
ticker := time.NewTicker(state.IntervalDuration())
|
||||
defer ticker.Stop()
|
||||
timeout := time.After(state.ExpiryDuration())
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return TokenResponse{}, ctx.Err()
|
||||
case <-ticker.C:
|
||||
res, err := a.getDeviceToken(ctx, state)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if res.Error != nil {
|
||||
if *res.Error == "authorization_pending" {
|
||||
continue
|
||||
}
|
||||
|
||||
return res, errors.New(res.ErrorDescription)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
case <-timeout:
|
||||
return TokenResponse{}, ErrTimeout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getToken calls the token endpoint of Auth0 and returns the response.
|
||||
func (a API) getDeviceToken(ctx context.Context, state State) (TokenResponse, error) {
|
||||
data := url.Values{
|
||||
"client_id": {a.ClientID},
|
||||
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
|
||||
"device_code": {state.DeviceCode},
|
||||
}
|
||||
oauthTokenURL := a.TenantURL + "/oauth/token"
|
||||
|
||||
resp, err := postForm(ctx, oauthTokenURL, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return TokenResponse{}, fmt.Errorf("failed to get tokens: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
// this endpoint returns a 403 with an `authorization_pending` error until the
|
||||
// user has authenticated, so we don't check the status code here and instead
|
||||
// decode the response and check for the error.
|
||||
var res TokenResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&res)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// RevokeToken revokes a refresh token with the tenant so that it can no longer
|
||||
// be used to get new tokens.
|
||||
func (a API) RevokeToken(ctx context.Context, refreshToken string) error {
|
||||
data := url.Values{
|
||||
"client_id": {a.ClientID},
|
||||
"token": {refreshToken},
|
||||
}
|
||||
|
||||
revokeURL := a.TenantURL + "/oauth/revoke"
|
||||
resp, err := postForm(ctx, revokeURL, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return tryDecodeOAuthError(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func postForm(ctx context.Context, reqURL string, data io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
cliVersion := strings.ReplaceAll(version.Version, ".", "_")
|
||||
req.Header.Set("User-Agent", fmt.Sprintf("docker-cli:%s:%s-%s", cliVersion, runtime.GOOS, runtime.GOARCH))
|
||||
|
||||
return http.DefaultClient.Do(req)
|
||||
}
|
||||
|
||||
func (a API) GetAutoPAT(ctx context.Context, audience string, res TokenResponse) (string, error) {
|
||||
patURL := audience + "/v2/access-tokens/desktop-generate"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, patURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+res.AccessToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
return "", fmt.Errorf("unexpected response from Hub: %s", resp.Status)
|
||||
}
|
||||
|
||||
var response patGenerateResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return response.Data.Token, nil
|
||||
}
|
||||
|
||||
type patGenerateResponse struct {
|
||||
Data struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
}
|
||||
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: 1,
|
||||
ExpiresIn: 1,
|
||||
}
|
||||
|
||||
_, err := api.WaitForDeviceToken(context.Background(), state)
|
||||
|
||||
assert.ErrorIs(t, err, ErrTimeout)
|
||||
})
|
||||
|
||||
t.Run("canceled context", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
pendingError := "authorization_pending"
|
||||
jsonResponse, err := json.Marshal(TokenResponse{
|
||||
Error: &pendingError,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
w.Write(jsonResponse)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
state := State{
|
||||
DeviceCode: "aDeviceCode",
|
||||
UserCode: "aUserCode",
|
||||
Interval: 1,
|
||||
ExpiresIn: 5,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
cancel()
|
||||
}()
|
||||
_, err := api.WaitForDeviceToken(ctx, state)
|
||||
|
||||
assert.ErrorContains(t, err, "context canceled")
|
||||
})
|
||||
}
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
assert.Equal(t, "/oauth/revoke", r.URL.Path)
|
||||
assert.Equal(t, r.FormValue("client_id"), "aClientID")
|
||||
assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token")
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
|
||||
err := api.RevokeToken(context.Background(), "v1.a-refresh-token")
|
||||
assert.NilError(t, err)
|
||||
})
|
||||
|
||||
t.Run("unexpected response", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
assert.Equal(t, "/oauth/revoke", r.URL.Path)
|
||||
assert.Equal(t, r.FormValue("client_id"), "aClientID")
|
||||
assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token")
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
|
||||
err := api.RevokeToken(context.Background(), "v1.a-refresh-token")
|
||||
assert.ErrorContains(t, err, "unexpected response from tenant: 404 Not Found")
|
||||
})
|
||||
|
||||
t.Run("error w/ description", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
jsonState, err := json.Marshal(TokenResponse{
|
||||
ErrorDescription: "invalid client id",
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write(jsonState)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
|
||||
err := api.RevokeToken(context.Background(), "v1.a-refresh-token")
|
||||
assert.ErrorContains(t, err, "invalid client id")
|
||||
})
|
||||
|
||||
t.Run("canceled context", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
assert.Equal(t, "/oauth/revoke", r.URL.Path)
|
||||
assert.Equal(t, r.FormValue("client_id"), "aClientID")
|
||||
assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token")
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
err := api.RevokeToken(ctx, "v1.a-refresh-token")
|
||||
|
||||
assert.ErrorContains(t, err, "context canceled")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAutoPAT(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
assert.Equal(t, "/v2/access-tokens/desktop-generate", r.URL.Path)
|
||||
assert.Equal(t, "Bearer bork", r.Header.Get("Authorization"))
|
||||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
|
||||
marshalledResponse, err := json.Marshal(patGenerateResponse{
|
||||
Data: struct {
|
||||
Token string `json:"token"`
|
||||
}{
|
||||
Token: "a-docker-pat",
|
||||
},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write(marshalledResponse)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
|
||||
pat, err := api.GetAutoPAT(context.Background(), ts.URL, TokenResponse{
|
||||
AccessToken: "bork",
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, "a-docker-pat", pat)
|
||||
})
|
||||
|
||||
t.Run("general error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
|
||||
_, err := api.GetAutoPAT(context.Background(), ts.URL, TokenResponse{
|
||||
AccessToken: "bork",
|
||||
})
|
||||
assert.ErrorContains(t, err, "unexpected response from Hub: 500 Internal Server Error")
|
||||
})
|
||||
|
||||
t.Run("context canceled", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
assert.Equal(t, "/v2/access-tokens/desktop-generate", r.URL.Path)
|
||||
assert.Equal(t, "Bearer bork", r.Header.Get("Authorization"))
|
||||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
|
||||
marshalledResponse, err := json.Marshal(patGenerateResponse{
|
||||
Data: struct {
|
||||
Token string `json:"token"`
|
||||
}{
|
||||
Token: "a-docker-pat",
|
||||
},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write(marshalledResponse)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
pat, err := api.GetAutoPAT(ctx, ts.URL, TokenResponse{
|
||||
AccessToken: "bork",
|
||||
})
|
||||
|
||||
assert.ErrorContains(t, err, "context canceled")
|
||||
assert.Equal(t, "", pat)
|
||||
})
|
||||
}
|
||||
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,
|
||||
|
||||
@ -562,8 +562,7 @@ Docker API v1.42 and up now ignores this option when set. Older versions of the
|
||||
API continue to accept the option, but depending on the OCI runtime used, may
|
||||
take no effect.
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> While not deprecated (yet) in Docker, the OCI runtime specification also
|
||||
> deprecated the `memory.kmem.tcp.limit_in_bytes` option. When using `runc` as
|
||||
> runtime, this option takes no effect. The linux kernel did not explicitly
|
||||
|
||||
@ -16,8 +16,7 @@ plugins using Docker Engine.
|
||||
For information about legacy (non-managed) plugins, refer to
|
||||
[Understand legacy Docker Engine plugins](legacy_plugins.md).
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> Docker Engine managed plugins are currently not supported on Windows daemons.
|
||||
|
||||
## Installing and using a plugin
|
||||
@ -38,8 +37,7 @@ operation, such as creating a volume.
|
||||
In the following example, you install the `sshfs` plugin, verify that it is
|
||||
enabled, and use it to create a volume.
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> This example is intended for instructional purposes only. Once the volume is
|
||||
> created, your SSH password to the remote host is exposed as plaintext when
|
||||
> inspecting the volume. Delete the volume as soon as you are done with the
|
||||
@ -126,8 +124,7 @@ commands and options, see the
|
||||
The `rootfs` directory represents the root filesystem of the plugin. In this
|
||||
example, it was created from a Dockerfile:
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> The `/run/docker/plugins` directory is mandatory inside of the
|
||||
> plugin's filesystem for Docker to communicate with the plugin.
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -1246,8 +1237,7 @@ the container and remove the file system when the container exits, use the
|
||||
--rm=false: Automatically remove the container when it exits
|
||||
```
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> If you set the `--rm` flag, Docker also removes the anonymous volumes
|
||||
> associated with the container when the container is removed. This is similar
|
||||
> to running `docker rm -v my-container`. Only volumes that are specified
|
||||
@ -1345,14 +1335,12 @@ $ docker run --ulimit nofile=1024:1024 --rm debian sh -c "ulimit -n"
|
||||
1024
|
||||
```
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> If you don't provide a hard limit value, Docker uses the soft limit value
|
||||
> for both values. If you don't provide any values, they are inherited from
|
||||
> the default `ulimits` set on the daemon.
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> The `as` option is deprecated.
|
||||
> In other words, the following script is not supported:
|
||||
>
|
||||
@ -1417,8 +1405,7 @@ the same content between containers.
|
||||
$ docker run --security-opt label=level:s0:c100,c200 -it fedora bash
|
||||
```
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> Automatic translation of MLS labels isn't supported.
|
||||
|
||||
To disable the security labeling for a container entirely, you can use
|
||||
@ -1436,8 +1423,7 @@ that's only allowed to listen on Apache ports:
|
||||
$ docker run --security-opt label=type:svirt_apache_t -it ubuntu bash
|
||||
```
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> You would have to write policy defining a `svirt_apache_t` type.
|
||||
|
||||
To prevent your container processes from gaining additional privileges, you can
|
||||
@ -1558,8 +1544,7 @@ network namespace, run this command:
|
||||
$ docker run --sysctl net.ipv4.ip_forward=1 someimage
|
||||
```
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> Not all sysctls are namespaced. Docker does not support changing sysctls
|
||||
> inside of a container that also modify the host system. As the kernel
|
||||
> evolves we expect to see more sysctls become namespaced.
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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-->
|
||||
|
||||
@ -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
|
||||
|
||||
@ -124,6 +124,7 @@ type `dockerd`.
|
||||
To run the daemon with debug output, use `dockerd --debug` or add `"debug": true`
|
||||
to [the `daemon.json` file](#daemon-configuration-file).
|
||||
|
||||
> [!NOTE]
|
||||
> **Enabling experimental features**
|
||||
>
|
||||
> Enable experimental features by starting `dockerd` with the `--experimental`
|
||||
@ -152,8 +153,7 @@ to learn about environment variables supported by the `docker` CLI.
|
||||
|
||||
### Proxy configuration
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> Refer to the [Docker Desktop manual](https://docs.docker.com/desktop/networking/#httphttps-proxy-support)
|
||||
> if you are running [Docker Desktop](https://docs.docker.com/desktop/).
|
||||
|
||||
@ -191,8 +191,7 @@ interface using its IP address: `-H tcp://192.168.59.103:2375`. It is
|
||||
conventional to use port `2375` for un-encrypted, and port `2376` for encrypted
|
||||
communication with the daemon.
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> If you're using an HTTPS encrypted socket, keep in mind that only
|
||||
> TLS version 1.0 and higher is supported. Protocols SSLv3 and below are not
|
||||
> supported for security reasons.
|
||||
@ -259,8 +258,7 @@ supported. If your key is protected with passphrase, you need to set up
|
||||
|
||||
#### Bind Docker to another host/port or a Unix socket
|
||||
|
||||
> **Warning**
|
||||
>
|
||||
> [!WARNING]
|
||||
> Changing the default `docker` daemon binding to a TCP port or Unix `docker`
|
||||
> user group introduces security risks, as it may allow non-root users to gain
|
||||
> root access on the host. Make sure you control access to `docker`. If you are
|
||||
@ -709,8 +707,7 @@ This option is useful when pushing images containing non-distributable artifacts
|
||||
to a registry on an air-gapped network so hosts on that network can pull the
|
||||
images without connecting to another server.
|
||||
|
||||
> **Warning**
|
||||
>
|
||||
> [!WARNING]
|
||||
> Non-distributable artifacts typically have restrictions on how
|
||||
> and where they can be distributed and shared. Only use this feature to push
|
||||
> artifacts to private registries and ensure that you are in compliance with
|
||||
@ -858,8 +855,7 @@ PING host.docker.internal (192.0.2.0): 56 data bytes
|
||||
|
||||
### Enable CDI devices
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> This is experimental feature and as such doesn't represent a stable API.
|
||||
>
|
||||
> This feature isn't enabled by default. To this feature, set `features.cdi` to
|
||||
@ -1145,8 +1141,7 @@ The following is a full example of the allowed configuration options on Linux:
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> You can't set options in `daemon.json` that have already been set on
|
||||
> daemon startup as a flag.
|
||||
> On systems that use systemd to start the Docker daemon, `-H` is already set, so
|
||||
@ -1242,7 +1237,7 @@ The list of feature options include:
|
||||
external names. The current default is `false`, it will change to `true` in
|
||||
a future release. This option is only allowed on Windows.
|
||||
|
||||
> **Warning**
|
||||
> [!WARNING]
|
||||
> The `windows-dns-proxy` feature flag will be removed in a future release.
|
||||
|
||||
#### Configuration reload behavior
|
||||
@ -1275,8 +1270,7 @@ The list of currently supported options that can be reconfigured is this:
|
||||
|
||||
### Run multiple daemons
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> Running multiple daemons on a single host is considered experimental.
|
||||
> You may encounter unsolved problems, and things may not work as expected in some cases.
|
||||
|
||||
|
||||
@ -69,8 +69,7 @@ to start an interactive shell in the container (if the image you select has an
|
||||
$ docker run -it IMAGE sh
|
||||
```
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> Depending on your Docker system configuration, you may be
|
||||
> required to preface the `docker run` command with `sudo`. To avoid
|
||||
> having to use `sudo` with the `docker` command, your system
|
||||
@ -696,8 +695,7 @@ By default, all containers get the same proportion of block IO bandwidth
|
||||
container's blkio weight relative to the weighting of all other running
|
||||
containers using the `--blkio-weight` flag.
|
||||
|
||||
> **Note:**
|
||||
>
|
||||
> [!NOTE]
|
||||
> The blkio weight setting is only available for direct IO. Buffered IO is not
|
||||
> currently supported.
|
||||
|
||||
@ -1039,8 +1037,7 @@ You can reset a containers entrypoint by passing an empty string, for example:
|
||||
$ docker run -it --entrypoint="" mysql bash
|
||||
```
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> Passing `--entrypoint` clears out any default command set on the image. That
|
||||
> is, any `CMD` instruction in the Dockerfile used to build it.
|
||||
|
||||
@ -1223,8 +1220,7 @@ The followings examples are all valid:
|
||||
--user=[ user | user:group | uid | uid:gid | user:gid | uid:group ]
|
||||
```
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> [!NOTE]
|
||||
> If you pass a numeric user ID, it must be in the range of 0-2147483647. If
|
||||
> you pass a username, the user must exist in the container.
|
||||
|
||||
|
||||
@ -214,7 +214,7 @@ func TestPromptExitCode(t *testing.T) {
|
||||
default:
|
||||
|
||||
if err := bufioWriter.Flush(); err != nil {
|
||||
return poll.Continue(err.Error())
|
||||
return poll.Continue("%v", err)
|
||||
}
|
||||
if strings.Contains(buf.String(), "[y/N]") {
|
||||
return poll.Success()
|
||||
|
||||
56
e2e/registry/login_test.go
Normal file
56
e2e/registry/login_test.go
Normal file
@ -0,0 +1,56 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestOauthLogin(t *testing.T) {
|
||||
t.Parallel()
|
||||
loginCmd := exec.Command("docker", "login")
|
||||
|
||||
p, err := pty.Start(loginCmd)
|
||||
assert.NilError(t, err)
|
||||
defer func() {
|
||||
_ = loginCmd.Wait()
|
||||
_ = p.Close()
|
||||
}()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
pid := loginCmd.Process.Pid
|
||||
t.Logf("terminating PID %d", pid)
|
||||
err = syscall.Kill(pid, syscall.SIGTERM)
|
||||
assert.NilError(t, err)
|
||||
|
||||
output, _ := io.ReadAll(p)
|
||||
assert.Check(t, strings.Contains(string(output), "USING WEB BASED LOGIN"), string(output))
|
||||
}
|
||||
|
||||
func TestLoginWithEscapeHatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
loginCmd := exec.Command("docker", "login")
|
||||
loginCmd.Env = append(loginCmd.Env, "DOCKER_CLI_DISABLE_OAUTH_LOGIN=1")
|
||||
|
||||
p, err := pty.Start(loginCmd)
|
||||
assert.NilError(t, err)
|
||||
defer func() {
|
||||
_ = loginCmd.Wait()
|
||||
_ = p.Close()
|
||||
}()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
pid := loginCmd.Process.Pid
|
||||
t.Logf("terminating PID %d", pid)
|
||||
err = syscall.Kill(pid, syscall.SIGTERM)
|
||||
assert.NilError(t, err)
|
||||
|
||||
output, _ := io.ReadAll(p)
|
||||
assert.Check(t, strings.Contains(string(output), "Username:"), string(output))
|
||||
}
|
||||
17
e2e/registry/main_test.go
Normal file
17
e2e/registry/main_test.go
Normal file
@ -0,0 +1,17 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test/environment"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if err := environment.Setup(); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(3)
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
@ -13,11 +13,12 @@ require (
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/cli-docs-tool v0.8.0
|
||||
github.com/docker/distribution v2.8.3+incompatible
|
||||
github.com/docker/docker v27.1.2-0.20240810135946-f9522e5e96c3+incompatible // 27.x branch (v27.1.2-dev)
|
||||
github.com/docker/docker v27.2.0-rc.1.0.20240827140014-3ab5c7d0036c+incompatible // 27.x branch (v27.2.0-dev)
|
||||
github.com/docker/docker-credential-helpers v0.8.2
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/fvbommel/sortorder v1.0.2
|
||||
github.com/fvbommel/sortorder v1.1.0
|
||||
github.com/go-jose/go-jose/v3 v3.0.3
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/google/go-cmp v0.6.0
|
||||
@ -79,11 +80,13 @@ require (
|
||||
github.com/moby/sys/symlink v0.2.0 // indirect
|
||||
github.com/moby/sys/user v0.3.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||
github.com/prometheus/client_golang v1.17.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
go.etcd.io/etcd/raft/v3 v3.5.6 // indirect
|
||||
|
||||
47
vendor.sum
47
vendor.sum
@ -57,8 +57,8 @@ github.com/docker/cli-docs-tool v0.8.0/go.mod h1:8TQQ3E7mOXoYUs811LiPdUnAhXrcVsB
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v27.1.2-0.20240810135946-f9522e5e96c3+incompatible h1:cDD1nJea6JwvYwLjAZG+6CFlzSTSi9swKUz0qaHwTYA=
|
||||
github.com/docker/docker v27.1.2-0.20240810135946-f9522e5e96c3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v27.2.0-rc.1.0.20240827140014-3ab5c7d0036c+incompatible h1:EuHKrI999zfPX8J2mF6AcHVAMQvSTkawrSGh81s5ncU=
|
||||
github.com/docker/docker v27.2.0-rc.1.0.20240827140014-3ab5c7d0036c+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
|
||||
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
|
||||
@ -80,9 +80,11 @@ github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo=
|
||||
github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
||||
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
|
||||
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
||||
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
@ -118,6 +120,7 @@ github.com/google/certificate-transparency-go v1.1.4 h1:hCyXHDbtqlr/lMXU0D4Wgbal
|
||||
github.com/google/certificate-transparency-go v1.1.4/go.mod h1:D6lvbfwckhNrbM9WVl1EVeMOyzC19mpIjMOI4nxBHtQ=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@ -210,6 +213,8 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@ -268,8 +273,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a h1:tlJ7tGUHvcvL1v3yR6NcCc9nOqh2L+CG6HWrYQtwzQ0=
|
||||
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a/go.mod h1:Y94A6rPp2OwNfP/7vmf8O2xx2IykP8pPXQ1DLouGnEw=
|
||||
github.com/tonistiigi/go-rosetta v0.0.0-20200727161949-f79598599c5d h1:wvQZpqy8p0D/FUia6ipKDhXrzPzBVJE4PZyPc5+5Ay0=
|
||||
@ -285,6 +290,7 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc h1:zkGwegkOW709y0oiAraH/3D8njopUR/pARHv4tZZ6pw=
|
||||
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc/go.mod h1:FM4U1E3NzlNMRnSUTU3P1UdukWhYGifqEsjk9fn7BCk=
|
||||
github.com/zmap/zlint/v3 v3.1.0 h1:WjVytZo79m/L1+/Mlphl09WBob6YTGljN5IGWZFpAv0=
|
||||
@ -326,10 +332,14 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@ -337,6 +347,10 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -345,6 +359,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -356,17 +372,34 @@ golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
@ -376,6 +409,8 @@ golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
2
vendor/github.com/docker/docker/api/common.go
generated
vendored
2
vendor/github.com/docker/docker/api/common.go
generated
vendored
@ -3,7 +3,7 @@ package api // import "github.com/docker/docker/api"
|
||||
// Common constants for daemon and client.
|
||||
const (
|
||||
// DefaultVersion of the current REST API.
|
||||
DefaultVersion = "1.46"
|
||||
DefaultVersion = "1.47"
|
||||
|
||||
// MinSupportedAPIVersion is the minimum API version that can be supported
|
||||
// by the API server, specified as "major.minor". Note that the daemon
|
||||
|
||||
142
vendor/github.com/docker/docker/api/swagger.yaml
generated
vendored
142
vendor/github.com/docker/docker/api/swagger.yaml
generated
vendored
@ -19,10 +19,10 @@ produces:
|
||||
consumes:
|
||||
- "application/json"
|
||||
- "text/plain"
|
||||
basePath: "/v1.46"
|
||||
basePath: "/v1.47"
|
||||
info:
|
||||
title: "Docker Engine API"
|
||||
version: "1.46"
|
||||
version: "1.47"
|
||||
x-logo:
|
||||
url: "https://docs.docker.com/assets/images/logo-docker-main.png"
|
||||
description: |
|
||||
@ -55,8 +55,8 @@ info:
|
||||
the URL is not supported by the daemon, a HTTP `400 Bad Request` error message
|
||||
is returned.
|
||||
|
||||
If you omit the version-prefix, the current version of the API (v1.46) is used.
|
||||
For example, calling `/info` is the same as calling `/v1.46/info`. Using the
|
||||
If you omit the version-prefix, the current version of the API (v1.47) is used.
|
||||
For example, calling `/info` is the same as calling `/v1.47/info`. Using the
|
||||
API without a version-prefix is deprecated and will be removed in a future release.
|
||||
|
||||
Engine releases in the near future should support this version of the API,
|
||||
@ -2265,6 +2265,19 @@ definitions:
|
||||
x-nullable: false
|
||||
type: "integer"
|
||||
example: 2
|
||||
Manifests:
|
||||
description: |
|
||||
Manifests is a list of manifests available in this image.
|
||||
It provides a more detailed view of the platform-specific image manifests
|
||||
or other image-attached data like build attestations.
|
||||
|
||||
WARNING: This is experimental and may change at any time without any backward
|
||||
compatibility.
|
||||
type: "array"
|
||||
x-nullable: false
|
||||
x-omitempty: true
|
||||
items:
|
||||
$ref: "#/definitions/ImageManifestSummary"
|
||||
|
||||
AuthConfig:
|
||||
type: "object"
|
||||
@ -5318,7 +5331,7 @@ definitions:
|
||||
description: |
|
||||
The default (and highest) API version that is supported by the daemon
|
||||
type: "string"
|
||||
example: "1.46"
|
||||
example: "1.47"
|
||||
MinAPIVersion:
|
||||
description: |
|
||||
The minimum API version that is supported by the daemon
|
||||
@ -6644,6 +6657,120 @@ definitions:
|
||||
additionalProperties:
|
||||
type: "string"
|
||||
|
||||
ImageManifestSummary:
|
||||
x-go-name: "ManifestSummary"
|
||||
description: |
|
||||
ImageManifestSummary represents a summary of an image manifest.
|
||||
type: "object"
|
||||
required: ["ID", "Descriptor", "Available", "Size", "Kind"]
|
||||
properties:
|
||||
ID:
|
||||
description: |
|
||||
ID is the content-addressable ID of an image and is the same as the
|
||||
digest of the image manifest.
|
||||
type: "string"
|
||||
example: "sha256:95869fbcf224d947ace8d61d0e931d49e31bb7fc67fffbbe9c3198c33aa8e93f"
|
||||
Descriptor:
|
||||
$ref: "#/definitions/OCIDescriptor"
|
||||
Available:
|
||||
description: Indicates whether all the child content (image config, layers) is fully available locally.
|
||||
type: "boolean"
|
||||
example: true
|
||||
Size:
|
||||
type: "object"
|
||||
x-nullable: false
|
||||
required: ["Content", "Total"]
|
||||
properties:
|
||||
Total:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
example: 8213251
|
||||
description: |
|
||||
Total is the total size (in bytes) of all the locally present
|
||||
data (both distributable and non-distributable) that's related to
|
||||
this manifest and its children.
|
||||
This equal to the sum of [Content] size AND all the sizes in the
|
||||
[Size] struct present in the Kind-specific data struct.
|
||||
For example, for an image kind (Kind == "image")
|
||||
this would include the size of the image content and unpacked
|
||||
image snapshots ([Size.Content] + [ImageData.Size.Unpacked]).
|
||||
Content:
|
||||
description: |
|
||||
Content is the size (in bytes) of all the locally present
|
||||
content in the content store (e.g. image config, layers)
|
||||
referenced by this manifest and its children.
|
||||
This only includes blobs in the content store.
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
example: 3987495
|
||||
Kind:
|
||||
type: "string"
|
||||
example: "image"
|
||||
enum:
|
||||
- "image"
|
||||
- "attestation"
|
||||
- "unknown"
|
||||
description: |
|
||||
The kind of the manifest.
|
||||
|
||||
kind | description
|
||||
-------------|-----------------------------------------------------------
|
||||
image | Image manifest that can be used to start a container.
|
||||
attestation | Attestation manifest produced by the Buildkit builder for a specific image manifest.
|
||||
ImageData:
|
||||
description: |
|
||||
The image data for the image manifest.
|
||||
This field is only populated when Kind is "image".
|
||||
type: "object"
|
||||
x-nullable: true
|
||||
x-omitempty: true
|
||||
required: ["Platform", "Containers", "Size", "UnpackedSize"]
|
||||
properties:
|
||||
Platform:
|
||||
$ref: "#/definitions/OCIPlatform"
|
||||
description: |
|
||||
OCI platform of the image. This will be the platform specified in the
|
||||
manifest descriptor from the index/manifest list.
|
||||
If it's not available, it will be obtained from the image config.
|
||||
Containers:
|
||||
description: |
|
||||
The IDs of the containers that are using this image.
|
||||
type: "array"
|
||||
items:
|
||||
type: "string"
|
||||
example: ["ede54ee1fda366ab42f824e8a5ffd195155d853ceaec74a927f249ea270c7430", "abadbce344c096744d8d6071a90d474d28af8f1034b5ea9fb03c3f4bfc6d005e"]
|
||||
Size:
|
||||
type: "object"
|
||||
x-nullable: false
|
||||
required: ["Unpacked"]
|
||||
properties:
|
||||
Unpacked:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
example: 3987495
|
||||
description: |
|
||||
Unpacked is the size (in bytes) of the locally unpacked
|
||||
(uncompressed) image content that's directly usable by the containers
|
||||
running this image.
|
||||
It's independent of the distributable content - e.g.
|
||||
the image might still have an unpacked data that's still used by
|
||||
some container even when the distributable/compressed content is
|
||||
already gone.
|
||||
AttestationData:
|
||||
description: |
|
||||
The image data for the attestation manifest.
|
||||
This field is only populated when Kind is "attestation".
|
||||
type: "object"
|
||||
x-nullable: true
|
||||
x-omitempty: true
|
||||
required: ["For"]
|
||||
properties:
|
||||
For:
|
||||
description: |
|
||||
The digest of the image manifest that this attestation is for.
|
||||
type: "string"
|
||||
example: "sha256:95869fbcf224d947ace8d61d0e931d49e31bb7fc67fffbbe9c3198c33aa8e93f"
|
||||
|
||||
paths:
|
||||
/containers/json:
|
||||
get:
|
||||
@ -8622,6 +8749,11 @@ paths:
|
||||
description: "Show digest information as a `RepoDigests` field on each image."
|
||||
type: "boolean"
|
||||
default: false
|
||||
- name: "manifests"
|
||||
in: "query"
|
||||
description: "Include `Manifests` in the image summary."
|
||||
type: "boolean"
|
||||
default: false
|
||||
tags: ["Image"]
|
||||
/build:
|
||||
post:
|
||||
|
||||
99
vendor/github.com/docker/docker/api/types/image/manifest.go
generated
vendored
Normal file
99
vendor/github.com/docker/docker/api/types/image/manifest.go
generated
vendored
Normal file
@ -0,0 +1,99 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
type ManifestKind string
|
||||
|
||||
const (
|
||||
ManifestKindImage ManifestKind = "image"
|
||||
ManifestKindAttestation ManifestKind = "attestation"
|
||||
ManifestKindUnknown ManifestKind = "unknown"
|
||||
)
|
||||
|
||||
type ManifestSummary struct {
|
||||
// ID is the content-addressable ID of an image and is the same as the
|
||||
// digest of the image manifest.
|
||||
//
|
||||
// Required: true
|
||||
ID string `json:"ID"`
|
||||
|
||||
// Descriptor is the OCI descriptor of the image.
|
||||
//
|
||||
// Required: true
|
||||
Descriptor ocispec.Descriptor `json:"Descriptor"`
|
||||
|
||||
// Indicates whether all the child content (image config, layers) is
|
||||
// fully available locally
|
||||
//
|
||||
// Required: true
|
||||
Available bool `json:"Available"`
|
||||
|
||||
// Size is the size information of the content related to this manifest.
|
||||
// Note: These sizes only take the locally available content into account.
|
||||
//
|
||||
// Required: true
|
||||
Size struct {
|
||||
// Content is the size (in bytes) of all the locally present
|
||||
// content in the content store (e.g. image config, layers)
|
||||
// referenced by this manifest and its children.
|
||||
// This only includes blobs in the content store.
|
||||
Content int64 `json:"Content"`
|
||||
|
||||
// Total is the total size (in bytes) of all the locally present
|
||||
// data (both distributable and non-distributable) that's related to
|
||||
// this manifest and its children.
|
||||
// This equal to the sum of [Content] size AND all the sizes in the
|
||||
// [Size] struct present in the Kind-specific data struct.
|
||||
// For example, for an image kind (Kind == ManifestKindImage),
|
||||
// this would include the size of the image content and unpacked
|
||||
// image snapshots ([Size.Content] + [ImageData.Size.Unpacked]).
|
||||
Total int64 `json:"Total"`
|
||||
} `json:"Size"`
|
||||
|
||||
// Kind is the kind of the image manifest.
|
||||
//
|
||||
// Required: true
|
||||
Kind ManifestKind `json:"Kind"`
|
||||
|
||||
// Fields below are specific to the kind of the image manifest.
|
||||
|
||||
// Present only if Kind == ManifestKindImage.
|
||||
ImageData *ImageProperties `json:"ImageData,omitempty"`
|
||||
|
||||
// Present only if Kind == ManifestKindAttestation.
|
||||
AttestationData *AttestationProperties `json:"AttestationData,omitempty"`
|
||||
}
|
||||
|
||||
type ImageProperties struct {
|
||||
// Platform is the OCI platform object describing the platform of the image.
|
||||
//
|
||||
// Required: true
|
||||
Platform ocispec.Platform `json:"Platform"`
|
||||
|
||||
Size struct {
|
||||
// Unpacked is the size (in bytes) of the locally unpacked
|
||||
// (uncompressed) image content that's directly usable by the containers
|
||||
// running this image.
|
||||
// It's independent of the distributable content - e.g.
|
||||
// the image might still have an unpacked data that's still used by
|
||||
// some container even when the distributable/compressed content is
|
||||
// already gone.
|
||||
//
|
||||
// Required: true
|
||||
Unpacked int64 `json:"Unpacked"`
|
||||
}
|
||||
|
||||
// Containers is an array containing the IDs of the containers that are
|
||||
// using this image.
|
||||
//
|
||||
// Required: true
|
||||
Containers []string `json:"Containers"`
|
||||
}
|
||||
|
||||
type AttestationProperties struct {
|
||||
// For is the digest of the image manifest that this attestation is for.
|
||||
For digest.Digest `json:"For"`
|
||||
}
|
||||
3
vendor/github.com/docker/docker/api/types/image/opts.go
generated
vendored
3
vendor/github.com/docker/docker/api/types/image/opts.go
generated
vendored
@ -76,6 +76,9 @@ type ListOptions struct {
|
||||
|
||||
// ContainerCount indicates whether container count should be computed.
|
||||
ContainerCount bool
|
||||
|
||||
// Manifests indicates whether the image manifests should be returned.
|
||||
Manifests bool
|
||||
}
|
||||
|
||||
// RemoveOptions holds parameters to remove images.
|
||||
|
||||
13
vendor/github.com/docker/docker/api/types/image/summary.go
generated
vendored
13
vendor/github.com/docker/docker/api/types/image/summary.go
generated
vendored
@ -1,10 +1,5 @@
|
||||
package image
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
// Summary summary
|
||||
// swagger:model Summary
|
||||
type Summary struct {
|
||||
|
||||
// Number of containers using this image. Includes both stopped and running
|
||||
@ -47,6 +42,14 @@ type Summary struct {
|
||||
// Required: true
|
||||
ParentID string `json:"ParentId"`
|
||||
|
||||
// Manifests is a list of image manifests available in this image. It
|
||||
// provides a more detailed view of the platform-specific image manifests or
|
||||
// other image-attached data like build attestations.
|
||||
//
|
||||
// WARNING: This is experimental and may change at any time without any backward
|
||||
// compatibility.
|
||||
Manifests []ManifestSummary `json:"Manifests,omitempty"`
|
||||
|
||||
// List of content-addressable digests of locally available image manifests
|
||||
// that the image is referenced from. Multiple manifests can refer to the
|
||||
// same image.
|
||||
|
||||
14
vendor/github.com/docker/docker/api/types/registry/authconfig.go
generated
vendored
14
vendor/github.com/docker/docker/api/types/registry/authconfig.go
generated
vendored
@ -34,10 +34,9 @@ type AuthConfig struct {
|
||||
}
|
||||
|
||||
// EncodeAuthConfig serializes the auth configuration as a base64url encoded
|
||||
// RFC4648, section 5) JSON string for sending through the X-Registry-Auth header.
|
||||
// ([RFC4648, section 5]) JSON string for sending through the X-Registry-Auth header.
|
||||
//
|
||||
// For details on base64url encoding, see:
|
||||
// - RFC4648, section 5: https://tools.ietf.org/html/rfc4648#section-5
|
||||
// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5
|
||||
func EncodeAuthConfig(authConfig AuthConfig) (string, error) {
|
||||
buf, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
@ -46,15 +45,14 @@ func EncodeAuthConfig(authConfig AuthConfig) (string, error) {
|
||||
return base64.URLEncoding.EncodeToString(buf), nil
|
||||
}
|
||||
|
||||
// DecodeAuthConfig decodes base64url encoded (RFC4648, section 5) JSON
|
||||
// DecodeAuthConfig decodes base64url encoded ([RFC4648, section 5]) JSON
|
||||
// authentication information as sent through the X-Registry-Auth header.
|
||||
//
|
||||
// This function always returns an AuthConfig, even if an error occurs. It is up
|
||||
// This function always returns an [AuthConfig], even if an error occurs. It is up
|
||||
// to the caller to decide if authentication is required, and if the error can
|
||||
// be ignored.
|
||||
//
|
||||
// For details on base64url encoding, see:
|
||||
// - RFC4648, section 5: https://tools.ietf.org/html/rfc4648#section-5
|
||||
// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5
|
||||
func DecodeAuthConfig(authEncoded string) (*AuthConfig, error) {
|
||||
if authEncoded == "" {
|
||||
return &AuthConfig{}, nil
|
||||
@ -69,7 +67,7 @@ func DecodeAuthConfig(authEncoded string) (*AuthConfig, error) {
|
||||
// clients and API versions. Current clients and API versions expect authentication
|
||||
// to be provided through the X-Registry-Auth header.
|
||||
//
|
||||
// Like DecodeAuthConfig, this function always returns an AuthConfig, even if an
|
||||
// Like [DecodeAuthConfig], this function always returns an [AuthConfig], even if an
|
||||
// error occurs. It is up to the caller to decide if authentication is required,
|
||||
// and if the error can be ignored.
|
||||
func DecodeAuthConfigBody(rdr io.ReadCloser) (*AuthConfig, error) {
|
||||
|
||||
8
vendor/github.com/docker/docker/client/image_list.go
generated
vendored
8
vendor/github.com/docker/docker/client/image_list.go
generated
vendored
@ -11,6 +11,11 @@ import (
|
||||
)
|
||||
|
||||
// ImageList returns a list of images in the docker host.
|
||||
//
|
||||
// Experimental: Setting the [options.Manifest] will populate
|
||||
// [image.Summary.Manifests] with information about image manifests.
|
||||
// This is experimental and might change in the future without any backward
|
||||
// compatibility.
|
||||
func (cli *Client) ImageList(ctx context.Context, options image.ListOptions) ([]image.Summary, error) {
|
||||
var images []image.Summary
|
||||
|
||||
@ -47,6 +52,9 @@ func (cli *Client) ImageList(ctx context.Context, options image.ListOptions) ([]
|
||||
if options.SharedSize && versions.GreaterThanOrEqualTo(cli.version, "1.42") {
|
||||
query.Set("shared-size", "1")
|
||||
}
|
||||
if options.Manifests && versions.GreaterThanOrEqualTo(cli.version, "1.47") {
|
||||
query.Set("manifests", "1")
|
||||
}
|
||||
|
||||
serverResp, err := cli.get(ctx, "/images/json", query, nil)
|
||||
defer ensureReaderClosed(serverResp)
|
||||
|
||||
4
vendor/github.com/fvbommel/sortorder/README.md
generated
vendored
4
vendor/github.com/fvbommel/sortorder/README.md
generated
vendored
@ -3,3 +3,7 @@
|
||||
import "github.com/fvbommel/sortorder"
|
||||
|
||||
Sort orders and comparison functions.
|
||||
|
||||
Case-insensitive sort orders are in the `casefolded` sub-package
|
||||
because it pulls in the Unicode tables in the standard library,
|
||||
which can add significantly to the size of binaries.
|
||||
|
||||
14
vendor/github.com/fvbommel/sortorder/natsort.go
generated
vendored
14
vendor/github.com/fvbommel/sortorder/natsort.go
generated
vendored
@ -4,7 +4,7 @@ package sortorder
|
||||
// means that e.g. "abc2" < "abc12".
|
||||
//
|
||||
// Non-digit sequences and numbers are compared separately. The former are
|
||||
// compared bytewise, while the latter are compared numerically (except that
|
||||
// compared bytewise, while digits are compared numerically (except that
|
||||
// the number of leading zeros is used as a tie-breaker, so e.g. "2" < "02")
|
||||
//
|
||||
// Limitation: only ASCII digits (0-9) are considered.
|
||||
@ -14,13 +14,13 @@ func (n Natural) Len() int { return len(n) }
|
||||
func (n Natural) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
|
||||
func (n Natural) Less(i, j int) bool { return NaturalLess(n[i], n[j]) }
|
||||
|
||||
func isdigit(b byte) bool { return '0' <= b && b <= '9' }
|
||||
func isDigit(b byte) bool { return '0' <= b && b <= '9' }
|
||||
|
||||
// NaturalLess compares two strings using natural ordering. This means that e.g.
|
||||
// "abc2" < "abc12".
|
||||
//
|
||||
// Non-digit sequences and numbers are compared separately. The former are
|
||||
// compared bytewise, while the latter are compared numerically (except that
|
||||
// compared bytewise, while digits are compared numerically (except that
|
||||
// the number of leading zeros is used as a tie-breaker, so e.g. "2" < "02")
|
||||
//
|
||||
// Limitation: only ASCII digits (0-9) are considered.
|
||||
@ -28,7 +28,7 @@ func NaturalLess(str1, str2 string) bool {
|
||||
idx1, idx2 := 0, 0
|
||||
for idx1 < len(str1) && idx2 < len(str2) {
|
||||
c1, c2 := str1[idx1], str2[idx2]
|
||||
dig1, dig2 := isdigit(c1), isdigit(c2)
|
||||
dig1, dig2 := isDigit(c1), isDigit(c2)
|
||||
switch {
|
||||
case dig1 != dig2: // Digits before other characters.
|
||||
return dig1 // True if LHS is a digit, false if the RHS is one.
|
||||
@ -48,16 +48,16 @@ func NaturalLess(str1, str2 string) bool {
|
||||
}
|
||||
// Eat all digits.
|
||||
nonZero1, nonZero2 := idx1, idx2
|
||||
for ; idx1 < len(str1) && isdigit(str1[idx1]); idx1++ {
|
||||
for ; idx1 < len(str1) && isDigit(str1[idx1]); idx1++ {
|
||||
}
|
||||
for ; idx2 < len(str2) && isdigit(str2[idx2]); idx2++ {
|
||||
for ; idx2 < len(str2) && isDigit(str2[idx2]); idx2++ {
|
||||
}
|
||||
// If lengths of numbers with non-zero prefix differ, the shorter
|
||||
// one is less.
|
||||
if len1, len2 := idx1-nonZero1, idx2-nonZero2; len1 != len2 {
|
||||
return len1 < len2
|
||||
}
|
||||
// If they're equal, string comparison is correct.
|
||||
// If they're equally long, string comparison is correct.
|
||||
if nr1, nr2 := str1[nonZero1:idx1], str2[nonZero2:idx2]; nr1 != nr2 {
|
||||
return nr1 < nr2
|
||||
}
|
||||
|
||||
2
vendor/github.com/go-jose/go-jose/v3/.gitignore
generated
vendored
Normal file
2
vendor/github.com/go-jose/go-jose/v3/.gitignore
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
jose-util/jose-util
|
||||
jose-util.t.err
|
||||
53
vendor/github.com/go-jose/go-jose/v3/.golangci.yml
generated
vendored
Normal file
53
vendor/github.com/go-jose/go-jose/v3/.golangci.yml
generated
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
# https://github.com/golangci/golangci-lint
|
||||
|
||||
run:
|
||||
skip-files:
|
||||
- doc_test.go
|
||||
modules-download-mode: readonly
|
||||
|
||||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
- gochecknoglobals
|
||||
- goconst
|
||||
- lll
|
||||
- maligned
|
||||
- nakedret
|
||||
- scopelint
|
||||
- unparam
|
||||
- funlen # added in 1.18 (requires go-jose changes before it can be enabled)
|
||||
|
||||
linters-settings:
|
||||
gocyclo:
|
||||
min-complexity: 35
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- text: "don't use ALL_CAPS in Go names"
|
||||
linters:
|
||||
- golint
|
||||
- text: "hardcoded credentials"
|
||||
linters:
|
||||
- gosec
|
||||
- text: "weak cryptographic primitive"
|
||||
linters:
|
||||
- gosec
|
||||
- path: json/
|
||||
linters:
|
||||
- dupl
|
||||
- errcheck
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- golint
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- stylecheck
|
||||
- unused
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- scopelint
|
||||
- path: jwk.go
|
||||
linters:
|
||||
- gocyclo
|
||||
33
vendor/github.com/go-jose/go-jose/v3/.travis.yml
generated
vendored
Normal file
33
vendor/github.com/go-jose/go-jose/v3/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
language: go
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
||||
go:
|
||||
- "1.13.x"
|
||||
- "1.14.x"
|
||||
- tip
|
||||
|
||||
before_script:
|
||||
- export PATH=$HOME/.local/bin:$PATH
|
||||
|
||||
before_install:
|
||||
- go get -u github.com/mattn/goveralls github.com/wadey/gocovmerge
|
||||
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.18.0
|
||||
- pip install cram --user
|
||||
|
||||
script:
|
||||
- go test -v -covermode=count -coverprofile=profile.cov .
|
||||
- go test -v -covermode=count -coverprofile=cryptosigner/profile.cov ./cryptosigner
|
||||
- go test -v -covermode=count -coverprofile=cipher/profile.cov ./cipher
|
||||
- go test -v -covermode=count -coverprofile=jwt/profile.cov ./jwt
|
||||
- go test -v ./json # no coverage for forked encoding/json package
|
||||
- golangci-lint run
|
||||
- cd jose-util && go build && PATH=$PWD:$PATH cram -v jose-util.t # cram tests jose-util
|
||||
- cd ..
|
||||
|
||||
after_success:
|
||||
- gocovmerge *.cov */*.cov > merged.coverprofile
|
||||
- goveralls -coverprofile merged.coverprofile -service=travis-ci
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user