Merge pull request #3071 from thaJeztah/bump_engine_28.0.2

vendor: github.com/docker/docker, docker/cli v28.0.2
This commit is contained in:
CrazyMax
2025-03-20 13:14:47 +01:00
committed by GitHub
71 changed files with 951 additions and 1416 deletions

View File

@ -11,7 +11,7 @@ import (
"github.com/docker/buildx/util/desktop" "github.com/docker/buildx/util/desktop"
"github.com/docker/buildx/version" "github.com/docker/buildx/version"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/debug" "github.com/docker/cli/cli/debug"
@ -64,7 +64,7 @@ func flushMetrics(cmd *command.DockerCli) {
func runPlugin(cmd *command.DockerCli) error { func runPlugin(cmd *command.DockerCli) error {
rootCmd := commands.NewRootCmd("buildx", true, cmd) rootCmd := commands.NewRootCmd("buildx", true, cmd)
return plugin.RunPlugin(cmd, rootCmd, manager.Metadata{ return plugin.RunPlugin(cmd, rootCmd, metadata.Metadata{
SchemaVersion: "0.1.0", SchemaVersion: "0.1.0",
Vendor: "Docker Inc.", Vendor: "Docker Inc.",
Version: version.Version, Version: version.Version,

6
go.mod
View File

@ -8,7 +8,7 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.27.27 github.com/aws/aws-sdk-go-v2/config v1.27.27
github.com/compose-spec/compose-go/v2 v2.4.8 github.com/compose-spec/compose-go/v2 v2.4.8
github.com/containerd/console v1.0.4 github.com/containerd/console v1.0.4
github.com/containerd/containerd/v2 v2.0.3 github.com/containerd/containerd/v2 v2.0.4
github.com/containerd/continuity v0.4.5 github.com/containerd/continuity v0.4.5
github.com/containerd/errdefs v1.0.0 github.com/containerd/errdefs v1.0.0
github.com/containerd/log v0.1.0 github.com/containerd/log v0.1.0
@ -17,9 +17,9 @@ require (
github.com/creack/pty v1.1.24 github.com/creack/pty v1.1.24
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/distribution/reference v0.6.0 github.com/distribution/reference v0.6.0
github.com/docker/cli v28.0.1+incompatible github.com/docker/cli v28.0.2+incompatible
github.com/docker/cli-docs-tool v0.9.0 github.com/docker/cli-docs-tool v0.9.0
github.com/docker/docker v28.0.1+incompatible github.com/docker/docker v28.0.2+incompatible
github.com/docker/go-units v0.5.0 github.com/docker/go-units v0.5.0
github.com/gofrs/flock v0.12.1 github.com/gofrs/flock v0.12.1
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510

12
go.sum
View File

@ -85,8 +85,8 @@ github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVMyTRdsD2bS0= github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVMyTRdsD2bS0=
github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc= github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc=
github.com/containerd/containerd/v2 v2.0.3 h1:zBKgwgZsuu+LPCMzCLgA4sC4MiZzZ59ZT31XkmiISQM= github.com/containerd/containerd/v2 v2.0.4 h1:+r7yJMwhTfMm3CDyiBjMBQO8a9CTBxL2Bg/JtqtIwB8=
github.com/containerd/containerd/v2 v2.0.3/go.mod h1:5j9QUUaV/cy9ZeAx4S+8n9ffpf+iYnEj4jiExgcbuLY= github.com/containerd/containerd/v2 v2.0.4/go.mod h1:5j9QUUaV/cy9ZeAx4S+8n9ffpf+iYnEj4jiExgcbuLY=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@ -122,15 +122,15 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v28.0.1+incompatible h1:g0h5NQNda3/CxIsaZfH4Tyf6vpxFth7PYl3hgCPOKzs= github.com/docker/cli v28.0.2+incompatible h1:cRPZ77FK3/IXTAIQQj1vmhlxiLS5m+MIUDwS6f57lrE=
github.com/docker/cli v28.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v28.0.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.9.0 h1:CVwQbE+ZziwlPqrJ7LRyUF6GvCA+6gj7MTCsayaK9t0= github.com/docker/cli-docs-tool v0.9.0 h1:CVwQbE+ZziwlPqrJ7LRyUF6GvCA+6gj7MTCsayaK9t0=
github.com/docker/cli-docs-tool v0.9.0/go.mod h1:ClrwlNW+UioiRyH9GiAOe1o3J/TsY3Tr1ipoypjAUtc= github.com/docker/cli-docs-tool v0.9.0/go.mod h1:ClrwlNW+UioiRyH9GiAOe1o3J/TsY3Tr1ipoypjAUtc=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 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 h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0= github.com/docker/docker v28.0.2+incompatible h1:9BILleFwug5FSSqWBgVevgL3ewDJfWWWyZVqlDMttE8=
github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.0.2+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 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= 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= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=

View File

@ -369,8 +369,8 @@ func Children(ctx context.Context, provider content.Provider, desc ocispec.Descr
} }
return append([]ocispec.Descriptor{}, index.Manifests...), nil return append([]ocispec.Descriptor{}, index.Manifests...), nil
} else if !IsLayerType(desc.MediaType) && !IsKnownConfig(desc.MediaType) { } else if !IsLayerType(desc.MediaType) && !IsKnownConfig(desc.MediaType) && !IsAttestationType(desc.MediaType) {
// Layers and configs are childless data types and should not be logged. // Layers, configs, and attestations are childless data types and should not be logged.
log.G(ctx).Debugf("encountered unknown type %v; children may not be fetched", desc.MediaType) log.G(ctx).Debugf("encountered unknown type %v; children may not be fetched", desc.MediaType)
} }
return nil, nil return nil, nil

View File

@ -58,6 +58,9 @@ const (
MediaTypeImageLayerEncrypted = ocispec.MediaTypeImageLayer + "+encrypted" MediaTypeImageLayerEncrypted = ocispec.MediaTypeImageLayer + "+encrypted"
MediaTypeImageLayerGzipEncrypted = ocispec.MediaTypeImageLayerGzip + "+encrypted" MediaTypeImageLayerGzipEncrypted = ocispec.MediaTypeImageLayerGzip + "+encrypted"
// In-toto attestation
MediaTypeInToto = "application/vnd.in-toto+json"
) )
// DiffCompression returns the compression as defined by the layer diff media // DiffCompression returns the compression as defined by the layer diff media
@ -193,6 +196,16 @@ func IsKnownConfig(mt string) bool {
return false return false
} }
// IsAttestationType returns true if the media type is an attestation type
func IsAttestationType(mt string) bool {
switch mt {
case MediaTypeInToto:
return true
default:
return false
}
}
// ChildGCLabels returns the label for a given descriptor to reference it // ChildGCLabels returns the label for a given descriptor to reference it
func ChildGCLabels(desc ocispec.Descriptor) []string { func ChildGCLabels(desc ocispec.Descriptor) []string {
mt := desc.MediaType mt := desc.MediaType

View File

@ -80,6 +80,8 @@ func MakeRefKey(ctx context.Context, desc ocispec.Descriptor) string {
return "layer-" + key return "layer-" + key
case images.IsKnownConfig(desc.MediaType): case images.IsKnownConfig(desc.MediaType):
return "config-" + key return "config-" + key
case images.IsAttestationType(desc.MediaType):
return "attestation-" + key
default: default:
log.G(ctx).Warnf("reference for unknown type: %s", desc.MediaType) log.G(ctx).Warnf("reference for unknown type: %s", desc.MediaType)
return "unknown-" + key return "unknown-" + key

View File

@ -19,11 +19,12 @@ package version
import "runtime" import "runtime"
var ( var (
Name = "containerd"
// Package is filled at linking time // Package is filled at linking time
Package = "github.com/containerd/containerd/v2" Package = "github.com/containerd/containerd/v2"
// Version holds the complete version number. Filled in at linking time. // Version holds the complete version number. Filled in at linking time.
Version = "2.0.3+unknown" Version = "2.0.4+unknown"
// Revision is filled with the VCS (e.g. git) revision being used to build // Revision is filled with the VCS (e.g. git) revision being used to build
// the program at linking time. // the program at linking time.

View File

@ -1,18 +0,0 @@
package hooks
import (
"fmt"
"io"
"github.com/morikuni/aec"
)
func PrintNextSteps(out io.Writer, messages []string) {
if len(messages) == 0 {
return
}
_, _ = fmt.Fprintln(out, aec.Bold.Apply("\nWhat's next:"))
for _, n := range messages {
_, _ = fmt.Fprintln(out, " ", n)
}
}

View File

@ -1,116 +0,0 @@
package hooks
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
"text/template"
"github.com/spf13/cobra"
)
type HookType int
const (
NextSteps = iota
)
// HookMessage represents a plugin hook response. Plugins
// declaring support for CLI hooks need to print a json
// representation of this type when their hook subcommand
// is invoked.
type HookMessage struct {
Type HookType
Template string
}
// TemplateReplaceSubcommandName returns a hook template string
// that will be replaced by the CLI subcommand being executed
//
// Example:
//
// "you ran the subcommand: " + TemplateReplaceSubcommandName()
//
// when being executed after the command:
// `docker run --name "my-container" alpine`
// will result in the message:
// `you ran the subcommand: run`
func TemplateReplaceSubcommandName() string {
return hookTemplateCommandName
}
// TemplateReplaceFlagValue returns a hook template string
// that will be replaced by the flags value.
//
// Example:
//
// "you ran a container named: " + TemplateReplaceFlagValue("name")
//
// when being executed after the command:
// `docker run --name "my-container" alpine`
// will result in the message:
// `you ran a container named: my-container`
func TemplateReplaceFlagValue(flag string) string {
return fmt.Sprintf(hookTemplateFlagValue, flag)
}
// TemplateReplaceArg takes an index i and returns a hook
// template string that the CLI will replace the template with
// the ith argument, after processing the passed flags.
//
// Example:
//
// "run this image with `docker run " + TemplateReplaceArg(0) + "`"
//
// when being executed after the command:
// `docker pull alpine`
// will result in the message:
// "Run this image with `docker run alpine`"
func TemplateReplaceArg(i int) string {
return fmt.Sprintf(hookTemplateArg, strconv.Itoa(i))
}
func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
tmpl := template.New("").Funcs(commandFunctions)
tmpl, err := tmpl.Parse(hookTemplate)
if err != nil {
return nil, err
}
b := bytes.Buffer{}
err = tmpl.Execute(&b, cmd)
if err != nil {
return nil, err
}
return strings.Split(b.String(), "\n"), nil
}
var ErrHookTemplateParse = errors.New("failed to parse hook template")
const (
hookTemplateCommandName = "{{.Name}}"
hookTemplateFlagValue = `{{flag . "%s"}}`
hookTemplateArg = "{{arg . %s}}"
)
var commandFunctions = template.FuncMap{
"flag": getFlagValue,
"arg": getArgValue,
}
func getFlagValue(cmd *cobra.Command, flag string) (string, error) {
cmdFlag := cmd.Flag(flag)
if cmdFlag == nil {
return "", ErrHookTemplateParse
}
return cmdFlag.Value.String(), nil
}
func getArgValue(cmd *cobra.Command, i int) (string, error) {
flags := cmd.Flags()
if flags == nil {
return "", ErrHookTemplateParse
}
return flags.Arg(i), nil
}

View File

@ -1,21 +0,0 @@
package manager
import "os/exec"
// Candidate represents a possible plugin candidate, for mocking purposes
type Candidate interface {
Path() string
Metadata() ([]byte, error)
}
type candidate struct {
path string
}
func (c *candidate) Path() string {
return c.path
}
func (c *candidate) Metadata() ([]byte, error) {
return exec.Command(c.path, MetadataSubcommandName).Output() // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
}

View File

@ -1,147 +0,0 @@
package manager
import (
"fmt"
"net/url"
"os"
"strings"
"sync"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel/attribute"
)
const (
// CommandAnnotationPlugin is added to every stub command added by
// AddPluginCommandStubs with the value "true" and so can be
// used to distinguish plugin stubs from regular commands.
CommandAnnotationPlugin = "com.docker.cli.plugin"
// CommandAnnotationPluginVendor is added to every stub command
// added by AddPluginCommandStubs and contains the vendor of
// that plugin.
CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor"
// CommandAnnotationPluginVersion is added to every stub command
// added by AddPluginCommandStubs and contains the version of
// that plugin.
CommandAnnotationPluginVersion = "com.docker.cli.plugin.version"
// CommandAnnotationPluginInvalid is added to any stub command
// added by AddPluginCommandStubs for an invalid command (that
// is, one which failed it's candidate test) and contains the
// reason for the failure.
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
// CommandAnnotationPluginCommandPath is added to overwrite the
// command path for a plugin invocation.
CommandAnnotationPluginCommandPath = "com.docker.cli.plugin.command_path"
)
var pluginCommandStubsOnce sync.Once
// AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid
// plugin. The command stubs will have several annotations added, see
// `CommandAnnotationPlugin*`.
func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err error) {
pluginCommandStubsOnce.Do(func() {
var plugins []Plugin
plugins, err = ListPlugins(dockerCli, rootCmd)
if err != nil {
return
}
for _, p := range plugins {
vendor := p.Vendor
if vendor == "" {
vendor = "unknown"
}
annotations := map[string]string{
CommandAnnotationPlugin: "true",
CommandAnnotationPluginVendor: vendor,
CommandAnnotationPluginVersion: p.Version,
}
if p.Err != nil {
annotations[CommandAnnotationPluginInvalid] = p.Err.Error()
}
rootCmd.AddCommand(&cobra.Command{
Use: p.Name,
Short: p.ShortDescription,
Run: func(_ *cobra.Command, _ []string) {},
Annotations: annotations,
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
flags := rootCmd.PersistentFlags()
flags.SetOutput(nil)
perr := flags.Parse(args)
if perr != nil {
return err
}
if flags.Changed("help") {
cmd.HelpFunc()(rootCmd, args)
return nil
}
return fmt.Errorf("docker: unknown command: docker %s\n\nRun 'docker --help' for more information", cmd.Name())
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// Delegate completion to plugin
cargs := []string{p.Path, cobra.ShellCompRequestCmd, p.Name}
cargs = append(cargs, args...)
cargs = append(cargs, toComplete)
os.Args = cargs
runCommand, runErr := PluginRunCommand(dockerCli, p.Name, cmd)
if runErr != nil {
return nil, cobra.ShellCompDirectiveError
}
runErr = runCommand.Run()
if runErr == nil {
os.Exit(0) // plugin already rendered complete data
}
return nil, cobra.ShellCompDirectiveError
},
})
}
})
return err
}
const (
dockerCliAttributePrefix = attribute.Key("docker.cli")
cobraCommandPath = attribute.Key("cobra.command_path")
)
func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
commandPath := cmd.Annotations[CommandAnnotationPluginCommandPath]
if commandPath == "" {
commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name)
}
attrSet := attribute.NewSet(
cobraCommandPath.String(commandPath),
)
kvs := make([]attribute.KeyValue, 0, attrSet.Len())
for iter := attrSet.Iter(); iter.Next(); {
attr := iter.Attribute()
kvs = append(kvs, attribute.KeyValue{
Key: dockerCliAttributePrefix + "." + attr.Key,
Value: attr.Value,
})
}
return attribute.NewSet(kvs...)
}
func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string {
if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 {
// values in environment variables need to be in baggage format
// otel/baggage package can be used after update to v1.22, currently it encodes incorrectly
attrsSlice := make([]string, attrs.Len())
for iter := attrs.Iter(); iter.Next(); {
i, v := iter.IndexedAttribute()
attrsSlice[i] = string(v.Key) + "=" + url.PathEscape(v.Value.AsString())
}
env = append(env, ResourceAttributesEnvvar+"="+strings.Join(attrsSlice, ","))
}
return env
}

View File

@ -1,54 +0,0 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.22
package manager
import (
"github.com/pkg/errors"
)
// pluginError is set as Plugin.Err by NewPlugin if the plugin
// candidate fails one of the candidate tests. This exists primarily
// to implement encoding.TextMarshaller such that rendering a plugin as JSON
// (e.g. for `docker info -f '{{json .CLIPlugins}}'`) renders the Err
// field as a useful string and not just `{}`. See
// https://github.com/golang/go/issues/10748 for some discussion
// around why the builtin error type doesn't implement this.
type pluginError struct {
cause error
}
// Error satisfies the core error interface for pluginError.
func (e *pluginError) Error() string {
return e.cause.Error()
}
// Cause satisfies the errors.causer interface for pluginError.
func (e *pluginError) Cause() error {
return e.cause
}
// Unwrap provides compatibility for Go 1.13 error chains.
func (e *pluginError) Unwrap() error {
return e.cause
}
// MarshalText marshalls the pluginError into a textual form.
func (e *pluginError) MarshalText() (text []byte, err error) {
return []byte(e.cause.Error()), nil
}
// wrapAsPluginError wraps an error in a pluginError with an
// additional message, analogous to errors.Wrapf.
func wrapAsPluginError(err error, msg string) error {
if err == nil {
return nil
}
return &pluginError{cause: errors.Wrap(err, msg)}
}
// NewPluginError creates a new pluginError, analogous to
// errors.Errorf.
func NewPluginError(msg string, args ...any) error {
return &pluginError{cause: errors.Errorf(msg, args...)}
}

View File

@ -1,199 +0,0 @@
package manager
import (
"context"
"encoding/json"
"strings"
"github.com/docker/cli/cli-plugins/hooks"
"github.com/docker/cli/cli/command"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// HookPluginData is the type representing the information
// that plugins declaring support for hooks get passed when
// being invoked following a CLI command execution.
type HookPluginData struct {
// RootCmd is a string representing the matching hook configuration
// which is currently being invoked. If a hook for `docker context` is
// configured and the user executes `docker context ls`, the plugin will
// be invoked with `context`.
RootCmd string
Flags map[string]string
CommandError string
}
// RunCLICommandHooks is the entrypoint into the hooks execution flow after
// a main CLI command was executed. It calls the hook subcommand for all
// present CLI plugins that declare support for hooks in their metadata and
// parses/prints their responses.
func RunCLICommandHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
flags := getCommandFlags(subCommand)
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, cmdErrorMessage)
}
// RunPluginHooks is the entrypoint for the hooks execution flow
// after a plugin command was just executed by the CLI.
func RunPluginHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) {
commandName := strings.Join(args, " ")
flags := getNaiveFlags(args)
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, "")
}
func runHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
nextSteps := invokeAndCollectHooks(ctx, dockerCli, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
hooks.PrintNextSteps(dockerCli.Err(), nextSteps)
}
func invokeAndCollectHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
// check if the context was cancelled before invoking hooks
select {
case <-ctx.Done():
return nil
default:
}
pluginsCfg := dockerCli.ConfigFile().Plugins
if pluginsCfg == nil {
return nil
}
nextSteps := make([]string, 0, len(pluginsCfg))
for pluginName, cfg := range pluginsCfg {
match, ok := pluginMatch(cfg, subCmdStr)
if !ok {
continue
}
p, err := GetPlugin(pluginName, dockerCli, rootCmd)
if err != nil {
continue
}
hookReturn, err := p.RunHook(ctx, HookPluginData{
RootCmd: match,
Flags: flags,
CommandError: cmdErrorMessage,
})
if err != nil {
// skip misbehaving plugins, but don't halt execution
continue
}
var hookMessageData hooks.HookMessage
err = json.Unmarshal(hookReturn, &hookMessageData)
if err != nil {
continue
}
// currently the only hook type
if hookMessageData.Type != hooks.NextSteps {
continue
}
processedHook, err := hooks.ParseTemplate(hookMessageData.Template, subCmd)
if err != nil {
continue
}
var appended bool
nextSteps, appended = appendNextSteps(nextSteps, processedHook)
if !appended {
logrus.Debugf("Plugin %s responded with an empty hook message %q. Ignoring.", pluginName, string(hookReturn))
}
}
return nextSteps
}
// appendNextSteps appends the processed hook output to the nextSteps slice.
// If the processed hook output is empty, it is not appended.
// Empty lines are not stripped if there's at least one non-empty line.
func appendNextSteps(nextSteps []string, processed []string) ([]string, bool) {
empty := true
for _, l := range processed {
if strings.TrimSpace(l) != "" {
empty = false
break
}
}
if empty {
return nextSteps, false
}
return append(nextSteps, processed...), true
}
// pluginMatch takes a plugin configuration and a string representing the
// command being executed (such as 'image ls' the root 'docker' is omitted)
// and, if the configuration includes a hook for the invoked command, returns
// the configured hook string.
func pluginMatch(pluginCfg map[string]string, subCmd string) (string, bool) {
configuredPluginHooks, ok := pluginCfg["hooks"]
if !ok || configuredPluginHooks == "" {
return "", false
}
commands := strings.Split(configuredPluginHooks, ",")
for _, hookCmd := range commands {
if hookMatch(hookCmd, subCmd) {
return hookCmd, true
}
}
return "", false
}
func hookMatch(hookCmd, subCmd string) bool {
hookCmdTokens := strings.Split(hookCmd, " ")
subCmdTokens := strings.Split(subCmd, " ")
if len(hookCmdTokens) > len(subCmdTokens) {
return false
}
for i, v := range hookCmdTokens {
if v != subCmdTokens[i] {
return false
}
}
return true
}
func getCommandFlags(cmd *cobra.Command) map[string]string {
flags := make(map[string]string)
cmd.Flags().Visit(func(f *pflag.Flag) {
var fValue string
if f.Value.Type() == "bool" {
fValue = f.Value.String()
}
flags[f.Name] = fValue
})
return flags
}
// getNaiveFlags string-matches argv and parses them into a map.
// This is used when calling hooks after a plugin command, since
// in this case we can't rely on the cobra command tree to parse
// flags in this case. In this case, no values are ever passed,
// since we don't have enough information to process them.
func getNaiveFlags(args []string) map[string]string {
flags := make(map[string]string)
for _, arg := range args {
if strings.HasPrefix(arg, "--") {
flags[arg[2:]] = ""
continue
}
if strings.HasPrefix(arg, "-") {
flags[arg[1:]] = ""
}
}
return flags
}

