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

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

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2025-03-19 17:58:27 +01:00
parent 882ef0db91
commit db4b96e62c
41 changed files with 781 additions and 372 deletions
+5 -15
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:
+16 -43
View File
@@ -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/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,19 +46,18 @@ 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
CurrentContext() string
DockerEndpoint() docker.Endpoint
TelemetryClient
DeprecatedNotaryClient
DeprecatedManifestClient
}
// DockerCli is an instance the docker command line client.
@@ -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
}
@@ -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,21 +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 {
@@ -292,6 +269,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
if cli.enableGlobalTracer {
cli.createGlobalTracerProvider(cli.baseCtx)
}
filterResourceAttributesEnvvar()
return nil
}
@@ -345,7 +323,10 @@ func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint,
// Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
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 +384,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
@@ -553,18 +529,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
+56
View File
@@ -0,0 +1,56 @@
package command
import (
"context"
"path/filepath"
"github.com/docker/cli/cli/config"
manifeststore "github.com/docker/cli/cli/manifest/store"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/cli/trust"
"github.com/docker/docker/api/types/registry"
notaryclient "github.com/theupdateframework/notary/client"
)
type DeprecatedNotaryClient interface {
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
//
// Deprecated: use [trust.GetNotaryRepository] instead. This method is no longer used and will be removed in the next release.
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
}
type DeprecatedManifestClient interface {
// ManifestStore returns a store for local manifests
//
// Deprecated: use [manifeststore.NewStore] instead. This method is no longer used and will be removed in the next release.
ManifestStore() manifeststore.Store
// RegistryClient returns a client for communicating with a Docker distribution
// registry.
//
// Deprecated: use [registryclient.NewRegistryClient]. This method is no longer used and will be removed in the next release.
RegistryClient(bool) registryclient.RegistryClient
}
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
}
// ManifestStore returns a store for local manifests
//
// Deprecated: use [manifeststore.NewStore] instead. This method is no longer used and will be removed in the next release.
func (*DockerCli) ManifestStore() manifeststore.Store {
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
}
// RegistryClient returns a client for communicating with a Docker distribution
// registry
//
// Deprecated: use [registryclient.NewRegistryClient]. This method is no longer used and will be removed in the next release.
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig {
return ResolveAuthConfig(cli.ConfigFile(), index)
}
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
}
+29
View File
@@ -1,6 +1,11 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.22
package formatter
import (
"fmt"
"strings"
"unicode/utf8"
"golang.org/x/text/width"
@@ -59,3 +64,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))
}
}
+2 -2
View File
@@ -76,9 +76,9 @@ 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) {
+49 -2
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, ",")
}
+5 -74
View File
@@ -13,10 +13,9 @@ import (
"runtime"
"strings"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/streams"
"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"
@@ -51,30 +50,6 @@ func CopyToFile(outfile string, r io.Reader) error {
return nil
}
// capitalizeFirst capitalizes the first character of string
func capitalizeFirst(s string) string {
switch l := len(s); l {
case 0:
return s
case 1:
return strings.ToLower(s)
default:
return strings.ToUpper(string(s[0])) + strings.ToLower(s[1:])
}
}
// PrettyPrint outputs arbitrary data for human formatted output by uppercasing the first letter.
func PrettyPrint(i any) string {
switch t := i.(type) {
case nil:
return "None"
case string:
return capitalizeFirst(t)
default:
return capitalizeFirst(fmt.Sprintf("%s", t))
}
}
var ErrPromptTerminated = errdefs.Cancelled(errors.New("prompt terminated"))
// DisableInputEcho disables input echo on the provided streams.In.
@@ -166,11 +141,12 @@ func PromptForConfirmation(ctx context.Context, ins io.Reader, outs io.Writer, m
}
// PruneFilters returns consolidated prune filters obtained from config.json and cli
func PruneFilters(dockerCli Cli, pruneFilters filters.Args) filters.Args {
if dockerCli.ConfigFile() == nil {
func PruneFilters(dockerCLI config.Provider, pruneFilters filters.Args) filters.Args {
cfg := dockerCLI.ConfigFile()
if cfg == nil {
return pruneFilters
}
for _, f := range dockerCli.ConfigFile().PruneFilters {
for _, f := range cfg.PruneFilters {
k, v, ok := strings.Cut(f, "=")
if !ok {
continue
@@ -239,48 +215,3 @@ 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
}
// StringSliceReplaceAt replaces the sub-slice find, with the sub-slice replace, in the string
// slice s, returning a new slice and a boolean indicating if the replacement happened.
// requireIdx is the index at which old needs to be found at (or -1 to disregard that).
func StringSliceReplaceAt(s, find, replace []string, requireIndex int) ([]string, bool) {
idx := stringSliceIndex(s, find)
if (requireIndex != -1 && requireIndex != idx) || idx == -1 {
return s, false
}
out := append([]string{}, s[:idx]...)
out = append(out, replace...)
out = append(out, s[idx+len(find):]...)
return out, true
}
// ValidateMountWithAPIVersion validates a mount with the server API version.
func ValidateMountWithAPIVersion(m mounttypes.Mount, serverAPIVersion string) error {
if m.BindOptions != nil {
if m.BindOptions.NonRecursive && versions.LessThan(serverAPIVersion, "1.40") {
return errors.Errorf("bind-recursive=disabled requires API v1.40 or later")
}
// ReadOnlyNonRecursive can be safely ignored when API < 1.44
if m.BindOptions.ReadOnlyForceRecursive && versions.LessThan(serverAPIVersion, "1.44") {
return errors.Errorf("bind-recursive=readonly requires API v1.44 or later")
}
}
return nil
}
+5
View File
@@ -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() {
+68
View File
@@ -0,0 +1,68 @@
package jsonstream
import (
"context"
"io"
"github.com/docker/docker/pkg/jsonmessage"
)
type (
Stream = jsonmessage.Stream
JSONMessage = jsonmessage.JSONMessage
JSONError = jsonmessage.JSONError
JSONProgress = jsonmessage.JSONProgress
)
type ctxReader struct {
err chan error
r io.Reader
}
func (r *ctxReader) Read(p []byte) (n int, err error) {
select {
case err = <-r.err:
return 0, err
default:
return r.r.Read(p)
}
}
type Options func(*options)
type options struct {
AuxCallback func(JSONMessage)
}
func WithAuxCallback(cb func(JSONMessage)) Options {
return func(o *options) {
o.AuxCallback = cb
}
}
// Display prints the JSON messages from the given reader to the given stream.
//
// It wraps the [jsonmessage.DisplayJSONMessagesStream] function to make it
// "context aware" and appropriately returns why the function was canceled.
//
// It returns an error if the context is canceled, but not if the input reader / stream is closed.
func Display(ctx context.Context, in io.Reader, stream Stream, opts ...Options) error {
if ctx.Err() != nil {
return ctx.Err()
}
reader := &ctxReader{err: make(chan error, 1), r: in}
stopFunc := context.AfterFunc(ctx, func() { reader.err <- ctx.Err() })
defer stopFunc()
o := options{}
for _, opt := range opts {
opt(&o)
}
if err := jsonmessage.DisplayJSONMessagesStream(reader, stream, stream.FD(), stream.IsTerminal(), o.AuxCallback); err != nil {
return err
}
return ctx.Err()
}
+16 -17
View File
@@ -8,7 +8,6 @@ import (
"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"
@@ -38,12 +37,6 @@ func NewRegistryClient(resolver AuthConfigResolver, userAgent string, insecure b
// 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
@@ -61,13 +54,13 @@ func (err ErrBlobCreated) Error() string {
err.From, err.Target)
}
// ErrHTTPProto returned if attempting to use TLS with a non-TLS registry
type ErrHTTPProto struct {
OrigErr string
// httpProtoError returned if attempting to use TLS with a non-TLS registry
type httpProtoError struct {
cause error
}
func (err ErrHTTPProto) Error() string {
return err.OrigErr
func (e httpProtoError) Error() string {
return e.cause.Error()
}
var _ RegistryClient = &client{}
@@ -78,7 +71,7 @@ func (c *client) MountBlob(ctx context.Context, sourceRef reference.Canonical, t
if err != nil {
return err
}
repoEndpoint.actions = trust.ActionsPushAndPull
repoEndpoint.actions = []string{"pull", "push"}
repo, err := c.getRepositoryForReference(ctx, targetRef, repoEndpoint)
if err != nil {
return err
@@ -104,7 +97,7 @@ func (c *client) PutManifest(ctx context.Context, ref reference.Named, manifest
return "", err
}
repoEndpoint.actions = trust.ActionsPushAndPull
repoEndpoint.actions = []string{"pull", "push"}
repo, err := c.getRepositoryForReference(ctx, ref, repoEndpoint)
if err != nil {
return "", err
@@ -121,7 +114,10 @@ func (c *client) PutManifest(ctx context.Context, ref reference.Named, manifest
}
dgst, err := manifestService.Put(ctx, manifest, opts...)
return dgst, errors.Wrapf(err, "failed to put manifest %s", ref)
if err != nil {
return dgst, errors.Wrapf(err, "failed to put manifest %s", ref)
}
return dgst, nil
}
func (c *client) getRepositoryForReference(ctx context.Context, ref reference.Named, repoEndpoint repositoryEndpoint) (distribution.Repository, error) {
@@ -135,7 +131,7 @@ func (c *client) getRepositoryForReference(ctx context.Context, ref reference.Na
return nil, err
}
if !repoEndpoint.endpoint.TLSConfig.InsecureSkipVerify {
return nil, ErrHTTPProto{OrigErr: err.Error()}
return nil, httpProtoError{cause: err}
}
// --insecure was set; fall back to plain HTTP
if url := repoEndpoint.endpoint.URL; url != nil && url.Scheme == "https" {
@@ -157,7 +153,10 @@ func (c *client) getHTTPTransportForRepoEndpoint(ctx context.Context, repoEndpoi
c.userAgent,
repoEndpoint.actions,
)
return httpTransport, errors.Wrap(err, "failed to configure transport")
if err != nil {
return nil, errors.Wrap(err, "failed to configure transport")
}
return httpTransport, nil
}
// GetManifest returns an ImageManifest for the reference
+6 -13
View File
@@ -6,7 +6,6 @@ import (
"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"
@@ -31,10 +30,7 @@ func (r repositoryEndpoint) BaseURL() string {
}
func newDefaultRepositoryEndpoint(ref reference.Named, insecure bool) (repositoryEndpoint, error) {
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return repositoryEndpoint{}, err
}
repoInfo, _ := registry.ParseRepositoryInfo(ref)
endpoint, err := getDefaultEndpointFromRepoInfo(repoInfo)
if err != nil {
return repositoryEndpoint{}, err
@@ -94,7 +90,7 @@ func getHTTPTransport(authConfig registrytypes.AuthConfig, endpoint registry.API
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler))
} else {
if len(actions) == 0 {
actions = trust.ActionsPullOnly
actions = []string{"pull"}
}
creds := registry.NewStaticCredentialStore(&authConfig)
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...)
@@ -104,14 +100,11 @@ func getHTTPTransport(authConfig registrytypes.AuthConfig, endpoint registry.API
return transport.NewTransport(base, modifiers...), nil
}
// RepoNameForReference returns the repository name from a reference
// RepoNameForReference returns the repository name from a reference.
//
// Deprecated: this function is no longer used and will be removed in the next release.
func RepoNameForReference(ref reference.Named) (string, error) {
// insecure is fine since this only returns the name
repo, err := newDefaultRepositoryEndpoint(ref, false)
if err != nil {
return "", err
}
return repo.Name(), nil
return reference.Path(reference.TrimNamed(ref)), nil
}
type existingTokenHandler struct {
+4 -10
View File
@@ -220,10 +220,7 @@ func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named,
return err
}
repoInfo, err := registry.ParseRepositoryInfo(namedRef)
if err != nil {
return err
}
repoInfo, _ := registry.ParseRepositoryInfo(namedRef)
confirmedTLSRegistries := make(map[string]bool)
for _, endpoint := range endpoints {
@@ -241,7 +238,8 @@ func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named,
repo, err := c.getRepositoryForReference(ctx, namedRef, repoEndpoint)
if err != nil {
logrus.Debugf("error %s with repo endpoint %+v", err, repoEndpoint)
if _, ok := err.(ErrHTTPProto); ok {
var protoErr httpProtoError
if errors.As(err, &protoErr) {
continue
}
return err
@@ -272,11 +270,6 @@ func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named,
// allEndpoints returns a list of endpoints ordered by priority (v2, http).
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))
@@ -286,6 +279,7 @@ func allEndpoints(namedRef reference.Named, insecure bool) ([]registry.APIEndpoi
if err != nil {
return []registry.APIEndpoint{}, err
}
repoInfo, _ := registry.ParseRepositoryInfo(namedRef)
endpoints, err := registryService.LookupPullEndpoints(reference.Domain(repoInfo.Name))
logrus.Debugf("endpoints for %s: %v", namedRef, endpoints)
return endpoints, err
+18 -7
View File
@@ -40,10 +40,11 @@ var (
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"
)
// NotaryServer is the endpoint serving the Notary trust server
const NotaryServer = "https://notary.docker.io"
// GetTrustDirectory returns the base trust directory name
func GetTrustDirectory() string {
return filepath.Join(config.Dir(), "trust")
@@ -238,6 +239,20 @@ func NotaryError(repoName string, err error) error {
return err
}
// AddToAllSignableRoles attempts to add the image target to all the top level
// delegation roles we can (based on whether we have the signing key and whether
// the role's path allows us to).
//
// If there are no delegation roles, we add to the targets role.
func AddToAllSignableRoles(repo client.Repository, target *client.Target) error {
signableRoles, err := GetSignableRoles(repo, target)
if err != nil {
return err
}
return repo.AddTarget(target, signableRoles...)
}
// GetSignableRoles returns a list of roles for which we have valid signing
// keys, given a notary repository and a target
func GetSignableRoles(repo client.Repository, target *client.Target) ([]data.RoleName, error) {
@@ -307,11 +322,7 @@ func GetImageReferencesAndAuth(ctx context.Context,
}
// Resolve the Repository name from fqn to RepositoryInfo
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return ImageRefAndAuth{}, err
}
repoInfo, _ := registry.ParseRepositoryInfo(ref)
authConfig := authResolver(ctx, repoInfo.Index)
return ImageRefAndAuth{
original: imgName,
+143
View File
@@ -0,0 +1,143 @@
package trust
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"sort"
"github.com/distribution/reference"
"github.com/docker/cli/cli/internal/jsonstream"
"github.com/docker/cli/cli/streams"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/registry"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/theupdateframework/notary/client"
"github.com/theupdateframework/notary/tuf/data"
)
// Streams is an interface which exposes the standard input and output streams.
//
// Same interface as [github.com/docker/cli/cli/command.Streams] but defined here to prevent a circular import.
type Streams interface {
In() *streams.In
Out() *streams.Out
Err() *streams.Out
}
// PushTrustedReference pushes a canonical reference to the trust server.
//
//nolint:gocyclo
func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader, userAgent string) error {
// If it is a trusted push we would like to find the target entry which match the
// tag provided in the function and then do an AddTarget later.
notaryTarget := &client.Target{}
// Count the times of calling for handleTarget,
// if it is called more that once, that should be considered an error in a trusted push.
cnt := 0
handleTarget := func(msg jsonstream.JSONMessage) {
cnt++
if cnt > 1 {
// handleTarget should only be called once. This will be treated as an error.
return
}
var pushResult types.PushResult
err := json.Unmarshal(*msg.Aux, &pushResult)
if err == nil && pushResult.Tag != "" {
if dgst, err := digest.Parse(pushResult.Digest); err == nil {
h, err := hex.DecodeString(dgst.Hex())
if err != nil {
notaryTarget = nil
return
}
notaryTarget.Name = pushResult.Tag
notaryTarget.Hashes = data.Hashes{string(dgst.Algorithm()): h}
notaryTarget.Length = int64(pushResult.Size)
}
}
}
var tag string
switch x := ref.(type) {
case reference.Canonical:
return errors.New("cannot push a digest reference")
case reference.NamedTagged:
tag = x.Tag()
default:
// We want trust signatures to always take an explicit tag,
// otherwise it will act as an untrusted push.
if err := jsonstream.Display(ctx, in, ioStreams.Out()); err != nil {
return err
}
_, _ = fmt.Fprintln(ioStreams.Err(), "No tag specified, skipping trust metadata push")
return nil
}
if err := jsonstream.Display(ctx, in, ioStreams.Out(), jsonstream.WithAuxCallback(handleTarget)); err != nil {
return err
}
if cnt > 1 {
return errors.Errorf("internal error: only one call to handleTarget expected")
}
if notaryTarget == nil {
return errors.Errorf("no targets found, provide a specific tag in order to sign it")
}
_, _ = fmt.Fprintln(ioStreams.Out(), "Signing and pushing trust metadata")
repo, err := GetNotaryRepository(ioStreams.In(), ioStreams.Out(), userAgent, repoInfo, &authConfig, "push", "pull")
if err != nil {
return errors.Wrap(err, "error establishing connection to trust repository")
}
// get the latest repository metadata so we can figure out which roles to sign
_, err = repo.ListTargets()
switch err.(type) {
case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
keys := repo.GetCryptoService().ListKeys(data.CanonicalRootRole)
var rootKeyID string
// always select the first root key
if len(keys) > 0 {
sort.Strings(keys)
rootKeyID = keys[0]
} else {
rootPublicKey, err := repo.GetCryptoService().Create(data.CanonicalRootRole, "", data.ECDSAKey)
if err != nil {
return err
}
rootKeyID = rootPublicKey.ID()
}
// Initialize the notary repository with a remotely managed snapshot key
if err := repo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil {
return NotaryError(repoInfo.Name.Name(), err)
}
_, _ = fmt.Fprintf(ioStreams.Out(), "Finished initializing %q\n", repoInfo.Name.Name())
err = repo.AddTarget(notaryTarget, data.CanonicalTargetsRole)
case nil:
// already initialized and we have successfully downloaded the latest metadata
err = AddToAllSignableRoles(repo, notaryTarget)
default:
return NotaryError(repoInfo.Name.Name(), err)
}
if err == nil {
err = repo.Publish()
}
if err != nil {
err = errors.Wrapf(err, "failed to sign %s:%s", repoInfo.Name.Name(), tag)
return NotaryError(repoInfo.Name.Name(), err)
}
_, _ = fmt.Fprintf(ioStreams.Out(), "Successfully signed %s:%s\n", repoInfo.Name.Name(), tag)
return nil
}
+22
View File
@@ -0,0 +1,22 @@
package trust
import (
"context"
"fmt"
"io"
"github.com/distribution/reference"
"github.com/docker/docker/client"
)
// TagTrusted tags a trusted ref. It is a shallow wrapper around [client.Client.ImageTag]
// that updates the given image references to their familiar format for tagging
// and printing.
func TagTrusted(ctx context.Context, apiClient client.ImageAPIClient, out io.Writer, trustedRef reference.Canonical, ref reference.NamedTagged) error {
// Use familiar references when interacting with client and output
familiarRef := reference.FamiliarString(ref)
trustedFamiliarRef := reference.FamiliarString(trustedRef)
_, _ = fmt.Fprintf(out, "Tagging %s as %s\n", trustedFamiliarRef, familiarRef)
return apiClient.ImageTag(ctx, trustedFamiliarRef, familiarRef)
}