vendor: github.com/docker/cli v28.0.2

full diff: https://github.com/docker/cli/compare/v28.0.1...v28.0.2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2025-03-19 17:50:34 +01:00
parent 882ef0db91
commit db4b96e62c
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
41 changed files with 781 additions and 372 deletions

2
go.mod
View File

@ -17,7 +17,7 @@ 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.2+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

4
go.sum
View File

@ -122,8 +122,8 @@ 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=

View File

@ -0,0 +1,30 @@
package manager
import "github.com/docker/cli/cli-plugins/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 = metadata.CommandAnnotationPlugin
// CommandAnnotationPluginVendor is added to every stub command
// added by AddPluginCommandStubs and contains the vendor of
// that plugin.
CommandAnnotationPluginVendor = metadata.CommandAnnotationPluginVendor
// CommandAnnotationPluginVersion is added to every stub command
// added by AddPluginCommandStubs and contains the version of
// that plugin.
CommandAnnotationPluginVersion = metadata.CommandAnnotationPluginVersion
// 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 = metadata.CommandAnnotationPluginInvalid
// CommandAnnotationPluginCommandPath is added to overwrite the
// command path for a plugin invocation.
CommandAnnotationPluginCommandPath = metadata.CommandAnnotationPluginCommandPath
)

View File

@ -1,6 +1,10 @@
package manager package manager
import "os/exec" import (
"os/exec"
"github.com/docker/cli/cli-plugins/metadata"
)
// Candidate represents a possible plugin candidate, for mocking purposes // Candidate represents a possible plugin candidate, for mocking purposes
type Candidate interface { type Candidate interface {
@ -17,5 +21,5 @@ func (c *candidate) Path() string {
} }
func (c *candidate) Metadata() ([]byte, error) { 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" return exec.Command(c.path, metadata.MetadataSubcommandName).Output() // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
} }

View File

