chore: bump deps

This commit is contained in:
2025-08-12 07:04:57 +02:00
committed by decentral1se
parent 157d131b37
commit 56a68dfa91
981 changed files with 36486 additions and 39650 deletions

View File

@ -3,15 +3,12 @@ package cli
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
pluginmanager "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/command"
cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/docker/pkg/homedir"
"github.com/docker/docker/registry"
"github.com/fvbommel/sortorder"
"github.com/moby/term"
"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
}
// 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
}
@ -252,7 +242,7 @@ func hasAdditionalHelp(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 {
@ -356,9 +346,9 @@ func decoratedName(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 := ""
if v, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVersion]; ok && v != "" {
if v, ok := cmd.Annotations[metadata.CommandAnnotationPluginVersion]; ok && v != "" {
version = ", " + v
}
return fmt.Sprintf("(%s%s)", vendor, version)
@ -417,7 +407,7 @@ func invalidPlugins(cmd *cobra.Command) []*cobra.Command {
}
func invalidPluginReason(cmd *cobra.Command) string {
return cmd.Annotations[pluginmanager.CommandAnnotationPluginInvalid]
return cmd.Annotations[metadata.CommandAnnotationPluginInvalid]
}
const usageTemplate = `Usage:

View File

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package command
@ -8,7 +8,6 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strconv"
"sync"
@ -21,21 +20,15 @@ import (
"github.com/docker/cli/cli/context/store"
"github.com/docker/cli/cli/debug"
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/trust"
"github.com/docker/cli/cli/version"
dopts "github.com/docker/cli/opts"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/build"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/docker/go-connections/tlsconfig"
"github.com/pkg/errors"
"github.com/spf13/cobra"
notaryclient "github.com/theupdateframework/notary/client"
)
const defaultInitTimeout = 2 * time.Second
@ -53,13 +46,10 @@ type Cli interface {
Streams
SetIn(in *streams.In)
Apply(ops ...CLIOption) error
ConfigFile() *configfile.ConfigFile
config.Provider
ServerInfo() ServerInfo
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
DefaultVersion() string
CurrentVersion() string
ManifestStore() manifeststore.Store
RegistryClient(bool) registryclient.RegistryClient
ContentTrustEnabled() bool
BuildKitEnabled() (bool, error)
ContextStore() store.Store
@ -69,7 +59,9 @@ type Cli interface {
}
// DockerCli is an instance the docker command line client.
// Instances of the client can be returned from NewDockerCli.
// Instances of the client should be created using the [NewDockerCli]
// constructor to make sure they are properly initialized with defaults
// set.
type DockerCli struct {
configFile *configfile.ConfigFile
options *cliflags.ClientOptions
@ -84,7 +76,7 @@ type DockerCli struct {
init sync.Once
initErr error
dockerEndpoint docker.Endpoint
contextStoreConfig store.Config
contextStoreConfig *store.Config
initTimeout time.Duration
res telemetryResource
@ -96,7 +88,7 @@ type DockerCli struct {
enableGlobalMeter, enableGlobalTracer bool
}
// DefaultVersion returns api.defaultVersion.
// DefaultVersion returns [api.DefaultVersion].
func (*DockerCli) DefaultVersion() string {
return api.DefaultVersion
}
@ -188,7 +180,7 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
}
si := cli.ServerInfo()
if si.BuildkitVersion == types.BuilderBuildKit {
if si.BuildkitVersion == build.BuilderBuildKit {
// The daemon advertised BuildKit as the preferred builder; this may
// be either a Linux daemon or a Windows daemon with experimental
// BuildKit support enabled.
@ -202,16 +194,16 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
// HooksEnabled returns whether plugin hooks are enabled.
func (cli *DockerCli) HooksEnabled() bool {
// legacy support DOCKER_CLI_HINTS env var
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
// use DOCKER_CLI_HOOKS env var value if set and not empty
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
}
return enabled
}
// use DOCKER_CLI_HOOKS env var value if set and not empty
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
// legacy support DOCKER_CLI_HINTS env var
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
@ -230,30 +222,6 @@ func (cli *DockerCli) HooksEnabled() bool {
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.
func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) CLIOption {
return func(dockerCli *DockerCli) error {
var err error
dockerCli.client, err = makeClient(dockerCli)
return err
}
}
// Initialize the dockerCli runs initialization that must happen after command
// line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error {
@ -275,13 +243,33 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
return errors.New("conflicting options: cannot specify both --host and --context")
}
if cli.contextStoreConfig == nil {
// This path can be hit when calling Initialize on a DockerCli that's
// not constructed through [NewDockerCli]. Using the default context
// store without a config set will result in Endpoints from contexts
// not being type-mapped correctly, and used as a generic "map[string]any",
// instead of a [docker.EndpointMeta].
//
// When looking up the API endpoint (using [EndpointFromContext]), no
// endpoint will be found, and a default, empty endpoint will be used
// instead which in its turn, causes newAPIClientFromEndpoint to
// be initialized with the default config instead of settings for
// the current context (which may mean; connecting with the wrong
// endpoint and/or TLS Config to be missing).
//
// [EndpointFromContext]: https://github.com/docker/cli/blob/33494921b80fd0b5a06acc3a34fa288de4bb2e6b/cli/context/docker/load.go#L139-L149
if err := WithDefaultContextStoreConfig()(cli); err != nil {
return err
}
}
cli.options = opts
cli.configFile = config.LoadDefaultConfigFile(cli.err)
cli.currentContext = resolveContextName(cli.options, cli.configFile)
cli.contextStore = &ContextStoreWithDefault{
Store: store.New(config.ContextStoreDir(), cli.contextStoreConfig),
Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig),
Resolver: func() (*DefaultContext, error) {
return ResolveDefaultContext(cli.options, cli.contextStoreConfig)
return ResolveDefaultContext(cli.options, *cli.contextStoreConfig)
},
}
@ -292,6 +280,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
if cli.enableGlobalTracer {
cli.createGlobalTracerProvider(cli.baseCtx)
}
filterResourceAttributesEnvvar()
return nil
}
@ -345,7 +334,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)
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 {
return docker.Endpoint{}, err
}
@ -403,11 +395,6 @@ func (cli *DockerCli) initializeFromClient() {
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
func (cli *DockerCli) ContextStore() store.Store {
return cli.contextStore
@ -523,7 +510,7 @@ func (cli *DockerCli) Apply(ops ...CLIOption) error {
type ServerInfo struct {
HasExperimental bool
OSType string
BuildkitVersion types.BuilderVersion
BuildkitVersion build.BuilderVersion
// SwarmStatus provides information about the current swarm status of the
// engine, obtained from the "Swarm" header in the API response.
@ -553,18 +540,15 @@ func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
return cli, nil
}
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
var host string
func getServerHost(hosts []string, defaultToTLS bool) (string, error) {
switch len(hosts) {
case 0:
host = os.Getenv(client.EnvOverrideHost)
return dopts.ParseHost(defaultToTLS, os.Getenv(client.EnvOverrideHost))
case 1:
host = hosts[0]
return dopts.ParseHost(defaultToTLS, hosts[0])
default:
return "", errors.New("Specify only one -H")
}
return dopts.ParseHost(tlsOptions != nil, host)
}
// UserAgent returns the user agent string used for making API requests

View File

@ -11,7 +11,6 @@ import (
"github.com/docker/cli/cli/streams"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/moby/term"
"github.com/pkg/errors"
)
@ -101,7 +100,8 @@ func WithContentTrust(enabled bool) CLIOption {
// WithDefaultContextStoreConfig configures the cli to use the default context store configuration.
func WithDefaultContextStoreConfig() CLIOption {
return func(cli *DockerCli) error {
cli.contextStoreConfig = DefaultContextStoreConfig()
cfg := DefaultContextStoreConfig()
cli.contextStoreConfig = &cfg
return nil
}
}
@ -114,6 +114,18 @@ func WithAPIClient(c client.APIClient) CLIOption {
}
}
// WithInitializeClient is passed to [DockerCli.Initialize] to initialize
// an API Client for use by the CLI.
func WithInitializeClient(makeClient func(*DockerCli) (client.APIClient, error)) CLIOption {
return func(cli *DockerCli) error {
c, err := makeClient(cli)
if err != nil {
return err
}
return WithAPIClient(c)(cli)
}
}
// envOverrideHTTPHeaders is the name of the environment-variable that can be
// used to set custom HTTP headers to be sent by the client. This environment
// variable is the equivalent to the HttpHeaders field in the configuration
@ -177,7 +189,7 @@ func withCustomHeadersFromEnv() client.Opt {
csvReader := csv.NewReader(strings.NewReader(value))
fields, err := csvReader.Read()
if err != nil {
return errdefs.InvalidParameter(errors.Errorf(
return invalidParameter(errors.Errorf(
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
envOverrideHTTPHeaders,
))
@ -194,7 +206,7 @@ func withCustomHeadersFromEnv() client.Opt {
k = strings.TrimSpace(k)
if k == "" {
return errdefs.InvalidParameter(errors.Errorf(
return invalidParameter(errors.Errorf(
`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`,
envOverrideHTTPHeaders, kv,
))
@ -205,7 +217,7 @@ func withCustomHeadersFromEnv() client.Opt {
// from an environment variable with the same name). In the meantime,
// produce an error to prevent users from depending on this.
if !hasValue {
return errdefs.InvalidParameter(errors.Errorf(
return invalidParameter(errors.Errorf(
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
envOverrideHTTPHeaders, kv,
))

View File

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package command

View File

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package command
@ -7,7 +7,6 @@ import (
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/store"
cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/docker/errdefs"
"github.com/pkg/errors"
)
@ -117,7 +116,7 @@ func (s *ContextStoreWithDefault) List() ([]store.Metadata, error) {
// CreateOrUpdate is not allowed for the default context and fails
func (s *ContextStoreWithDefault) CreateOrUpdate(meta store.Metadata) error {
if meta.Name == DefaultContextName {
return errdefs.InvalidParameter(errors.New("default context cannot be created nor updated"))
return invalidParameter(errors.New("default context cannot be created nor updated"))
}
return s.Store.CreateOrUpdate(meta)
}
@ -125,7 +124,7 @@ func (s *ContextStoreWithDefault) CreateOrUpdate(meta store.Metadata) error {
// Remove is not allowed for the default context and fails
func (s *ContextStoreWithDefault) Remove(name string) error {
if name == DefaultContextName {
return errdefs.InvalidParameter(errors.New("default context cannot be removed"))
return invalidParameter(errors.New("default context cannot be removed"))
}
return s.Store.Remove(name)
}
@ -145,7 +144,7 @@ func (s *ContextStoreWithDefault) GetMetadata(name string) (store.Metadata, erro
// ResetTLSMaterial is not implemented for default context and fails
func (s *ContextStoreWithDefault) ResetTLSMaterial(name string, data *store.ContextTLSData) error {
if name == DefaultContextName {
return errdefs.InvalidParameter(errors.New("default context cannot be edited"))
return invalidParameter(errors.New("default context cannot be edited"))
}
return s.Store.ResetTLSMaterial(name, data)
}
@ -153,7 +152,7 @@ func (s *ContextStoreWithDefault) ResetTLSMaterial(name string, data *store.Cont
// ResetEndpointTLSMaterial is not implemented for default context and fails
func (s *ContextStoreWithDefault) ResetEndpointTLSMaterial(contextName string, endpointName string, data *store.EndpointTLSData) error {
if contextName == DefaultContextName {
return errdefs.InvalidParameter(errors.New("default context cannot be edited"))
return invalidParameter(errors.New("default context cannot be edited"))
}
return s.Store.ResetEndpointTLSMaterial(contextName, endpointName, data)
}
@ -186,7 +185,7 @@ func (s *ContextStoreWithDefault) GetTLSData(contextName, endpointName, fileName
return nil, err
}
if defaultContext.TLS.Endpoints[endpointName].Files[fileName] == nil {
return nil, errdefs.NotFound(errors.Errorf("TLS data for %s/%s/%s does not exist", DefaultContextName, endpointName, fileName))
return nil, notFound(errors.Errorf("TLS data for %s/%s/%s does not exist", DefaultContextName, endpointName, fileName))
}
return defaultContext.TLS.Endpoints[endpointName].Files[fileName], nil
}

View File

@ -6,8 +6,7 @@ import (
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/api/types/build"
"github.com/docker/go-units"
)
@ -52,7 +51,7 @@ shared: {{.Shared}}
return Format(source)
}
func buildCacheSort(buildCache []*types.BuildCache) {
func buildCacheSort(buildCache []*build.CacheRecord) {
sort.Slice(buildCache, func(i, j int) bool {
lui, luj := buildCache[i].LastUsedAt, buildCache[j].LastUsedAt
switch {
@ -71,7 +70,7 @@ func buildCacheSort(buildCache []*types.BuildCache) {
}
// BuildCacheWrite renders the context for a list of containers
func BuildCacheWrite(ctx Context, buildCaches []*types.BuildCache) error {
func BuildCacheWrite(ctx Context, buildCaches []*build.CacheRecord) error {
render := func(format func(subContext SubContext) error) error {
buildCacheSort(buildCaches)
for _, bc := range buildCaches {
@ -88,7 +87,7 @@ func BuildCacheWrite(ctx Context, buildCaches []*types.BuildCache) error {
type buildCacheContext struct {
HeaderContext
trunc bool
v *types.BuildCache
v *build.CacheRecord
}
func newBuildCacheContext() *buildCacheContext {
@ -115,7 +114,7 @@ func (c *buildCacheContext) MarshalJSON() ([]byte, error) {
func (c *buildCacheContext) ID() string {
id := c.v.ID
if c.trunc {
id = stringid.TruncateID(c.v.ID)
id = TruncateID(c.v.ID)
}
if c.v.InUse {
return id + "*"
@ -131,7 +130,7 @@ func (c *buildCacheContext) Parent() string {
parent = c.v.Parent //nolint:staticcheck // Ignore SA1019: Field was deprecated in API v1.42, but kept for backward compatibility
}
if c.trunc {
return stringid.TruncateID(parent)
return TruncateID(parent)
}
return parent
}

View File

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package formatter
@ -11,10 +11,11 @@ import (
"strings"
"time"
"github.com/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
const (
@ -26,8 +27,18 @@ const (
mountsHeader = "MOUNTS"
localVolumes = "LOCAL VOLUMES"
networksHeader = "NETWORKS"
platformHeader = "PLATFORM"
)
// Platform wraps a [ocispec.Platform] to implement the stringer interface.
type Platform struct {
ocispec.Platform
}
func (p Platform) String() string {
return platforms.FormatAll(p.Platform)
}
// NewContainerFormat returns a Format for rendering using a Context
func NewContainerFormat(source string, quiet bool, size bool) Format {
switch source {
@ -68,16 +79,14 @@ ports: {{- pad .Ports 1 0}}
// ContainerWrite renders the context for a list of containers
func ContainerWrite(ctx Context, containers []container.Summary) error {
render := func(format func(subContext SubContext) error) error {
return ctx.Write(NewContainerContext(), func(format func(subContext SubContext) error) error {
for _, ctr := range containers {
err := format(&ContainerContext{trunc: ctx.Trunc, c: ctr})
if err != nil {
if err := format(&ContainerContext{trunc: ctx.Trunc, c: ctr}); err != nil {
return err
}
}
return nil
}
return ctx.Write(NewContainerContext(), render)
})
}
// ContainerContext is a struct used for rendering a list of containers in a Go template.
@ -111,6 +120,7 @@ func NewContainerContext() *ContainerContext {
"Mounts": mountsHeader,
"LocalVolumes": localVolumes,
"Networks": networksHeader,
"Platform": platformHeader,
}
return &containerCtx
}
@ -124,7 +134,7 @@ func (c *ContainerContext) MarshalJSON() ([]byte, error) {
// option being set, the full or truncated ID is returned.
func (c *ContainerContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.c.ID)
return TruncateID(c.c.ID)
}
return c.c.ID
}
@ -161,7 +171,7 @@ func (c *ContainerContext) Image() string {
return "<no image>"
}
if c.trunc {
if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
if trunc := TruncateID(c.c.ImageID); trunc == TruncateID(c.c.Image) {
return trunc
}
// truncate digest if no-trunc option was not selected
@ -210,6 +220,16 @@ func (c *ContainerContext) RunningFor() string {
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
}
// Platform returns a human-readable representation of the container's
// platform if it is available.
func (c *ContainerContext) Platform() *Platform {
p := c.c.ImageManifestDescriptor
if p == nil || p.Platform == nil {
return nil
}
return &Platform{*p.Platform}
}
// Ports returns a comma-separated string representing open ports of the container
// e.g. "0.0.0.0:80->9090/tcp, 9988/tcp"
// it's used by command 'docker ps'
@ -218,7 +238,8 @@ func (c *ContainerContext) Ports() string {
return DisplayablePorts(c.c.Ports)
}
// State returns the container's current state (e.g. "running" or "paused")
// State returns the container's current state (e.g. "running" or "paused").
// Refer to [container.ContainerState] for possible states.
func (c *ContainerContext) State() string {
return c.c.State
}
@ -255,6 +276,7 @@ func (c *ContainerContext) Labels() string {
for k, v := range c.c.Labels {
joinLabels = append(joinLabels, k+"="+v)
}
sort.Strings(joinLabels)
return strings.Join(joinLabels, ",")
}

View File

@ -1,7 +1,5 @@
package formatter
import "encoding/json"
const (
// ClientContextTableFormat is the default client context format.
ClientContextTableFormat = "table {{.Name}}{{if .Current}} *{{end}}\t{{.Description}}\t{{.DockerEndpoint}}\t{{.Error}}"
@ -30,13 +28,6 @@ type ClientContext struct {
DockerEndpoint string
Current bool
Error string
// ContextType is a temporary field for compatibility with
// Visual Studio, which depends on this from the "cloud integration"
// wrapper.
//
// Deprecated: this type is only for backward-compatibility. Do not use.
ContextType string `json:"ContextType,omitempty"`
}
// ClientContextWrite writes formatted contexts using the Context
@ -69,13 +60,6 @@ func newClientContextContext() *clientContextContext {
}
func (c *clientContextContext) MarshalJSON() ([]byte, error) {
if c.c.ContextType != "" {
// We only have ContextType set for plain "json" or "{{json .}}" formatting,
// so we should be able to just use the default json.Marshal with no
// special handling.
return json.Marshal(c.c)
}
// FIXME(thaJeztah): why do we need a special marshal function here?
return MarshalJSON(c)
}

View File

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package formatter

View File

@ -4,15 +4,14 @@ import (
"bytes"
"fmt"
"strconv"
"strings"
"text/template"
"github.com/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/build"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/volume"
units "github.com/docker/go-units"
"github.com/docker/go-units"
)
const (
@ -39,12 +38,12 @@ type DiskUsageContext struct {
Images []*image.Summary
Containers []*container.Summary
Volumes []*volume.Volume
BuildCache []*types.BuildCache
BuildCache []*build.CacheRecord
BuilderSize int64
}
func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template, error) {
ctx.buffer = bytes.NewBufferString("")
ctx.buffer = &bytes.Buffer{}
ctx.header = ""
ctx.Format = Format(format)
ctx.preFormat()
@ -88,7 +87,7 @@ func (ctx *DiskUsageContext) Write() (err error) {
if ctx.Verbose {
return ctx.verboseWrite()
}
ctx.buffer = bytes.NewBufferString("")
ctx.buffer = &bytes.Buffer{}
ctx.preFormat()
tmpl, err := ctx.parseFormat()
@ -330,9 +329,15 @@ func (c *diskUsageContainersContext) TotalCount() string {
}
func (*diskUsageContainersContext) isActive(ctr container.Summary) bool {
return strings.Contains(ctr.State, "running") ||
strings.Contains(ctr.State, "paused") ||
strings.Contains(ctr.State, "restarting")
switch ctr.State {
case container.StateRunning, container.StatePaused, container.StateRestarting:
return true
case container.StateCreated, container.StateRemoving, container.StateExited, container.StateDead:
return false
default:
// Unknown state (should never happen).
return false
}
}
func (c *diskUsageContainersContext) Active() string {
@ -436,7 +441,7 @@ func (c *diskUsageVolumesContext) Reclaimable() string {
type diskUsageBuilderContext struct {
HeaderContext
builderSize int64
buildCache []*types.BuildCache
buildCache []*build.CacheRecord
}
func (c *diskUsageBuilderContext) MarshalJSON() ([]byte, error) {

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.23
package formatter
import (
"fmt"
"strings"
"unicode/utf8"
"golang.org/x/text/width"
@ -15,11 +20,32 @@ func charWidth(r rune) int {
switch width.LookupRune(r).Kind() {
case width.EastAsianWide, width.EastAsianFullwidth:
return 2
case width.Neutral, width.EastAsianAmbiguous, width.EastAsianNarrow, width.EastAsianHalfwidth:
return 1
default:
return 1
}
}
const shortLen = 12
// TruncateID returns a shorthand version of a string identifier for presentation,
// after trimming digest algorithm prefix (if any).
//
// This function is a copy of [stringid.TruncateID] for presentation / formatting
// purposes.
//
// [stringid.TruncateID]: https://github.com/moby/moby/blob/v28.3.2/pkg/stringid/stringid.go#L19
func TruncateID(id string) string {
if i := strings.IndexRune(id, ':'); i >= 0 {
id = id[i+1:]
}
if len(id) > shortLen {
id = id[:shortLen]
}
return id
}
// Ellipsis truncates a string to fit within maxDisplayWidth, and appends ellipsis (…).
// For maxDisplayWidth of 1 and lower, no ellipsis is appended.
// For maxDisplayWidth of 1, first char of string will return even if its width > 1.
@ -59,3 +85,27 @@ func Ellipsis(s string, maxDisplayWidth int) string {
}
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

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package formatter
@ -76,12 +76,15 @@ func (c *Context) preFormat() {
func (c *Context) parseFormat() (*template.Template, error) {
tmpl, err := templates.Parse(c.finalFormat)
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) {
if c.Output == nil {
c.Output = io.Discard
}
if c.Format.IsTable() {
t := tabwriter.NewWriter(c.Output, 10, 1, 3, ' ', 0)
buffer := bytes.NewBufferString("")
@ -111,7 +114,7 @@ type SubFormat func(func(SubContext) error) error
// Write the template to the buffer using this Context
func (c *Context) Write(sub SubContext, f SubFormat) error {
c.buffer = bytes.NewBufferString("")
c.buffer = &bytes.Buffer{}
c.preFormat()
tmpl, err := c.parseFormat()

View File

@ -6,8 +6,7 @@ import (
"github.com/distribution/reference"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units"
"github.com/docker/go-units"
)
const (
@ -216,7 +215,7 @@ func (c *imageContext) MarshalJSON() ([]byte, error) {
func (c *imageContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.i.ID)
return TruncateID(c.i.ID)
}
return c.i.ID
}

View File

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package formatter

View File

@ -12,7 +12,7 @@
// based on https://github.com/golang/go/blob/master/src/text/tabwriter/tabwriter.go Last modified 690ac40 on 31 Jan
//nolint:gocyclo,nakedret,stylecheck,unused // ignore linting errors, so that we can stick close to upstream
//nolint:gocyclo,nakedret,unused // ignore linting errors, so that we can stick close to upstream
package tabwriter
import (

View File

@ -6,7 +6,7 @@ import (
"strings"
"github.com/docker/docker/api/types/volume"
units "github.com/docker/go-units"
"github.com/docker/go-units"
)
const (

View File

@ -13,9 +13,9 @@ import (
configtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/hints"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/prompt"
"github.com/docker/cli/internal/tui"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/registry"
"github.com/morikuni/aec"
"github.com/pkg/errors"
)
@ -28,16 +28,22 @@ const (
"for organizations using SSO. Learn more at https://docs.docker.com/go/access-tokens/"
)
// authConfigKey is the key used to store credentials for Docker Hub. It is
// a copy of [registry.IndexServer].
//
// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker/registry#IndexServer
const authConfigKey = "https://index.docker.io/v1/"
// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
// for the given command.
// for the given command to prompt the user for username and password.
func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) registrytypes.RequestAuthConfig {
configKey := getAuthConfigKey(index.Name)
isDefaultRegistry := configKey == authConfigKey || index.Official
return func(ctx context.Context) (string, error) {
_, _ = fmt.Fprintf(cli.Out(), "\nLogin prior to %s:\n", cmdName)
indexServer := registry.GetAuthConfigKey(index)
isDefaultRegistry := indexServer == registry.IndexServer
authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, indexServer, isDefaultRegistry)
authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, configKey, isDefaultRegistry)
if err != nil {
_, _ = fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", indexServer, err)
_, _ = fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", configKey, err)
}
select {
@ -46,7 +52,7 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
default:
}
authConfig, err = PromptUserForCredentials(ctx, cli, "", "", authConfig.Username, indexServer)
authConfig, err = PromptUserForCredentials(ctx, cli, "", "", authConfig.Username, configKey)
if err != nil {
return "", err
}
@ -63,7 +69,7 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
func ResolveAuthConfig(cfg *configfile.ConfigFile, index *registrytypes.IndexInfo) registrytypes.AuthConfig {
configKey := index.Name
if index.Official {
configKey = registry.IndexServer
configKey = authConfigKey
}
a, _ := cfg.GetAuthConfig(configKey)
@ -132,7 +138,7 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
argUser = strings.TrimSpace(argUser)
if argUser == "" {
if serverAddress == registry.IndexServer {
if serverAddress == authConfigKey {
// When signing in to the default (Docker Hub) registry, we display
// hints for creating an account, and (if hints are enabled), using
// a token instead of a password.
@ -143,16 +149,16 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
}
}
var prompt string
var msg string
defaultUsername = strings.TrimSpace(defaultUsername)
if defaultUsername == "" {
prompt = "Username: "
msg = "Username: "
} else {
prompt = fmt.Sprintf("Username (%s): ", defaultUsername)
msg = fmt.Sprintf("Username (%s): ", defaultUsername)
}
var err error
argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
argUser, err = prompt.ReadInput(ctx, cli.In(), cli.Out(), msg)
if err != nil {
return registrytypes.AuthConfig{}, err
}
@ -166,7 +172,7 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
argPassword = strings.TrimSpace(argPassword)
if argPassword == "" {
restoreInput, err := DisableInputEcho(cli.In())
restoreInput, err := prompt.DisableInputEcho(cli.In())
if err != nil {
return registrytypes.AuthConfig{}, err
}
@ -180,10 +186,13 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
}
}()
out := tui.NewOutput(cli.Err())
out.PrintNote("A Personal Access Token (PAT) can be used instead.\n" +
"To create a PAT, visit " + aec.Underline.Apply("https://app.docker.com/settings") + "\n\n")
argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
if serverAddress == authConfigKey {
out := tui.NewOutput(cli.Err())
out.PrintNote("A Personal Access Token (PAT) can be used instead.\n" +
"To create a PAT, visit " + aec.Underline.Apply("https://app.docker.com/settings") + "\n\n")
}
argPassword, err = prompt.ReadInput(ctx, cli.In(), cli.Out(), "Password: ")
if err != nil {
return registrytypes.AuthConfig{}, err
}
@ -225,9 +234,25 @@ func resolveAuthConfigFromImage(cfg *configfile.ConfigFile, image string) (regis
if err != nil {
return registrytypes.AuthConfig{}, err
}
repoInfo, err := registry.ParseRepositoryInfo(registryRef)
configKey := getAuthConfigKey(reference.Domain(registryRef))
a, err := cfg.GetAuthConfig(configKey)
if err != nil {
return registrytypes.AuthConfig{}, err
}
return ResolveAuthConfig(cfg, repoInfo.Index), nil
return registrytypes.AuthConfig(a), nil
}
// getAuthConfigKey special-cases using the full index address of the official
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
//
// It is similar to [registry.GetAuthConfigKey], but does not require on
// [registrytypes.IndexInfo] as intermediate.
//
// [registry.GetAuthConfigKey]: https://pkg.go.dev/github.com/docker/docker/registry#GetAuthConfigKey
// [registrytypes.IndexInfo]:https://pkg.go.dev/github.com/docker/docker/api/types/registry#IndexInfo
func getAuthConfigKey(domainName string) string {
if domainName == "docker.io" || domainName == "index.docker.io" {
return authConfigKey
}
return domainName
}

View File

@ -11,13 +11,12 @@ import (
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/docker/docker/pkg/stringid"
)
var (
@ -89,7 +88,7 @@ func ServiceProgress(ctx context.Context, apiClient client.APIClient, serviceID
)
for {
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{})
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, swarm.ServiceInspectOptions{})
if err != nil {
return err
}
@ -143,7 +142,7 @@ func ServiceProgress(ctx context.Context, apiClient client.APIClient, serviceID
return nil
}
tasks, err := apiClient.TaskList(ctx, types.TaskListOptions{Filters: filters.NewArgs(
tasks, err := apiClient.TaskList(ctx, swarm.TaskListOptions{Filters: filters.NewArgs(
filters.KeyValuePair{Key: "service", Value: service.ID},
filters.KeyValuePair{Key: "_up-to-date", Value: "true"},
)})
@ -217,7 +216,7 @@ func ServiceProgress(ctx context.Context, apiClient client.APIClient, serviceID
//
// TODO(thaJeztah): this should really be a filter on [apiClient.NodeList] instead of being filtered on the client side.
func getActiveNodes(ctx context.Context, apiClient client.NodeAPIClient) (map[string]struct{}, error) {
nodes, err := apiClient.NodeList(ctx, types.NodeListOptions{})
nodes, err := apiClient.NodeList(ctx, swarm.NodeListOptions{})
if err != nil {
return nil, err
}
@ -506,7 +505,7 @@ func (u *globalProgressUpdater) writeTaskProgress(task swarm.Task, nodeCount int
if task.Status.Err != "" {
u.progressOut.WriteProgress(progress.Progress{
ID: stringid.TruncateID(task.NodeID),
ID: formatter.TruncateID(task.NodeID),
Action: truncError(task.Status.Err),
})
return
@ -514,7 +513,7 @@ func (u *globalProgressUpdater) writeTaskProgress(task swarm.Task, nodeCount int
if !terminalState(task.DesiredState) && !terminalState(task.Status.State) {
u.progressOut.WriteProgress(progress.Progress{
ID: stringid.TruncateID(task.NodeID),
ID: formatter.TruncateID(task.NodeID),
Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State),
Current: numberedStates[task.Status.State],
Total: maxProgress,

View File

@ -4,10 +4,11 @@ import (
"context"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/docker/distribution/uuid"
"github.com/google/uuid"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/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
// OTEL processors may think the same process is restarting
// continuously.
semconv.ServiceInstanceID(uuid.Generate().String()),
semconv.ServiceInstanceID(uuid.NewString()),
),
resource.WithFromEnv(),
resource.WithTelemetrySDK(),
@ -216,3 +217,49 @@ func (r *cliReader) ForceFlush(ctx context.Context) error {
func deltaTemporality(_ sdkmetric.InstrumentKind) metricdata.Temporality {
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

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package command

View File

@ -1,95 +1,45 @@
// 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
//go:build go1.23
package command
import (
"bufio"
"context"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/prompt"
"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/moby/sys/sequential"
"github.com/moby/term"
"github.com/moby/sys/atomicwriter"
"github.com/pkg/errors"
"github.com/spf13/pflag"
)
// CopyToFile writes the content of the reader to the specified file
//
// Deprecated: use [atomicwriter.New].
func CopyToFile(outfile string, r io.Reader) error {
// We use sequential file access here to avoid depleting the standby list
// on Windows. On Linux, this is a call directly to os.CreateTemp
tmpFile, err := sequential.CreateTemp(filepath.Dir(outfile), ".docker_temp_")
writer, err := atomicwriter.New(outfile, 0o600)
if err != nil {
return err
}
tmpPath := tmpFile.Name()
_, err = io.Copy(tmpFile, r)
tmpFile.Close()
if err != nil {
os.Remove(tmpPath)
return err
}
if err = os.Rename(tmpPath, outfile); err != nil {
os.Remove(tmpPath)
return err
}
return nil
defer writer.Close()
_, err = io.Copy(writer, r)
return err
}
// 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"))
const ErrPromptTerminated = prompt.ErrTerminated
// DisableInputEcho disables input echo on the provided streams.In.
// This is useful when the user provides sensitive information like passwords.
// The function returns a restore function that should be called to restore the
// terminal state.
func DisableInputEcho(ins *streams.In) (restore func() error, err error) {
oldState, err := term.SaveState(ins.FD())
if err != nil {
return nil, err
}
restore = func() error {
return term.RestoreTerminal(ins.FD(), oldState)
}
return restore, term.DisableEcho(ins.FD(), oldState)
return prompt.DisableInputEcho(ins)
}
// PromptForInput requests input from the user.
@ -100,23 +50,7 @@ func DisableInputEcho(ins *streams.In) (restore func() error, err error) {
// the stack and close the io.Reader used for the prompt which will prevent the
// background goroutine from blocking indefinitely.
func PromptForInput(ctx context.Context, in io.Reader, out io.Writer, message string) (string, error) {
_, _ = fmt.Fprint(out, message)
result := make(chan string)
go func() {
scanner := bufio.NewScanner(in)
if scanner.Scan() {
result <- strings.TrimSpace(scanner.Text())
}
}()
select {
case <-ctx.Done():
_, _ = fmt.Fprintln(out, "")
return "", ErrPromptTerminated
case r := <-result:
return r, nil
}
return prompt.ReadInput(ctx, in, out, message)
}
// PromptForConfirmation requests and checks confirmation from the user.
@ -130,67 +64,45 @@ func PromptForInput(ctx context.Context, in io.Reader, out io.Writer, message st
// the stack and close the io.Reader used for the prompt which will prevent the
// background goroutine from blocking indefinitely.
func PromptForConfirmation(ctx context.Context, ins io.Reader, outs io.Writer, message string) (bool, error) {
if message == "" {
message = "Are you sure you want to proceed?"
}
message += " [y/N] "
_, _ = fmt.Fprint(outs, message)
// On Windows, force the use of the regular OS stdin stream.
if runtime.GOOS == "windows" {
ins = streams.NewIn(os.Stdin)
}
result := make(chan bool)
go func() {
var res bool
scanner := bufio.NewScanner(ins)
if scanner.Scan() {
answer := strings.TrimSpace(scanner.Text())
if strings.EqualFold(answer, "y") {
res = true
}
}
result <- res
}()
select {
case <-ctx.Done():
_, _ = fmt.Fprintln(outs, "")
return false, ErrPromptTerminated
case r := <-result:
return r, nil
}
return prompt.Confirm(ctx, ins, outs, message)
}
// PruneFilters returns consolidated prune filters obtained from config.json and cli
func PruneFilters(dockerCli Cli, pruneFilters filters.Args) filters.Args {
if dockerCli.ConfigFile() == nil {
// PruneFilters merges prune filters specified in config.json with those specified
// as command-line flags.
//
// CLI label filters have precedence over those specified in config.json. If a
// label filter specified as flag conflicts with a label defined in config.json
// (i.e., "label=some-value" conflicts with "label!=some-value", and vice versa),
// then the filter defined in config.json is omitted.
func PruneFilters(dockerCLI config.Provider, pruneFilters filters.Args) filters.Args {
cfg := dockerCLI.ConfigFile()
if cfg == nil {
return pruneFilters
}
for _, f := range dockerCli.ConfigFile().PruneFilters {
// Merge filters provided through the CLI with default filters defined
// in the CLI-configfile.
for _, f := range cfg.PruneFilters {
k, v, ok := strings.Cut(f, "=")
if !ok {
continue
}
if k == "label" {
// CLI label filter supersede config.json.
// If CLI label filter conflict with config.json,
// skip adding label! filter in config.json.
if pruneFilters.Contains("label!") && pruneFilters.ExactMatch("label!", v) {
switch k {
case "label":
// "label != some-value" conflicts with "label = some-value"
if pruneFilters.ExactMatch("label!", v) {
continue
}
} else if k == "label!" {
// CLI label! filter supersede config.json.
// If CLI label! filter conflict with config.json,
// skip adding label filter in config.json.
if pruneFilters.Contains("label") && pruneFilters.ExactMatch("label", v) {
pruneFilters.Add(k, v)
case "label!":
// "label != some-value" conflicts with "label = some-value"
if pruneFilters.ExactMatch("label", v) {
continue
}
pruneFilters.Add(k, v)
default:
pruneFilters.Add(k, v)
}
pruneFilters.Add(k, v)
}
return pruneFilters
@ -202,7 +114,7 @@ func AddPlatformFlag(flags *pflag.FlagSet, target *string) {
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
}
// ValidateOutputPath validates the output paths of the `export` and `save` commands.
// ValidateOutputPath validates the output paths of the "docker cp" command.
func ValidateOutputPath(path string) error {
dir := filepath.Dir(filepath.Clean(path))
if dir != "" && dir != "." {
@ -228,8 +140,8 @@ func ValidateOutputPath(path string) error {
return nil
}
// ValidateOutputPathFileMode validates the output paths of the `cp` command and serves as a
// helper to `ValidateOutputPath`
// ValidateOutputPathFileMode validates the output paths of the "docker cp" command
// and serves as a helper to [ValidateOutputPath]
func ValidateOutputPathFileMode(fileMode os.FileMode) error {
switch {
case fileMode&os.ModeDevice != 0:
@ -240,47 +152,21 @@ func ValidateOutputPathFileMode(fileMode os.FileMode) error {
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
func invalidParameter(err error) error {
return invalidParameterErr{err}
}
// 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
type invalidParameterErr struct{ error }
func (invalidParameterErr) InvalidParameter() {}
func notFound(err error) error {
return notFoundErr{err}
}
// 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
type notFoundErr struct{ error }
func (notFoundErr) NotFound() {}
func (e notFoundErr) Unwrap() error {
return e.error
}

View File

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package interpolation
@ -67,7 +67,10 @@ func recursiveInterpolate(value any, path Path, opts Options) (any, error) {
return newValue, nil
}
casted, err := caster(newValue)
return casted, newPathError(path, errors.Wrap(err, "failed to cast to expected type"))
if err != nil {
return casted, newPathError(path, errors.Wrap(err, "failed to cast to expected type"))
}
return casted, nil
case map[string]any:
out := map[string]any{}

View File

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package loader

View File

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package loader
@ -18,9 +18,10 @@ import (
"github.com/docker/cli/cli/compose/template"
"github.com/docker/cli/cli/compose/types"
"github.com/docker/cli/opts"
"github.com/docker/cli/opts/swarmopts"
"github.com/docker/docker/api/types/versions"
"github.com/docker/go-connections/nat"
units "github.com/docker/go-units"
"github.com/docker/go-units"
"github.com/go-viper/mapstructure/v2"
"github.com/google/shlex"
"github.com/pkg/errors"
@ -925,7 +926,7 @@ func toServicePortConfigs(value string) ([]any, error) {
for _, key := range keys {
// Reuse ConvertPortToPortConfig so that it is consistent
portConfig, err := opts.ConvertPortToPortConfig(nat.Port(key), portBindings)
portConfig, err := swarmopts.ConvertPortToPortConfig(nat.Port(key), portBindings)
if err != nil {
return nil, err
}

View File

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package loader

View File

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package schema

View File

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package template
@ -7,6 +7,8 @@ import (
"fmt"
"regexp"
"strings"
"github.com/docker/cli/internal/lazyregexp"
)
const (
@ -14,11 +16,21 @@ const (
subst = "[_a-z][_a-z0-9]*(?::?[-?][^}]*)?"
)
var defaultPattern = regexp.MustCompile(fmt.Sprintf(
var defaultPattern = lazyregexp.New(fmt.Sprintf(
"%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))",
delimiter, delimiter, subst, subst,
))
// regexper is an internal interface to allow passing a [lazyregexp.Regexp]
// in places where a custom ("regular") [regexp.Regexp] is accepted. It defines
// only the methods we currently use.
type regexper interface {
FindAllStringSubmatch(s string, n int) [][]string
FindStringSubmatch(s string) []string
ReplaceAllStringFunc(src string, repl func(string) string) string
SubexpNames() []string
}
// DefaultSubstituteFuncs contains the default SubstituteFunc used by the docker cli
var DefaultSubstituteFuncs = []SubstituteFunc{
softDefault,
@ -51,10 +63,16 @@ type SubstituteFunc func(string, Mapping) (string, bool, error)
// SubstituteWith substitutes variables in the string with their values.
// It accepts additional substitute function.
func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, subsFuncs ...SubstituteFunc) (string, error) {
return substituteWith(template, mapping, pattern, subsFuncs...)
}
// SubstituteWith substitutes variables in the string with their values.
// It accepts additional substitute function.
func substituteWith(template string, mapping Mapping, pattern regexper, subsFuncs ...SubstituteFunc) (string, error) {
var err error
result := pattern.ReplaceAllStringFunc(template, func(substring string) string {
matches := pattern.FindStringSubmatch(substring)
groups := matchGroups(matches, pattern)
groups := matchGroups(matches, defaultPattern)
if escaped := groups["escaped"]; escaped != "" {
return escaped
}
@ -93,38 +111,42 @@ func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, su
// Substitute variables in the string with their values
func Substitute(template string, mapping Mapping) (string, error) {
return SubstituteWith(template, mapping, defaultPattern, DefaultSubstituteFuncs...)
return substituteWith(template, mapping, defaultPattern, DefaultSubstituteFuncs...)
}
// ExtractVariables returns a map of all the variables defined in the specified
// composefile (dict representation) and their default value if any.
func ExtractVariables(configDict map[string]any, pattern *regexp.Regexp) map[string]string {
return extractVariables(configDict, pattern)
}
func extractVariables(configDict map[string]any, pattern regexper) map[string]string {
if pattern == nil {
pattern = defaultPattern
}
return recurseExtract(configDict, pattern)
}
func recurseExtract(value any, pattern *regexp.Regexp) map[string]string {
func recurseExtract(value any, pattern regexper) map[string]string {
m := map[string]string{}
switch value := value.(type) {
switch val := value.(type) {
case string:
if values, is := extractVariable(value, pattern); is {
if values, is := extractVariable(val, pattern); is {
for _, v := range values {
m[v.name] = v.value
}
}
case map[string]any:
for _, elem := range value {
for _, elem := range val {
submap := recurseExtract(elem, pattern)
for key, value := range submap {
m[key] = value
for k, v := range submap {
m[k] = v
}
}
case []any:
for _, elem := range value {
for _, elem := range val {
if values, is := extractVariable(elem, pattern); is {
for _, v := range values {
m[v.name] = v.value
@ -141,7 +163,7 @@ type extractedValue struct {
value string
}
func extractVariable(value any, pattern *regexp.Regexp) ([]extractedValue, bool) {
func extractVariable(value any, pattern regexper) ([]extractedValue, bool) {
sValue, ok := value.(string)
if !ok {
return []extractedValue{}, false
@ -227,7 +249,7 @@ func withRequired(substitution string, mapping Mapping, sep string, valid func(s
return value, true, nil
}
func matchGroups(matches []string, pattern *regexp.Regexp) map[string]string {
func matchGroups(matches []string, pattern regexper) map[string]string {
groups := make(map[string]string)
for i, name := range pattern.SubexpNames()[1:] {
groups[name] = matches[i+1]

View File

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package types

View File

@ -58,7 +58,7 @@ func resetConfigDir() {
// getHomeDir is a copy of [pkg/homedir.Get] to prevent adding docker/docker
// as dependency for consumers that only need to read the config-file.
//
// [pkg/homedir.Get]: https://pkg.go.dev/github.com/docker/docker@v26.1.4+incompatible/pkg/homedir#Get
// [pkg/homedir.Get]: https://pkg.go.dev/github.com/docker/docker@v28.0.3+incompatible/pkg/homedir#Get
func getHomeDir() string {
home, _ := os.UserHomeDir()
if home == "" && runtime.GOOS != "windows" {
@ -69,6 +69,11 @@ func getHomeDir() string {
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
func Dir() string {
initConfigDir.Do(func() {

View File

@ -3,12 +3,14 @@ package configfile
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/config/memorystore"
"github.com/docker/cli/cli/config/types"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@ -36,14 +38,41 @@ type ConfigFile struct {
NodesFormat string `json:"nodesFormat,omitempty"`
PruneFilters []string `json:"pruneFilters,omitempty"`
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
Experimental string `json:"experimental,omitempty"`
CurrentContext string `json:"currentContext,omitempty"`
CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"`
Plugins map[string]map[string]string `json:"plugins,omitempty"`
Aliases map[string]string `json:"aliases,omitempty"`
Features map[string]string `json:"features,omitempty"`
// Deprecated: experimental CLI features are always enabled and this field is no longer used. Use [Features] instead for optional features. This field will be removed in a future release.
Experimental string `json:"experimental,omitempty"`
}
type configEnvAuth struct {
Auth string `json:"auth"`
}
type configEnv struct {
AuthConfigs map[string]configEnvAuth `json:"auths"`
}
// DockerEnvConfigKey is an environment variable that contains a JSON encoded
// credential config. It only supports storing the credentials as a base64
// encoded string in the format base64("username:pat").
//
// Adding additional fields will produce a parsing error.
//
// Example:
//
// {
// "auths": {
// "example.test": {
// "auth": base64-encoded-username-pat
// }
// }
// }
const DockerEnvConfigKey = "DOCKER_AUTH_CONFIG"
// ProxyConfig contains proxy configuration settings
type ProxyConfig struct {
HTTPProxy string `json:"httpProxy,omitempty"`
@ -150,7 +179,8 @@ func (configFile *ConfigFile) Save() (retErr error) {
return err
}
defer func() {
temp.Close()
// ignore error as the file may already be closed when we reach this.
_ = temp.Close()
if retErr != nil {
if err := os.Remove(temp.Name()); err != nil {
logrus.WithError(err).WithField("file", temp.Name()).Debug("Error cleaning up temp file")
@ -167,10 +197,16 @@ func (configFile *ConfigFile) Save() (retErr error) {
return errors.Wrap(err, "error closing temp file")
}
// Handle situation where the configfile is a symlink
// Handle situation where the configfile is a symlink, and allow for dangling symlinks
cfgFile := configFile.Filename
if f, err := os.Readlink(cfgFile); err == nil {
if f, err := filepath.EvalSymlinks(cfgFile); err == nil {
cfgFile = f
} else if os.IsNotExist(err) {
// extract the path from the error if the configfile does not exist or is a dangling symlink
var pathError *os.PathError
if errors.As(err, &pathError) {
cfgFile = pathError.Path
}
}
// Try copying the current config file (if any) ownership and permissions
@ -254,10 +290,64 @@ func decodeAuth(authStr string) (string, string, error) {
// GetCredentialsStore returns a new credentials store from the settings in the
// configuration file
func (configFile *ConfigFile) GetCredentialsStore(registryHostname string) credentials.Store {
store := credentials.NewFileStore(configFile)
if helper := getConfiguredCredentialStore(configFile, registryHostname); helper != "" {
return newNativeStore(configFile, helper)
store = newNativeStore(configFile, helper)
}
return credentials.NewFileStore(configFile)
envConfig := os.Getenv(DockerEnvConfigKey)
if envConfig == "" {
return store
}
authConfig, err := parseEnvConfig(envConfig)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to create credential store from DOCKER_AUTH_CONFIG: ", err)
return store
}
// use DOCKER_AUTH_CONFIG if set
// it uses the native or file store as a fallback to fetch and store credentials
envStore, err := memorystore.New(
memorystore.WithAuthConfig(authConfig),
memorystore.WithFallbackStore(store),
)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to create credential store from DOCKER_AUTH_CONFIG: ", err)
return store
}
return envStore
}
func parseEnvConfig(v string) (map[string]types.AuthConfig, error) {
envConfig := &configEnv{}
decoder := json.NewDecoder(strings.NewReader(v))
decoder.DisallowUnknownFields()
if err := decoder.Decode(envConfig); err != nil && !errors.Is(err, io.EOF) {
return nil, err
}
if decoder.More() {
return nil, errors.New("DOCKER_AUTH_CONFIG does not support more than one JSON object")
}
authConfigs := make(map[string]types.AuthConfig)
for addr, envAuth := range envConfig.AuthConfigs {
if envAuth.Auth == "" {
return nil, fmt.Errorf("DOCKER_AUTH_CONFIG environment variable is missing key `auth` for %s", addr)
}
username, password, err := decodeAuth(envAuth.Auth)
if err != nil {
return nil, err
}
authConfigs[addr] = types.AuthConfig{
Username: username,
Password: password,
ServerAddress: addr,
}
}
return authConfigs, nil
}
// var for unit testing.

View File

@ -0,0 +1,126 @@
//go:build go1.23
package memorystore
import (
"errors"
"fmt"
"maps"
"os"
"sync"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/config/types"
)
var errValueNotFound = errors.New("value not found")
func IsErrValueNotFound(err error) bool {
return errors.Is(err, errValueNotFound)
}
type Config struct {
lock sync.RWMutex
memoryCredentials map[string]types.AuthConfig
fallbackStore credentials.Store
}
func (e *Config) Erase(serverAddress string) error {
e.lock.Lock()
defer e.lock.Unlock()
delete(e.memoryCredentials, serverAddress)
if e.fallbackStore != nil {
err := e.fallbackStore.Erase(serverAddress)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "memorystore: ", err)
}
}
return nil
}
func (e *Config) Get(serverAddress string) (types.AuthConfig, error) {
e.lock.RLock()
defer e.lock.RUnlock()
authConfig, ok := e.memoryCredentials[serverAddress]
if !ok {
if e.fallbackStore != nil {
return e.fallbackStore.Get(serverAddress)
}
return types.AuthConfig{}, errValueNotFound
}
return authConfig, nil
}
func (e *Config) GetAll() (map[string]types.AuthConfig, error) {
e.lock.RLock()
defer e.lock.RUnlock()
creds := make(map[string]types.AuthConfig)
if e.fallbackStore != nil {
fileCredentials, err := e.fallbackStore.GetAll()
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "memorystore: ", err)
} else {
creds = fileCredentials
}
}
maps.Copy(creds, e.memoryCredentials)
return creds, nil
}
func (e *Config) Store(authConfig types.AuthConfig) error {
e.lock.Lock()
defer e.lock.Unlock()
e.memoryCredentials[authConfig.ServerAddress] = authConfig
if e.fallbackStore != nil {
return e.fallbackStore.Store(authConfig)
}
return nil
}
// WithFallbackStore sets a fallback store.
//
// Write operations will be performed on both the memory store and the
// fallback store.
//
// Read operations will first check the memory store, and if the credential
// is not found, it will then check the fallback store.
//
// Retrieving all credentials will return from both the memory store and the
// fallback store, merging the results from both stores into a single map.
//
// Data stored in the memory store will take precedence over data in the
// fallback store.
func WithFallbackStore(store credentials.Store) Options {
return func(s *Config) error {
s.fallbackStore = store
return nil
}
}
// WithAuthConfig allows to set the initial credentials in the memory store.
func WithAuthConfig(config map[string]types.AuthConfig) Options {
return func(s *Config) error {
s.memoryCredentials = config
return nil
}
}
type Options func(*Config) error
// New creates a new in memory credential store
func New(opts ...Options) (credentials.Store, error) {
m := &Config{
memoryCredentials: make(map[string]types.AuthConfig),
}
for _, opt := range opts {
if err := opt(m); err != nil {
return nil, err
}
}
return m, nil
}

View File

@ -28,7 +28,6 @@ import (
"syscall"
"time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
@ -149,7 +148,7 @@ func (c *commandConn) handleEOF(err error) error {
c.stderrMu.Lock()
stderr := c.stderr.String()
c.stderrMu.Unlock()
return errors.Errorf("command %v did not exit after %v: stderr=%q", c.cmd.Args, err, stderr)
return fmt.Errorf("command %v did not exit after %v: stderr=%q", c.cmd.Args, err, stderr)
}
}
@ -159,7 +158,7 @@ func (c *commandConn) handleEOF(err error) error {
c.stderrMu.Lock()
stderr := c.stderr.String()
c.stderrMu.Unlock()
return errors.Errorf("command %v has exited with %v, make sure the URL is valid, and Docker 18.09 or later is installed on the remote host: stderr=%s", c.cmd.Args, werr, stderr)
return fmt.Errorf("command %v has exited with %v, make sure the URL is valid, and Docker 18.09 or later is installed on the remote host: stderr=%s", c.cmd.Args, werr, stderr)
}
func ignorableCloseError(err error) bool {

View File

@ -3,13 +3,13 @@ package connhelper
import (
"context"
"fmt"
"net"
"net/url"
"strings"
"github.com/docker/cli/cli/connhelper/commandconn"
"github.com/docker/cli/cli/connhelper/ssh"
"github.com/pkg/errors"
)
// ConnectionHelper allows to connect to a remote host with custom stream provider binary.
@ -41,20 +41,25 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*ConnectionHelper
return nil, err
}
if u.Scheme == "ssh" {
sp, err := ssh.ParseURL(daemonURL)
sp, err := ssh.NewSpec(u)
if err != nil {
return nil, errors.Wrap(err, "ssh host connection is not valid")
return nil, fmt.Errorf("ssh host connection is not valid: %w", err)
}
sshFlags = addSSHTimeout(sshFlags)
sshFlags = disablePseudoTerminalAllocation(sshFlags)
remoteCommand := []string{"docker", "system", "dial-stdio"}
socketPath := sp.Path
if strings.Trim(sp.Path, "/") != "" {
remoteCommand = []string{"docker", "--host=unix://" + socketPath, "system", "dial-stdio"}
}
sshArgs, err := sp.Command(sshFlags, remoteCommand...)
if err != nil {
return nil, err
}
return &ConnectionHelper{
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
args := []string{"docker"}
if sp.Path != "" {
args = append(args, "--host", "unix://"+sp.Path)
}
args = append(args, "system", "dial-stdio")
return commandconn.New(ctx, "ssh", append(sshFlags, sp.Args(args...)...)...)
return commandconn.New(ctx, "ssh", sshArgs...)
},
Host: "http://docker.example.com",
}, nil

View File

@ -0,0 +1,27 @@
Copyright (c) 2016, Daniel Martí. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,13 @@
// Package syntax is a fork of [mvdan.cc/sh/v3@v3.10.0/syntax].
//
// Copyright (c) 2016, Daniel Martí. All rights reserved.
//
// It is a reduced set of the package to only provide the [Quote] function,
// and contains the [LICENSE], [quote.go] and [parser.go] files at the given
// revision.
//
// [quote.go]: https://raw.githubusercontent.com/mvdan/sh/refs/tags/v3.10.0/syntax/quote.go
// [parser.go]: https://raw.githubusercontent.com/mvdan/sh/refs/tags/v3.10.0/syntax/parser.go
// [LICENSE]: https://raw.githubusercontent.com/mvdan/sh/refs/tags/v3.10.0/LICENSE
// [mvdan.cc/sh/v3@v3.10.0/syntax]: https://pkg.go.dev/mvdan.cc/sh/v3@v3.10.0/syntax
package syntax

View File

@ -0,0 +1,95 @@
// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package syntax
// LangVariant describes a shell language variant to use when tokenizing and
// parsing shell code. The zero value is [LangBash].
type LangVariant int
const (
// LangBash corresponds to the GNU Bash language, as described in its
// manual at https://www.gnu.org/software/bash/manual/bash.html.
//
// We currently follow Bash version 5.2.
//
// Its string representation is "bash".
LangBash LangVariant = iota
// LangPOSIX corresponds to the POSIX Shell language, as described at
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html.
//
// Its string representation is "posix" or "sh".
LangPOSIX
// LangMirBSDKorn corresponds to the MirBSD Korn Shell, also known as
// mksh, as described at http://www.mirbsd.org/htman/i386/man1/mksh.htm.
// Note that it shares some features with Bash, due to the shared
// ancestry that is ksh.
//
// We currently follow mksh version 59.
//
// Its string representation is "mksh".
LangMirBSDKorn
// LangBats corresponds to the Bash Automated Testing System language,
// as described at https://github.com/bats-core/bats-core. Note that
// it's just a small extension of the Bash language.
//
// Its string representation is "bats".
LangBats
// LangAuto corresponds to automatic language detection,
// commonly used by end-user applications like shfmt,
// which can guess a file's language variant given its filename or shebang.
//
// At this time, [Variant] does not support LangAuto.
LangAuto
)
func (l LangVariant) String() string {
switch l {
case LangBash:
return "bash"
case LangPOSIX:
return "posix"
case LangMirBSDKorn:
return "mksh"
case LangBats:
return "bats"
case LangAuto:
return "auto"
}
return "unknown shell language variant"
}
// IsKeyword returns true if the given word is part of the language keywords.
func IsKeyword(word string) bool {
// This list has been copied from the bash 5.1 source code, file y.tab.c +4460
switch word {
case
"!",
"[[", // only if COND_COMMAND is defined
"]]", // only if COND_COMMAND is defined
"case",
"coproc", // only if COPROCESS_SUPPORT is defined
"do",
"done",
"else",
"esac",
"fi",
"for",
"function",
"if",
"in",
"select", // only if SELECT_COMMAND is defined
"then",
"time", // only if COMMAND_TIMING is defined
"until",
"while",
"{",
"}":
return true
}
return false
}

View File

@ -0,0 +1,187 @@
// Copyright (c) 2021, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package syntax
import (
"fmt"
"strings"
"unicode"
"unicode/utf8"
)
type QuoteError struct {
ByteOffset int
Message string
}
func (e QuoteError) Error() string {
return fmt.Sprintf("cannot quote character at byte %d: %s", e.ByteOffset, e.Message)
}
const (
quoteErrNull = "shell strings cannot contain null bytes"
quoteErrPOSIX = "POSIX shell lacks escape sequences"
quoteErrRange = "rune out of range"
quoteErrMksh = "mksh cannot escape codepoints above 16 bits"
)
// Quote returns a quoted version of the input string,
// so that the quoted version is expanded or interpreted
// as the original string in the given language variant.
//
// Quoting is necessary when using arbitrary literal strings
// as words in a shell script or command.
// Without quoting, one can run into syntax errors,
// as well as the possibility of running unintended code.
//
// An error is returned when a string cannot be quoted for a variant.
// For instance, POSIX lacks escape sequences for non-printable characters,
// and no language variant can represent a string containing null bytes.
// In such cases, the returned error type will be *QuoteError.
//
// The quoting strategy is chosen on a best-effort basis,
// to minimize the amount of extra bytes necessary.
//
// Some strings do not require any quoting and are returned unchanged.
// Those strings can be directly surrounded in single quotes as well.
//
//nolint:gocyclo // ignore "cyclomatic complexity 35 of func `Quote` is high (> 16) (gocyclo)"
func Quote(s string, lang LangVariant) (string, error) {
if s == "" {
// Special case; an empty string must always be quoted,
// as otherwise it expands to zero fields.
return "''", nil
}
shellChars := false
nonPrintable := false
offs := 0
for rem := s; len(rem) > 0; {
r, size := utf8.DecodeRuneInString(rem)
switch r {
// Like regOps; token characters.
case ';', '"', '\'', '(', ')', '$', '|', '&', '>', '<', '`',
// Whitespace; might result in multiple fields.
' ', '\t', '\r', '\n',
// Escape sequences would be expanded.
'\\',
// Would start a comment unless quoted.
'#',
// Might result in brace expansion.
'{',
// Might result in tilde expansion.
'~',
// Might result in globbing.
'*', '?', '[',
// Might result in an assignment.
'=':
shellChars = true
case '\x00':
return "", &QuoteError{ByteOffset: offs, Message: quoteErrNull}
}
if r == utf8.RuneError || !unicode.IsPrint(r) {
if lang == LangPOSIX {
return "", &QuoteError{ByteOffset: offs, Message: quoteErrPOSIX}
}
nonPrintable = true
}
rem = rem[size:]
offs += size
}
if !shellChars && !nonPrintable && !IsKeyword(s) {
// Nothing to quote; avoid allocating.
return s, nil
}
// Single quotes are usually best,
// as they don't require any escaping of characters.
// If we have any invalid utf8 or non-printable runes,
// use $'' so that we can escape them.
// Note that we can't use double quotes for those.
var b strings.Builder
if nonPrintable {
b.WriteString("$'")
lastRequoteIfHex := false
offs = 0
for rem := s; len(rem) > 0; {
nextRequoteIfHex := false
r, size := utf8.DecodeRuneInString(rem)
switch {
case r == '\'', r == '\\':
b.WriteByte('\\')
b.WriteRune(r)
case unicode.IsPrint(r) && r != utf8.RuneError:
if lastRequoteIfHex && isHex(r) {
b.WriteString("'$'")
}
b.WriteRune(r)
case r == '\a':
b.WriteString(`\a`)
case r == '\b':
b.WriteString(`\b`)
case r == '\f':
b.WriteString(`\f`)
case r == '\n':
b.WriteString(`\n`)
case r == '\r':
b.WriteString(`\r`)
case r == '\t':
b.WriteString(`\t`)
case r == '\v':
b.WriteString(`\v`)
case r < utf8.RuneSelf, r == utf8.RuneError && size == 1:
// \xXX, fixed at two hexadecimal characters.
fmt.Fprintf(&b, "\\x%02x", rem[0])
// Unfortunately, mksh allows \x to consume more hex characters.
// Ensure that we don't allow it to read more than two.
if lang == LangMirBSDKorn {
nextRequoteIfHex = true
}
case r > utf8.MaxRune:
// Not a valid Unicode code point?
return "", &QuoteError{ByteOffset: offs, Message: quoteErrRange}
case lang == LangMirBSDKorn && r > 0xFFFD:
// From the CAVEATS section in R59's man page:
//
// mksh currently uses OPTU-16 internally, which is the same as
// UTF-8 and CESU-8 with 0000..FFFD being valid codepoints.
return "", &QuoteError{ByteOffset: offs, Message: quoteErrMksh}
case r < 0x10000:
// \uXXXX, fixed at four hexadecimal characters.
fmt.Fprintf(&b, "\\u%04x", r)
default:
// \UXXXXXXXX, fixed at eight hexadecimal characters.
fmt.Fprintf(&b, "\\U%08x", r)
}
rem = rem[size:]
lastRequoteIfHex = nextRequoteIfHex
offs += size
}
b.WriteString("'")
return b.String(), nil
}
// Single quotes without any need for escaping.
if !strings.Contains(s, "'") {
return "'" + s + "'", nil
}
// The string contains single quotes,
// so fall back to double quotes.
b.WriteByte('"')
for _, r := range s {
switch r {
case '"', '\\', '`', '$':
b.WriteByte('\\')
}
b.WriteRune(r)
}
b.WriteByte('"')
return b.String(), nil
}
func isHex(r rune) bool {
return (r >= '0' && r <= '9') ||
(r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F')
}

View File

@ -2,19 +2,48 @@
package ssh
import (
"errors"
"fmt"
"net/url"
"github.com/pkg/errors"
"github.com/docker/cli/cli/connhelper/internal/syntax"
)
// ParseURL parses URL
// ParseURL creates a [Spec] from the given ssh URL. It returns an error if
// the URL is using the wrong scheme, contains fragments, query-parameters,
// or contains a password.
func ParseURL(daemonURL string) (*Spec, error) {
u, err := url.Parse(daemonURL)
if err != nil {
return nil, err
var urlErr *url.Error
if errors.As(err, &urlErr) {
err = urlErr.Unwrap()
}
return nil, fmt.Errorf("invalid SSH URL: %w", err)
}
return NewSpec(u)
}
// NewSpec creates a [Spec] from the given ssh URL's properties. It returns
// an error if the URL is using the wrong scheme, contains fragments,
// query-parameters, or contains a password.
func NewSpec(sshURL *url.URL) (*Spec, error) {
s, err := newSpec(sshURL)
if err != nil {
return nil, fmt.Errorf("invalid SSH URL: %w", err)
}
return s, nil
}
func newSpec(u *url.URL) (*Spec, error) {
if u == nil {
return nil, errors.New("URL is nil")
}
if u.Scheme == "" {
return nil, errors.New("no scheme provided")
}
if u.Scheme != "ssh" {
return nil, errors.Errorf("expected scheme ssh, got %q", u.Scheme)
return nil, errors.New("incorrect scheme: " + u.Scheme)
}
var sp Spec
@ -27,17 +56,18 @@ func ParseURL(daemonURL string) (*Spec, error) {
}
sp.Host = u.Hostname()
if sp.Host == "" {
return nil, errors.Errorf("no host specified")
return nil, errors.New("hostname is empty")
}
sp.Port = u.Port()
sp.Path = u.Path
if u.RawQuery != "" {
return nil, errors.Errorf("extra query after the host: %q", u.RawQuery)
return nil, fmt.Errorf("query parameters are not allowed: %q", u.RawQuery)
}
if u.Fragment != "" {
return nil, errors.Errorf("extra fragment after the host: %q", u.Fragment)
return nil, fmt.Errorf("fragments are not allowed: %q", u.Fragment)
}
return &sp, err
return &sp, nil
}
// Spec of SSH URL
@ -48,16 +78,106 @@ type Spec struct {
Path string
}
// Args returns args except "ssh" itself combined with optional additional command args
func (sp *Spec) Args(add ...string) []string {
// Args returns args except "ssh" itself combined with optional additional
// command and args to be executed on the remote host. It attempts to quote
// the given arguments to account for ssh executing the remote command in a
// shell. It returns nil when unable to quote the remote command.
func (sp *Spec) Args(remoteCommandAndArgs ...string) []string {
// Format the remote command to run using the ssh connection, quoting
// values where needed because ssh executes these in a POSIX shell.
remoteCommand, err := quoteCommand(remoteCommandAndArgs...)
if err != nil {
return nil
}
sshArgs, err := sp.args()
if err != nil {
return nil
}
if remoteCommand != "" {
sshArgs = append(sshArgs, remoteCommand)
}
return sshArgs
}
func (sp *Spec) args(sshFlags ...string) ([]string, error) {
var args []string
if sp.Host == "" {
return nil, errors.New("no host specified")
}
if sp.User != "" {
args = append(args, "-l", sp.User)
// Quote user, as it's obtained from the URL.
usr, err := syntax.Quote(sp.User, syntax.LangPOSIX)
if err != nil {
return nil, fmt.Errorf("invalid user: %w", err)
}
args = append(args, "-l", usr)
}
if sp.Port != "" {
args = append(args, "-p", sp.Port)
// Quote port, as it's obtained from the URL.
port, err := syntax.Quote(sp.Port, syntax.LangPOSIX)
if err != nil {
return nil, fmt.Errorf("invalid port: %w", err)
}
args = append(args, "-p", port)
}
args = append(args, "--", sp.Host)
args = append(args, add...)
return args
// We consider "sshFlags" to be "trusted", and set from code only,
// as they are not parsed from the DOCKER_HOST URL.
args = append(args, sshFlags...)
host, err := syntax.Quote(sp.Host, syntax.LangPOSIX)
if err != nil {
return nil, fmt.Errorf("invalid host: %w", err)
}
return append(args, "--", host), nil
}
// Command returns the ssh flags and arguments to execute a command
// (remoteCommandAndArgs) on the remote host. Where needed, it quotes
// values passed in remoteCommandAndArgs to account for ssh executing
// the remote command in a shell. It returns an error if no remote command
// is passed, or when unable to quote the remote command.
//
// Important: to preserve backward-compatibility, Command does not currently
// perform sanitization or quoting on the sshFlags and callers are expected
// to sanitize this argument.
func (sp *Spec) Command(sshFlags []string, remoteCommandAndArgs ...string) ([]string, error) {
if len(remoteCommandAndArgs) == 0 {
return nil, errors.New("no remote command specified")
}
sshArgs, err := sp.args(sshFlags...)
if err != nil {
return nil, err
}
remoteCommand, err := quoteCommand(remoteCommandAndArgs...)
if err != nil {
return nil, err
}
if remoteCommand != "" {
sshArgs = append(sshArgs, remoteCommand)
}
return sshArgs, nil
}
// quoteCommand returns the remote command to run using the ssh connection
// as a single string, quoting values where needed because ssh executes
// these in a POSIX shell.
func quoteCommand(commandAndArgs ...string) (string, error) {
var quotedCmd string
for i, arg := range commandAndArgs {
a, err := syntax.Quote(arg, syntax.LangPOSIX)
if err != nil {
return "", fmt.Errorf("invalid argument: %w", err)
}
if i == 0 {
quotedCmd = a
continue
}
quotedCmd += " " + a
}
// each part is quoted appropriately, so now we'll have a full
// shell command to pass off to "ssh"
return quotedCmd, nil
}

View File

@ -6,6 +6,7 @@ import (
"encoding/pem"
"net"
"net/http"
"strings"
"time"
"github.com/docker/cli/cli/connhelper"
@ -90,14 +91,19 @@ func (ep *Endpoint) ClientOpts() ([]client.Opt, error) {
return nil, err
}
if helper == nil {
tlsConfig, err := ep.tlsConfig()
if err != nil {
return nil, err
// Check if we're connecting over a socket, because there's no
// need to configure TLS for a socket connection.
//
// TODO(thaJeztah); make resolveDockerEndpoint and resolveDefaultDockerEndpoint not load TLS data,
// and load TLS files lazily; see https://github.com/docker/cli/pull/1581
if !isSocket(ep.Host) {
tlsConfig, err := ep.tlsConfig()
if err != nil {
return nil, err
}
result = append(result, withHTTPClient(tlsConfig))
}
result = append(result,
withHTTPClient(tlsConfig),
client.WithHost(ep.Host),
)
result = append(result, client.WithHost(ep.Host))
} else {
result = append(result,
client.WithHTTPClient(&http.Client{
@ -116,6 +122,17 @@ func (ep *Endpoint) ClientOpts() ([]client.Opt, error) {
return result, nil
}
// isSocket checks if the given address is a Unix-socket (linux),
// named pipe (Windows), or file-descriptor.
func isSocket(addr string) bool {
switch proto, _, _ := strings.Cut(addr, "://"); proto {
case "unix", "npipe", "fd":
return true
default:
return false
}
}
func withHTTPClient(tlsConfig *tls.Config) func(*client.Client) error {
return func(c *client.Client) error {
if tlsConfig == nil {

View File

@ -0,0 +1,28 @@
package store
import cerrdefs "github.com/containerd/errdefs"
func invalidParameter(err error) error {
if err == nil || cerrdefs.IsInvalidArgument(err) {
return err
}
return invalidParameterErr{err}
}
type invalidParameterErr struct{ error }
func (invalidParameterErr) InvalidParameter() {}
func notFound(err error) error {
if err == nil || cerrdefs.IsNotFound(err) {
return err
}
return notFoundErr{err}
}
type notFoundErr struct{ error }
func (notFoundErr) NotFound() {}
func (e notFoundErr) Unwrap() error {
return e.error
}

View File

@ -5,14 +5,14 @@ import (
"io"
)
// LimitedReader is a fork of io.LimitedReader to override Read.
type LimitedReader struct {
// limitedReader is a fork of [io.LimitedReader] to override Read.
type limitedReader struct {
R io.Reader
N int64 // max bytes remaining
}
// Read is a fork of io.LimitedReader.Read that returns an error when limit exceeded.
func (l *LimitedReader) Read(p []byte) (n int, err error) {
// Read is a fork of [io.LimitedReader.Read] that returns an error when limit exceeded.
func (l *limitedReader) Read(p []byte) (n int, err error) {
if l.N < 0 {
return 0, errors.New("read exceeds the defined limit")
}

View File

@ -1,20 +1,19 @@
// 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
//go:build go1.23
package store
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/atomicwriter"
"github.com/fvbommel/sortorder"
"github.com/pkg/errors"
"github.com/moby/sys/atomicwriter"
)
const (
@ -64,7 +63,7 @@ func parseTypedOrMap(payload []byte, getter TypeGetter) (any, error) {
func (s *metadataStore) get(name string) (Metadata, error) {
m, err := s.getByID(contextdirOf(name))
if err != nil {
return m, errors.Wrapf(err, "context %q", name)
return m, fmt.Errorf("context %q: %w", name, err)
}
return m, nil
}
@ -74,7 +73,7 @@ func (s *metadataStore) getByID(id contextdir) (Metadata, error) {
bytes, err := os.ReadFile(fileName)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return Metadata{}, errdefs.NotFound(errors.Wrap(err, "context not found"))
return Metadata{}, notFound(fmt.Errorf("context not found: %w", err))
}
return Metadata{}, err
}
@ -99,7 +98,7 @@ func (s *metadataStore) getByID(id contextdir) (Metadata, error) {
func (s *metadataStore) remove(name string) error {
if err := os.RemoveAll(s.contextDir(contextdirOf(name))); err != nil {
return errors.Wrapf(err, "failed to remove metadata")
return fmt.Errorf("failed to remove metadata: %w", err)
}
return nil
}
@ -119,7 +118,7 @@ func (s *metadataStore) list() ([]Metadata, error) {
if errors.Is(err, os.ErrNotExist) {
continue
}
return nil, errors.Wrap(err, "failed to read metadata")
return nil, fmt.Errorf("failed to read metadata: %w", err)
}
res = append(res, c)
}

View File

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package store
@ -10,21 +10,21 @@ import (
"bytes"
_ "crypto/sha256" // ensure ids can be computed
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/docker/docker/errdefs"
"github.com/docker/cli/internal/lazyregexp"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
const restrictedNamePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$"
var restrictedNameRegEx = regexp.MustCompile(restrictedNamePattern)
var restrictedNameRegEx = lazyregexp.New(restrictedNamePattern)
// Store provides a context store for easily remembering endpoints configuration
type Store interface {
@ -146,10 +146,10 @@ func (s *ContextStore) CreateOrUpdate(meta Metadata) error {
// Remove deletes the context with the given name, if found.
func (s *ContextStore) Remove(name string) error {
if err := s.meta.remove(name); err != nil {
return errors.Wrapf(err, "failed to remove context %s", name)
return fmt.Errorf("failed to remove context %s: %w", name, err)
}
if err := s.tls.remove(name); err != nil {
return errors.Wrapf(err, "failed to remove context %s", name)
return fmt.Errorf("failed to remove context %s: %w", name, err)
}
return nil
}
@ -226,7 +226,7 @@ func ValidateContextName(name string) error {
return errors.New(`"default" is a reserved context name`)
}
if !restrictedNameRegEx.MatchString(name) {
return errors.Errorf("context name %q is invalid, names are validated against regexp %q", name, restrictedNamePattern)
return fmt.Errorf("context name %q is invalid, names are validated against regexp %q", name, restrictedNamePattern)
}
return nil
}
@ -356,7 +356,7 @@ func isValidFilePath(p string) error {
}
func importTar(name string, s Writer, reader io.Reader) error {
tr := tar.NewReader(&LimitedReader{R: reader, N: maxAllowedFileSizeToImport})
tr := tar.NewReader(&limitedReader{R: reader, N: maxAllowedFileSizeToImport})
tlsData := ContextTLSData{
Endpoints: map[string]EndpointTLSData{},
}
@ -374,7 +374,7 @@ func importTar(name string, s Writer, reader io.Reader) error {
continue
}
if err := isValidFilePath(hdr.Name); err != nil {
return errors.Wrap(err, hdr.Name)
return fmt.Errorf("%s: %w", hdr.Name, err)
}
if hdr.Name == metaFile {
data, err := io.ReadAll(tr)
@ -400,13 +400,13 @@ func importTar(name string, s Writer, reader io.Reader) error {
}
}
if !importedMetaFile {
return errdefs.InvalidParameter(errors.New("invalid context: no metadata found"))
return invalidParameter(errors.New("invalid context: no metadata found"))
}
return s.ResetTLSMaterial(name, &tlsData)
}
func importZip(name string, s Writer, reader io.Reader) error {
body, err := io.ReadAll(&LimitedReader{R: reader, N: maxAllowedFileSizeToImport})
body, err := io.ReadAll(&limitedReader{R: reader, N: maxAllowedFileSizeToImport})
if err != nil {
return err
}
@ -426,7 +426,7 @@ func importZip(name string, s Writer, reader io.Reader) error {
continue
}
if err := isValidFilePath(zf.Name); err != nil {
return errors.Wrap(err, zf.Name)
return fmt.Errorf("%s: %w", zf.Name, err)
}
if zf.Name == metaFile {
f, err := zf.Open()
@ -434,7 +434,7 @@ func importZip(name string, s Writer, reader io.Reader) error {
return err
}
data, err := io.ReadAll(&LimitedReader{R: f, N: maxAllowedFileSizeToImport})
data, err := io.ReadAll(&limitedReader{R: f, N: maxAllowedFileSizeToImport})
defer f.Close()
if err != nil {
return err
@ -464,7 +464,7 @@ func importZip(name string, s Writer, reader io.Reader) error {
}
}
if !importedMetaFile {
return errdefs.InvalidParameter(errors.New("invalid context: no metadata found"))
return invalidParameter(errors.New("invalid context: no metadata found"))
}
return s.ResetTLSMaterial(name, &tlsData)
}

View File

@ -1,5 +1,5 @@
// 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
//go:build go1.23
package store

View File

@ -1,12 +1,11 @@
package store
import (
"fmt"
"os"
"path/filepath"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/atomicwriter"
"github.com/pkg/errors"
"github.com/moby/sys/atomicwriter"
)
const tlsDir = "tls"
@ -39,9 +38,9 @@ func (s *tlsStore) getData(name, endpointName, filename string) ([]byte, error)
data, err := os.ReadFile(filepath.Join(s.endpointDir(name, endpointName), filename))
if err != nil {
if os.IsNotExist(err) {
return nil, errdefs.NotFound(errors.Errorf("TLS data for %s/%s/%s does not exist", name, endpointName, filename))
return nil, notFound(fmt.Errorf("TLS data for %s/%s/%s does not exist", name, endpointName, filename))
}
return nil, errors.Wrapf(err, "failed to read TLS data for endpoint %s", endpointName)
return nil, fmt.Errorf("failed to read TLS data for endpoint %s: %w", endpointName, err)
}
return data, nil
}
@ -49,14 +48,14 @@ func (s *tlsStore) getData(name, endpointName, filename string) ([]byte, error)
// remove deletes all TLS data for the given context.
func (s *tlsStore) remove(name string) error {
if err := os.RemoveAll(s.contextDir(name)); err != nil {
return errors.Wrapf(err, "failed to remove TLS data")
return fmt.Errorf("failed to remove TLS data: %w", err)
}
return nil
}
func (s *tlsStore) removeEndpoint(name, endpointName string) error {
if err := os.RemoveAll(s.endpointDir(name, endpointName)); err != nil {
return errors.Wrapf(err, "failed to remove TLS data for endpoint %s", endpointName)
return fmt.Errorf("failed to remove TLS data for endpoint %s: %w", endpointName, err)
}
return nil
}
@ -68,7 +67,7 @@ func (s *tlsStore) listContextData(name string) (map[string]EndpointFiles, error
if os.IsNotExist(err) {
return map[string]EndpointFiles{}, nil
}
return nil, errors.Wrapf(err, "failed to list TLS files for context %s", name)
return nil, fmt.Errorf("failed to list TLS files for context %s: %w", name, err)
}
r := make(map[string]EndpointFiles)
for _, epFS := range epFSs {
@ -78,7 +77,7 @@ func (s *tlsStore) listContextData(name string) (map[string]EndpointFiles, error
continue
}
if err != nil {
return nil, errors.Wrapf(err, "failed to list TLS files for endpoint %s", epFS.Name())
return nil, fmt.Errorf("failed to list TLS files for endpoint %s: %w", epFS.Name(), err)
}
var files EndpointFiles
for _, fs := range fss {

View File

@ -33,5 +33,8 @@ func IsEnabled() bool {
// The default is to log to the debug level which is only
// enabled when debugging is enabled.
var OTELErrorHandler otel.ErrorHandler = otel.ErrorHandlerFunc(func(err error) {
if err == nil {
return
}
logrus.WithError(err).Debug("otel error")
})

View File

@ -1,178 +0,0 @@
package store
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"github.com/distribution/reference"
"github.com/docker/cli/cli/manifest/types"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
// Store manages local storage of image distribution manifests
type Store interface {
Remove(listRef reference.Reference) error
Get(listRef reference.Reference, manifest reference.Reference) (types.ImageManifest, error)
GetList(listRef reference.Reference) ([]types.ImageManifest, error)
Save(listRef reference.Reference, manifest reference.Reference, image types.ImageManifest) error
}
// fsStore manages manifest files stored on the local filesystem
type fsStore struct {
root string
}
// NewStore returns a new store for a local file path
func NewStore(root string) Store {
return &fsStore{root: root}
}
// Remove a manifest list from local storage
func (s *fsStore) Remove(listRef reference.Reference) error {
path := filepath.Join(s.root, makeFilesafeName(listRef.String()))
return os.RemoveAll(path)
}
// Get returns the local manifest
func (s *fsStore) Get(listRef reference.Reference, manifest reference.Reference) (types.ImageManifest, error) {
filename := manifestToFilename(s.root, listRef.String(), manifest.String())
return s.getFromFilename(manifest, filename)
}
func (*fsStore) getFromFilename(ref reference.Reference, filename string) (types.ImageManifest, error) {
bytes, err := os.ReadFile(filename)
switch {
case os.IsNotExist(err):
return types.ImageManifest{}, newNotFoundError(ref.String())
case err != nil:
return types.ImageManifest{}, err
}
var manifestInfo struct {
types.ImageManifest
// Deprecated Fields, replaced by Descriptor
Digest digest.Digest
Platform *manifestlist.PlatformSpec
}
if err := json.Unmarshal(bytes, &manifestInfo); err != nil {
return types.ImageManifest{}, err
}
// Compatibility with image manifests created before
// descriptor, newer versions omit Digest and Platform
if manifestInfo.Digest != "" {
mediaType, raw, err := manifestInfo.Payload()
if err != nil {
return types.ImageManifest{}, err
}
if dgst := digest.FromBytes(raw); dgst != manifestInfo.Digest {
return types.ImageManifest{}, errors.Errorf("invalid manifest file %v: image manifest digest mismatch (%v != %v)", filename, manifestInfo.Digest, dgst)
}
manifestInfo.ImageManifest.Descriptor = ocispec.Descriptor{
Digest: manifestInfo.Digest,
Size: int64(len(raw)),
MediaType: mediaType,
Platform: types.OCIPlatform(manifestInfo.Platform),
}
}
return manifestInfo.ImageManifest, nil
}
// GetList returns all the local manifests for a transaction
func (s *fsStore) GetList(listRef reference.Reference) ([]types.ImageManifest, error) {
filenames, err := s.listManifests(listRef.String())
switch {
case err != nil:
return nil, err
case filenames == nil:
return nil, newNotFoundError(listRef.String())
}
manifests := []types.ImageManifest{}
for _, filename := range filenames {
filename = filepath.Join(s.root, makeFilesafeName(listRef.String()), filename)
manifest, err := s.getFromFilename(listRef, filename)
if err != nil {
return nil, err
}
manifests = append(manifests, manifest)
}
return manifests, nil
}
// listManifests stored in a transaction
func (s *fsStore) listManifests(transaction string) ([]string, error) {
transactionDir := filepath.Join(s.root, makeFilesafeName(transaction))
fileInfos, err := os.ReadDir(transactionDir)
switch {
case os.IsNotExist(err):
return nil, nil
case err != nil:
return nil, err
}
filenames := make([]string, 0, len(fileInfos))
for _, info := range fileInfos {
filenames = append(filenames, info.Name())
}
return filenames, nil
}
// Save a manifest as part of a local manifest list
func (s *fsStore) Save(listRef reference.Reference, manifest reference.Reference, image types.ImageManifest) error {
if err := s.createManifestListDirectory(listRef.String()); err != nil {
return err
}
filename := manifestToFilename(s.root, listRef.String(), manifest.String())
bytes, err := json.Marshal(image)
if err != nil {
return err
}
return os.WriteFile(filename, bytes, 0o644)
}
func (s *fsStore) createManifestListDirectory(transaction string) error {
path := filepath.Join(s.root, makeFilesafeName(transaction))
return os.MkdirAll(path, 0o755)
}
func manifestToFilename(root, manifestList, manifest string) string {
return filepath.Join(root, makeFilesafeName(manifestList), makeFilesafeName(manifest))
}
func makeFilesafeName(ref string) string {
fileName := strings.ReplaceAll(ref, ":", "-")
return strings.ReplaceAll(fileName, "/", "_")
}
type notFoundError struct {
object string
}
func newNotFoundError(ref string) *notFoundError {
return &notFoundError{object: ref}
}
func (n *notFoundError) Error() string {
return "No such manifest: " + n.object
}
// NotFound interface
func (*notFoundError) NotFound() {}
// IsNotFound returns true if the error is a not found error
func IsNotFound(err error) bool {
_, ok := err.(notFound)
return ok
}
type notFound interface {
NotFound()
}

View File

@ -1,154 +0,0 @@
package types
import (
"encoding/json"
"github.com/distribution/reference"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/ocischema"
"github.com/docker/distribution/manifest/schema2"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
// ImageManifest contains info to output for a manifest object.
type ImageManifest struct {
Ref *SerializableNamed
Descriptor ocispec.Descriptor
Raw []byte `json:",omitempty"`
// SchemaV2Manifest is used for inspection
SchemaV2Manifest *schema2.DeserializedManifest `json:",omitempty"`
// OCIManifest is used for inspection
OCIManifest *ocischema.DeserializedManifest `json:",omitempty"`
}
// OCIPlatform creates an OCI platform from a manifest list platform spec
func OCIPlatform(ps *manifestlist.PlatformSpec) *ocispec.Platform {
if ps == nil {
return nil
}
return &ocispec.Platform{
Architecture: ps.Architecture,
OS: ps.OS,
OSVersion: ps.OSVersion,
OSFeatures: ps.OSFeatures,
Variant: ps.Variant,
}
}
// PlatformSpecFromOCI creates a platform spec from OCI platform
func PlatformSpecFromOCI(p *ocispec.Platform) *manifestlist.PlatformSpec {
if p == nil {
return nil
}
return &manifestlist.PlatformSpec{
Architecture: p.Architecture,
OS: p.OS,
OSVersion: p.OSVersion,
OSFeatures: p.OSFeatures,
Variant: p.Variant,
}
}
// Blobs returns the digests for all the blobs referenced by this manifest
func (i ImageManifest) Blobs() []digest.Digest {
var digests []digest.Digest
switch {
case i.SchemaV2Manifest != nil:
refs := i.SchemaV2Manifest.References()
digests = make([]digest.Digest, 0, len(refs))
for _, descriptor := range refs {
digests = append(digests, descriptor.Digest)
}
case i.OCIManifest != nil:
refs := i.OCIManifest.References()
digests = make([]digest.Digest, 0, len(refs))
for _, descriptor := range refs {
digests = append(digests, descriptor.Digest)
}
}
return digests
}
// Payload returns the media type and bytes for the manifest
func (i ImageManifest) Payload() (string, []byte, error) {
// TODO: If available, read content from a content store by digest
switch {
case i.SchemaV2Manifest != nil:
return i.SchemaV2Manifest.Payload()
case i.OCIManifest != nil:
return i.OCIManifest.Payload()
default:
return "", nil, errors.Errorf("%s has no payload", i.Ref)
}
}
// References implements the distribution.Manifest interface. It delegates to
// the underlying manifest.
func (i ImageManifest) References() []distribution.Descriptor {
switch {
case i.SchemaV2Manifest != nil:
return i.SchemaV2Manifest.References()
case i.OCIManifest != nil:
return i.OCIManifest.References()
default:
return nil
}
}
// NewImageManifest returns a new ImageManifest object. The values for Platform
// are initialized from those in the image
func NewImageManifest(ref reference.Named, desc ocispec.Descriptor, manifest *schema2.DeserializedManifest) ImageManifest {
raw, err := manifest.MarshalJSON()
if err != nil {
raw = nil
}
return ImageManifest{
Ref: &SerializableNamed{Named: ref},
Descriptor: desc,
Raw: raw,
SchemaV2Manifest: manifest,
}
}
// NewOCIImageManifest returns a new ImageManifest object. The values for
// Platform are initialized from those in the image
func NewOCIImageManifest(ref reference.Named, desc ocispec.Descriptor, manifest *ocischema.DeserializedManifest) ImageManifest {
raw, err := manifest.MarshalJSON()
if err != nil {
raw = nil
}
return ImageManifest{
Ref: &SerializableNamed{Named: ref},
Descriptor: desc,
Raw: raw,
OCIManifest: manifest,
}
}
// SerializableNamed is a reference.Named that can be serialized and deserialized
// from JSON
type SerializableNamed struct {
reference.Named
}
// UnmarshalJSON loads the Named reference from JSON bytes
func (s *SerializableNamed) UnmarshalJSON(b []byte) error {
var raw string
if err := json.Unmarshal(b, &raw); err != nil {
return errors.Wrapf(err, "invalid named reference bytes: %s", b)
}
var err error
s.Named, err = reference.ParseNamed(raw)
return err
}
// MarshalJSON returns the JSON bytes representation
func (s *SerializableNamed) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}

View File

@ -1,198 +0,0 @@
package client
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/distribution/reference"
manifesttypes "github.com/docker/cli/cli/manifest/types"
"github.com/docker/cli/cli/trust"
"github.com/docker/distribution"
distributionclient "github.com/docker/distribution/registry/client"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// RegistryClient is a client used to communicate with a Docker distribution
// registry
type RegistryClient interface {
GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error)
GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error)
MountBlob(ctx context.Context, source reference.Canonical, target reference.Named) error
PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error)
}
// NewRegistryClient returns a new RegistryClient with a resolver
func NewRegistryClient(resolver AuthConfigResolver, userAgent string, insecure bool) RegistryClient {
return &client{
authConfigResolver: resolver,
insecureRegistry: insecure,
userAgent: userAgent,
}
}
// AuthConfigResolver returns Auth Configuration for an index
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 {
authConfigResolver AuthConfigResolver
insecureRegistry bool
userAgent string
}
// ErrBlobCreated returned when a blob mount request was created
type ErrBlobCreated struct {
From reference.Named
Target reference.Named
}
func (err ErrBlobCreated) Error() string {
return fmt.Sprintf("blob mounted from: %v to: %v",
err.From, err.Target)
}
// ErrHTTPProto returned if attempting to use TLS with a non-TLS registry
type ErrHTTPProto struct {
OrigErr string
}
func (err ErrHTTPProto) Error() string {
return err.OrigErr
}
var _ RegistryClient = &client{}
// MountBlob into the registry, so it can be referenced by a manifest
func (c *client) MountBlob(ctx context.Context, sourceRef reference.Canonical, targetRef reference.Named) error {
repoEndpoint, err := newDefaultRepositoryEndpoint(targetRef, c.insecureRegistry)
if err != nil {
return err
}
repoEndpoint.actions = trust.ActionsPushAndPull
repo, err := c.getRepositoryForReference(ctx, targetRef, repoEndpoint)
if err != nil {
return err
}
lu, err := repo.Blobs(ctx).Create(ctx, distributionclient.WithMountFrom(sourceRef))
switch err.(type) {
case distribution.ErrBlobMounted:
logrus.Debugf("mount of blob %s succeeded", sourceRef)
return nil
case nil:
default:
return errors.Wrapf(err, "failed to mount blob %s to %s", sourceRef, targetRef)
}
lu.Cancel(ctx)
logrus.Debugf("mount of blob %s created", sourceRef)
return ErrBlobCreated{From: sourceRef, Target: targetRef}
}
// PutManifest sends the manifest to a registry and returns the new digest
func (c *client) PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error) {
repoEndpoint, err := newDefaultRepositoryEndpoint(ref, c.insecureRegistry)
if err != nil {
return "", err
}
repoEndpoint.actions = trust.ActionsPushAndPull
repo, err := c.getRepositoryForReference(ctx, ref, repoEndpoint)
if err != nil {
return "", err
}
manifestService, err := repo.Manifests(ctx)
if err != nil {
return "", err
}
_, opts, err := getManifestOptionsFromReference(ref)
if err != nil {
return "", err
}
dgst, err := manifestService.Put(ctx, manifest, opts...)
return dgst, errors.Wrapf(err, "failed to put manifest %s", ref)
}
func (c *client) getRepositoryForReference(ctx context.Context, ref reference.Named, repoEndpoint repositoryEndpoint) (distribution.Repository, error) {
repoName, err := reference.WithName(repoEndpoint.Name())
if err != nil {
return nil, errors.Wrapf(err, "failed to parse repo name from %s", ref)
}
httpTransport, err := c.getHTTPTransportForRepoEndpoint(ctx, repoEndpoint)
if err != nil {
if !strings.Contains(err.Error(), "server gave HTTP response to HTTPS client") {
return nil, err
}
if !repoEndpoint.endpoint.TLSConfig.InsecureSkipVerify {
return nil, ErrHTTPProto{OrigErr: err.Error()}
}
// --insecure was set; fall back to plain HTTP
if url := repoEndpoint.endpoint.URL; url != nil && url.Scheme == "https" {
url.Scheme = "http"
httpTransport, err = c.getHTTPTransportForRepoEndpoint(ctx, repoEndpoint)
if err != nil {
return nil, err
}
}
}
return distributionclient.NewRepository(repoName, repoEndpoint.BaseURL(), httpTransport)
}
func (c *client) getHTTPTransportForRepoEndpoint(ctx context.Context, repoEndpoint repositoryEndpoint) (http.RoundTripper, error) {
httpTransport, err := getHTTPTransport(
c.authConfigResolver(ctx, repoEndpoint.info.Index),
repoEndpoint.endpoint,
repoEndpoint.Name(),
c.userAgent,
repoEndpoint.actions,
)
return httpTransport, errors.Wrap(err, "failed to configure transport")
}
// GetManifest returns an ImageManifest for the reference
func (c *client) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
var result manifesttypes.ImageManifest
fetch := func(ctx context.Context, repo distribution.Repository, ref reference.Named) (bool, error) {
var err error
result, err = fetchManifest(ctx, repo, ref)
return result.Ref != nil, err
}
err := c.iterateEndpoints(ctx, ref, fetch)
return result, err
}
// GetManifestList returns a list of ImageManifest for the reference
func (c *client) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
result := []manifesttypes.ImageManifest{}
fetch := func(ctx context.Context, repo distribution.Repository, ref reference.Named) (bool, error) {
var err error
result, err = fetchList(ctx, repo, ref)
return len(result) > 0, err
}
err := c.iterateEndpoints(ctx, ref, fetch)
return result, err
}
func getManifestOptionsFromReference(ref reference.Named) (digest.Digest, []distribution.ManifestServiceOption, error) {
if tagged, isTagged := ref.(reference.NamedTagged); isTagged {
tag := tagged.Tag()
return "", []distribution.ManifestServiceOption{distribution.WithTag(tag)}, nil
}
if digested, isDigested := ref.(reference.Canonical); isDigested {
return digested.Digest(), []distribution.ManifestServiceOption{}, nil
}
return "", nil, errors.Errorf("%s no tag or digest", ref)
}

View File

@ -1,128 +0,0 @@
package client
import (
"net"
"net/http"
"time"
"github.com/distribution/reference"
"github.com/docker/cli/cli/trust"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/transport"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/registry"
"github.com/pkg/errors"
)
type repositoryEndpoint struct {
info *registry.RepositoryInfo
endpoint registry.APIEndpoint
actions []string
}
// Name returns the repository name
func (r repositoryEndpoint) Name() string {
return reference.Path(r.info.Name)
}
// BaseURL returns the endpoint url
func (r repositoryEndpoint) BaseURL() string {
return r.endpoint.URL.String()
}
func newDefaultRepositoryEndpoint(ref reference.Named, insecure bool) (repositoryEndpoint, error) {
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return repositoryEndpoint{}, err
}
endpoint, err := getDefaultEndpointFromRepoInfo(repoInfo)
if err != nil {
return repositoryEndpoint{}, err
}
if insecure {
endpoint.TLSConfig.InsecureSkipVerify = true
}
return repositoryEndpoint{info: repoInfo, endpoint: endpoint}, nil
}
func getDefaultEndpointFromRepoInfo(repoInfo *registry.RepositoryInfo) (registry.APIEndpoint, error) {
var err error
options := registry.ServiceOptions{}
registryService, err := registry.NewService(options)
if err != nil {
return registry.APIEndpoint{}, err
}
endpoints, err := registryService.LookupPushEndpoints(reference.Domain(repoInfo.Name))
if err != nil {
return registry.APIEndpoint{}, err
}
// Default to the highest priority endpoint to return
endpoint := endpoints[0]
if !repoInfo.Index.Secure {
for _, ep := range endpoints {
if ep.URL.Scheme == "http" {
endpoint = ep
}
}
}
return endpoint, nil
}
// getHTTPTransport builds a transport for use in communicating with a registry
func getHTTPTransport(authConfig registrytypes.AuthConfig, endpoint registry.APIEndpoint, repoName, userAgent string, actions []string) (http.RoundTripper, error) {
// get the http transport, this will be used in a client to upload manifest
base := &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: endpoint.TLSConfig,
DisableKeepAlives: true,
}
modifiers := registry.Headers(userAgent, http.Header{})
authTransport := transport.NewTransport(base, modifiers...)
challengeManager, err := registry.PingV2Registry(endpoint.URL, authTransport)
if err != nil {
return nil, errors.Wrap(err, "error pinging v2 registry")
}
if authConfig.RegistryToken != "" {
passThruTokenHandler := &existingTokenHandler{token: authConfig.RegistryToken}
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler))
} else {
if len(actions) == 0 {
actions = trust.ActionsPullOnly
}
creds := registry.NewStaticCredentialStore(&authConfig)
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...)
basicHandler := auth.NewBasicHandler(creds)
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
}
return transport.NewTransport(base, modifiers...), nil
}
// RepoNameForReference returns the repository name from a reference
func RepoNameForReference(ref reference.Named) (string, error) {
// insecure is fine since this only returns the name
repo, err := newDefaultRepositoryEndpoint(ref, false)
if err != nil {
return "", err
}
return repo.Name(), nil
}
type existingTokenHandler struct {
token string
}
func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, _ map[string]string) error {
req.Header.Set("Authorization", "Bearer "+th.token)
return nil
}
func (*existingTokenHandler) Scheme() string {
return "bearer"
}

View File

@ -1,307 +0,0 @@
package client
import (
"context"
"encoding/json"
"github.com/distribution/reference"
"github.com/docker/cli/cli/manifest/types"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/ocischema"
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/registry/api/errcode"
v2 "github.com/docker/distribution/registry/api/v2"
distclient "github.com/docker/distribution/registry/client"
"github.com/docker/docker/registry"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// fetchManifest pulls a manifest from a registry and returns it. An error
// is returned if no manifest is found matching namedRef.
func fetchManifest(ctx context.Context, repo distribution.Repository, ref reference.Named) (types.ImageManifest, error) {
manifest, err := getManifest(ctx, repo, ref)
if err != nil {
return types.ImageManifest{}, err
}
switch v := manifest.(type) {
// Removed Schema 1 support
case *schema2.DeserializedManifest:
return pullManifestSchemaV2(ctx, ref, repo, *v)
case *ocischema.DeserializedManifest:
return pullManifestOCISchema(ctx, ref, repo, *v)
case *manifestlist.DeserializedManifestList:
return types.ImageManifest{}, errors.Errorf("%s is a manifest list", ref)
}
return types.ImageManifest{}, errors.Errorf("%s is not a manifest", ref)
}
func fetchList(ctx context.Context, repo distribution.Repository, ref reference.Named) ([]types.ImageManifest, error) {
manifest, err := getManifest(ctx, repo, ref)
if err != nil {
return nil, err
}
switch v := manifest.(type) {
case *manifestlist.DeserializedManifestList:
return pullManifestList(ctx, ref, repo, *v)
default:
return nil, errors.Errorf("unsupported manifest format: %v", v)
}
}
func getManifest(ctx context.Context, repo distribution.Repository, ref reference.Named) (distribution.Manifest, error) {
manSvc, err := repo.Manifests(ctx)
if err != nil {
return nil, err
}
dgst, opts, err := getManifestOptionsFromReference(ref)
if err != nil {
return nil, errors.Errorf("image manifest for %q does not exist", ref)
}
return manSvc.Get(ctx, dgst, opts...)
}
func pullManifestSchemaV2(ctx context.Context, ref reference.Named, repo distribution.Repository, mfst schema2.DeserializedManifest) (types.ImageManifest, error) {
manifestDesc, err := validateManifestDigest(ref, mfst)
if err != nil {
return types.ImageManifest{}, err
}
configJSON, err := pullManifestSchemaV2ImageConfig(ctx, mfst.Target().Digest, repo)
if err != nil {
return types.ImageManifest{}, err
}
if manifestDesc.Platform == nil {
manifestDesc.Platform = &ocispec.Platform{}
}
// Fill in os and architecture fields from config JSON
if err := json.Unmarshal(configJSON, manifestDesc.Platform); err != nil {
return types.ImageManifest{}, err
}
return types.NewImageManifest(ref, manifestDesc, &mfst), nil
}
func pullManifestOCISchema(ctx context.Context, ref reference.Named, repo distribution.Repository, mfst ocischema.DeserializedManifest) (types.ImageManifest, error) {
manifestDesc, err := validateManifestDigest(ref, mfst)
if err != nil {
return types.ImageManifest{}, err
}
configJSON, err := pullManifestSchemaV2ImageConfig(ctx, mfst.Target().Digest, repo)
if err != nil {
return types.ImageManifest{}, err
}
if manifestDesc.Platform == nil {
manifestDesc.Platform = &ocispec.Platform{}
}
// Fill in os and architecture fields from config JSON
if err := json.Unmarshal(configJSON, manifestDesc.Platform); err != nil {
return types.ImageManifest{}, err
}
return types.NewOCIImageManifest(ref, manifestDesc, &mfst), nil
}
func pullManifestSchemaV2ImageConfig(ctx context.Context, dgst digest.Digest, repo distribution.Repository) ([]byte, error) {
blobs := repo.Blobs(ctx)
configJSON, err := blobs.Get(ctx, dgst)
if err != nil {
return nil, err
}
verifier := dgst.Verifier()
if _, err := verifier.Write(configJSON); err != nil {
return nil, err
}
if !verifier.Verified() {
return nil, errors.Errorf("image config verification failed for digest %s", dgst)
}
return configJSON, nil
}
// validateManifestDigest computes the manifest digest, and, if pulling by
// digest, ensures that it matches the requested digest.
func validateManifestDigest(ref reference.Named, mfst distribution.Manifest) (ocispec.Descriptor, error) {
mediaType, canonical, err := mfst.Payload()
if err != nil {
return ocispec.Descriptor{}, err
}
desc := ocispec.Descriptor{
Digest: digest.FromBytes(canonical),
Size: int64(len(canonical)),
MediaType: mediaType,
}
// If pull by digest, then verify the manifest digest.
if digested, isDigested := ref.(reference.Canonical); isDigested && digested.Digest() != desc.Digest {
return ocispec.Descriptor{}, errors.Errorf("manifest verification failed for digest %s", digested.Digest())
}
return desc, nil
}
// pullManifestList handles "manifest lists" which point to various
// platform-specific manifests.
func pullManifestList(ctx context.Context, ref reference.Named, repo distribution.Repository, mfstList manifestlist.DeserializedManifestList) ([]types.ImageManifest, error) {
if _, err := validateManifestDigest(ref, mfstList); err != nil {
return nil, err
}
infos := make([]types.ImageManifest, 0, len(mfstList.Manifests))
for _, manifestDescriptor := range mfstList.Manifests {
manSvc, err := repo.Manifests(ctx)
if err != nil {
return nil, err
}
manifest, err := manSvc.Get(ctx, manifestDescriptor.Digest)
if err != nil {
return nil, err
}
manifestRef, err := reference.WithDigest(ref, manifestDescriptor.Digest)
if err != nil {
return nil, err
}
var imageManifest types.ImageManifest
switch v := manifest.(type) {
case *schema2.DeserializedManifest:
imageManifest, err = pullManifestSchemaV2(ctx, manifestRef, repo, *v)
case *ocischema.DeserializedManifest:
imageManifest, err = pullManifestOCISchema(ctx, manifestRef, repo, *v)
default:
err = errors.Errorf("unsupported manifest type: %T", manifest)
}
if err != nil {
return nil, err
}
// Replace platform from config
p := manifestDescriptor.Platform
imageManifest.Descriptor.Platform = types.OCIPlatform(&p)
infos = append(infos, imageManifest)
}
return infos, nil
}
func continueOnError(err error) bool {
switch v := err.(type) {
case errcode.Errors:
if len(v) == 0 {
return true
}
return continueOnError(v[0])
case errcode.Error:
switch e := err.(errcode.Error); e.Code {
case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown:
return true
default:
return false
}
case *distclient.UnexpectedHTTPResponseError:
return true
}
return false
}
func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named, each func(context.Context, distribution.Repository, reference.Named) (bool, error)) error {
endpoints, err := allEndpoints(namedRef, c.insecureRegistry)
if err != nil {
return err
}
repoInfo, err := registry.ParseRepositoryInfo(namedRef)
if err != nil {
return err
}
confirmedTLSRegistries := make(map[string]bool)
for _, endpoint := range endpoints {
if endpoint.URL.Scheme != "https" {
if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS {
logrus.Debugf("skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL)
continue
}
}
if c.insecureRegistry {
endpoint.TLSConfig.InsecureSkipVerify = true
}
repoEndpoint := repositoryEndpoint{endpoint: endpoint, info: repoInfo}
repo, err := c.getRepositoryForReference(ctx, namedRef, repoEndpoint)
if err != nil {
logrus.Debugf("error %s with repo endpoint %+v", err, repoEndpoint)
if _, ok := err.(ErrHTTPProto); ok {
continue
}
return err
}
if endpoint.URL.Scheme == "http" && !c.insecureRegistry {
logrus.Debugf("skipping non-tls registry endpoint: %s", endpoint.URL)
continue
}
done, err := each(ctx, repo, namedRef)
if err != nil {
if continueOnError(err) {
if endpoint.URL.Scheme == "https" {
confirmedTLSRegistries[endpoint.URL.Host] = true
}
logrus.Debugf("continuing on error (%T) %s", err, err)
continue
}
logrus.Debugf("not continuing on error (%T) %s", err, err)
return err
}
if done {
return nil
}
}
return newNotFoundError(namedRef.String())
}
// allEndpoints returns a list of endpoints ordered by priority (v2, http).
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
if insecure {
logrus.Debugf("allowing insecure registry for: %s", reference.Domain(namedRef))
serviceOpts.InsecureRegistries = []string{reference.Domain(namedRef)}
}
registryService, err := registry.NewService(serviceOpts)
if err != nil {
return []registry.APIEndpoint{}, err
}
endpoints, err := registryService.LookupPullEndpoints(reference.Domain(repoInfo.Name))
logrus.Debugf("endpoints for %s: %v", namedRef, endpoints)
return endpoints, err
}
func newNotFoundError(ref string) *notFoundError {
return &notFoundError{err: errors.New("no such manifest: " + ref)}
}
type notFoundError struct {
err error
}
func (n *notFoundError) Error() string {
return n.err.Error()
}
// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
func (notFoundError) NotFound() {}

View File

@ -1,376 +0,0 @@
package trust
import (
"context"
"encoding/json"
"io"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"time"
"github.com/distribution/reference"
"github.com/docker/cli/cli/config"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/auth/challenge"
"github.com/docker/distribution/registry/client/transport"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/registry"
"github.com/docker/go-connections/tlsconfig"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/theupdateframework/notary"
"github.com/theupdateframework/notary/client"
"github.com/theupdateframework/notary/passphrase"
"github.com/theupdateframework/notary/storage"
"github.com/theupdateframework/notary/trustmanager"
"github.com/theupdateframework/notary/trustpinning"
"github.com/theupdateframework/notary/tuf/data"
"github.com/theupdateframework/notary/tuf/signed"
)
var (
// ReleasesRole is the role named "releases"
ReleasesRole = data.RoleName(path.Join(data.CanonicalTargetsRole.String(), "releases"))
// ActionsPullOnly defines the actions for read-only interactions with a Notary Repository
ActionsPullOnly = []string{"pull"}
// ActionsPushAndPull defines the actions for read-write interactions with a Notary Repository
ActionsPushAndPull = []string{"pull", "push"}
// NotaryServer is the endpoint serving the Notary trust server
NotaryServer = "https://notary.docker.io"
)
// GetTrustDirectory returns the base trust directory name
func GetTrustDirectory() string {
return filepath.Join(config.Dir(), "trust")
}
// certificateDirectory returns the directory containing
// TLS certificates for the given server. An error is
// returned if there was an error parsing the server string.
func certificateDirectory(server string) (string, error) {
u, err := url.Parse(server)
if err != nil {
return "", err
}
return filepath.Join(config.Dir(), "tls", u.Host), nil
}
// Server returns the base URL for the trust server.
func Server(index *registrytypes.IndexInfo) (string, error) {
if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" {
urlObj, err := url.Parse(s)
if err != nil || urlObj.Scheme != "https" {
return "", errors.Errorf("valid https URL required for trust server, got %s", s)
}
return s, nil
}
if index.Official {
return NotaryServer, nil
}
return "https://" + index.Name, nil
}
type simpleCredentialStore struct {
auth registrytypes.AuthConfig
}
func (scs simpleCredentialStore) Basic(*url.URL) (string, string) {
return scs.auth.Username, scs.auth.Password
}
func (scs simpleCredentialStore) RefreshToken(*url.URL, string) string {
return scs.auth.IdentityToken
}
func (simpleCredentialStore) SetRefreshToken(*url.URL, string, string) {}
// GetNotaryRepository returns a NotaryRepository which stores all the
// information needed to operate on a notary repository.
// It creates an HTTP transport providing authentication support.
func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo *registry.RepositoryInfo, authConfig *registrytypes.AuthConfig, actions ...string) (client.Repository, error) {
server, err := Server(repoInfo.Index)
if err != nil {
return nil, err
}
cfg := tlsconfig.ClientDefault()
cfg.InsecureSkipVerify = !repoInfo.Index.Secure
// Get certificate base directory
certDir, err := certificateDirectory(server)
if err != nil {
return nil, err
}
logrus.Debugf("reading certificate directory: %s", certDir)
if err := registry.ReadCertsDirectory(cfg, certDir); err != nil {
return nil, err
}
base := &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: cfg,
DisableKeepAlives: true,
}
// Skip configuration headers since request is not going to Docker daemon
modifiers := registry.Headers(userAgent, http.Header{})
authTransport := transport.NewTransport(base, modifiers...)
pingClient := &http.Client{
Transport: authTransport,
Timeout: 5 * time.Second,
}
endpointStr := server + "/v2/"
req, err := http.NewRequest(http.MethodGet, endpointStr, nil)
if err != nil {
return nil, err
}
challengeManager := challenge.NewSimpleManager()
resp, err := pingClient.Do(req)
if err != nil {
// Ignore error on ping to operate in offline mode
logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err)
} else {
defer resp.Body.Close()
// Add response to the challenge manager to parse out
// authentication header and register authentication method
if err := challengeManager.AddResponse(resp); err != nil {
return nil, err
}
}
scope := auth.RepositoryScope{
Repository: repoInfo.Name.Name(),
Actions: actions,
}
creds := simpleCredentialStore{auth: *authConfig}
tokenHandlerOptions := auth.TokenHandlerOptions{
Transport: authTransport,
Credentials: creds,
Scopes: []auth.Scope{scope},
ClientID: registry.AuthClientID,
}
tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions)
basicHandler := auth.NewBasicHandler(creds)
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
tr := transport.NewTransport(base, modifiers...)
return client.NewFileCachedRepository(
GetTrustDirectory(),
data.GUN(repoInfo.Name.Name()),
server,
tr,
GetPassphraseRetriever(in, out),
trustpinning.TrustPinConfig{})
}
// GetPassphraseRetriever returns a passphrase retriever that utilizes Content Trust env vars
func GetPassphraseRetriever(in io.Reader, out io.Writer) notary.PassRetriever {
aliasMap := map[string]string{
"root": "root",
"snapshot": "repository",
"targets": "repository",
"default": "repository",
}
baseRetriever := passphrase.PromptRetrieverWithInOut(in, out, aliasMap)
env := map[string]string{
"root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"),
"snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
"targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
"default": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
}
return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) {
if v := env[alias]; v != "" {
return v, numAttempts > 1, nil
}
// For non-root roles, we can also try the "default" alias if it is specified
if v := env["default"]; v != "" && alias != data.CanonicalRootRole.String() {
return v, numAttempts > 1, nil
}
return baseRetriever(keyName, alias, createNew, numAttempts)
}
}
// NotaryError formats an error message received from the notary service
func NotaryError(repoName string, err error) error {
switch err.(type) {
case *json.SyntaxError:
logrus.Debugf("Notary syntax error: %s", err)
return errors.Errorf("Error: no trust data available for remote repository %s. Try running notary server and setting DOCKER_CONTENT_TRUST_SERVER to its HTTPS address?", repoName)
case signed.ErrExpired:
return errors.Errorf("Error: remote repository %s out-of-date: %v", repoName, err)
case trustmanager.ErrKeyNotFound:
return errors.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err)
case storage.NetworkError:
return errors.Errorf("Error: error contacting notary server: %v", err)
case storage.ErrMetaNotFound:
return errors.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err)
case trustpinning.ErrRootRotationFail, trustpinning.ErrValidationFail, signed.ErrInvalidKeyType:
return errors.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err)
case signed.ErrNoKeys:
return errors.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err)
case signed.ErrLowVersion:
return errors.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err)
case signed.ErrRoleThreshold:
return errors.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err)
case client.ErrRepositoryNotExist:
return errors.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err)
case signed.ErrInsufficientSignatures:
return errors.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err)
}
return err
}
// GetSignableRoles returns a list of roles for which we have valid signing
// keys, given a notary repository and a target
func GetSignableRoles(repo client.Repository, target *client.Target) ([]data.RoleName, error) {
var signableRoles []data.RoleName
// translate the full key names, which includes the GUN, into just the key IDs
allCanonicalKeyIDs := make(map[string]struct{})
for fullKeyID := range repo.GetCryptoService().ListAllKeys() {
allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{}
}
allDelegationRoles, err := repo.GetDelegationRoles()
if err != nil {
return signableRoles, err
}
// if there are no delegation roles, then just try to sign it into the targets role
if len(allDelegationRoles) == 0 {
signableRoles = append(signableRoles, data.CanonicalTargetsRole)
return signableRoles, nil
}
// there are delegation roles, find every delegation role we have a key for,
// and attempt to sign in to all those roles.
for _, delegationRole := range allDelegationRoles {
// We do not support signing any delegation role that isn't a direct child of the targets role.
// Also don't bother checking the keys if we can't add the target
// to this role due to path restrictions
if path.Dir(delegationRole.Name.String()) != data.CanonicalTargetsRole.String() || !delegationRole.CheckPaths(target.Name) {
continue
}
for _, canonicalKeyID := range delegationRole.KeyIDs {
if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok {
signableRoles = append(signableRoles, delegationRole.Name)
break
}
}
}
if len(signableRoles) == 0 {
return signableRoles, errors.Errorf("no valid signing keys for delegation roles")
}
return signableRoles, nil
}
// ImageRefAndAuth contains all reference information and the auth config for an image request
type ImageRefAndAuth struct {
original string
authConfig *registrytypes.AuthConfig
reference reference.Named
repoInfo *registry.RepositoryInfo
tag string
digest digest.Digest
}
// GetImageReferencesAndAuth retrieves the necessary reference and auth information for an image name
// as an ImageRefAndAuth struct
func GetImageReferencesAndAuth(ctx context.Context,
authResolver func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig,
imgName string,
) (ImageRefAndAuth, error) {
ref, err := reference.ParseNormalizedNamed(imgName)
if err != nil {
return ImageRefAndAuth{}, err
}
// Resolve the Repository name from fqn to RepositoryInfo
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return ImageRefAndAuth{}, err
}
authConfig := authResolver(ctx, repoInfo.Index)
return ImageRefAndAuth{
original: imgName,
authConfig: &authConfig,
reference: ref,
repoInfo: repoInfo,
tag: getTag(ref),
digest: getDigest(ref),
}, nil
}
func getTag(ref reference.Named) string {
switch x := ref.(type) {
case reference.Canonical, reference.Digested:
return ""
case reference.NamedTagged:
return x.Tag()
default:
return ""
}
}
func getDigest(ref reference.Named) digest.Digest {
switch x := ref.(type) {
case reference.Canonical:
return x.Digest()
case reference.Digested:
return x.Digest()
default:
return digest.Digest("")
}
}
// AuthConfig returns the auth information (username, etc) for a given ImageRefAndAuth
func (imgRefAuth *ImageRefAndAuth) AuthConfig() *registrytypes.AuthConfig {
return imgRefAuth.authConfig
}
// Reference returns the Image reference for a given ImageRefAndAuth
func (imgRefAuth *ImageRefAndAuth) Reference() reference.Named {
return imgRefAuth.reference
}
// RepoInfo returns the repository information for a given ImageRefAndAuth
func (imgRefAuth *ImageRefAndAuth) RepoInfo() *registry.RepositoryInfo {
return imgRefAuth.repoInfo
}
// Tag returns the Image tag for a given ImageRefAndAuth
func (imgRefAuth *ImageRefAndAuth) Tag() string {
return imgRefAuth.tag
}
// Digest returns the Image digest for a given ImageRefAndAuth
func (imgRefAuth *ImageRefAndAuth) Digest() digest.Digest {
return imgRefAuth.digest
}
// Name returns the image name used to initialize the ImageRefAndAuth
func (imgRefAuth *ImageRefAndAuth) Name() string {
return imgRefAuth.original
}