View File

@ -1,247 +0,0 @@
package manager
import (
"context"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/fvbommel/sortorder"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)
const (
// ReexecEnvvar is the name of an ennvar which is set to the command
// used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI.
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
// ResourceAttributesEnvvar is the name of the envvar that includes additional
// resource attributes for OTEL.
ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES"
)
// errPluginNotFound is the error returned when a plugin could not be found.
type errPluginNotFound string
func (errPluginNotFound) NotFound() {}
func (e errPluginNotFound) Error() string {
return "Error: No such CLI plugin: " + string(e)
}
type notFound interface{ NotFound() }
// IsNotFound is true if the given error is due to a plugin not being found.
func IsNotFound(err error) bool {
if e, ok := err.(*pluginError); ok {
err = e.Cause()
}
_, ok := err.(notFound)
return ok
}
// getPluginDirs returns the platform-specific locations to search for plugins
// in order of preference.
//
// Plugin-discovery is performed in the following order of preference:
//
// 1. The "cli-plugins" directory inside the CLIs [config.Path] (usually "~/.docker/cli-plugins").
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs].
// 3. Platform-specific defaultSystemPluginDirs.
//
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
func getPluginDirs(cfg *configfile.ConfigFile) ([]string, error) {
var pluginDirs []string
if cfg != nil {
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
}
pluginDir, err := config.Path("cli-plugins")
if err != nil {
return nil, err
}
pluginDirs = append(pluginDirs, pluginDir)
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
return pluginDirs, nil
}
func addPluginCandidatesFromDir(res map[string][]string, d string) {
dentries, err := os.ReadDir(d)
// Silently ignore any directories which we cannot list (e.g. due to
// permissions or anything else) or which is not a directory
if err != nil {
return
}
for _, dentry := range dentries {
switch dentry.Type() & os.ModeType {
case 0, os.ModeSymlink:
// Regular file or symlink, keep going
default:
// Something else, ignore.
continue
}
name := dentry.Name()
if !strings.HasPrefix(name, NamePrefix) {
continue
}
name = strings.TrimPrefix(name, NamePrefix)
var err error
if name, err = trimExeSuffix(name); err != nil {
continue
}
res[name] = append(res[name], filepath.Join(d, dentry.Name()))
}
}
// listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority.
func listPluginCandidates(dirs []string) map[string][]string {
result := make(map[string][]string)
for _, d := range dirs {
addPluginCandidatesFromDir(result, d)
}
return result
}
// GetPlugin returns a plugin on the system by its name
func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plugin, error) {
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
if err != nil {
return nil, err
}
candidates := listPluginCandidates(pluginDirs)
if paths, ok := candidates[name]; ok {
if len(paths) == 0 {
return nil, errPluginNotFound(name)
}
c := &candidate{paths[0]}
p, err := newPlugin(c, rootcmd.Commands())
if err != nil {
return nil, err
}
if !IsNotFound(p.Err) {
p.ShadowedPaths = paths[1:]
}
return &p, nil
}
return nil, errPluginNotFound(name)
}
// ListPlugins produces a list of the plugins available on the system
func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
if err != nil {
return nil, err
}
candidates := listPluginCandidates(pluginDirs)
var plugins []Plugin
var mu sync.Mutex
eg, _ := errgroup.WithContext(context.TODO())
cmds := rootcmd.Commands()
for _, paths := range candidates {
func(paths []string) {
eg.Go(func() error {
if len(paths) == 0 {
return nil
}
c := &candidate{paths[0]}
p, err := newPlugin(c, cmds)
if err != nil {
return err
}
if !IsNotFound(p.Err) {
p.ShadowedPaths = paths[1:]
mu.Lock()
defer mu.Unlock()
plugins = append(plugins, p)
}
return nil
})
}(paths)
}
if err := eg.Wait(); err != nil {
return nil, err
}
sort.Slice(plugins, func(i, j int) bool {
return sortorder.NaturalLess(plugins[i].Name, plugins[j].Name)
})
return plugins, nil
}
// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
// The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
// This uses the full original args, not the args which may
// have been provided by cobra to our caller. This is because
// they lack e.g. global options which we must propagate here.
args := os.Args[1:]
if !pluginNameRe.MatchString(name) {
// We treat this as "not found" so that callers will
// fallback to their "invalid" command path.
return nil, errPluginNotFound(name)
}
exename := addExeSuffix(NamePrefix + name)
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
if err != nil {
return nil, err
}
for _, d := range pluginDirs {
path := filepath.Join(d, exename)
// We stat here rather than letting the exec tell us
// ENOENT because the latter does not distinguish a
// file not existing from its dynamic loader or one of
// its libraries not existing.
if _, err := os.Stat(path); os.IsNotExist(err) {
continue
}
c := &candidate{path: path}
plugin, err := newPlugin(c, rootcmd.Commands())
if err != nil {
return nil, err
}
if plugin.Err != nil {
// TODO: why are we not returning plugin.Err?
return nil, errPluginNotFound(name)
}
cmd := exec.Command(plugin.Path, args...) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
// Using dockerCli.{In,Out,Err}() here results in a hang until something is input.
// See: - https://github.com/golang/go/issues/10338
// - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab
// os.Stdin is a *os.File which avoids this behaviour. We don't need the functionality
// of the wrappers here anyway.
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = append(cmd.Environ(), ReexecEnvvar+"="+os.Args[0])
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
return cmd, nil
}
return nil, errPluginNotFound(name)
}
// IsPluginCommand checks if the given cmd is a plugin-stub.
func IsPluginCommand(cmd *cobra.Command) bool {
return cmd.Annotations[CommandAnnotationPlugin] == "true"
}

View File

@ -1,20 +0,0 @@
//go:build !windows
package manager
// defaultSystemPluginDirs are the platform-specific locations to search
// for plugins in order of preference.
//
// Plugin-discovery is performed in the following order of preference:
//
// 1. The "cli-plugins" directory inside the CLIs config-directory (usually "~/.docker/cli-plugins").
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs].
// 3. Platform-specific defaultSystemPluginDirs (as defined below).
//
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
var defaultSystemPluginDirs = []string{
"/usr/local/lib/docker/cli-plugins",
"/usr/local/libexec/docker/cli-plugins",
"/usr/lib/docker/cli-plugins",
"/usr/libexec/docker/cli-plugins",
}

View File

@ -1,21 +0,0 @@
package manager
import (
"os"
"path/filepath"
)
// defaultSystemPluginDirs are the platform-specific locations to search
// for plugins in order of preference.
//
// Plugin-discovery is performed in the following order of preference:
//
// 1. The "cli-plugins" directory inside the CLIs config-directory (usually "~/.docker/cli-plugins").
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs].
// 3. Platform-specific defaultSystemPluginDirs (as defined below).
//
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
var defaultSystemPluginDirs = []string{
filepath.Join(os.Getenv("ProgramData"), "Docker", "cli-plugins"),
filepath.Join(os.Getenv("ProgramFiles"), "Docker", "cli-plugins"),
}

View File

@ -1,124 +0,0 @@
package manager
import (
"context"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
var pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
// Plugin represents a potential plugin with all it's metadata.
type Plugin struct {
Metadata
Name string `json:",omitempty"`
Path string `json:",omitempty"`
// Err is non-nil if the plugin failed one of the candidate tests.
Err error `json:",omitempty"`
// ShadowedPaths contains the paths of any other plugins which this plugin takes precedence over.
ShadowedPaths []string `json:",omitempty"`
}
// newPlugin determines if the given candidate is valid and returns a
// Plugin. If the candidate fails one of the tests then `Plugin.Err`
// is set, and is always a `pluginError`, but the `Plugin` is still
// returned with no error. An error is only returned due to a
// non-recoverable error.
func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
path := c.Path()
if path == "" {
return Plugin{}, errors.New("plugin candidate path cannot be empty")
}
// The candidate listing process should have skipped anything
// which would fail here, so there are all real errors.
fullname := filepath.Base(path)
if fullname == "." {
return Plugin{}, errors.Errorf("unable to determine basename of plugin candidate %q", path)
}
var err error
if fullname, err = trimExeSuffix(fullname); err != nil {
return Plugin{}, errors.Wrapf(err, "plugin candidate %q", path)
}
if !strings.HasPrefix(fullname, NamePrefix) {
return Plugin{}, errors.Errorf("plugin candidate %q: does not have %q prefix", path, NamePrefix)
}
p := Plugin{
Name: strings.TrimPrefix(fullname, NamePrefix),
Path: path,
}
// Now apply the candidate tests, so these update p.Err.
if !pluginNameRe.MatchString(p.Name) {
p.Err = NewPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String())
return p, nil
}
for _, cmd := range cmds {
// Ignore conflicts with commands which are
// just plugin stubs (i.e. from a previous
// call to AddPluginCommandStubs).
if IsPluginCommand(cmd) {
continue
}
if cmd.Name() == p.Name {
p.Err = NewPluginError("plugin %q duplicates builtin command", p.Name)
return p, nil
}
if cmd.HasAlias(p.Name) {
p.Err = NewPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name())
return p, nil
}
}
// We are supposed to check for relevant execute permissions here. Instead we rely on an attempt to execute.
meta, err := c.Metadata()
if err != nil {
p.Err = wrapAsPluginError(err, "failed to fetch metadata")
return p, nil
}
if err := json.Unmarshal(meta, &p.Metadata); err != nil {
p.Err = wrapAsPluginError(err, "invalid metadata")
return p, nil
}
if p.Metadata.SchemaVersion != "0.1.0" {
p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion)
return p, nil
}
if p.Metadata.Vendor == "" {
p.Err = NewPluginError("plugin metadata does not define a vendor")
return p, nil
}
return p, nil
}
// RunHook executes the plugin's hooks command
// and returns its unprocessed output.
func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte, error) {
hDataBytes, err := json.Marshal(hookData)
if err != nil {
return nil, wrapAsPluginError(err, "failed to marshall hook data")
}
pCmd := exec.CommandContext(ctx, p.Path, p.Name, HookSubcommandName, string(hDataBytes)) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
pCmd.Env = os.Environ()
pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0])
hookCmdOutput, err := pCmd.Output()
if err != nil {
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")
}
return hookCmdOutput, nil
}

View File

@ -1,11 +0,0 @@
//go:build !windows
package manager
func trimExeSuffix(s string) (string, error) {
return s, nil
}
func addExeSuffix(s string) string {
return s
}

View File