@ -2,41 +2,12 @@ package manager
import ( import (
"fmt" "fmt"
"net/url"
"os" "os"
"strings"
"sync" "sync"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/config"
"github.com/spf13/cobra" "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 var pluginCommandStubsOnce sync.Once
@ -44,10 +15,10 @@ var pluginCommandStubsOnce sync.Once
// AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid // AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid
// plugin. The command stubs will have several annotations added, see // plugin. The command stubs will have several annotations added, see
// `CommandAnnotationPlugin*`. // `CommandAnnotationPlugin*`.
func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err error) { func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (err error) {
pluginCommandStubsOnce.Do(func() { pluginCommandStubsOnce.Do(func() {
var plugins []Plugin var plugins []Plugin
plugins, err = ListPlugins(dockerCli, rootCmd) plugins, err = ListPlugins(dockerCLI, rootCmd)
if err != nil { if err != nil {
return return
} }
@ -57,12 +28,12 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e
vendor = "unknown" vendor = "unknown"
} }
annotations := map[string]string{ annotations := map[string]string{
CommandAnnotationPlugin: "true", metadata.CommandAnnotationPlugin: "true",
CommandAnnotationPluginVendor: vendor, metadata.CommandAnnotationPluginVendor: vendor,
CommandAnnotationPluginVersion: p.Version, metadata.CommandAnnotationPluginVersion: p.Version,
} }
if p.Err != nil { if p.Err != nil {
annotations[CommandAnnotationPluginInvalid] = p.Err.Error() annotations[metadata.CommandAnnotationPluginInvalid] = p.Err.Error()
} }
rootCmd.AddCommand(&cobra.Command{ rootCmd.AddCommand(&cobra.Command{
Use: p.Name, Use: p.Name,
@ -89,7 +60,7 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e
cargs = append(cargs, args...) cargs = append(cargs, args...)
cargs = append(cargs, toComplete) cargs = append(cargs, toComplete)
os.Args = cargs os.Args = cargs
runCommand, runErr := PluginRunCommand(dockerCli, p.Name, cmd) runCommand, runErr := PluginRunCommand(dockerCLI, p.Name, cmd)
if runErr != nil { if runErr != nil {
return nil, cobra.ShellCompDirectiveError return nil, cobra.ShellCompDirectiveError
} }
@ -104,44 +75,3 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e
}) })
return err 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

@ -4,7 +4,7 @@
package manager package manager
import ( import (
"github.com/pkg/errors" "fmt"
) )
// pluginError is set as Plugin.Err by NewPlugin if the plugin // pluginError is set as Plugin.Err by NewPlugin if the plugin
@ -39,16 +39,16 @@ func (e *pluginError) MarshalText() (text []byte, err error) {
} }
// wrapAsPluginError wraps an error in a pluginError with an // wrapAsPluginError wraps an error in a pluginError with an
// additional message, analogous to errors.Wrapf. // additional message.
func wrapAsPluginError(err error, msg string) error { func wrapAsPluginError(err error, msg string) error {
if err == nil { if err == nil {
return nil return nil
} }
return &pluginError{cause: errors.Wrap(err, msg)} return &pluginError{cause: fmt.Errorf("%s: %w", msg, err)}
} }
// NewPluginError creates a new pluginError, analogous to // NewPluginError creates a new pluginError, analogous to
// errors.Errorf. // errors.Errorf.
func NewPluginError(msg string, args ...any) error { func NewPluginError(msg string, args ...any) error {
return &pluginError{cause: errors.Errorf(msg, args...)} return &pluginError{cause: fmt.Errorf(msg, args...)}
} }

View File

@ -6,7 +6,8 @@ import (
"strings" "strings"
"github.com/docker/cli/cli-plugins/hooks" "github.com/docker/cli/cli-plugins/hooks"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -29,29 +30,28 @@ type HookPluginData struct {
// a main CLI command was executed. It calls the hook subcommand for all // 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 // present CLI plugins that declare support for hooks in their metadata and
// parses/prints their responses. // parses/prints their responses.
func RunCLICommandHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) { func RunCLICommandHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ") commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
flags := getCommandFlags(subCommand) flags := getCommandFlags(subCommand)
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, cmdErrorMessage) runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, cmdErrorMessage)
} }
// RunPluginHooks is the entrypoint for the hooks execution flow // RunPluginHooks is the entrypoint for the hooks execution flow
// after a plugin command was just executed by the CLI. // after a plugin command was just executed by the CLI.
func RunPluginHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) { func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, args []string) {
commandName := strings.Join(args, " ") commandName := strings.Join(args, " ")
flags := getNaiveFlags(args) flags := getNaiveFlags(args)
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, "") runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, "")
} }
func runHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) { func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
nextSteps := invokeAndCollectHooks(ctx, dockerCli, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage) nextSteps := invokeAndCollectHooks(ctx, cfg, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
hooks.PrintNextSteps(subCommand.ErrOrStderr(), nextSteps)
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 { func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
// check if the context was cancelled before invoking hooks // check if the context was cancelled before invoking hooks
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -59,11 +59,15 @@ func invokeAndCollectHooks(ctx context.Context, dockerCli command.Cli, rootCmd,
default: default:
} }
pluginsCfg := dockerCli.ConfigFile().Plugins pluginsCfg := cfg.Plugins
if pluginsCfg == nil { if pluginsCfg == nil {
return nil return nil
} }
pluginDirs, err := getPluginDirs(cfg)
if err != nil {
return nil
}
nextSteps := make([]string, 0, len(pluginsCfg)) nextSteps := make([]string, 0, len(pluginsCfg))
for pluginName, cfg := range pluginsCfg { for pluginName, cfg := range pluginsCfg {
match, ok := pluginMatch(cfg, subCmdStr) match, ok := pluginMatch(cfg, subCmdStr)
@ -71,7 +75,7 @@ func invokeAndCollectHooks(ctx context.Context, dockerCli command.Cli, rootCmd,
continue continue
} }
p, err := GetPlugin(pluginName, dockerCli, rootCmd) p, err := getPlugin(pluginName, pluginDirs, rootCmd)
if err != nil { if err != nil {
continue continue
} }

View File

@ -9,7 +9,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
@ -22,10 +22,12 @@ const (
// used to originally invoke the docker CLI when executing a // used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow // plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI. // the plugin to re-execute the original CLI.
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND" ReexecEnvvar = metadata.ReexecEnvvar
// ResourceAttributesEnvvar is the name of the envvar that includes additional // ResourceAttributesEnvvar is the name of the envvar that includes additional
// resource attributes for OTEL. // resource attributes for OTEL.
//
// Deprecated: The "OTEL_RESOURCE_ATTRIBUTES" env-var is part of the OpenTelemetry specification; users should define their own const for this. This const will be removed in the next release.
ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES" ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES"
) )
@ -91,10 +93,10 @@ func addPluginCandidatesFromDir(res map[string][]string, d string) {
continue continue
} }
name := dentry.Name() name := dentry.Name()
if !strings.HasPrefix(name, NamePrefix) { if !strings.HasPrefix(name, metadata.NamePrefix) {
continue continue
} }
name = strings.TrimPrefix(name, NamePrefix) name = strings.TrimPrefix(name, metadata.NamePrefix)
var err error var err error
if name, err = trimExeSuffix(name); err != nil { if name, err = trimExeSuffix(name); err != nil {
continue continue
@ -113,12 +115,15 @@ func listPluginCandidates(dirs []string) map[string][]string {
} }
// GetPlugin returns a plugin on the system by its name // GetPlugin returns a plugin on the system by its name
func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plugin, error) { func GetPlugin(name string, dockerCLI config.Provider, rootcmd *cobra.Command) (*Plugin, error) {
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile()) pluginDirs, err := getPluginDirs(dockerCLI.ConfigFile())
if err != nil { if err != nil {
return nil, err return nil, err
} }
return getPlugin(name, pluginDirs, rootcmd)
}
func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugin, error) {
candidates := listPluginCandidates(pluginDirs) candidates := listPluginCandidates(pluginDirs)
if paths, ok := candidates[name]; ok { if paths, ok := candidates[name]; ok {
if len(paths) == 0 { if len(paths) == 0 {
@ -139,7 +144,7 @@ func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plu
} }
// ListPlugins produces a list of the plugins available on the system // ListPlugins produces a list of the plugins available on the system
func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) { func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, error) {
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile()) pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
if err != nil { if err != nil {
return nil, err return nil, err
@ -186,7 +191,7 @@ func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error
// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin. // 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 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. // 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) { func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
// This uses the full original args, not the args which may // This uses the full original args, not the args which may
// have been provided by cobra to our caller. This is because // have been provided by cobra to our caller. This is because
// they lack e.g. global options which we must propagate here. // they lack e.g. global options which we must propagate here.
@ -196,7 +201,7 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
// fallback to their "invalid" command path. // fallback to their "invalid" command path.
return nil, errPluginNotFound(name) return nil, errPluginNotFound(name)
} }
exename := addExeSuffix(NamePrefix + name) exename := addExeSuffix(metadata.NamePrefix + name)
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile()) pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
if err != nil { if err != nil {
return nil, err return nil, err
@ -233,7 +238,7 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Env = append(cmd.Environ(), ReexecEnvvar+"="+os.Args[0]) cmd.Env = append(cmd.Environ(), metadata.ReexecEnvvar+"="+os.Args[0])
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin) cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
return cmd, nil return cmd, nil
@ -243,5 +248,5 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
// IsPluginCommand checks if the given cmd is a plugin-stub. // IsPluginCommand checks if the given cmd is a plugin-stub.
func IsPluginCommand(cmd *cobra.Command) bool { func IsPluginCommand(cmd *cobra.Command) bool {
return cmd.Annotations[CommandAnnotationPlugin] == "true" return cmd.Annotations[metadata.CommandAnnotationPlugin] == "true"
} }

View File

@ -1,30 +1,23 @@
package manager package manager
import (
"github.com/docker/cli/cli-plugins/metadata"
)
const ( const (
// NamePrefix is the prefix required on all plugin binary names // NamePrefix is the prefix required on all plugin binary names
NamePrefix = "docker-" NamePrefix = metadata.NamePrefix
// MetadataSubcommandName is the name of the plugin subcommand // MetadataSubcommandName is the name of the plugin subcommand
// which must be supported by every plugin and returns the // which must be supported by every plugin and returns the
// plugin metadata. // plugin metadata.
MetadataSubcommandName = "docker-cli-plugin-metadata" MetadataSubcommandName = metadata.MetadataSubcommandName
// HookSubcommandName is the name of the plugin subcommand // HookSubcommandName is the name of the plugin subcommand
// 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 = metadata.HookSubcommandName
) )
// Metadata provided by the plugin. // Metadata provided by the plugin.
type Metadata struct { type Metadata = metadata.Metadata
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
SchemaVersion string `json:",omitempty"`
// Vendor is the name of the plugin vendor. Mandatory
Vendor string `json:",omitempty"`
// Version is the optional version of this plugin.
Version string `json:",omitempty"`
// ShortDescription should be suitable for a single line help message.
ShortDescription string `json:",omitempty"`
// URL is a pointer to the plugin's homepage.
URL string `json:",omitempty"`
}

View File

@ -3,13 +3,15 @@ package manager
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"github.com/pkg/errors" "github.com/docker/cli/cli-plugins/metadata"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -17,7 +19,7 @@ var pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
// Plugin represents a potential plugin with all it's metadata. // Plugin represents a potential plugin with all it's metadata.
type Plugin struct { type Plugin struct {
Metadata metadata.Metadata
Name string `json:",omitempty"` Name string `json:",omitempty"`
Path string `json:",omitempty"` Path string `json:",omitempty"`
@ -44,18 +46,18 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
// which would fail here, so there are all real errors. // which would fail here, so there are all real errors.
fullname := filepath.Base(path) fullname := filepath.Base(path)
if fullname == "." { if fullname == "." {
return Plugin{}, errors.Errorf("unable to determine basename of plugin candidate %q", path) return Plugin{}, fmt.Errorf("unable to determine basename of plugin candidate %q", path)
} }
var err error var err error
if fullname, err = trimExeSuffix(fullname); err != nil { if fullname, err = trimExeSuffix(fullname); err != nil {
return Plugin{}, errors.Wrapf(err, "plugin candidate %q", path) return Plugin{}, fmt.Errorf("plugin candidate %q: %w", path, err)
} }
if !strings.HasPrefix(fullname, NamePrefix) { if !strings.HasPrefix(fullname, metadata.NamePrefix) {
return Plugin{}, errors.Errorf("plugin candidate %q: does not have %q prefix", path, NamePrefix) return Plugin{}, fmt.Errorf("plugin candidate %q: does not have %q prefix", path, metadata.NamePrefix)
} }
p := Plugin{ p := Plugin{
Name: strings.TrimPrefix(fullname, NamePrefix), Name: strings.TrimPrefix(fullname, metadata.NamePrefix),
Path: path, Path: path,
} }
@ -112,9 +114,9 @@ func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte,
return nil, wrapAsPluginError(err, "failed to marshall hook data") 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 := exec.CommandContext(ctx, p.Path, p.Name, metadata.HookSubcommandName, string(hDataBytes)) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
pCmd.Env = os.Environ() pCmd.Env = os.Environ()
pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0]) pCmd.Env = append(pCmd.Env, metadata.ReexecEnvvar+"="+os.Args[0])
hookCmdOutput, err := pCmd.Output() hookCmdOutput, err := pCmd.Output()
if err != nil { if err != nil {
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand") return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")

View File

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

View File

@ -0,0 +1,85 @@
package manager
import (
"fmt"
"os"
"strings"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/baggage"
)
const (
// 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
resourceAttributesEnvVar = "OTEL_RESOURCE_ATTRIBUTES"
// dockerCLIAttributePrefix is the prefix for any docker cli OTEL attributes.
//
// It is a copy of the const defined in [command.dockerCLIAttributePrefix].
dockerCLIAttributePrefix = "docker.cli."
cobraCommandPath = attribute.Key("cobra.command_path")
)
func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
commandPath := cmd.Annotations[metadata.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 {
// Construct baggage members for each of the attributes.
// Ignore any failures as these aren't significant and
// represent an internal issue.
members := make([]baggage.Member, 0, attrs.Len())
for iter := attrs.Iter(); iter.Next(); {
attr := iter.Attribute()
m, err := baggage.NewMemberRaw(string(attr.Key), attr.Value.AsString())
if err != nil {
otel.Handle(err)
continue
}
members = append(members, m)
}
// Combine plugin added resource attributes with ones found in the environment
// variable. Our own attributes should be namespaced so there shouldn't be a
// conflict. We do not parse the environment variable because we do not want
// to handle errors in user configuration.
attrsSlice := make([]string, 0, 2)
if v := strings.TrimSpace(os.Getenv(resourceAttributesEnvVar)); v != "" {
attrsSlice = append(attrsSlice, v)
}
if b, err := baggage.New(members...); err != nil {
otel.Handle(err)
} else if b.Len() > 0 {
attrsSlice = append(attrsSlice, b.String())
}
if len(attrsSlice) > 0 {
env = append(env, resourceAttributesEnvVar+"="+strings.Join(attrsSlice, ","))
}
}
return env
}

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

@ -0,0 +1,36 @@
package metadata
const (
// NamePrefix is the prefix required on all plugin binary names
NamePrefix = "docker-"
// MetadataSubcommandName is the name of the plugin subcommand
// which must be supported by every plugin and returns the
// plugin metadata.
MetadataSubcommandName = "docker-cli-plugin-metadata"
// HookSubcommandName is the name of the plugin subcommand
// which must be implemented by plugins declaring support
// for hooks in their metadata.
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.
type Metadata struct {
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
SchemaVersion string `json:",omitempty"`
// Vendor is the name of the plugin vendor. Mandatory
Vendor string `json:",omitempty"`
// Version is the optional version of this plugin.
Version string `json:",omitempty"`
// ShortDescription should be suitable for a single line help message.
ShortDescription string `json:",omitempty"`
// URL is a pointer to the plugin's homepage.
URL string `json:",omitempty"`
}

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
} }

5
vendor/modules.txt vendored
View File

@ -229,11 +229,12 @@ 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/hooks
github.com/docker/cli/cli-plugins/manager 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-plugins/socket github.com/docker/cli/cli-plugins/socket
github.com/docker/cli/cli/command github.com/docker/cli/cli/command
@ -252,6 +253,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 +262,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