@ -1,26 +0,0 @@
package manager
import (
"path/filepath"
"strings"
"github.com/pkg/errors"
)
// This is made slightly more complex due to needing to be case insensitive.
func trimExeSuffix(s string) (string, error) {
ext := filepath.Ext(s)
if ext == "" {
return "", errors.Errorf("path %q lacks required file extension", s)
}
exe := ".exe"
if !strings.EqualFold(ext, exe) {
return "", errors.Errorf("path %q lacks required %q suffix", s, exe)
}
return strings.TrimSuffix(s, ext), nil
}
func addExeSuffix(s string) string {
return s + ".exe"
}

View File

@ -0,0 +1,28 @@
package metadata
const (
// CommandAnnotationPlugin is added to every stub command added by
// AddPluginCommandStubs with the value "true" and so can be
// used to distinguish plugin stubs from regular commands.
CommandAnnotationPlugin = "com.docker.cli.plugin"
// CommandAnnotationPluginVendor is added to every stub command
// added by AddPluginCommandStubs and contains the vendor of
// that plugin.
CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor"
// CommandAnnotationPluginVersion is added to every stub command
// added by AddPluginCommandStubs and contains the version of
// that plugin.
CommandAnnotationPluginVersion = "com.docker.cli.plugin.version"
// CommandAnnotationPluginInvalid is added to any stub command
// added by AddPluginCommandStubs for an invalid command (that
// is, one which failed it's candidate test) and contains the
// reason for the failure.
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
// CommandAnnotationPluginCommandPath is added to overwrite the
// command path for a plugin invocation.
CommandAnnotationPluginCommandPath = "com.docker.cli.plugin.command_path"
)

View File

@ -1,4 +1,4 @@
package manager package metadata
const ( const (
// NamePrefix is the prefix required on all plugin binary names // NamePrefix is the prefix required on all plugin binary names
@ -13,6 +13,12 @@ const (
// which must be implemented by plugins declaring support // which must be implemented by plugins declaring support
// for hooks in their metadata. // for hooks in their metadata.
HookSubcommandName = "docker-cli-plugin-hooks" HookSubcommandName = "docker-cli-plugin-hooks"
// ReexecEnvvar is the name of an ennvar which is set to the command
// used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI.
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
) )
// Metadata provided by the plugin. // Metadata provided by the plugin.

View File

@ -9,7 +9,7 @@ import (
"sync" "sync"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli-plugins/socket" "github.com/docker/cli/cli-plugins/socket"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/connhelper" "github.com/docker/cli/cli/connhelper"
@ -30,7 +30,7 @@ import (
var PersistentPreRunE func(*cobra.Command, []string) error var PersistentPreRunE func(*cobra.Command, []string) error
// RunPlugin executes the specified plugin command // RunPlugin executes the specified plugin command
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) error { func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) error {
tcmd := newPluginCommand(dockerCli, plugin, meta) tcmd := newPluginCommand(dockerCli, plugin, meta)
var persistentPreRunOnce sync.Once var persistentPreRunOnce sync.Once
@ -81,7 +81,7 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager
} }
// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function. // Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) { func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) {
otel.SetErrorHandler(debug.OTELErrorHandler) otel.SetErrorHandler(debug.OTELErrorHandler)
dockerCli, err := command.NewDockerCli() dockerCli, err := command.NewDockerCli()
@ -111,7 +111,7 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
func withPluginClientConn(name string) command.CLIOption { func withPluginClientConn(name string) command.CLIOption {
return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) { return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) {
cmd := "docker" cmd := "docker"
if x := os.Getenv(manager.ReexecEnvvar); x != "" { if x := os.Getenv(metadata.ReexecEnvvar); x != "" {
cmd = x cmd = x
} }
var flags []string var flags []string
@ -140,9 +140,9 @@ func withPluginClientConn(name string) command.CLIOption {
}) })
} }
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cli.TopLevelCommand { func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) *cli.TopLevelCommand {
name := plugin.Name() name := plugin.Name()
fullname := manager.NamePrefix + name fullname := metadata.NamePrefix + name
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name), Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name),
@ -177,12 +177,12 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags()) return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags())
} }
func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command { func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra.Command {
if meta.ShortDescription == "" { if meta.ShortDescription == "" {
meta.ShortDescription = plugin.Short meta.ShortDescription = plugin.Short
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: manager.MetadataSubcommandName, Use: metadata.MetadataSubcommandName,
Hidden: true, Hidden: true,
// Suppress the global/parent PersistentPreRunE, which // Suppress the global/parent PersistentPreRunE, which
// needlessly initializes the client and tries to // needlessly initializes the client and tries to
@ -200,8 +200,8 @@ func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.
// RunningStandalone tells a CLI plugin it is run standalone by direct execution // RunningStandalone tells a CLI plugin it is run standalone by direct execution
func RunningStandalone() bool { func RunningStandalone() bool {
if os.Getenv(manager.ReexecEnvvar) != "" { if os.Getenv(metadata.ReexecEnvvar) != "" {
return false return false
} }
return len(os.Args) < 2 || os.Args[1] != manager.MetadataSubcommandName return len(os.Args) < 2 || os.Args[1] != metadata.MetadataSubcommandName
} }

View File

@ -3,15 +3,12 @@ package cli
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"sort" "sort"
"strings" "strings"
pluginmanager "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
cliflags "github.com/docker/cli/cli/flags" cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/docker/pkg/homedir"
"github.com/docker/docker/registry"
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
"github.com/moby/term" "github.com/moby/term"
"github.com/morikuni/aec" "github.com/morikuni/aec"
@ -62,13 +59,6 @@ func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *c
"docs.code-delimiter": `"`, // https://github.com/docker/cli-docs-tool/blob/77abede22166eaea4af7335096bdcedd043f5b19/annotation/annotation.go#L20-L22 "docs.code-delimiter": `"`, // https://github.com/docker/cli-docs-tool/blob/77abede22166eaea4af7335096bdcedd043f5b19/annotation/annotation.go#L20-L22
} }
// Configure registry.CertsDir() when running in rootless-mode
if os.Getenv("ROOTLESSKIT_STATE_DIR") != "" {
if configHome, err := homedir.GetConfigHome(); err == nil {
registry.SetCertsDir(filepath.Join(configHome, "docker/certs.d"))
}
}
return opts, helpCommand return opts, helpCommand
} }
@ -252,7 +242,7 @@ func hasAdditionalHelp(cmd *cobra.Command) bool {
} }
func isPlugin(cmd *cobra.Command) bool { func isPlugin(cmd *cobra.Command) bool {
return pluginmanager.IsPluginCommand(cmd) return cmd.Annotations[metadata.CommandAnnotationPlugin] == "true"
} }
func hasAliases(cmd *cobra.Command) bool { func hasAliases(cmd *cobra.Command) bool {
@ -356,9 +346,9 @@ func decoratedName(cmd *cobra.Command) string {
} }
func vendorAndVersion(cmd *cobra.Command) string { func vendorAndVersion(cmd *cobra.Command) string {
if vendor, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) { if vendor, ok := cmd.Annotations[metadata.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) {
version := "" version := ""
if v, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVersion]; ok && v != "" { if v, ok := cmd.Annotations[metadata.CommandAnnotationPluginVersion]; ok && v != "" {
version = ", " + v version = ", " + v
} }
return fmt.Sprintf("(%s%s)", vendor, version) return fmt.Sprintf("(%s%s)", vendor, version)
@ -417,7 +407,7 @@ func invalidPlugins(cmd *cobra.Command) []*cobra.Command {
} }
func invalidPluginReason(cmd *cobra.Command) string { func invalidPluginReason(cmd *cobra.Command) string {
return cmd.Annotations[pluginmanager.CommandAnnotationPluginInvalid] return cmd.Annotations[metadata.CommandAnnotationPluginInvalid]
} }
const usageTemplate = `Usage: const usageTemplate = `Usage:

View File

@ -8,7 +8,6 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"sync" "sync"
@ -21,21 +20,15 @@ import (
"github.com/docker/cli/cli/context/store" "github.com/docker/cli/cli/context/store"
"github.com/docker/cli/cli/debug" "github.com/docker/cli/cli/debug"
cliflags "github.com/docker/cli/cli/flags" cliflags "github.com/docker/cli/cli/flags"
manifeststore "github.com/docker/cli/cli/manifest/store"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/cli/version" "github.com/docker/cli/cli/version"
dopts "github.com/docker/cli/opts" dopts "github.com/docker/cli/opts"
"github.com/docker/docker/api" "github.com/docker/docker/api"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/go-connections/tlsconfig"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
notaryclient "github.com/theupdateframework/notary/client"
) )
const defaultInitTimeout = 2 * time.Second const defaultInitTimeout = 2 * time.Second
@ -53,19 +46,18 @@ type Cli interface {
Streams Streams
SetIn(in *streams.In) SetIn(in *streams.In)
Apply(ops ...CLIOption) error Apply(ops ...CLIOption) error
ConfigFile() *configfile.ConfigFile config.Provider
ServerInfo() ServerInfo ServerInfo() ServerInfo
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
DefaultVersion() string DefaultVersion() string
CurrentVersion() string CurrentVersion() string
ManifestStore() manifeststore.Store
RegistryClient(bool) registryclient.RegistryClient
ContentTrustEnabled() bool ContentTrustEnabled() bool
BuildKitEnabled() (bool, error) BuildKitEnabled() (bool, error)
ContextStore() store.Store ContextStore() store.Store
CurrentContext() string CurrentContext() string
DockerEndpoint() docker.Endpoint DockerEndpoint() docker.Endpoint
TelemetryClient TelemetryClient
DeprecatedNotaryClient
DeprecatedManifestClient
} }
// DockerCli is an instance the docker command line client. // DockerCli is an instance the docker command line client.
@ -96,7 +88,7 @@ type DockerCli struct {
enableGlobalMeter, enableGlobalTracer bool enableGlobalMeter, enableGlobalTracer bool
} }
// DefaultVersion returns api.defaultVersion. // DefaultVersion returns [api.DefaultVersion].
func (*DockerCli) DefaultVersion() string { func (*DockerCli) DefaultVersion() string {
return api.DefaultVersion return api.DefaultVersion
} }
@ -202,16 +194,16 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
// HooksEnabled returns whether plugin hooks are enabled. // HooksEnabled returns whether plugin hooks are enabled.
func (cli *DockerCli) HooksEnabled() bool { func (cli *DockerCli) HooksEnabled() bool {
// legacy support DOCKER_CLI_HINTS env var // use DOCKER_CLI_HOOKS env var value if set and not empty
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" { if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
enabled, err := strconv.ParseBool(v) enabled, err := strconv.ParseBool(v)
if err != nil { if err != nil {
return false return false
} }
return enabled return enabled
} }
// use DOCKER_CLI_HOOKS env var value if set and not empty // legacy support DOCKER_CLI_HINTS env var
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" { if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
enabled, err := strconv.ParseBool(v) enabled, err := strconv.ParseBool(v)
if err != nil { if err != nil {
return false return false
@ -230,21 +222,6 @@ func (cli *DockerCli) HooksEnabled() bool {
return false return false
} }
// ManifestStore returns a store for local manifests
func (*DockerCli) ManifestStore() manifeststore.Store {
// TODO: support override default location from config file
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
}
// RegistryClient returns a client for communicating with a Docker distribution
// registry
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig {
return ResolveAuthConfig(cli.ConfigFile(), index)
}
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
}
// WithInitializeClient is passed to DockerCli.Initialize by callers who wish to set a particular API Client for use by the CLI. // WithInitializeClient is passed to DockerCli.Initialize by callers who wish to set a particular API Client for use by the CLI.
func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) CLIOption { func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) CLIOption {
return func(dockerCli *DockerCli) error { return func(dockerCli *DockerCli) error {
@ -292,6 +269,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
if cli.enableGlobalTracer { if cli.enableGlobalTracer {
cli.createGlobalTracerProvider(cli.baseCtx) cli.createGlobalTracerProvider(cli.baseCtx)
} }
filterResourceAttributesEnvvar()
return nil return nil
} }
@ -345,7 +323,10 @@ func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint,
// Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags) // Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) { func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) {
host, err := getServerHost(opts.Hosts, opts.TLSOptions) // defaultToTLS determines whether we should use a TLS host as default
// if nothing was configured by the user.
defaultToTLS := opts.TLSOptions != nil
host, err := getServerHost(opts.Hosts, defaultToTLS)
if err != nil { if err != nil {
return docker.Endpoint{}, err return docker.Endpoint{}, err
} }
@ -403,11 +384,6 @@ func (cli *DockerCli) initializeFromClient() {
cli.client.NegotiateAPIVersionPing(ping) cli.client.NegotiateAPIVersionPing(ping)
} }
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
}
// ContextStore returns the ContextStore // ContextStore returns the ContextStore
func (cli *DockerCli) ContextStore() store.Store { func (cli *DockerCli) ContextStore() store.Store {
return cli.contextStore return cli.contextStore
@ -553,18 +529,15 @@ func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
return cli, nil return cli, nil
} }
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) { func getServerHost(hosts []string, defaultToTLS bool) (string, error) {
var host string
switch len(hosts) { switch len(hosts) {
case 0: case 0:
host = os.Getenv(client.EnvOverrideHost) return dopts.ParseHost(defaultToTLS, os.Getenv(client.EnvOverrideHost))
case 1: case 1:
host = hosts[0] return dopts.ParseHost(defaultToTLS, hosts[0])
default: default:
return "", errors.New("Specify only one -H") return "", errors.New("Specify only one -H")
} }
return dopts.ParseHost(tlsOptions != nil, host)
} }
// UserAgent returns the user agent string used for making API requests // UserAgent returns the user agent string used for making API requests

View File

@ -0,0 +1,56 @@
package command
import (
"context"
"path/filepath"
"github.com/docker/cli/cli/config"
manifeststore "github.com/docker/cli/cli/manifest/store"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/cli/trust"
"github.com/docker/docker/api/types/registry"
notaryclient "github.com/theupdateframework/notary/client"
)
type DeprecatedNotaryClient interface {
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
//
// Deprecated: use [trust.GetNotaryRepository] instead. This method is no longer used and will be removed in the next release.
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
}
type DeprecatedManifestClient interface {
// ManifestStore returns a store for local manifests
//
// Deprecated: use [manifeststore.NewStore] instead. This method is no longer used and will be removed in the next release.
ManifestStore() manifeststore.Store
// RegistryClient returns a client for communicating with a Docker distribution
// registry.
//
// Deprecated: use [registryclient.NewRegistryClient]. This method is no longer used and will be removed in the next release.
RegistryClient(bool) registryclient.RegistryClient
}
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
}
// ManifestStore returns a store for local manifests
//
// Deprecated: use [manifeststore.NewStore] instead. This method is no longer used and will be removed in the next release.
func (*DockerCli) ManifestStore() manifeststore.Store {
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
}
// RegistryClient returns a client for communicating with a Docker distribution
// registry
//
// Deprecated: use [registryclient.NewRegistryClient]. This method is no longer used and will be removed in the next release.
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig {
return ResolveAuthConfig(cli.ConfigFile(), index)
}
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
}

View File

@ -1,6 +1,11 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.22
package formatter package formatter
import ( import (
"fmt"
"strings"
"unicode/utf8" "unicode/utf8"
"golang.org/x/text/width" "golang.org/x/text/width"
@ -59,3 +64,27 @@ func Ellipsis(s string, maxDisplayWidth int) string {
} }
return s return s
} }
// capitalizeFirst capitalizes the first character of string
func capitalizeFirst(s string) string {
switch l := len(s); l {
case 0:
return s
case 1:
return strings.ToLower(s)
default:
return strings.ToUpper(string(s[0])) + strings.ToLower(s[1:])
}
}
// PrettyPrint outputs arbitrary data for human formatted output by uppercasing the first letter.
func PrettyPrint(i any) string {
switch t := i.(type) {
case nil:
return "None"
case string:
return capitalizeFirst(t)
default:
return capitalizeFirst(fmt.Sprintf("%s", t))
}
}

View File

@ -76,9 +76,9 @@ func (c *Context) preFormat() {
func (c *Context) parseFormat() (*template.Template, error) { func (c *Context) parseFormat() (*template.Template, error) {
tmpl, err := templates.Parse(c.finalFormat) tmpl, err := templates.Parse(c.finalFormat)
if err != nil { if err != nil {
return tmpl, errors.Wrap(err, "template parsing error") return nil, errors.Wrap(err, "template parsing error")
} }
return tmpl, err return tmpl, nil
} }
func (c *Context) postFormat(tmpl *template.Template, subContext SubContext) { func (c *Context) postFormat(tmpl *template.Template, subContext SubContext) {

View File

@ -4,10 +4,11 @@ import (
"context" "context"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"time" "time"
"github.com/docker/distribution/uuid" "github.com/google/uuid"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric"
sdkmetric "go.opentelemetry.io/otel/sdk/metric" sdkmetric "go.opentelemetry.io/otel/sdk/metric"
@ -142,7 +143,7 @@ func defaultResourceOptions() []resource.Option {
// of the CLI is its own instance. Without this, downstream // of the CLI is its own instance. Without this, downstream
// OTEL processors may think the same process is restarting // OTEL processors may think the same process is restarting
// continuously. // continuously.
semconv.ServiceInstanceID(uuid.Generate().String()), semconv.ServiceInstanceID(uuid.NewString()),
), ),
resource.WithFromEnv(), resource.WithFromEnv(),
resource.WithTelemetrySDK(), resource.WithTelemetrySDK(),
@ -216,3 +217,49 @@ func (r *cliReader) ForceFlush(ctx context.Context) error {
func deltaTemporality(_ sdkmetric.InstrumentKind) metricdata.Temporality { func deltaTemporality(_ sdkmetric.InstrumentKind) metricdata.Temporality {
return metricdata.DeltaTemporality return metricdata.DeltaTemporality
} }
// resourceAttributesEnvVar is the name of the envvar that includes additional
// resource attributes for OTEL as defined in the [OpenTelemetry specification].
//
// [OpenTelemetry specification]: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration
const resourceAttributesEnvVar = "OTEL_RESOURCE_ATTRIBUTES"
func filterResourceAttributesEnvvar() {
if v := os.Getenv(resourceAttributesEnvVar); v != "" {
if filtered := filterResourceAttributes(v); filtered != "" {
_ = os.Setenv(resourceAttributesEnvVar, filtered)
} else {
_ = os.Unsetenv(resourceAttributesEnvVar)
}
}
}
// dockerCLIAttributePrefix is the prefix for any docker cli OTEL attributes.
// When updating, make sure to also update the copy in cli-plugins/manager.
//
// TODO(thaJeztah): move telemetry-related code to an (internal) package to reduce dependency on cli/command in cli-plugins, which has too many imports.
const dockerCLIAttributePrefix = "docker.cli."
func filterResourceAttributes(s string) string {
if trimmed := strings.TrimSpace(s); trimmed == "" {
return trimmed
}
pairs := strings.Split(s, ",")
elems := make([]string, 0, len(pairs))
for _, p := range pairs {
k, _, found := strings.Cut(p, "=")
if !found {
// Do not interact with invalid otel resources.
elems = append(elems, p)
continue
}
// Skip attributes that have our docker.cli prefix.
if strings.HasPrefix(k, dockerCLIAttributePrefix) {
continue
}
elems = append(elems, p)
}
return strings.Join(elems, ",")
}

View File

@ -13,10 +13,9 @@ import (
"runtime" "runtime"
"strings" "strings"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
mounttypes "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/moby/sys/sequential" "github.com/moby/sys/sequential"
"github.com/moby/term" "github.com/moby/term"
@ -51,30 +50,6 @@ func CopyToFile(outfile string, r io.Reader) error {
return nil return nil
} }
// capitalizeFirst capitalizes the first character of string
func capitalizeFirst(s string) string {
switch l := len(s); l {
case 0:
return s
case 1:
return strings.ToLower(s)
default:
return strings.ToUpper(string(s[0])) + strings.ToLower(s[1:])
}
}
// PrettyPrint outputs arbitrary data for human formatted output by uppercasing the first letter.
func PrettyPrint(i any) string {
switch t := i.(type) {
case nil:
return "None"
case string:
return capitalizeFirst(t)
default:
return capitalizeFirst(fmt.Sprintf("%s", t))
}
}
var ErrPromptTerminated = errdefs.Cancelled(errors.New("prompt terminated")) var ErrPromptTerminated = errdefs.Cancelled(errors.New("prompt terminated"))
// DisableInputEcho disables input echo on the provided streams.In. // DisableInputEcho disables input echo on the provided streams.In.
@ -166,11 +141,12 @@ func PromptForConfirmation(ctx context.Context, ins io.Reader, outs io.Writer, m
} }
// PruneFilters returns consolidated prune filters obtained from config.json and cli // PruneFilters returns consolidated prune filters obtained from config.json and cli
func PruneFilters(dockerCli Cli, pruneFilters filters.Args) filters.Args { func PruneFilters(dockerCLI config.Provider, pruneFilters filters.Args) filters.Args {
if dockerCli.ConfigFile() == nil { cfg := dockerCLI.ConfigFile()
if cfg == nil {
return pruneFilters return pruneFilters
} }
for _, f := range dockerCli.ConfigFile().PruneFilters { for _, f := range cfg.PruneFilters {
k, v, ok := strings.Cut(f, "=") k, v, ok := strings.Cut(f, "=")
if !ok { if !ok {
continue continue
@ -239,48 +215,3 @@ func ValidateOutputPathFileMode(fileMode os.FileMode) error {
} }
return nil return nil
} }
func stringSliceIndex(s, subs []string) int {
j := 0
if len(subs) > 0 {
for i, x := range s {
if j < len(subs) && subs[j] == x {
j++
} else {
j = 0
}
if len(subs) == j {
return i + 1 - j
}
}
}
return -1
}
// StringSliceReplaceAt replaces the sub-slice find, with the sub-slice replace, in the string
// slice s, returning a new slice and a boolean indicating if the replacement happened.
// requireIdx is the index at which old needs to be found at (or -1 to disregard that).
func StringSliceReplaceAt(s, find, replace []string, requireIndex int) ([]string, bool) {
idx := stringSliceIndex(s, find)
if (requireIndex != -1 && requireIndex != idx) || idx == -1 {
return s, false
}
out := append([]string{}, s[:idx]...)
out = append(out, replace...)
out = append(out, s[idx+len(find):]...)
return out, true
}
// ValidateMountWithAPIVersion validates a mount with the server API version.
func ValidateMountWithAPIVersion(m mounttypes.Mount, serverAPIVersion string) error {
if m.BindOptions != nil {
if m.BindOptions.NonRecursive && versions.LessThan(serverAPIVersion, "1.40") {
return errors.Errorf("bind-recursive=disabled requires API v1.40 or later")
}
// ReadOnlyNonRecursive can be safely ignored when API < 1.44
if m.BindOptions.ReadOnlyForceRecursive && versions.LessThan(serverAPIVersion, "1.44") {
return errors.Errorf("bind-recursive=readonly requires API v1.44 or later")
}
}
return nil
}

View File

@ -69,6 +69,11 @@ func getHomeDir() string {
return home return home
} }
// Provider defines an interface for providing the CLI config.
type Provider interface {
ConfigFile() *configfile.ConfigFile
}
// Dir returns the directory the configuration file is stored in // Dir returns the directory the configuration file is stored in
func Dir() string { func Dir() string {
initConfigDir.Do(func() { initConfigDir.Do(func() {

View File

@ -0,0 +1,68 @@
package jsonstream
import (
"context"
"io"
"github.com/docker/docker/pkg/jsonmessage"
)
type (
Stream = jsonmessage.Stream
JSONMessage = jsonmessage.JSONMessage
JSONError = jsonmessage.JSONError
JSONProgress = jsonmessage.JSONProgress
)
type ctxReader struct {
err chan error
r io.Reader
}
func (r *ctxReader) Read(p []byte) (n int, err error) {
select {
case err = <-r.err:
return 0, err
default:
return r.r.Read(p)
}
}
type Options func(*options)
type options struct {
AuxCallback func(JSONMessage)
}
func WithAuxCallback(cb func(JSONMessage)) Options {
return func(o *options) {
o.AuxCallback = cb
}
}
// Display prints the JSON messages from the given reader to the given stream.
//
// It wraps the [jsonmessage.DisplayJSONMessagesStream] function to make it
// "context aware" and appropriately returns why the function was canceled.
//
// It returns an error if the context is canceled, but not if the input reader / stream is closed.
func Display(ctx context.Context, in io.Reader, stream Stream, opts ...Options) error {
if ctx.Err() != nil {
return ctx.Err()
}
reader := &ctxReader{err: make(chan error, 1), r: in}
stopFunc := context.AfterFunc(ctx, func() { reader.err <- ctx.Err() })
defer stopFunc()
o := options{}
for _, opt := range opts {
opt(&o)
}
if err := jsonmessage.DisplayJSONMessagesStream(reader, stream, stream.FD(), stream.IsTerminal(), o.AuxCallback); err != nil {
return err
}
return ctx.Err()
}

View File

@ -8,7 +8,6 @@ import (
"github.com/distribution/reference" "github.com/distribution/reference"
manifesttypes "github.com/docker/cli/cli/manifest/types" manifesttypes "github.com/docker/cli/cli/manifest/types"
"github.com/docker/cli/cli/trust"
"github.com/docker/distribution" "github.com/docker/distribution"
distributionclient "github.com/docker/distribution/registry/client" distributionclient "github.com/docker/distribution/registry/client"
registrytypes "github.com/docker/docker/api/types/registry" registrytypes "github.com/docker/docker/api/types/registry"
@ -38,12 +37,6 @@ func NewRegistryClient(resolver AuthConfigResolver, userAgent string, insecure b
// AuthConfigResolver returns Auth Configuration for an index // AuthConfigResolver returns Auth Configuration for an index
type AuthConfigResolver func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig type AuthConfigResolver func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig
// PutManifestOptions is the data sent to push a manifest
type PutManifestOptions struct {
MediaType string
Payload []byte
}
type client struct { type client struct {
authConfigResolver AuthConfigResolver authConfigResolver AuthConfigResolver
insecureRegistry bool insecureRegistry bool
@ -61,13 +54,13 @@ func (err ErrBlobCreated) Error() string {
err.From, err.Target) err.From, err.Target)
} }
// ErrHTTPProto returned if attempting to use TLS with a non-TLS registry // httpProtoError returned if attempting to use TLS with a non-TLS registry
type ErrHTTPProto struct { type httpProtoError struct {
OrigErr string cause error
} }
func (err ErrHTTPProto) Error() string { func (e httpProtoError) Error() string {
return err.OrigErr return e.cause.Error()
} }
var _ RegistryClient = &client{} var _ RegistryClient = &client{}
@ -78,7 +71,7 @@ func (c *client) MountBlob(ctx context.Context, sourceRef reference.Canonical, t
if err != nil { if err != nil {
return err return err
} }
repoEndpoint.actions = trust.ActionsPushAndPull repoEndpoint.actions = []string{"pull", "push"}
repo, err := c.getRepositoryForReference(ctx, targetRef, repoEndpoint) repo, err := c.getRepositoryForReference(ctx, targetRef, repoEndpoint)
if err != nil { if err != nil {
return err return err
@ -104,7 +97,7 @@ func (c *client) PutManifest(ctx context.Context, ref reference.Named, manifest
return "", err return "", err
} }
repoEndpoint.actions = trust.ActionsPushAndPull repoEndpoint.actions = []string{"pull", "push"}
repo, err := c.getRepositoryForReference(ctx, ref, repoEndpoint) repo, err := c.getRepositoryForReference(ctx, ref, repoEndpoint)
if err != nil { if err != nil {
return "", err return "", err
@ -121,7 +114,10 @@ func (c *client) PutManifest(ctx context.Context, ref reference.Named, manifest
} }
dgst, err := manifestService.Put(ctx, manifest, opts...) dgst, err := manifestService.Put(ctx, manifest, opts...)
return dgst, errors.Wrapf(err, "failed to put manifest %s", ref) if err != nil {
return dgst, errors.Wrapf(err, "failed to put manifest %s", ref)
}
return dgst, nil
} }
func (c *client) getRepositoryForReference(ctx context.Context, ref reference.Named, repoEndpoint repositoryEndpoint) (distribution.Repository, error) { func (c *client) getRepositoryForReference(ctx context.Context, ref reference.Named, repoEndpoint repositoryEndpoint) (distribution.Repository, error) {
@ -135,7 +131,7 @@ func (c *client) getRepositoryForReference(ctx context.Context, ref reference.Na
return nil, err return nil, err
} }
if !repoEndpoint.endpoint.TLSConfig.InsecureSkipVerify { if !repoEndpoint.endpoint.TLSConfig.InsecureSkipVerify {
return nil, ErrHTTPProto{OrigErr: err.Error()} return nil, httpProtoError{cause: err}
} }
// --insecure was set; fall back to plain HTTP // --insecure was set; fall back to plain HTTP
if url := repoEndpoint.endpoint.URL; url != nil && url.Scheme == "https" { if url := repoEndpoint.endpoint.URL; url != nil && url.Scheme == "https" {
@ -157,7 +153,10 @@ func (c *client) getHTTPTransportForRepoEndpoint(ctx context.Context, repoEndpoi
c.userAgent, c.userAgent,
repoEndpoint.actions, repoEndpoint.actions,
) )
return httpTransport, errors.Wrap(err, "failed to configure transport") if err != nil {
return nil, errors.Wrap(err, "failed to configure transport")
}
return httpTransport, nil
} }
// GetManifest returns an ImageManifest for the reference // GetManifest returns an ImageManifest for the reference

View File

@ -6,7 +6,6 @@ import (
"time" "time"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/docker/cli/cli/trust"
"github.com/docker/distribution/registry/client/auth" "github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/transport" "github.com/docker/distribution/registry/client/transport"
registrytypes "github.com/docker/docker/api/types/registry" registrytypes "github.com/docker/docker/api/types/registry"
@ -31,10 +30,7 @@ func (r repositoryEndpoint) BaseURL() string {
} }
func newDefaultRepositoryEndpoint(ref reference.Named, insecure bool) (repositoryEndpoint, error) { func newDefaultRepositoryEndpoint(ref reference.Named, insecure bool) (repositoryEndpoint, error) {
repoInfo, err := registry.ParseRepositoryInfo(ref) repoInfo, _ := registry.ParseRepositoryInfo(ref)
if err != nil {
return repositoryEndpoint{}, err
}
endpoint, err := getDefaultEndpointFromRepoInfo(repoInfo) endpoint, err := getDefaultEndpointFromRepoInfo(repoInfo)
if err != nil { if err != nil {
return repositoryEndpoint{}, err return repositoryEndpoint{}, err
@ -94,7 +90,7 @@ func getHTTPTransport(authConfig registrytypes.AuthConfig, endpoint registry.API
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler)) modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler))
} else { } else {
if len(actions) == 0 { if len(actions) == 0 {
actions = trust.ActionsPullOnly actions = []string{"pull"}
} }
creds := registry.NewStaticCredentialStore(&authConfig) creds := registry.NewStaticCredentialStore(&authConfig)
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...) tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...)
@ -104,14 +100,11 @@ func getHTTPTransport(authConfig registrytypes.AuthConfig, endpoint registry.API
return transport.NewTransport(base, modifiers...), nil return transport.NewTransport(base, modifiers...), nil
} }
// RepoNameForReference returns the repository name from a reference // RepoNameForReference returns the repository name from a reference.
//
// Deprecated: this function is no longer used and will be removed in the next release.
func RepoNameForReference(ref reference.Named) (string, error) { func RepoNameForReference(ref reference.Named) (string, error) {
// insecure is fine since this only returns the name return reference.Path(reference.TrimNamed(ref)), nil
repo, err := newDefaultRepositoryEndpoint(ref, false)
if err != nil {
return "", err
}
return repo.Name(), nil
} }
type existingTokenHandler struct { type existingTokenHandler struct {

View File

@ -220,10 +220,7 @@ func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named,
return err return err
} }
repoInfo, err := registry.ParseRepositoryInfo(namedRef) repoInfo, _ := registry.ParseRepositoryInfo(namedRef)
if err != nil {
return err
}
confirmedTLSRegistries := make(map[string]bool) confirmedTLSRegistries := make(map[string]bool)
for _, endpoint := range endpoints { for _, endpoint := range endpoints {
@ -241,7 +238,8 @@ func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named,
repo, err := c.getRepositoryForReference(ctx, namedRef, repoEndpoint) repo, err := c.getRepositoryForReference(ctx, namedRef, repoEndpoint)
if err != nil { if err != nil {
logrus.Debugf("error %s with repo endpoint %+v", err, repoEndpoint) logrus.Debugf("error %s with repo endpoint %+v", err, repoEndpoint)
if _, ok := err.(ErrHTTPProto); ok { var protoErr httpProtoError
if errors.As(err, &protoErr) {
continue continue
} }
return err return err
@ -272,11 +270,6 @@ func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named,
// allEndpoints returns a list of endpoints ordered by priority (v2, http). // allEndpoints returns a list of endpoints ordered by priority (v2, http).
func allEndpoints(namedRef reference.Named, insecure bool) ([]registry.APIEndpoint, error) { func allEndpoints(namedRef reference.Named, insecure bool) ([]registry.APIEndpoint, error) {
repoInfo, err := registry.ParseRepositoryInfo(namedRef)
if err != nil {
return nil, err
}
var serviceOpts registry.ServiceOptions var serviceOpts registry.ServiceOptions
if insecure { if insecure {
logrus.Debugf("allowing insecure registry for: %s", reference.Domain(namedRef)) logrus.Debugf("allowing insecure registry for: %s", reference.Domain(namedRef))
@ -286,6 +279,7 @@ func allEndpoints(namedRef reference.Named, insecure bool) ([]registry.APIEndpoi
if err != nil { if err != nil {
return []registry.APIEndpoint{}, err return []registry.APIEndpoint{}, err
} }
repoInfo, _ := registry.ParseRepositoryInfo(namedRef)
endpoints, err := registryService.LookupPullEndpoints(reference.Domain(repoInfo.Name)) endpoints, err := registryService.LookupPullEndpoints(reference.Domain(repoInfo.Name))
logrus.Debugf("endpoints for %s: %v", namedRef, endpoints) logrus.Debugf("endpoints for %s: %v", namedRef, endpoints)
return endpoints, err return endpoints, err

View File

@ -40,10 +40,11 @@ var (
ActionsPullOnly = []string{"pull"} ActionsPullOnly = []string{"pull"}
// ActionsPushAndPull defines the actions for read-write interactions with a Notary Repository // ActionsPushAndPull defines the actions for read-write interactions with a Notary Repository
ActionsPushAndPull = []string{"pull", "push"} ActionsPushAndPull = []string{"pull", "push"}
// NotaryServer is the endpoint serving the Notary trust server
NotaryServer = "https://notary.docker.io"
) )
// NotaryServer is the endpoint serving the Notary trust server
const NotaryServer = "https://notary.docker.io"
// GetTrustDirectory returns the base trust directory name // GetTrustDirectory returns the base trust directory name
func GetTrustDirectory() string { func GetTrustDirectory() string {
return filepath.Join(config.Dir(), "trust") return filepath.Join(config.Dir(), "trust")
@ -238,6 +239,20 @@ func NotaryError(repoName string, err error) error {
return err return err
} }
// AddToAllSignableRoles attempts to add the image target to all the top level
// delegation roles we can (based on whether we have the signing key and whether
// the role's path allows us to).
//
// If there are no delegation roles, we add to the targets role.
func AddToAllSignableRoles(repo client.Repository, target *client.Target) error {
signableRoles, err := GetSignableRoles(repo, target)
if err != nil {
return err
}
return repo.AddTarget(target, signableRoles...)
}
// GetSignableRoles returns a list of roles for which we have valid signing // GetSignableRoles returns a list of roles for which we have valid signing
// keys, given a notary repository and a target // keys, given a notary repository and a target
func GetSignableRoles(repo client.Repository, target *client.Target) ([]data.RoleName, error) { func GetSignableRoles(repo client.Repository, target *client.Target) ([]data.RoleName, error) {
@ -307,11 +322,7 @@ func GetImageReferencesAndAuth(ctx context.Context,
} }
// Resolve the Repository name from fqn to RepositoryInfo // Resolve the Repository name from fqn to RepositoryInfo
repoInfo, err := registry.ParseRepositoryInfo(ref) repoInfo, _ := registry.ParseRepositoryInfo(ref)
if err != nil {
return ImageRefAndAuth{}, err
}
authConfig := authResolver(ctx, repoInfo.Index) authConfig := authResolver(ctx, repoInfo.Index)
return ImageRefAndAuth{ return ImageRefAndAuth{
original: imgName, original: imgName,

143
vendor/github.com/docker/cli/cli/trust/trust_push.go generated vendored Normal file
View File

@ -0,0 +1,143 @@
package trust
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"sort"
"github.com/distribution/reference"
"github.com/docker/cli/cli/internal/jsonstream"
"github.com/docker/cli/cli/streams"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/registry"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/theupdateframework/notary/client"
"github.com/theupdateframework/notary/tuf/data"
)
// Streams is an interface which exposes the standard input and output streams.
//
// Same interface as [github.com/docker/cli/cli/command.Streams] but defined here to prevent a circular import.
type Streams interface {
In() *streams.In
Out() *streams.Out
Err() *streams.Out
}
// PushTrustedReference pushes a canonical reference to the trust server.
//
//nolint:gocyclo
func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader, userAgent string) error {
// If it is a trusted push we would like to find the target entry which match the
// tag provided in the function and then do an AddTarget later.
notaryTarget := &client.Target{}
// Count the times of calling for handleTarget,
// if it is called more that once, that should be considered an error in a trusted push.
cnt := 0
handleTarget := func(msg jsonstream.JSONMessage) {
cnt++
if cnt > 1 {
// handleTarget should only be called once. This will be treated as an error.
return
}
var pushResult types.PushResult
err := json.Unmarshal(*msg.Aux, &pushResult)
if err == nil && pushResult.Tag != "" {
if dgst, err := digest.Parse(pushResult.Digest); err == nil {
h, err := hex.DecodeString(dgst.Hex())
if err != nil {
notaryTarget = nil
return
}
notaryTarget.Name = pushResult.Tag
notaryTarget.Hashes = data.Hashes{string(dgst.Algorithm()): h}
notaryTarget.Length = int64(pushResult.Size)
}
}
}
var tag string
switch x := ref.(type) {
case reference.Canonical:
return errors.New("cannot push a digest reference")
case reference.NamedTagged:
tag = x.Tag()
default:
// We want trust signatures to always take an explicit tag,
// otherwise it will act as an untrusted push.
if err := jsonstream.Display(ctx, in, ioStreams.Out()); err != nil {
return err
}
_, _ = fmt.Fprintln(ioStreams.Err(), "No tag specified, skipping trust metadata push")
return nil
}
if err := jsonstream.Display(ctx, in, ioStreams.Out(), jsonstream.WithAuxCallback(handleTarget)); err != nil {
return err
}
if cnt > 1 {
return errors.Errorf("internal error: only one call to handleTarget expected")
}
if notaryTarget == nil {
return errors.Errorf("no targets found, provide a specific tag in order to sign it")
}
_, _ = fmt.Fprintln(ioStreams.Out(), "Signing and pushing trust metadata")
repo, err := GetNotaryRepository(ioStreams.In(), ioStreams.Out(), userAgent, repoInfo, &authConfig, "push", "pull")
if err != nil {
return errors.Wrap(err, "error establishing connection to trust repository")
}
// get the latest repository metadata so we can figure out which roles to sign
_, err = repo.ListTargets()
switch err.(type) {
case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
keys := repo.GetCryptoService().ListKeys(data.CanonicalRootRole)
var rootKeyID string
// always select the first root key
if len(keys) > 0 {
sort.Strings(keys)
rootKeyID = keys[0]
} else {
rootPublicKey, err := repo.GetCryptoService().Create(data.CanonicalRootRole, "", data.ECDSAKey)
if err != nil {
return err
}
rootKeyID = rootPublicKey.ID()
}
// Initialize the notary repository with a remotely managed snapshot key
if err := repo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil {
return NotaryError(repoInfo.Name.Name(), err)
}
_, _ = fmt.Fprintf(ioStreams.Out(), "Finished initializing %q\n", repoInfo.Name.Name())
err = repo.AddTarget(notaryTarget, data.CanonicalTargetsRole)
case nil:
// already initialized and we have successfully downloaded the latest metadata
err = AddToAllSignableRoles(repo, notaryTarget)
default:
return NotaryError(repoInfo.Name.Name(), err)
}
if err == nil {
err = repo.Publish()
}
if err != nil {
err = errors.Wrapf(err, "failed to sign %s:%s", repoInfo.Name.Name(), tag)
return NotaryError(repoInfo.Name.Name(), err)
}
_, _ = fmt.Fprintf(ioStreams.Out(), "Successfully signed %s:%s\n", repoInfo.Name.Name(), tag)
return nil
}

22
vendor/github.com/docker/cli/cli/trust/trust_tag.go generated vendored Normal file
View File

@ -0,0 +1,22 @@
package trust
import (
"context"
"fmt"
"io"
"github.com/distribution/reference"
"github.com/docker/docker/client"
)
// TagTrusted tags a trusted ref. It is a shallow wrapper around [client.Client.ImageTag]
// that updates the given image references to their familiar format for tagging
// and printing.
func TagTrusted(ctx context.Context, apiClient client.ImageAPIClient, out io.Writer, trustedRef reference.Canonical, ref reference.NamedTagged) error {
// Use familiar references when interacting with client and output
familiarRef := reference.FamiliarString(ref)
trustedFamiliarRef := reference.FamiliarString(trustedRef)
_, _ = fmt.Fprintf(out, "Tagging %s as %s\n", trustedFamiliarRef, familiarRef)
return apiClient.ImageTag(ctx, trustedFamiliarRef, familiarRef)
}

View File

@ -1,9 +1,8 @@
package opts package opts
import ( import (
"errors"
"time" "time"
"github.com/pkg/errors"
) )
// PositiveDurationOpt is an option type for time.Duration that uses a pointer. // PositiveDurationOpt is an option type for time.Duration that uses a pointer.
@ -20,7 +19,7 @@ func (d *PositiveDurationOpt) Set(s string) error {
return err return err
} }
if *d.DurationOpt.value < 0 { if *d.DurationOpt.value < 0 {
return errors.Errorf("duration cannot be negative") return errors.New("duration cannot be negative")
} }
return nil return nil
} }

View File

@ -1,10 +1,9 @@
package opts package opts
import ( import (
"errors"
"os" "os"
"strings" "strings"
"github.com/pkg/errors"
) )
// ValidateEnv validates an environment variable and returns it. // ValidateEnv validates an environment variable and returns it.

View File

@ -2,12 +2,12 @@ package opts
import ( import (
"encoding/csv" "encoding/csv"
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/pkg/errors"
) )
// GpuOpts is a Value type for parsing mounts // GpuOpts is a Value type for parsing mounts
@ -20,7 +20,14 @@ func parseCount(s string) (int, error) {
return -1, nil return -1, nil
} }
i, err := strconv.Atoi(s) i, err := strconv.Atoi(s)
return i, errors.Wrap(err, "count must be an integer") if err != nil {
var numErr *strconv.NumError
if errors.As(err, &numErr) {
err = numErr.Err
}
return 0, fmt.Errorf(`invalid count (%s): value must be either "all" or an integer: %w`, s, err)
}
return i, nil
} }
// Set a new mount value // Set a new mount value
@ -69,7 +76,7 @@ func (o *GpuOpts) Set(value string) error {
r := csv.NewReader(strings.NewReader(val)) r := csv.NewReader(strings.NewReader(val))
optFields, err := r.Read() optFields, err := r.Read()
if err != nil { if err != nil {
return errors.Wrap(err, "failed to read gpu options") return fmt.Errorf("failed to read gpu options: %w", err)
} }
req.Options = ConvertKVStringsToMap(optFields) req.Options = ConvertKVStringsToMap(optFields)
default: default:

View File

@ -135,8 +135,7 @@ func (m *MountOpt) Set(value string) error {
// TODO: implicitly set propagation and error if the user specifies a propagation in a future refactor/UX polish pass // TODO: implicitly set propagation and error if the user specifies a propagation in a future refactor/UX polish pass
// https://github.com/docker/cli/pull/4316#discussion_r1341974730 // https://github.com/docker/cli/pull/4316#discussion_r1341974730
default: default:
return fmt.Errorf("invalid value for %s: %s (must be \"enabled\", \"disabled\", \"writable\", or \"readonly\")", return fmt.Errorf(`invalid value for %s: %s (must be "enabled", "disabled", "writable", or "readonly")`, key, val)
key, val)
} }
case "volume-subpath": case "volume-subpath":
volumeOptions().Subpath = val volumeOptions().Subpath = val

View File

@ -89,7 +89,11 @@ func (n *NetworkOpt) Set(value string) error { //nolint:gocyclo
case gwPriorityOpt: case gwPriorityOpt:
netOpt.GwPriority, err = strconv.Atoi(val) netOpt.GwPriority, err = strconv.Atoi(val)
if err != nil { if err != nil {
return fmt.Errorf("invalid gw-priority: %w", err) var numErr *strconv.NumError
if errors.As(err, &numErr) {
err = numErr.Err
}
return fmt.Errorf("invalid gw-priority (%s): %w", val, err)
} }
default: default:
return errors.New("invalid field key " + key) return errors.New("invalid field key " + key)

View File

@ -1,6 +1,7 @@
package opts package opts
import ( import (
"errors"
"fmt" "fmt"
"math/big" "math/big"
"net" "net"
@ -9,8 +10,7 @@ import (
"strings" "strings"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
units "github.com/docker/go-units" "github.com/docker/go-units"
"github.com/pkg/errors"
) )
var ( var (

18
vendor/github.com/docker/cli/opts/opts_deprecated.go generated vendored Normal file
View File

@ -0,0 +1,18 @@
package opts
import "github.com/docker/cli/opts/swarmopts"
// PortOpt represents a port config in swarm mode.
//
// Deprecated: use [swarmopts.PortOpt]
type PortOpt = swarmopts.PortOpt
// ConfigOpt is a Value type for parsing configs.
//
// Deprecated: use [swarmopts.ConfigOpt]
type ConfigOpt = swarmopts.ConfigOpt
// SecretOpt is a Value type for parsing secrets
//
// Deprecated: use [swarmopts.SecretOpt]
type SecretOpt = swarmopts.SecretOpt

View File

@ -1,4 +1,4 @@
package opts package swarmopts
import ( import (
"encoding/csv" "encoding/csv"
@ -8,12 +8,12 @@ import (
"strconv" "strconv"
"strings" "strings"
swarmtypes "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
) )
// ConfigOpt is a Value type for parsing configs // ConfigOpt is a Value type for parsing configs
type ConfigOpt struct { type ConfigOpt struct {
values []*swarmtypes.ConfigReference values []*swarm.ConfigReference
} }
// Set a new config value // Set a new config value
@ -24,8 +24,8 @@ func (o *ConfigOpt) Set(value string) error {
return err return err
} }
options := &swarmtypes.ConfigReference{ options := &swarm.ConfigReference{
File: &swarmtypes.ConfigReferenceFileTarget{ File: &swarm.ConfigReferenceFileTarget{
UID: "0", UID: "0",
GID: "0", GID: "0",
Mode: 0o444, Mode: 0o444,
@ -95,6 +95,6 @@ func (o *ConfigOpt) String() string {
} }
// Value returns the config requests // Value returns the config requests
func (o *ConfigOpt) Value() []*swarmtypes.ConfigReference { func (o *ConfigOpt) Value() []*swarm.ConfigReference {
return o.values return o.values
} }

View File

@ -1,4 +1,4 @@
package opts package swarmopts
import ( import (
"encoding/csv" "encoding/csv"
@ -46,42 +46,50 @@ func (p *PortOpt) Set(value string) error {
// TODO(thaJeztah): these options should not be case-insensitive. // TODO(thaJeztah): these options should not be case-insensitive.
key, val, ok := strings.Cut(strings.ToLower(field), "=") key, val, ok := strings.Cut(strings.ToLower(field), "=")
if !ok || key == "" { if !ok || key == "" {
return fmt.Errorf("invalid field %s", field) return fmt.Errorf("invalid field: %s", field)
} }
switch key { switch key {
case portOptProtocol: case portOptProtocol:
if val != string(swarm.PortConfigProtocolTCP) && val != string(swarm.PortConfigProtocolUDP) && val != string(swarm.PortConfigProtocolSCTP) { if val != string(swarm.PortConfigProtocolTCP) && val != string(swarm.PortConfigProtocolUDP) && val != string(swarm.PortConfigProtocolSCTP) {
return fmt.Errorf("invalid protocol value %s", val) return fmt.Errorf("invalid protocol value '%s'", val)
} }
pConfig.Protocol = swarm.PortConfigProtocol(val) pConfig.Protocol = swarm.PortConfigProtocol(val)
case portOptMode: case portOptMode:
if val != string(swarm.PortConfigPublishModeIngress) && val != string(swarm.PortConfigPublishModeHost) { if val != string(swarm.PortConfigPublishModeIngress) && val != string(swarm.PortConfigPublishModeHost) {
return fmt.Errorf("invalid publish mode value %s", val) return fmt.Errorf("invalid publish mode value (%s): must be either '%s' or '%s'", val, swarm.PortConfigPublishModeIngress, swarm.PortConfigPublishModeHost)
} }
pConfig.PublishMode = swarm.PortConfigPublishMode(val) pConfig.PublishMode = swarm.PortConfigPublishMode(val)
case portOptTargetPort: case portOptTargetPort:
tPort, err := strconv.ParseUint(val, 10, 16) tPort, err := strconv.ParseUint(val, 10, 16)
if err != nil { if err != nil {
return err var numErr *strconv.NumError
if errors.As(err, &numErr) {
err = numErr.Err
}
return fmt.Errorf("invalid target port (%s): value must be an integer: %w", val, err)
} }
pConfig.TargetPort = uint32(tPort) pConfig.TargetPort = uint32(tPort)
case portOptPublishedPort: case portOptPublishedPort:
pPort, err := strconv.ParseUint(val, 10, 16) pPort, err := strconv.ParseUint(val, 10, 16)
if err != nil { if err != nil {
return err var numErr *strconv.NumError
if errors.As(err, &numErr) {
err = numErr.Err
}
return fmt.Errorf("invalid published port (%s): value must be an integer: %w", val, err)
} }
pConfig.PublishedPort = uint32(pPort) pConfig.PublishedPort = uint32(pPort)
default: default:
return fmt.Errorf("invalid field key %s", key) return fmt.Errorf("invalid field key: %s", key)
} }
} }
if pConfig.TargetPort == 0 { if pConfig.TargetPort == 0 {
return fmt.Errorf("missing mandatory field %q", portOptTargetPort) return fmt.Errorf("missing mandatory field '%s'", portOptTargetPort)
} }
if pConfig.PublishMode == "" { if pConfig.PublishMode == "" {

View File

@ -1,4 +1,4 @@
package opts package swarmopts
import ( import (
"encoding/csv" "encoding/csv"
@ -8,12 +8,12 @@ import (
"strconv" "strconv"
"strings" "strings"
swarmtypes "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
) )
// SecretOpt is a Value type for parsing secrets // SecretOpt is a Value type for parsing secrets
type SecretOpt struct { type SecretOpt struct {
values []*swarmtypes.SecretReference values []*swarm.SecretReference
} }
// Set a new secret value // Set a new secret value
@ -24,8 +24,8 @@ func (o *SecretOpt) Set(value string) error {
return err return err
} }
options := &swarmtypes.SecretReference{ options := &swarm.SecretReference{
File: &swarmtypes.SecretReferenceFileTarget{ File: &swarm.SecretReferenceFileTarget{
UID: "0", UID: "0",
GID: "0", GID: "0",
Mode: 0o444, Mode: 0o444,
@ -94,6 +94,6 @@ func (o *SecretOpt) String() string {
} }
// Value returns the secret requests // Value returns the secret requests
func (o *SecretOpt) Value() []*swarmtypes.SecretReference { func (o *SecretOpt) Value() []*swarm.SecretReference {
return o.values return o.values
} }

View File

@ -5508,8 +5508,11 @@ definitions:
com.example.some-other-label: "some-other-value" com.example.some-other-label: "some-other-value"
Data: Data:
description: | description: |
Base64-url-safe-encoded ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-5)) Data is the data to store as a secret, formatted as a Base64-url-safe-encoded
data to store as secret. ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-5)) string.
It must be empty if the Driver field is set, in which case the data is
loaded from an external secret store. The maximum allowed size is 500KB,
as defined in [MaxSecretSize](https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/api/validation#MaxSecretSize).
This field is only used to _create_ a secret, and is not returned by This field is only used to _create_ a secret, and is not returned by
other endpoints. other endpoints.
@ -5560,8 +5563,9 @@ definitions:
type: "string" type: "string"
Data: Data:
description: | description: |
Base64-url-safe-encoded ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-5)) Data is the data to store as a config, formatted as a Base64-url-safe-encoded
config data. ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-5)) string.
The maximum allowed size is 1000KB, as defined in [MaxConfigSize](https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/manager/controlapi#MaxConfigSize).
type: "string" type: "string"
Templating: Templating:
description: | description: |

View File

@ -49,15 +49,17 @@ func (ipnet *NetIPNet) MarshalJSON() ([]byte, error) {
} }
// UnmarshalJSON sets the IPNet from a byte array of JSON // UnmarshalJSON sets the IPNet from a byte array of JSON
func (ipnet *NetIPNet) UnmarshalJSON(b []byte) (err error) { func (ipnet *NetIPNet) UnmarshalJSON(b []byte) error {
var ipnetStr string var ipnetStr string
if err = json.Unmarshal(b, &ipnetStr); err == nil { if err := json.Unmarshal(b, &ipnetStr); err != nil {
var cidr *net.IPNet return err
if _, cidr, err = net.ParseCIDR(ipnetStr); err == nil {
*ipnet = NetIPNet(*cidr)
}
} }
return _, cidr, err := net.ParseCIDR(ipnetStr)
if err != nil {
return err
}
*ipnet = NetIPNet(*cidr)
return nil
} }
// IndexInfo contains information about a registry // IndexInfo contains information about a registry

View File

@ -12,6 +12,12 @@ type Config struct {
// ConfigSpec represents a config specification from a config in swarm // ConfigSpec represents a config specification from a config in swarm
type ConfigSpec struct { type ConfigSpec struct {
Annotations Annotations
// Data is the data to store as a config.
//
// The maximum allowed size is 1000KB, as defined in [MaxConfigSize].
//
// [MaxConfigSize]: https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/manager/controlapi#MaxConfigSize
Data []byte `json:",omitempty"` Data []byte `json:",omitempty"`
// Templating controls whether and how to evaluate the config payload as // Templating controls whether and how to evaluate the config payload as

View File

@ -12,8 +12,22 @@ type Secret struct {
// SecretSpec represents a secret specification from a secret in swarm // SecretSpec represents a secret specification from a secret in swarm
type SecretSpec struct { type SecretSpec struct {
Annotations Annotations
Data []byte `json:",omitempty"`
Driver *Driver `json:",omitempty"` // name of the secrets driver used to fetch the secret's value from an external secret store // Data is the data to store as a secret. It must be empty if a
// [Driver] is used, in which case the data is loaded from an external
// secret store. The maximum allowed size is 500KB, as defined in
// [MaxSecretSize].
//
// This field is only used to create the secret, and is not returned
// by other endpoints.
//
// [MaxSecretSize]: https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/api/validation#MaxSecretSize
Data []byte `json:",omitempty"`
// Driver is the name of the secrets driver used to fetch the secret's
// value from an external secret store. If not set, the default built-in
// store is used.
Driver *Driver `json:",omitempty"`
// Templating controls whether and how to evaluate the secret payload as // Templating controls whether and how to evaluate the secret payload as
// a template. If it is not set, no templating is used. // a template. If it is not set, no templating is used.

View File

@ -3,6 +3,7 @@ package client // import "github.com/docker/docker/client"
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"net/url" "net/url"
"path" "path"
"sort" "sort"
@ -54,6 +55,19 @@ func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config
// When using API under 1.42, the Linux daemon doesn't respect the ConsoleSize // When using API under 1.42, the Linux daemon doesn't respect the ConsoleSize
hostConfig.ConsoleSize = [2]uint{0, 0} hostConfig.ConsoleSize = [2]uint{0, 0}
} }
if versions.LessThan(cli.ClientVersion(), "1.44") {
for _, m := range hostConfig.Mounts {
if m.BindOptions != nil {
// ReadOnlyNonRecursive can be safely ignored when API < 1.44
if m.BindOptions.ReadOnlyForceRecursive {
return response, errors.New("bind-recursive=readonly requires API v1.44 or later")
}
if m.BindOptions.NonRecursive && versions.LessThan(cli.ClientVersion(), "1.40") {
return response, errors.New("bind-recursive=disabled requires API v1.40 or later")
}
}
}
}
hostConfig.CapAdd = normalizeCapabilities(hostConfig.CapAdd) hostConfig.CapAdd = normalizeCapabilities(hostConfig.CapAdd)
hostConfig.CapDrop = normalizeCapabilities(hostConfig.CapDrop) hostConfig.CapDrop = normalizeCapabilities(hostConfig.CapDrop)

View File

@ -37,6 +37,11 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec,
if err := validateServiceSpec(service); err != nil { if err := validateServiceSpec(service); err != nil {
return response, err return response, err
} }
if versions.LessThan(cli.version, "1.30") {
if err := validateAPIVersion(service, cli.version); err != nil {
return response, err
}
}
// ensure that the image is tagged // ensure that the image is tagged
var resolveWarning string var resolveWarning string
@ -191,3 +196,18 @@ func validateServiceSpec(s swarm.ServiceSpec) error {
} }
return nil return nil
} }
func validateAPIVersion(c swarm.ServiceSpec, apiVersion string) error {
for _, m := range c.TaskTemplate.ContainerSpec.Mounts {
if m.BindOptions != nil {
if m.BindOptions.NonRecursive && versions.LessThan(apiVersion, "1.40") {
return errors.Errorf("bind-recursive=disabled requires API v1.40 or later")
}
// ReadOnlyNonRecursive can be safely ignored when API < 1.44
if m.BindOptions.ReadOnlyForceRecursive && versions.LessThan(apiVersion, "1.44") {
return errors.Errorf("bind-recursive=readonly requires API v1.44 or later")
}
}
}
return nil
}

View File

@ -236,11 +236,9 @@ func (r *readCloserWrapper) Close() error {
return nil return nil
} }
var ( var bufioReader32KPool = &sync.Pool{
bufioReader32KPool = &sync.Pool{ New: func() interface{} { return bufio.NewReaderSize(nil, 32*1024) },
New: func() interface{} { return bufio.NewReaderSize(nil, 32*1024) }, }
}
)
type bufferedReader struct { type bufferedReader struct {
buf *bufio.Reader buf *bufio.Reader
@ -252,17 +250,17 @@ func newBufferedReader(r io.Reader) *bufferedReader {
return &bufferedReader{buf} return &bufferedReader{buf}
} }
func (r *bufferedReader) Read(p []byte) (n int, err error) { func (r *bufferedReader) Read(p []byte) (int, error) {
if r.buf == nil { if r.buf == nil {
return 0, io.EOF return 0, io.EOF
} }
n, err = r.buf.Read(p) n, err := r.buf.Read(p)
if err == io.EOF { if err == io.EOF {
r.buf.Reset(nil) r.buf.Reset(nil)
bufioReader32KPool.Put(r.buf) bufioReader32KPool.Put(r.buf)
r.buf = nil r.buf = nil
} }
return return n, err
} }
func (r *bufferedReader) Peek(n int) ([]byte, error) { func (r *bufferedReader) Peek(n int) ([]byte, error) {
@ -428,7 +426,7 @@ func ReplaceFileTarWrapper(inputTarStream io.ReadCloser, mods map[string]TarModi
pipeWriter.CloseWithError(err) pipeWriter.CloseWithError(err)
return return
} }
if _, err := copyWithBuffer(tarWriter, tarReader); err != nil { if err := copyWithBuffer(tarWriter, tarReader); err != nil {
pipeWriter.CloseWithError(err) pipeWriter.CloseWithError(err)
return return
} }
@ -731,7 +729,7 @@ func (ta *tarAppender) addTarFile(path, name string) error {
return err return err
} }
_, err = copyWithBuffer(ta.TarWriter, file) err = copyWithBuffer(ta.TarWriter, file)
file.Close() file.Close()
if err != nil { if err != nil {
return err return err
@ -778,11 +776,11 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o
if err != nil { if err != nil {
return err return err
} }
if _, err := copyWithBuffer(file, reader); err != nil { if err := copyWithBuffer(file, reader); err != nil {
file.Close() _ = file.Close()
return err return err
} }
file.Close() _ = file.Close()
case tar.TypeBlock, tar.TypeChar: case tar.TypeBlock, tar.TypeChar:
if inUserns { // cannot create devices in a userns if inUserns { // cannot create devices in a userns
@ -1438,7 +1436,7 @@ func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) {
if err := tw.WriteHeader(hdr); err != nil { if err := tw.WriteHeader(hdr); err != nil {
return err return err
} }
if _, err := copyWithBuffer(tw, srcF); err != nil { if err := copyWithBuffer(tw, srcF); err != nil {
return err return err
} }
return nil return nil

View File

@ -20,7 +20,7 @@ func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter {
type overlayWhiteoutConverter struct{} type overlayWhiteoutConverter struct{}
func (overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os.FileInfo) (wo *tar.Header, err error) { func (overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os.FileInfo) (wo *tar.Header, _ error) {
// convert whiteouts to AUFS format // convert whiteouts to AUFS format
if fi.Mode()&os.ModeCharDevice != 0 && hdr.Devmajor == 0 && hdr.Devminor == 0 { if fi.Mode()&os.ModeCharDevice != 0 && hdr.Devmajor == 0 && hdr.Devminor == 0 {
// we just rename the file and make it normal // we just rename the file and make it normal
@ -31,38 +31,41 @@ func (overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os
hdr.Size = 0 hdr.Size = 0
} }
if fi.Mode()&os.ModeDir != 0 { if fi.Mode()&os.ModeDir == 0 {
opaqueXattrName := "trusted.overlay.opaque" // FIXME(thaJeztah): return a sentinel error instead of nil, nil
if userns.RunningInUserNS() { return nil, nil
opaqueXattrName = "user.overlay.opaque"
}
// convert opaque dirs to AUFS format by writing an empty file with the prefix
opaque, err := lgetxattr(path, opaqueXattrName)
if err != nil {
return nil, err
}
if len(opaque) == 1 && opaque[0] == 'y' {
delete(hdr.PAXRecords, paxSchilyXattr+opaqueXattrName)
// create a header for the whiteout file
// it should inherit some properties from the parent, but be a regular file
wo = &tar.Header{
Typeflag: tar.TypeReg,
Mode: hdr.Mode & int64(os.ModePerm),
Name: filepath.Join(hdr.Name, WhiteoutOpaqueDir), // #nosec G305 -- An archive is being created, not extracted.
Size: 0,
Uid: hdr.Uid,
Uname: hdr.Uname,
Gid: hdr.Gid,
Gname: hdr.Gname,
AccessTime: hdr.AccessTime,
ChangeTime: hdr.ChangeTime,
}
}
} }
return opaqueXattrName := "trusted.overlay.opaque"
if userns.RunningInUserNS() {
opaqueXattrName = "user.overlay.opaque"
}
// convert opaque dirs to AUFS format by writing an empty file with the prefix
opaque, err := lgetxattr(path, opaqueXattrName)
if err != nil {
return nil, err
}
if len(opaque) != 1 || opaque[0] != 'y' {
// FIXME(thaJeztah): return a sentinel error instead of nil, nil
return nil, nil
}
delete(hdr.PAXRecords, paxSchilyXattr+opaqueXattrName)
// create a header for the whiteout file
// it should inherit some properties from the parent, but be a regular file
return &tar.Header{
Typeflag: tar.TypeReg,
Mode: hdr.Mode & int64(os.ModePerm),
Name: filepath.Join(hdr.Name, WhiteoutOpaqueDir), // #nosec G305 -- An archive is being created, not extracted.
Size: 0,
Uid: hdr.Uid,
Uname: hdr.Uname,
Gid: hdr.Gid,
Gname: hdr.Gname,
AccessTime: hdr.AccessTime,
ChangeTime: hdr.ChangeTime,
}, nil
} }
func (c overlayWhiteoutConverter) ConvertRead(hdr *tar.Header, path string) (bool, error) { func (c overlayWhiteoutConverter) ConvertRead(hdr *tar.Header, path string) (bool, error) {

View File

@ -73,14 +73,13 @@ func statUnix(fi os.FileInfo, hdr *tar.Header) error {
return nil return nil
} }
func getInodeFromStat(stat interface{}) (inode uint64, err error) { func getInodeFromStat(stat interface{}) (uint64, error) {
s, ok := stat.(*syscall.Stat_t) s, ok := stat.(*syscall.Stat_t)
if !ok {
if ok { // FIXME(thaJeztah): this should likely return an error; see https://github.com/moby/moby/pull/49493#discussion_r1979152897
inode = s.Ino return 0, nil
} }
return s.Ino, nil
return
} }
func getFileUIDGID(stat interface{}) (idtools.Identity, error) { func getFileUIDGID(stat interface{}) (idtools.Identity, error) {

View File

@ -48,9 +48,9 @@ func setHeaderForSpecialDevice(hdr *tar.Header, name string, stat interface{}) (
return return
} }
func getInodeFromStat(stat interface{}) (inode uint64, err error) { func getInodeFromStat(stat interface{}) (uint64, error) {
// do nothing. no notion of Inode in stat on Windows // do nothing. no notion of Inode in stat on Windows
return return 0, nil
} }
// handleTarTypeBlockCharFifo is an OS-specific helper function used by // handleTarTypeBlockCharFifo is an OS-specific helper function used by

View File

@ -83,7 +83,7 @@ func aufsMetadataSkip(path string) (skip bool, err error) {
if err != nil { if err != nil {
skip = true skip = true
} }
return return skip, err
} }
func aufsDeletedFile(root, path string, fi os.FileInfo) (string, error) { func aufsDeletedFile(root, path string, fi os.FileInfo) (string, error) {

View File

@ -25,11 +25,11 @@ var copyPool = sync.Pool{
New: func() interface{} { s := make([]byte, 32*1024); return &s }, New: func() interface{} { s := make([]byte, 32*1024); return &s },
} }
func copyWithBuffer(dst io.Writer, src io.Reader) (written int64, err error) { func copyWithBuffer(dst io.Writer, src io.Reader) error {
buf := copyPool.Get().(*[]byte) buf := copyPool.Get().(*[]byte)
written, err = io.CopyBuffer(dst, src, *buf) _, err := io.CopyBuffer(dst, src, *buf)
copyPool.Put(buf) copyPool.Put(buf)
return return err
} }
// PreserveTrailingDotOrSeparator returns the given cleaned path (after // PreserveTrailingDotOrSeparator returns the given cleaned path (after
@ -105,13 +105,13 @@ func TarResource(sourceInfo CopyInfo) (content io.ReadCloser, err error) {
// TarResourceRebase is like TarResource but renames the first path element of // TarResourceRebase is like TarResource but renames the first path element of
// items in the resulting tar archive to match the given rebaseName if not "". // items in the resulting tar archive to match the given rebaseName if not "".
func TarResourceRebase(sourcePath, rebaseName string) (content io.ReadCloser, err error) { func TarResourceRebase(sourcePath, rebaseName string) (content io.ReadCloser, _ error) {
sourcePath = normalizePath(sourcePath) sourcePath = normalizePath(sourcePath)
if _, err = os.Lstat(sourcePath); err != nil { if _, err := os.Lstat(sourcePath); err != nil {
// Catches the case where the source does not exist or is not a // Catches the case where the source does not exist or is not a
// directory if asserted to be a directory, as this also causes an // directory if asserted to be a directory, as this also causes an
// error. // error.
return return nil, err
} }
// Separate the source path between its directory and // Separate the source path between its directory and
@ -442,11 +442,12 @@ func CopyTo(content io.Reader, srcInfo CopyInfo, dstPath string) error {
// whether to follow symbol link or not, if followLink is true, resolvedPath will return // whether to follow symbol link or not, if followLink is true, resolvedPath will return
// link target of any symbol link file, else it will only resolve symlink of directory // link target of any symbol link file, else it will only resolve symlink of directory
// but return symbol link file itself without resolving. // but return symbol link file itself without resolving.
func ResolveHostSourcePath(path string, followLink bool) (resolvedPath, rebaseName string, err error) { func ResolveHostSourcePath(path string, followLink bool) (resolvedPath, rebaseName string, _ error) {
if followLink { if followLink {
var err error
resolvedPath, err = filepath.EvalSymlinks(path) resolvedPath, err = filepath.EvalSymlinks(path)
if err != nil { if err != nil {
return return "", "", err
} }
resolvedPath, rebaseName = GetRebaseName(path, resolvedPath) resolvedPath, rebaseName = GetRebaseName(path, resolvedPath)
@ -454,10 +455,9 @@ func ResolveHostSourcePath(path string, followLink bool) (resolvedPath, rebaseNa
dirPath, basePath := filepath.Split(path) dirPath, basePath := filepath.Split(path)
// if not follow symbol link, then resolve symbol link of parent dir // if not follow symbol link, then resolve symbol link of parent dir
var resolvedDirPath string resolvedDirPath, err := filepath.EvalSymlinks(dirPath)
resolvedDirPath, err = filepath.EvalSymlinks(dirPath)
if err != nil { if err != nil {
return return "", "", err
} }
// resolvedDirPath will have been cleaned (no trailing path separators) so // resolvedDirPath will have been cleaned (no trailing path separators) so
// we can manually join it with the base path element. // we can manually join it with the base path element.

View File

@ -17,12 +17,13 @@ func chtimes(name string, atime time.Time, mtime time.Time) error {
return os.Chtimes(name, atime, mtime) return os.Chtimes(name, atime, mtime)
} }
func timeToTimespec(time time.Time) (ts unix.Timespec) { func timeToTimespec(time time.Time) unix.Timespec {
if time.IsZero() { if time.IsZero() {
// Return UTIME_OMIT special value // Return UTIME_OMIT special value
ts.Sec = 0 return unix.Timespec{
ts.Nsec = (1 << 30) - 2 Sec: 0,
return Nsec: (1 << 30) - 2,
}
} }
return unix.NsecToTimespec(time.UnixNano()) return unix.NsecToTimespec(time.UnixNano())
} }

View File

@ -45,8 +45,8 @@ func Generate(input ...string) (io.Reader, error) {
return buf, nil return buf, nil
} }
func parseStringPairs(input ...string) (output [][2]string) { func parseStringPairs(input ...string) [][2]string {
output = make([][2]string, 0, len(input)/2+1) output := make([][2]string, 0, len(input)/2+1)
for i := 0; i < len(input); i += 2 { for i := 0; i < len(input); i += 2 {
var pair [2]string var pair [2]string
pair[0] = input[i] pair[0] = input[i]
@ -55,5 +55,5 @@ func parseStringPairs(input ...string) (output [][2]string) {
} }
output = append(output, pair) output = append(output, pair)
} }
return return output
} }

View File

@ -11,12 +11,12 @@ import (
// destination path. Writing and closing concurrently is not allowed. // destination path. Writing and closing concurrently is not allowed.
// NOTE: umask is not considered for the file's permissions. // NOTE: umask is not considered for the file's permissions.
func New(filename string, perm os.FileMode) (io.WriteCloser, error) { func New(filename string, perm os.FileMode) (io.WriteCloser, error) {
f, err := os.CreateTemp(filepath.Dir(filename), ".tmp-"+filepath.Base(filename)) abspath, err := filepath.Abs(filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
abspath, err := filepath.Abs(filename) f, err := os.CreateTemp(filepath.Dir(abspath), ".tmp-"+filepath.Base(filename))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -43,9 +43,9 @@ type stdWriter struct {
// It inserts the prefix header before the buffer, // It inserts the prefix header before the buffer,
// so stdcopy.StdCopy knows where to multiplex the output. // so stdcopy.StdCopy knows where to multiplex the output.
// It makes stdWriter to implement io.Writer. // It makes stdWriter to implement io.Writer.
func (w *stdWriter) Write(p []byte) (n int, err error) { func (w *stdWriter) Write(p []byte) (int, error) {
if w == nil || w.Writer == nil { if w == nil || w.Writer == nil {
return 0, errors.New("Writer not instantiated") return 0, errors.New("writer not instantiated")
} }
if p == nil { if p == nil {
return 0, nil return 0, nil
@ -57,7 +57,7 @@ func (w *stdWriter) Write(p []byte) (n int, err error) {
buf.Write(header[:]) buf.Write(header[:])
buf.Write(p) buf.Write(p)
n, err = w.Writer.Write(buf.Bytes()) n, err := w.Writer.Write(buf.Bytes())
n -= stdWriterPrefixLen n -= stdWriterPrefixLen
if n < 0 { if n < 0 {
n = 0 n = 0
@ -65,7 +65,7 @@ func (w *stdWriter) Write(p []byte) (n int, err error) {
buf.Reset() buf.Reset()
bufPool.Put(buf) bufPool.Put(buf)
return return n, err
} }
// NewStdWriter instantiates a new Writer. // NewStdWriter instantiates a new Writer.

View File

@ -66,13 +66,13 @@ func (scs staticCredentialStore) SetRefreshToken(*url.URL, string, string) {
// loginV2 tries to login to the v2 registry server. The given registry // loginV2 tries to login to the v2 registry server. The given registry
// endpoint will be pinged to get authorization challenges. These challenges // endpoint will be pinged to get authorization challenges. These challenges
// will be used to authenticate against the registry to validate credentials. // will be used to authenticate against the registry to validate credentials.
func loginV2(authConfig *registry.AuthConfig, endpoint APIEndpoint, userAgent string) (status string, token string, _ error) { func loginV2(authConfig *registry.AuthConfig, endpoint APIEndpoint, userAgent string) (token string, _ error) {
endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/" endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
log.G(context.TODO()).Debugf("attempting v2 login to registry endpoint %s", endpointStr) log.G(context.TODO()).Debugf("attempting v2 login to registry endpoint %s", endpointStr)
req, err := http.NewRequest(http.MethodGet, endpointStr, nil) req, err := http.NewRequest(http.MethodGet, endpointStr, nil)
if err != nil { if err != nil {
return "", "", err return "", err
} }
var ( var (
@ -84,22 +84,22 @@ func loginV2(authConfig *registry.AuthConfig, endpoint APIEndpoint, userAgent st
loginClient, err := v2AuthHTTPClient(endpoint.URL, authTrans, modifiers, creds, nil) loginClient, err := v2AuthHTTPClient(endpoint.URL, authTrans, modifiers, creds, nil)
if err != nil { if err != nil {
return "", "", err return "", err
} }
resp, err := loginClient.Do(req) resp, err := loginClient.Do(req)
if err != nil { if err != nil {
err = translateV2AuthError(err) err = translateV2AuthError(err)
return "", "", err return "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode == http.StatusOK { if resp.StatusCode != http.StatusOK {
return "Login Succeeded", credentialAuthConfig.IdentityToken, nil // TODO(dmcgowan): Attempt to further interpret result, status code and error code string
return "", errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
} }
// TODO(dmcgowan): Attempt to further interpret result, status code and error code string return credentialAuthConfig.IdentityToken, nil
return "", "", errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
} }
func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, error) { func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, error) {

View File

@ -4,13 +4,17 @@ import (
"context" "context"
"net" "net"
"net/url" "net/url"
"os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync"
"github.com/containerd/log" "github.com/containerd/log"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/internal/lazyregexp" "github.com/docker/docker/internal/lazyregexp"
"github.com/docker/docker/pkg/homedir"
) )
// ServiceOptions holds command line options. // ServiceOptions holds command line options.
@ -56,26 +60,52 @@ var (
Host: DefaultRegistryHost, Host: DefaultRegistryHost,
} }
emptyServiceConfig, _ = newServiceConfig(ServiceOptions{}) validHostPortRegex = lazyregexp.New(`^` + reference.DomainRegexp.String() + `$`)
validHostPortRegex = lazyregexp.New(`^` + reference.DomainRegexp.String() + `$`)
// certsDir is used to override defaultCertsDir. // certsDir is used to override defaultCertsDir when running with rootlessKit.
certsDir string //
// TODO(thaJeztah): change to a sync.OnceValue once we remove [SetCertsDir]
// TODO(thaJeztah): certsDir should not be a package variable, but stored in our config, and passed when needed.
setCertsDirOnce sync.Once
certsDir string
) )
func setCertsDir(dir string) string {
setCertsDirOnce.Do(func() {
if dir != "" {
certsDir = dir
return
}
if os.Getenv("ROOTLESSKIT_STATE_DIR") != "" {
// Configure registry.CertsDir() when running in rootless-mode
// This is the equivalent of [rootless.RunningWithRootlessKit],
// but inlining it to prevent adding that as a dependency
// for docker/cli.
//
// [rootless.RunningWithRootlessKit]: https://github.com/moby/moby/blob/b4bdf12daec84caaf809a639f923f7370d4926ad/pkg/rootless/rootless.go#L5-L8
if configHome, _ := homedir.GetConfigHome(); configHome != "" {
certsDir = filepath.Join(configHome, "docker/certs.d")
return
}
}
certsDir = defaultCertsDir
})
return certsDir
}
// SetCertsDir allows the default certs directory to be changed. This function // SetCertsDir allows the default certs directory to be changed. This function
// is used at daemon startup to set the correct location when running in // is used at daemon startup to set the correct location when running in
// rootless mode. // rootless mode.
//
// Deprecated: the cert-directory is now automatically selected when running with rootlessKit, and should no longer be set manually.
func SetCertsDir(path string) { func SetCertsDir(path string) {
certsDir = path setCertsDir(path)
} }
// CertsDir is the directory where certificates are stored. // CertsDir is the directory where certificates are stored.
func CertsDir() string { func CertsDir() string {
if certsDir != "" { // call setCertsDir with an empty path to synchronise with [SetCertsDir]
return certsDir return setCertsDir("")
}
return defaultCertsDir
} }
// newServiceConfig returns a new instance of ServiceConfig // newServiceConfig returns a new instance of ServiceConfig
@ -181,7 +211,7 @@ skip:
// Assume `host:port` if not CIDR. // Assume `host:port` if not CIDR.
indexConfigs[r] = &registry.IndexInfo{ indexConfigs[r] = &registry.IndexInfo{
Name: r, Name: r,
Mirrors: make([]string, 0), Mirrors: []string{},
Secure: false, Secure: false,
Official: false, Official: false,
} }
@ -288,16 +318,22 @@ func ValidateMirror(val string) (string, error) {
// ValidateIndexName validates an index name. It is used by the daemon to // ValidateIndexName validates an index name. It is used by the daemon to
// validate the daemon configuration. // validate the daemon configuration.
func ValidateIndexName(val string) (string, error) { func ValidateIndexName(val string) (string, error) {
// TODO: upstream this to check to reference package val = normalizeIndexName(val)
if val == "index.docker.io" {
val = "docker.io"
}
if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") { if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") {
return "", invalidParamf("invalid index name (%s). Cannot begin or end with a hyphen", val) return "", invalidParamf("invalid index name (%s). Cannot begin or end with a hyphen", val)
} }
return val, nil return val, nil
} }
func normalizeIndexName(val string) string {
// TODO(thaJeztah): consider normalizing other known options, such as "(https://)registry-1.docker.io", "https://index.docker.io/v1/".
// TODO: upstream this to check to reference package
if val == "index.docker.io" {
return "docker.io"
}
return val
}
func hasScheme(reposName string) bool { func hasScheme(reposName string) bool {
return strings.Contains(reposName, "://") return strings.Contains(reposName, "://")
} }
@ -327,25 +363,20 @@ func validateHostPort(s string) error {
} }
// newIndexInfo returns IndexInfo configuration from indexName // newIndexInfo returns IndexInfo configuration from indexName
func newIndexInfo(config *serviceConfig, indexName string) (*registry.IndexInfo, error) { func newIndexInfo(config *serviceConfig, indexName string) *registry.IndexInfo {
var err error indexName = normalizeIndexName(indexName)
indexName, err = ValidateIndexName(indexName)
if err != nil {
return nil, err
}
// Return any configured index info, first. // Return any configured index info, first.
if index, ok := config.IndexConfigs[indexName]; ok { if index, ok := config.IndexConfigs[indexName]; ok {
return index, nil return index
} }
// Construct a non-configured index info. // Construct a non-configured index info.
return &registry.IndexInfo{ return &registry.IndexInfo{
Name: indexName, Name: indexName,
Mirrors: make([]string, 0), Mirrors: []string{},
Secure: config.isSecureIndex(indexName), Secure: config.isSecureIndex(indexName),
Official: false, }
}, nil
} }
// GetAuthConfigKey special-cases using the full index address of the official // GetAuthConfigKey special-cases using the full index address of the official
@ -358,18 +389,22 @@ func GetAuthConfigKey(index *registry.IndexInfo) string {
} }
// newRepositoryInfo validates and breaks down a repository name into a RepositoryInfo // newRepositoryInfo validates and breaks down a repository name into a RepositoryInfo
func newRepositoryInfo(config *serviceConfig, name reference.Named) (*RepositoryInfo, error) { func newRepositoryInfo(config *serviceConfig, name reference.Named) *RepositoryInfo {
index, err := newIndexInfo(config, reference.Domain(name)) index := newIndexInfo(config, reference.Domain(name))
if err != nil { var officialRepo bool
return nil, err if index.Official {
// RepositoryInfo.Official indicates whether the image repository
// is an official (docker library official images) repository.
//
// We only need to check this if the image-repository is on Docker Hub.
officialRepo = !strings.ContainsRune(reference.FamiliarName(name), '/')
} }
official := !strings.ContainsRune(reference.FamiliarName(name), '/')
return &RepositoryInfo{ return &RepositoryInfo{
Name: reference.TrimNamed(name), Name: reference.TrimNamed(name),
Index: index, Index: index,
Official: official, Official: officialRepo,
}, nil }
} }
// ParseRepositoryInfo performs the breakdown of a repository name into a // ParseRepositoryInfo performs the breakdown of a repository name into a
@ -377,5 +412,70 @@ func newRepositoryInfo(config *serviceConfig, name reference.Named) (*Repository
// //
// It is used by the Docker cli to interact with registry-related endpoints. // It is used by the Docker cli to interact with registry-related endpoints.
func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) { func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
return newRepositoryInfo(emptyServiceConfig, reposName) indexName := normalizeIndexName(reference.Domain(reposName))
if indexName == IndexName {
officialRepo := !strings.ContainsRune(reference.FamiliarName(reposName), '/')
return &RepositoryInfo{
Name: reference.TrimNamed(reposName),
Index: &registry.IndexInfo{
Name: IndexName,
Mirrors: []string{},
Secure: true,
Official: true,
},
Official: officialRepo,
}, nil
}
insecure := false
if isInsecure(indexName) {
insecure = true
}
return &RepositoryInfo{
Name: reference.TrimNamed(reposName),
Index: &registry.IndexInfo{
Name: indexName,
Mirrors: []string{},
Secure: !insecure,
},
}, nil
}
// isInsecure is used to detect whether a registry domain or IP-address is allowed
// to use an insecure (non-TLS, or self-signed cert) connection according to the
// defaults, which allows for insecure connections with registries running on a
// loopback address ("localhost", "::1/128", "127.0.0.0/8").
//
// It is used in situations where we don't have access to the daemon's configuration,
// for example, when used from the client / CLI.
func isInsecure(hostNameOrIP string) bool {
// Attempt to strip port if present; this also strips brackets for
// IPv6 addresses with a port (e.g. "[::1]:5000").
//
// This is best-effort; we'll continue using the address as-is if it fails.
if host, _, err := net.SplitHostPort(hostNameOrIP); err == nil {
hostNameOrIP = host
}
if hostNameOrIP == "127.0.0.1" || hostNameOrIP == "::1" || strings.EqualFold(hostNameOrIP, "localhost") {
// Fast path; no need to resolve these, assuming nobody overrides
// "localhost" for anything else than a loopback address (sorry, not sorry).
return true
}
var addresses []net.IP
if ip := net.ParseIP(hostNameOrIP); ip != nil {
addresses = append(addresses, ip)
} else {
// Try to resolve the host's IP-addresses.
addrs, _ := lookupIP(hostNameOrIP)
addresses = append(addresses, addrs...)
}
for _, addr := range addresses {
if addr.IsLoopback() {
return true
}
}
return false
} }

View File

@ -8,7 +8,6 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/containerd/log" "github.com/containerd/log"
@ -18,7 +17,14 @@ import (
) )
// HostCertsDir returns the config directory for a specific host. // HostCertsDir returns the config directory for a specific host.
//
// Deprecated: this function was only used internally, and will be removed in a future release.
func HostCertsDir(hostname string) string { func HostCertsDir(hostname string) string {
return hostCertsDir(hostname)
}
// hostCertsDir returns the config directory for a specific host.
func hostCertsDir(hostname string) string {
return filepath.Join(CertsDir(), cleanPath(hostname)) return filepath.Join(CertsDir(), cleanPath(hostname))
} }
@ -26,11 +32,10 @@ func HostCertsDir(hostname string) string {
func newTLSConfig(hostname string, isSecure bool) (*tls.Config, error) { func newTLSConfig(hostname string, isSecure bool) (*tls.Config, error) {
// PreferredServerCipherSuites should have no effect // PreferredServerCipherSuites should have no effect
tlsConfig := tlsconfig.ServerDefault() tlsConfig := tlsconfig.ServerDefault()
tlsConfig.InsecureSkipVerify = !isSecure tlsConfig.InsecureSkipVerify = !isSecure
if isSecure && CertsDir() != "" { if isSecure {
hostDir := HostCertsDir(hostname) hostDir := hostCertsDir(hostname)
log.G(context.TODO()).Debugf("hostDir: %s", hostDir) log.G(context.TODO()).Debugf("hostDir: %s", hostDir)
if err := ReadCertsDirectory(tlsConfig, hostDir); err != nil { if err := ReadCertsDirectory(tlsConfig, hostDir); err != nil {
return nil, err return nil, err
@ -59,7 +64,8 @@ func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
} }
for _, f := range fs { for _, f := range fs {
if strings.HasSuffix(f.Name(), ".crt") { switch filepath.Ext(f.Name()) {
case ".crt":
if tlsConfig.RootCAs == nil { if tlsConfig.RootCAs == nil {
systemPool, err := tlsconfig.SystemCertPool() systemPool, err := tlsconfig.SystemCertPool()
if err != nil { if err != nil {
@ -67,17 +73,17 @@ func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
} }
tlsConfig.RootCAs = systemPool tlsConfig.RootCAs = systemPool
} }
log.G(context.TODO()).Debugf("crt: %s", filepath.Join(directory, f.Name())) fileName := filepath.Join(directory, f.Name())
data, err := os.ReadFile(filepath.Join(directory, f.Name())) log.G(context.TODO()).Debugf("crt: %s", fileName)
data, err := os.ReadFile(fileName)
if err != nil { if err != nil {
return err return err
} }
tlsConfig.RootCAs.AppendCertsFromPEM(data) tlsConfig.RootCAs.AppendCertsFromPEM(data)
} case ".cert":
if strings.HasSuffix(f.Name(), ".cert") {
certName := f.Name() certName := f.Name()
keyName := certName[:len(certName)-5] + ".key" keyName := certName[:len(certName)-5] + ".key"
log.G(context.TODO()).Debugf("cert: %s", filepath.Join(directory, f.Name())) log.G(context.TODO()).Debugf("cert: %s", filepath.Join(directory, certName))
if !hasFile(fs, keyName) { if !hasFile(fs, keyName) {
return invalidParamf("missing key %s for client certificate %s. CA certificates must use the extension .crt", keyName, certName) return invalidParamf("missing key %s for client certificate %s. CA certificates must use the extension .crt", keyName, certName)
} }
@ -86,11 +92,10 @@ func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
return err return err
} }
tlsConfig.Certificates = append(tlsConfig.Certificates, cert) tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
} case ".key":
if strings.HasSuffix(f.Name(), ".key") {
keyName := f.Name() keyName := f.Name()
certName := keyName[:len(keyName)-4] + ".cert" certName := keyName[:len(keyName)-4] + ".cert"
log.G(context.TODO()).Debugf("key: %s", filepath.Join(directory, f.Name())) log.G(context.TODO()).Debugf("key: %s", filepath.Join(directory, keyName))
if !hasFile(fs, certName) { if !hasFile(fs, certName) {
return invalidParamf("missing client certificate %s for key %s", certName, keyName) return invalidParamf("missing client certificate %s for key %s", certName, keyName)
} }

View File

@ -93,12 +93,8 @@ func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int,
// Search is a long-running operation, just lock s.config to avoid block others. // Search is a long-running operation, just lock s.config to avoid block others.
s.mu.RLock() s.mu.RLock()
index, err := newIndexInfo(s.config, indexName) index := newIndexInfo(s.config, indexName)
s.mu.RUnlock() s.mu.RUnlock()
if err != nil {
return nil, err
}
if index.Official { if index.Official {
// If pull "library/foo", it's stored locally under "foo" // If pull "library/foo", it's stored locally under "foo"
remoteName = strings.TrimPrefix(remoteName, "library/") remoteName = strings.TrimPrefix(remoteName, "library/")
@ -158,5 +154,24 @@ func splitReposSearchTerm(reposName string) (string, string) {
// for that. // for that.
func ParseSearchIndexInfo(reposName string) (*registry.IndexInfo, error) { func ParseSearchIndexInfo(reposName string) (*registry.IndexInfo, error) {
indexName, _ := splitReposSearchTerm(reposName) indexName, _ := splitReposSearchTerm(reposName)
return newIndexInfo(emptyServiceConfig, indexName) indexName = normalizeIndexName(indexName)
if indexName == IndexName {
return &registry.IndexInfo{
Name: IndexName,
Mirrors: []string{},
Secure: true,
Official: true,
}, nil
}
insecure := false
if isInsecure(indexName) {
insecure = true
}
return &registry.IndexInfo{
Name: indexName,
Mirrors: []string{},
Secure: !insecure,
}, nil
} }

View File

@ -83,12 +83,12 @@ type onEOFReader struct {
Fn func() Fn func()
} }
func (r *onEOFReader) Read(p []byte) (n int, err error) { func (r *onEOFReader) Read(p []byte) (int, error) {
n, err = r.Rc.Read(p) n, err := r.Rc.Read(p)
if err == io.EOF { if err == io.EOF {
r.runFunc() r.runFunc()
} }
return return n, err
} }
// Close closes the file and run the function. // Close closes the file and run the function.

View File

@ -52,7 +52,7 @@ func (s *Service) ReplaceConfig(options ServiceOptions) (commit func(), err erro
// Auth contacts the public registry with the provided credentials, // Auth contacts the public registry with the provided credentials,
// and returns OK if authentication was successful. // and returns OK if authentication was successful.
// It can be used to verify the validity of a client's credentials. // It can be used to verify the validity of a client's credentials.
func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (status, token string, err error) { func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (statusMessage, token string, _ error) {
// TODO Use ctx when searching for repositories // TODO Use ctx when searching for repositories
registryHostName := IndexHostname registryHostName := IndexHostname
@ -77,19 +77,28 @@ func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, use
return "", "", invalidParam(err) return "", "", invalidParam(err)
} }
var lastErr error
for _, endpoint := range endpoints { for _, endpoint := range endpoints {
status, token, err = loginV2(authConfig, endpoint, userAgent) authToken, err := loginV2(authConfig, endpoint, userAgent)
if err == nil { if err != nil {
return if errdefs.IsUnauthorized(err) {
// Failed to authenticate; don't continue with (non-TLS) endpoints.
return "", "", err
}
// Try next endpoint
log.G(ctx).WithFields(log.Fields{
"error": err,
"endpoint": endpoint,
}).Infof("Error logging in to endpoint, trying next endpoint")
lastErr = err
continue
} }
if errdefs.IsUnauthorized(err) {
// Failed to authenticate; don't continue with (non-TLS) endpoints. // TODO(thaJeztah): move the statusMessage to the API endpoint; we don't need to produce that here?
return status, token, err return "Login Succeeded", authToken, nil
}
log.G(ctx).WithError(err).Infof("Error logging in to endpoint, trying next endpoint")
} }
return "", "", err return "", "", lastErr
} }
// ResolveRepository splits a repository name into its components // ResolveRepository splits a repository name into its components
@ -97,7 +106,8 @@ func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, use
func (s *Service) ResolveRepository(name reference.Named) (*RepositoryInfo, error) { func (s *Service) ResolveRepository(name reference.Named) (*RepositoryInfo, error) {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
return newRepositoryInfo(s.config, name) // TODO(thaJeztah): remove error return as it's no longer used.
return newRepositoryInfo(s.config, name), nil
} }
// APIEndpoint represents a remote API endpoint // APIEndpoint represents a remote API endpoint

View File

@ -13,6 +13,8 @@ type RepositoryInfo struct {
// Official indicates whether the repository is considered official. // Official indicates whether the repository is considered official.
// If the registry is official, and the normalized name does not // If the registry is official, and the normalized name does not
// contain a '/' (e.g. "foo"), then it is considered an official repo. // contain a '/' (e.g. "foo"), then it is considered an official repo.
//
// Deprecated: this field is no longer used and will be removed in the next release. The information captured in this field can be obtained from the [Name] field instead.
Official bool Official bool
// Class represents the class of the repository, such as "plugin" // Class represents the class of the repository, such as "plugin"
// or "image". // or "image".

11
vendor/modules.txt vendored
View File

@ -155,7 +155,7 @@ github.com/containerd/console
# github.com/containerd/containerd/api v1.8.0 # github.com/containerd/containerd/api v1.8.0
## explicit; go 1.21 ## explicit; go 1.21
github.com/containerd/containerd/api/services/content/v1 github.com/containerd/containerd/api/services/content/v1
# github.com/containerd/containerd/v2 v2.0.3 # github.com/containerd/containerd/v2 v2.0.4
## explicit; go 1.22.0 ## explicit; go 1.22.0
github.com/containerd/containerd/v2/core/content github.com/containerd/containerd/v2/core/content
github.com/containerd/containerd/v2/core/content/proxy github.com/containerd/containerd/v2/core/content/proxy
@ -229,11 +229,10 @@ github.com/davecgh/go-spew/spew
# github.com/distribution/reference v0.6.0 # github.com/distribution/reference v0.6.0
## explicit; go 1.20 ## explicit; go 1.20
github.com/distribution/reference github.com/distribution/reference
# github.com/docker/cli v28.0.1+incompatible # github.com/docker/cli v28.0.2+incompatible
## explicit ## explicit
github.com/docker/cli/cli github.com/docker/cli/cli
github.com/docker/cli/cli-plugins/hooks github.com/docker/cli/cli-plugins/metadata
github.com/docker/cli/cli-plugins/manager
github.com/docker/cli/cli-plugins/plugin github.com/docker/cli/cli-plugins/plugin
github.com/docker/cli/cli-plugins/socket github.com/docker/cli/cli-plugins/socket
github.com/docker/cli/cli/command github.com/docker/cli/cli/command
@ -252,6 +251,7 @@ github.com/docker/cli/cli/context/store
github.com/docker/cli/cli/debug github.com/docker/cli/cli/debug
github.com/docker/cli/cli/flags github.com/docker/cli/cli/flags
github.com/docker/cli/cli/hints github.com/docker/cli/cli/hints
github.com/docker/cli/cli/internal/jsonstream
github.com/docker/cli/cli/manifest/store github.com/docker/cli/cli/manifest/store
github.com/docker/cli/cli/manifest/types github.com/docker/cli/cli/manifest/types
github.com/docker/cli/cli/registry/client github.com/docker/cli/cli/registry/client
@ -260,6 +260,7 @@ github.com/docker/cli/cli/trust
github.com/docker/cli/cli/version github.com/docker/cli/cli/version
github.com/docker/cli/internal/tui github.com/docker/cli/internal/tui
github.com/docker/cli/opts github.com/docker/cli/opts
github.com/docker/cli/opts/swarmopts
github.com/docker/cli/pkg/kvfile github.com/docker/cli/pkg/kvfile
github.com/docker/cli/templates github.com/docker/cli/templates
# github.com/docker/cli-docs-tool v0.9.0 # github.com/docker/cli-docs-tool v0.9.0
@ -283,7 +284,7 @@ github.com/docker/distribution/registry/client/transport
github.com/docker/distribution/registry/storage/cache github.com/docker/distribution/registry/storage/cache
github.com/docker/distribution/registry/storage/cache/memory github.com/docker/distribution/registry/storage/cache/memory
github.com/docker/distribution/uuid github.com/docker/distribution/uuid
# github.com/docker/docker v28.0.1+incompatible # github.com/docker/docker v28.0.2+incompatible
## explicit ## explicit
github.com/docker/docker/api github.com/docker/docker/api
github.com/docker/docker/api/types github.com/docker/docker/api/types