chore: bump deps
All checks were successful
continuous-integration/drone/push Build is passing

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

31
vendor/github.com/docker/cli/AUTHORS generated vendored
View File

@ -48,6 +48,7 @@ Alfred Landrum <alfred.landrum@docker.com>
Ali Rostami <rostami.ali@gmail.com>
Alicia Lauerman <alicia@eta.im>
Allen Sun <allensun.shl@alibaba-inc.com>
Allie Sadler <allie.sadler@docker.com>
Alvin Deng <alvin.q.deng@utexas.edu>
Amen Belayneh <amenbelayneh@gmail.com>
Amey Shrivastava <72866602+AmeyShrivastava@users.noreply.github.com>
@ -81,6 +82,7 @@ Antonis Kalipetis <akalipetis@gmail.com>
Anusha Ragunathan <anusha.ragunathan@docker.com>
Ao Li <la9249@163.com>
Arash Deshmeh <adeshmeh@ca.ibm.com>
Archimedes Trajano <developer@trajano.net>
Arko Dasgupta <arko@tetrate.io>
Arnaud Porterie <icecrime@gmail.com>
Arnaud Rebillout <elboulangero@gmail.com>
@ -88,6 +90,7 @@ Arthur Peka <arthur.peka@outlook.com>
Ashly Mathew <ashly.mathew@sap.com>
Ashwini Oruganti <ashwini.oruganti@gmail.com>
Aslam Ahemad <aslamahemad@gmail.com>
Austin Vazquez <austin.vazquez.dev@gmail.com>
Azat Khuyiyakhmetov <shadow_uz@mail.ru>
Bardia Keyoumarsi <bkeyouma@ucsc.edu>
Barnaby Gray <barnaby@pickle.me.uk>
@ -132,6 +135,7 @@ Cao Weiwei <cao.weiwei30@zte.com.cn>
Carlo Mion <mion00@gmail.com>
Carlos Alexandro Becker <caarlos0@gmail.com>
Carlos de Paula <me@carlosedp.com>
Carston Schilds <Carston.Schilds@visier.com>
Casey Korver <casey@korver.dev>
Ce Gao <ce.gao@outlook.com>
Cedric Davies <cedricda@microsoft.com>
@ -189,6 +193,7 @@ Daisuke Ito <itodaisuke00@gmail.com>
dalanlan <dalanlan925@gmail.com>
Damien Nadé <github@livna.org>
Dan Cotora <dan@bluevision.ro>
Dan Wallis <dan@wallis.nz>
Danial Gharib <danial.mail.gh@gmail.com>
Daniel Artine <daniel.artine@ufrj.br>
Daniel Cassidy <mail@danielcassidy.me.uk>
@ -237,6 +242,7 @@ Deshi Xiao <dxiao@redhat.com>
Dharmit Shah <shahdharmit@gmail.com>
Dhawal Yogesh Bhanushali <dbhanushali@vmware.com>
Dieter Reuter <dieter.reuter@me.com>
Dilep Dev <34891655+DilepDev@users.noreply.github.com>
Dima Stopel <dima@twistlock.com>
Dimitry Andric <d.andric@activevideo.com>
Ding Fei <dingfei@stars.org.cn>
@ -308,6 +314,8 @@ George MacRorie <gmacr31@gmail.com>
George Margaritis <gmargaritis@protonmail.com>
George Xie <georgexsh@gmail.com>
Gianluca Borello <g.borello@gmail.com>
Giau. Tran Minh <hello@giautm.dev>
Giedrius Jonikas <giedriusj1@gmail.com>
Gildas Cuisinier <gildas.cuisinier@gcuisinier.net>
Gio d'Amelio <giodamelio@gmail.com>
Gleb Stsenov <gleb.stsenov@gmail.com>
@ -344,6 +352,7 @@ Hugo Gabriel Eyherabide <hugogabriel.eyherabide@gmail.com>
huqun <huqun@zju.edu.cn>
Huu Nguyen <huu@prismskylabs.com>
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com>
Iain MacDonald <IJMacD@gmail.com>
Iain Samuel McLean Elder <iain@isme.es>
Ian Campbell <ian.campbell@docker.com>
Ian Philpot <ian.philpot@microsoft.com>
@ -393,6 +402,7 @@ Jesse Adametz <jesseadametz@gmail.com>
Jessica Frazelle <jess@oxide.computer>
Jezeniel Zapanta <jpzapanta22@gmail.com>
Jian Zhang <zhangjian.fnst@cn.fujitsu.com>
Jianyong Wu <wujianyong@hygon.cn>
Jie Luo <luo612@zju.edu.cn>
Jilles Oldenbeuving <ojilles@gmail.com>
Jim Chen <njucjc@gmail.com>
@ -446,6 +456,7 @@ Julian <gitea+julian@ic.thejulian.uk>
Julien Barbier <write0@gmail.com>
Julien Kassar <github@kassisol.com>
Julien Maitrehenry <julien.maitrehenry@me.com>
Julio Cesar Garcia <juliogarciamelgarejo@gmail.com>
Justas Brazauskas <brazauskasjustas@gmail.com>
Justin Chadwell <me@jedevc.com>
Justin Cormack <justin.cormack@docker.com>
@ -490,19 +501,22 @@ Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp>
Kyle Mitofsky <Kylemit@gmail.com>
Lachlan Cooper <lachlancooper@gmail.com>
Lai Jiangshan <jiangshanlai@gmail.com>
Lajos Papp <lajos.papp@sequenceiq.com>
Lars Kellogg-Stedman <lars@redhat.com>
Laura Brehm <laurabrehm@hey.com>
Laura Frank <ljfrank@gmail.com>
Laurent Erignoux <lerignoux@gmail.com>
Laurent Goderre <laurent.goderre@docker.com>
Lee Gaines <eightlimbed@gmail.com>
Lei Jitang <leijitang@huawei.com>
Lennie <github@consolejunkie.net>
lentil32 <lentil32@icloud.com>
Leo Gallucci <elgalu3@gmail.com>
Leonid Skorospelov <leosko94@gmail.com>
Lewis Daly <lewisdaly@me.com>
Li Fu Bang <lifubang@acmcoder.com>
Li Yi <denverdino@gmail.com>
Li Yi <weiyuan.yl@alibaba-inc.com>
Li Zeghong <zeghong@hotmail.com>
Liang-Chi Hsieh <viirya@gmail.com>
Lihua Tang <lhtang@alauda.io>
Lily Guo <lily.guo@docker.com>
@ -515,6 +529,7 @@ lixiaobing10051267 <li.xiaobing1@zte.com.cn>
Lloyd Dewolf <foolswisdom@gmail.com>
Lorenzo Fontana <lo@linux.com>
Louis Opter <kalessin@kalessin.fr>
Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
Luca Favatella <luca.favatella@erlang-solutions.com>
Luca Marturana <lucamarturana@gmail.com>
Lucas Chan <lucas-github@lucaschan.com>
@ -559,6 +574,7 @@ Matt Robenolt <matt@ydekproductions.com>
Matteo Orefice <matteo.orefice@bites4bits.software>
Matthew Heon <mheon@redhat.com>
Matthieu Hauglustaine <matt.hauglustaine@gmail.com>
Matthieu MOREL <matthieu.morel35@gmail.com>
Mauro Porras P <mauroporrasp@gmail.com>
Max Shytikov <mshytikov@gmail.com>
Max-Julian Pogner <max-julian@pogner.at>
@ -566,6 +582,7 @@ Maxime Petazzoni <max@signalfuse.com>
Maximillian Fan Xavier <maximillianfx@gmail.com>
Mei ChunTao <mei.chuntao@zte.com.cn>
Melroy van den Berg <melroy@melroy.org>
Mert Şişmanoğlu <mert190737fb@gmail.com>
Metal <2466052+tedhexaflow@users.noreply.github.com>
Micah Zoltu <micah@newrelic.com>
Michael A. Smith <michael@smith-li.com>
@ -598,7 +615,9 @@ Mindaugas Rukas <momomg@gmail.com>
Miroslav Gula <miroslav.gula@naytrolabs.com>
Misty Stanley-Jones <misty@docker.com>
Mohammad Banikazemi <mb@us.ibm.com>
Mohammad Hossein <mhm98035@gmail.com>
Mohammed Aaqib Ansari <maaquib@gmail.com>
Mohammed Aminu Futa <mohammedfuta2000@gmail.com>
Mohini Anne Dsouza <mohini3917@gmail.com>
Moorthy RS <rsmoorthy@gmail.com>
Morgan Bauer <mbauer@us.ibm.com>
@ -633,9 +652,11 @@ Nicolas De Loof <nicolas.deloof@gmail.com>
Nikhil Chawla <chawlanikhil24@gmail.com>
Nikolas Garofil <nikolas.garofil@uantwerpen.be>
Nikolay Milovanov <nmil@itransformers.net>
NinaLua <iturf@sina.cn>
Nir Soffer <nsoffer@redhat.com>
Nishant Totla <nishanttotla@gmail.com>
NIWA Hideyuki <niwa.niwa@nifty.ne.jp>
Noah Silas <noah@hustle.com>
Noah Treuhaft <noah.treuhaft@docker.com>
O.S. Tezer <ostezer@gmail.com>
Oded Arbel <oded@geek.co.il>
@ -653,10 +674,12 @@ Patrick Böänziger <patrick.baenziger@bsi-software.com>
Patrick Daigle <114765035+pdaig@users.noreply.github.com>
Patrick Hemmer <patrick.hemmer@gmail.com>
Patrick Lang <plang@microsoft.com>
Patrick St. laurent <patrick@saint-laurent.us>
Paul <paul9869@gmail.com>
Paul Kehrer <paul.l.kehrer@gmail.com>
Paul Lietar <paul@lietar.net>
Paul Mulders <justinkb@gmail.com>
Paul Rogalski <mail@paul-rogalski.de>
Paul Seyfert <pseyfert.mathphys@gmail.com>
Paul Weaver <pauweave@cisco.com>
Pavel Pospisil <pospispa@gmail.com>
@ -678,7 +701,6 @@ Philip Alexander Etling <paetling@gmail.com>
Philipp Gillé <philipp.gille@gmail.com>
Philipp Schmied <pschmied@schutzwerk.com>
Phong Tran <tran.pho@northeastern.edu>
pidster <pid@pidster.com>
Pieter E Smit <diepes@github.com>
pixelistik <pixelistik@users.noreply.github.com>
Pratik Karki <prertik@outlook.com>
@ -738,6 +760,7 @@ Samuel Cochran <sj26@sj26.com>
Samuel Karp <skarp@amazon.com>
Sandro Jäckel <sandro.jaeckel@gmail.com>
Santhosh Manohar <santhosh@docker.com>
Sarah Sanders <sarah.sanders@docker.com>
Sargun Dhillon <sargun@netflix.com>
Saswat Bhattacharya <sas.saswat@gmail.com>
Saurabh Kumar <saurabhkumar0184@gmail.com>
@ -770,6 +793,7 @@ Spencer Brown <spencer@spencerbrown.org>
Spring Lee <xi.shuai@outlook.com>
squeegels <lmscrewy@gmail.com>
Srini Brahmaroutu <srbrahma@us.ibm.com>
Stavros Panakakis <stavrospanakakis@gmail.com>
Stefan S. <tronicum@user.github.com>
Stefan Scherer <stefan.scherer@docker.com>
Stefan Weil <sw@weilnetz.de>
@ -780,6 +804,7 @@ Steve Durrheimer <s.durrheimer@gmail.com>
Steve Richards <steve.richards@docker.com>
Steven Burgess <steven.a.burgess@hotmail.com>
Stoica-Marcu Floris-Andrei <floris.sm@gmail.com>
Stuart Williams <pid@pidster.com>
Subhajit Ghosh <isubuz.g@gmail.com>
Sun Jianbo <wonderflow.sun@gmail.com>
Sune Keller <absukl@almbrand.dk>
@ -867,6 +892,7 @@ Wang Yumu <37442693@qq.com>
Wataru Ishida <ishida.wataru@lab.ntt.co.jp>
Wayne Song <wsong@docker.com>
Wen Cheng Ma <wenchma@cn.ibm.com>
Wenlong Zhang <zhangwenlong@loongson.cn>
Wenzhi Liang <wenzhi.liang@gmail.com>
Wes Morgan <cap10morgan@gmail.com>
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
@ -908,3 +934,4 @@ Zhuo Zhi <h.dwwwwww@gmail.com>
Átila Camurça Alves <camurca.home@gmail.com>
Александр Менщиков <__Singleton__@hackerdom.ru>
徐俊杰 <paco.xu@daocloud.io>
林博仁 Buo-ren Lin <Buo.Ren.Lin@gmail.com>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@ -1,4 +1,4 @@
Copyright (c) 2012 The Go Authors. All rights reserved.
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
@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
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 Google Inc. nor the names of its
* 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.

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
}

View File

@ -0,0 +1,98 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Code below was largely copied from golang.org/x/mod@v0.22;
// https://github.com/golang/mod/blob/v0.22.0/internal/lazyregexp/lazyre.go
// with some additional methods added.
// Package lazyregexp is a thin wrapper over regexp, allowing the use of global
// regexp variables without forcing them to be compiled at init.
package lazyregexp
import (
"os"
"regexp"
"strings"
"sync"
)
// Regexp is a wrapper around [regexp.Regexp], where the underlying regexp will be
// compiled the first time it is needed.
type Regexp struct {
str string
once sync.Once
rx *regexp.Regexp
}
func (r *Regexp) re() *regexp.Regexp {
r.once.Do(r.build)
return r.rx
}
func (r *Regexp) build() {
r.rx = regexp.MustCompile(r.str)
r.str = ""
}
func (r *Regexp) FindSubmatch(s []byte) [][]byte {
return r.re().FindSubmatch(s)
}
func (r *Regexp) FindAllStringSubmatch(s string, n int) [][]string {
return r.re().FindAllStringSubmatch(s, n)
}
func (r *Regexp) FindStringSubmatch(s string) []string {
return r.re().FindStringSubmatch(s)
}
func (r *Regexp) FindStringSubmatchIndex(s string) []int {
return r.re().FindStringSubmatchIndex(s)
}
func (r *Regexp) ReplaceAllString(src, repl string) string {
return r.re().ReplaceAllString(src, repl)
}
func (r *Regexp) FindString(s string) string {
return r.re().FindString(s)
}
func (r *Regexp) FindAllString(s string, n int) []string {
return r.re().FindAllString(s, n)
}
func (r *Regexp) MatchString(s string) bool {
return r.re().MatchString(s)
}
func (r *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string {
return r.re().ReplaceAllStringFunc(src, repl)
}
func (r *Regexp) ReplaceAllLiteralString(src, repl string) string {
return r.re().ReplaceAllLiteralString(src, repl)
}
func (r *Regexp) String() string {
return r.re().String()
}
func (r *Regexp) SubexpNames() []string {
return r.re().SubexpNames()
}
var inTest = len(os.Args) > 0 && strings.HasSuffix(strings.TrimSuffix(os.Args[0], ".exe"), ".test")
// New creates a new lazy regexp, delaying the compiling work until it is first
// needed. If the code is being run as part of tests, the regexp compiling will
// happen immediately.
func New(str string) *Regexp {
lr := &Regexp{str: str}
if inTest {
// In tests, always compile the regexps early.
lr.re()
}
return lr
}

116
vendor/github.com/docker/cli/internal/prompt/prompt.go generated vendored Normal file
View File

@ -0,0 +1,116 @@
// Package prompt provides utilities to prompt the user for input.
package prompt
import (
"bufio"
"context"
"io"
"os"
"runtime"
"strings"
"github.com/docker/cli/cli/streams"
"github.com/moby/term"
)
const ErrTerminated cancelledErr = "prompt terminated"
type cancelledErr string
func (e cancelledErr) Error() string {
return string(e)
}
func (cancelledErr) Cancelled() {}
// 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.
//
// TODO(thaJeztah): implement without depending on streams?
func DisableInputEcho(ins *streams.In) (restore func() error, _ 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)
}
// ReadInput requests input from the user.
//
// It returns an empty string ("") with an [ErrTerminated] if the user terminates
// the CLI with SIGINT or SIGTERM while the prompt is active. If the prompt
// returns an error, the caller should close the [io.Reader] used for the prompt
// and propagate the error up the stack to prevent the background goroutine
// from blocking indefinitely.
func ReadInput(ctx context.Context, in io.Reader, out io.Writer, message string) (string, error) {
_, _ = out.Write([]byte(message))
result := make(chan string)
go func() {
scanner := bufio.NewScanner(in)
if scanner.Scan() {
result <- strings.TrimSpace(scanner.Text())
}
}()
select {
case <-ctx.Done():
_, _ = out.Write([]byte("\n"))
return "", ErrTerminated
case r := <-result:
return r, nil
}
}
// Confirm requests and checks confirmation from the user.
//
// It displays the provided message followed by "[y/N]". If the user
// input 'y' or 'Y' it returns true otherwise false. If no message is provided,
// "Are you sure you want to proceed? [y/N] " will be used instead.
//
// It returns false with an [ErrTerminated] if the user terminates
// the CLI with SIGINT or SIGTERM while the prompt is active. If the prompt
// returns an error, the caller should close the [io.Reader] used for the prompt
// and propagate the error up the stack to prevent the background goroutine
// from blocking indefinitely.
func Confirm(ctx context.Context, in io.Reader, out io.Writer, message string) (bool, error) {
if message == "" {
message = "Are you sure you want to proceed?"
}
message += " [y/N] "
_, _ = out.Write([]byte(message))
// On Windows, force the use of the regular OS stdin stream.
if runtime.GOOS == "windows" {
in = streams.NewIn(os.Stdin)
}
result := make(chan bool)
go func() {
var res bool
scanner := bufio.NewScanner(in)
if scanner.Scan() {
answer := strings.TrimSpace(scanner.Text())
if strings.EqualFold(answer, "y") {
res = true
}
}
result <- res
}()
select {
case <-ctx.Done():
_, _ = out.Write([]byte("\n"))
return false, ErrTerminated
case r := <-result:
return r, nil
}
}

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 tui

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 tui

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 tui

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 tui
@ -15,19 +15,39 @@ var InfoHeader = Str{
Fancy: aec.Bold.Apply(aec.LightCyanB.Apply(aec.BlackF.Apply("i")) + " " + aec.LightCyanF.Apply("Info → ")),
}
func (o Output) PrintNote(format string, args ...any) {
type options struct {
header Str
}
type noteOptions func(o *options)
func withHeader(header Str) noteOptions {
return func(o *options) {
o.header = header
}
}
func (o Output) printNoteWithOptions(format string, args []any, opts ...noteOptions) {
if o.isTerminal {
// TODO: Handle all flags
format = strings.ReplaceAll(format, "--platform", ColorFlag.Apply("--platform"))
}
header := o.Sprint(InfoHeader)
opt := &options{
header: InfoHeader,
}
_, _ = fmt.Fprint(o, "\n", header)
for _, override := range opts {
override(opt)
}
h := o.Sprint(opt.header)
_, _ = fmt.Fprint(o, "\n", h)
s := fmt.Sprintf(format, args...)
for idx, line := range strings.Split(s, "\n") {
if idx > 0 {
_, _ = fmt.Fprint(o, strings.Repeat(" ", Width(header)))
_, _ = fmt.Fprint(o, strings.Repeat(" ", Width(h)))
}
l := line
@ -37,3 +57,16 @@ func (o Output) PrintNote(format string, args ...any) {
_, _ = fmt.Fprintln(o, l)
}
}
func (o Output) PrintNote(format string, args ...any) {
o.printNoteWithOptions(format, args, withHeader(InfoHeader))
}
var warningHeader = Str{
Plain: " Warn -> ",
Fancy: aec.Bold.Apply(aec.LightYellowB.Apply(aec.BlackF.Apply("w")) + " " + ColorWarning.Apply("Warn → ")),
}
func (o Output) PrintWarning(format string, args ...any) {
o.printNoteWithOptions(format, args, withHeader(warningHeader))
}

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 tui

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 tui

View File

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

View File

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

View File

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

View File

@ -100,7 +100,7 @@ func (m *MountOpt) Set(value string) error {
mount.Type = mounttypes.Type(strings.ToLower(val))
case "source", "src":
mount.Source = val
if strings.HasPrefix(val, "."+string(filepath.Separator)) || val == "." {
if !filepath.IsAbs(val) && strings.HasPrefix(val, ".") {
if abs, err := filepath.Abs(val); err == nil {
mount.Source = abs
}
@ -135,8 +135,7 @@ func (m *MountOpt) Set(value string) error {
// TODO: implicitly set propagation and error if the user specifies a propagation in a future refactor/UX polish pass
// https://github.com/docker/cli/pull/4316#discussion_r1341974730
default:
return fmt.Errorf("invalid value for %s: %s (must be \"enabled\", \"disabled\", \"writable\", or \"readonly\")",
key, val)
return fmt.Errorf(`invalid value for %s: %s (must be "enabled", "disabled", "writable", or "readonly")`, key, val)
}
case "volume-subpath":
volumeOptions().Subpath = val

View File

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

View File

@ -1,21 +1,21 @@
package opts
import (
"errors"
"fmt"
"math/big"
"net"
"path"
"regexp"
"strings"
"github.com/docker/cli/internal/lazyregexp"
"github.com/docker/docker/api/types/filters"
units "github.com/docker/go-units"
"github.com/pkg/errors"
"github.com/docker/go-units"
)
var (
alphaRegexp = regexp.MustCompile(`[a-zA-Z]`)
domainRegexp = regexp.MustCompile(`^(:?(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]))(:?\.(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])))*)\.?\s*$`)
alphaRegexp = lazyregexp.New(`[a-zA-Z]`)
domainRegexp = lazyregexp.New(`^(:?(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]))(:?\.(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])))*)\.?\s*$`)
)
// ListOpts holds a list of values and a validation function.
@ -80,10 +80,22 @@ func (opts *ListOpts) GetMap() map[string]struct{} {
}
// GetAll returns the values of slice.
//
// Deprecated: use [ListOpts.GetSlice] instead. This method will be removed in a future release.
func (opts *ListOpts) GetAll() []string {
return *opts.values
}
// GetSlice returns the values of slice.
//
// It implements [cobra.SliceValue] to allow shell completion to be provided
// multiple times.
//
// [cobra.SliceValue]: https://pkg.go.dev/github.com/spf13/cobra@v1.9.1#SliceValue
func (opts *ListOpts) GetSlice() []string {
return *opts.values
}
// GetAllOrEmpty returns the values of the slice
// or an empty slice when there are no values.
func (opts *ListOpts) GetAllOrEmpty() []string {

View File

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

View File

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

View File

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

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 templates

View File

@ -1 +0,0 @@
package manifest

View File

@ -1,239 +0,0 @@
package manifestlist
import (
"encoding/json"
"errors"
"fmt"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest"
"github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
const (
// MediaTypeManifestList specifies the mediaType for manifest lists.
MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json"
)
// SchemaVersion provides a pre-initialized version structure for this
// packages version of the manifest.
var SchemaVersion = manifest.Versioned{
SchemaVersion: 2,
MediaType: MediaTypeManifestList,
}
// OCISchemaVersion provides a pre-initialized version structure for this
// packages OCIschema version of the manifest.
var OCISchemaVersion = manifest.Versioned{
SchemaVersion: 2,
MediaType: v1.MediaTypeImageIndex,
}
func init() {
manifestListFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
m := new(DeserializedManifestList)
err := m.UnmarshalJSON(b)
if err != nil {
return nil, distribution.Descriptor{}, err
}
if m.MediaType != MediaTypeManifestList {
err = fmt.Errorf("mediaType in manifest list should be '%s' not '%s'",
MediaTypeManifestList, m.MediaType)
return nil, distribution.Descriptor{}, err
}
dgst := digest.FromBytes(b)
return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: MediaTypeManifestList}, err
}
err := distribution.RegisterManifestSchema(MediaTypeManifestList, manifestListFunc)
if err != nil {
panic(fmt.Sprintf("Unable to register manifest: %s", err))
}
imageIndexFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
if err := validateIndex(b); err != nil {
return nil, distribution.Descriptor{}, err
}
m := new(DeserializedManifestList)
err := m.UnmarshalJSON(b)
if err != nil {
return nil, distribution.Descriptor{}, err
}
if m.MediaType != "" && m.MediaType != v1.MediaTypeImageIndex {
err = fmt.Errorf("if present, mediaType in image index should be '%s' not '%s'",
v1.MediaTypeImageIndex, m.MediaType)
return nil, distribution.Descriptor{}, err
}
dgst := digest.FromBytes(b)
return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: v1.MediaTypeImageIndex}, err
}
err = distribution.RegisterManifestSchema(v1.MediaTypeImageIndex, imageIndexFunc)
if err != nil {
panic(fmt.Sprintf("Unable to register OCI Image Index: %s", err))
}
}
// PlatformSpec specifies a platform where a particular image manifest is
// applicable.
type PlatformSpec struct {
// Architecture field specifies the CPU architecture, for example
// `amd64` or `ppc64`.
Architecture string `json:"architecture"`
// OS specifies the operating system, for example `linux` or `windows`.
OS string `json:"os"`
// OSVersion is an optional field specifying the operating system
// version, for example `10.0.10586`.
OSVersion string `json:"os.version,omitempty"`
// OSFeatures is an optional field specifying an array of strings,
// each listing a required OS feature (for example on Windows `win32k`).
OSFeatures []string `json:"os.features,omitempty"`
// Variant is an optional field specifying a variant of the CPU, for
// example `ppc64le` to specify a little-endian version of a PowerPC CPU.
Variant string `json:"variant,omitempty"`
// Features is an optional field specifying an array of strings, each
// listing a required CPU feature (for example `sse4` or `aes`).
Features []string `json:"features,omitempty"`
}
// A ManifestDescriptor references a platform-specific manifest.
type ManifestDescriptor struct {
distribution.Descriptor
// Platform specifies which platform the manifest pointed to by the
// descriptor runs on.
Platform PlatformSpec `json:"platform"`
}
// ManifestList references manifests for various platforms.
type ManifestList struct {
manifest.Versioned
// Config references the image configuration as a blob.
Manifests []ManifestDescriptor `json:"manifests"`
}
// References returns the distribution descriptors for the referenced image
// manifests.
func (m ManifestList) References() []distribution.Descriptor {
dependencies := make([]distribution.Descriptor, len(m.Manifests))
for i := range m.Manifests {
dependencies[i] = m.Manifests[i].Descriptor
}
return dependencies
}
// DeserializedManifestList wraps ManifestList with a copy of the original
// JSON.
type DeserializedManifestList struct {
ManifestList
// canonical is the canonical byte representation of the Manifest.
canonical []byte
}
// FromDescriptors takes a slice of descriptors, and returns a
// DeserializedManifestList which contains the resulting manifest list
// and its JSON representation.
func FromDescriptors(descriptors []ManifestDescriptor) (*DeserializedManifestList, error) {
var mediaType string
if len(descriptors) > 0 && descriptors[0].Descriptor.MediaType == v1.MediaTypeImageManifest {
mediaType = v1.MediaTypeImageIndex
} else {
mediaType = MediaTypeManifestList
}
return FromDescriptorsWithMediaType(descriptors, mediaType)
}
// FromDescriptorsWithMediaType is for testing purposes, it's useful to be able to specify the media type explicitly
func FromDescriptorsWithMediaType(descriptors []ManifestDescriptor, mediaType string) (*DeserializedManifestList, error) {
m := ManifestList{
Versioned: manifest.Versioned{
SchemaVersion: 2,
MediaType: mediaType,
},
}
m.Manifests = make([]ManifestDescriptor, len(descriptors))
copy(m.Manifests, descriptors)
deserialized := DeserializedManifestList{
ManifestList: m,
}
var err error
deserialized.canonical, err = json.MarshalIndent(&m, "", " ")
return &deserialized, err
}
// UnmarshalJSON populates a new ManifestList struct from JSON data.
func (m *DeserializedManifestList) UnmarshalJSON(b []byte) error {
m.canonical = make([]byte, len(b))
// store manifest list in canonical
copy(m.canonical, b)
// Unmarshal canonical JSON into ManifestList object
var manifestList ManifestList
if err := json.Unmarshal(m.canonical, &manifestList); err != nil {
return err
}
m.ManifestList = manifestList
return nil
}
// MarshalJSON returns the contents of canonical. If canonical is empty,
// marshals the inner contents.
func (m *DeserializedManifestList) MarshalJSON() ([]byte, error) {
if len(m.canonical) > 0 {
return m.canonical, nil
}
return nil, errors.New("JSON representation not initialized in DeserializedManifestList")
}
// Payload returns the raw content of the manifest list. The contents can be
// used to calculate the content identifier.
func (m DeserializedManifestList) Payload() (string, []byte, error) {
var mediaType string
if m.MediaType == "" {
mediaType = v1.MediaTypeImageIndex
} else {
mediaType = m.MediaType
}
return mediaType, m.canonical, nil
}
// unknownDocument represents a manifest, manifest list, or index that has not
// yet been validated
type unknownDocument struct {
Config interface{} `json:"config,omitempty"`
Layers interface{} `json:"layers,omitempty"`
}
// validateIndex returns an error if the byte slice is invalid JSON or if it
// contains fields that belong to a manifest
func validateIndex(b []byte) error {
var doc unknownDocument
if err := json.Unmarshal(b, &doc); err != nil {
return err
}
if doc.Config != nil || doc.Layers != nil {
return errors.New("index: expected index but found manifest")
}
return nil
}

View File

@ -1,107 +0,0 @@
package ocischema
import (
"context"
"errors"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest"
"github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
// Builder is a type for constructing manifests.
type Builder struct {
// bs is a BlobService used to publish the configuration blob.
bs distribution.BlobService
// configJSON references
configJSON []byte
// layers is a list of layer descriptors that gets built by successive
// calls to AppendReference.
layers []distribution.Descriptor
// Annotations contains arbitrary metadata relating to the targeted content.
annotations map[string]string
// For testing purposes
mediaType string
}
// NewManifestBuilder is used to build new manifests for the current schema
// version. It takes a BlobService so it can publish the configuration blob
// as part of the Build process, and annotations.
func NewManifestBuilder(bs distribution.BlobService, configJSON []byte, annotations map[string]string) distribution.ManifestBuilder {
mb := &Builder{
bs: bs,
configJSON: make([]byte, len(configJSON)),
annotations: annotations,
mediaType: v1.MediaTypeImageManifest,
}
copy(mb.configJSON, configJSON)
return mb
}
// SetMediaType assigns the passed mediatype or error if the mediatype is not a
// valid media type for oci image manifests currently: "" or "application/vnd.oci.image.manifest.v1+json"
func (mb *Builder) SetMediaType(mediaType string) error {
if mediaType != "" && mediaType != v1.MediaTypeImageManifest {
return errors.New("invalid media type for OCI image manifest")
}
mb.mediaType = mediaType
return nil
}
// Build produces a final manifest from the given references.
func (mb *Builder) Build(ctx context.Context) (distribution.Manifest, error) {
m := Manifest{
Versioned: manifest.Versioned{
SchemaVersion: 2,
MediaType: mb.mediaType,
},
Layers: make([]distribution.Descriptor, len(mb.layers)),
Annotations: mb.annotations,
}
copy(m.Layers, mb.layers)
configDigest := digest.FromBytes(mb.configJSON)
var err error
m.Config, err = mb.bs.Stat(ctx, configDigest)
switch err {
case nil:
// Override MediaType, since Put always replaces the specified media
// type with application/octet-stream in the descriptor it returns.
m.Config.MediaType = v1.MediaTypeImageConfig
return FromStruct(m)
case distribution.ErrBlobUnknown:
// nop
default:
return nil, err
}
// Add config to the blob store
m.Config, err = mb.bs.Put(ctx, v1.MediaTypeImageConfig, mb.configJSON)
// Override MediaType, since Put always replaces the specified media
// type with application/octet-stream in the descriptor it returns.
m.Config.MediaType = v1.MediaTypeImageConfig
if err != nil {
return nil, err
}
return FromStruct(m)
}
// AppendReference adds a reference to the current ManifestBuilder.
func (mb *Builder) AppendReference(d distribution.Describable) error {
mb.layers = append(mb.layers, d.Descriptor())
return nil
}
// References returns the current references added to this builder.
func (mb *Builder) References() []distribution.Descriptor {
return mb.layers
}

View File

@ -1,146 +0,0 @@
package ocischema
import (
"encoding/json"
"errors"
"fmt"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest"
"github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
var (
// SchemaVersion provides a pre-initialized version structure for this
// packages version of the manifest.
SchemaVersion = manifest.Versioned{
SchemaVersion: 2, // historical value here.. does not pertain to OCI or docker version
MediaType: v1.MediaTypeImageManifest,
}
)
func init() {
ocischemaFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
if err := validateManifest(b); err != nil {
return nil, distribution.Descriptor{}, err
}
m := new(DeserializedManifest)
err := m.UnmarshalJSON(b)
if err != nil {
return nil, distribution.Descriptor{}, err
}
dgst := digest.FromBytes(b)
return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: v1.MediaTypeImageManifest}, err
}
err := distribution.RegisterManifestSchema(v1.MediaTypeImageManifest, ocischemaFunc)
if err != nil {
panic(fmt.Sprintf("Unable to register manifest: %s", err))
}
}
// Manifest defines a ocischema manifest.
type Manifest struct {
manifest.Versioned
// Config references the image configuration as a blob.
Config distribution.Descriptor `json:"config"`
// Layers lists descriptors for the layers referenced by the
// configuration.
Layers []distribution.Descriptor `json:"layers"`
// Annotations contains arbitrary metadata for the image manifest.
Annotations map[string]string `json:"annotations,omitempty"`
}
// References returns the descriptors of this manifests references.
func (m Manifest) References() []distribution.Descriptor {
references := make([]distribution.Descriptor, 0, 1+len(m.Layers))
references = append(references, m.Config)
references = append(references, m.Layers...)
return references
}
// Target returns the target of this manifest.
func (m Manifest) Target() distribution.Descriptor {
return m.Config
}
// DeserializedManifest wraps Manifest with a copy of the original JSON.
// It satisfies the distribution.Manifest interface.
type DeserializedManifest struct {
Manifest
// canonical is the canonical byte representation of the Manifest.
canonical []byte
}
// FromStruct takes a Manifest structure, marshals it to JSON, and returns a
// DeserializedManifest which contains the manifest and its JSON representation.
func FromStruct(m Manifest) (*DeserializedManifest, error) {
var deserialized DeserializedManifest
deserialized.Manifest = m
var err error
deserialized.canonical, err = json.MarshalIndent(&m, "", " ")
return &deserialized, err
}
// UnmarshalJSON populates a new Manifest struct from JSON data.
func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
m.canonical = make([]byte, len(b))
// store manifest in canonical
copy(m.canonical, b)
// Unmarshal canonical JSON into Manifest object
var manifest Manifest
if err := json.Unmarshal(m.canonical, &manifest); err != nil {
return err
}
if manifest.MediaType != "" && manifest.MediaType != v1.MediaTypeImageManifest {
return fmt.Errorf("if present, mediaType in manifest should be '%s' not '%s'",
v1.MediaTypeImageManifest, manifest.MediaType)
}
m.Manifest = manifest
return nil
}
// MarshalJSON returns the contents of canonical. If canonical is empty,
// marshals the inner contents.
func (m *DeserializedManifest) MarshalJSON() ([]byte, error) {
if len(m.canonical) > 0 {
return m.canonical, nil
}
return nil, errors.New("JSON representation not initialized in DeserializedManifest")
}
// Payload returns the raw content of the manifest. The contents can be used to
// calculate the content identifier.
func (m DeserializedManifest) Payload() (string, []byte, error) {
return v1.MediaTypeImageManifest, m.canonical, nil
}
// unknownDocument represents a manifest, manifest list, or index that has not
// yet been validated
type unknownDocument struct {
Manifests interface{} `json:"manifests,omitempty"`
}
// validateManifest returns an error if the byte slice is invalid JSON or if it
// contains fields that belong to a index
func validateManifest(b []byte) error {
var doc unknownDocument
if err := json.Unmarshal(b, &doc); err != nil {
return err
}
if doc.Manifests != nil {
return errors.New("ocimanifest: expected manifest but found index")
}
return nil
}

View File

@ -1,85 +0,0 @@
package schema2
import (
"context"
"github.com/docker/distribution"
"github.com/opencontainers/go-digest"
)
// builder is a type for constructing manifests.
type builder struct {
// bs is a BlobService used to publish the configuration blob.
bs distribution.BlobService
// configMediaType is media type used to describe configuration
configMediaType string
// configJSON references
configJSON []byte
// dependencies is a list of descriptors that gets built by successive
// calls to AppendReference. In case of image configuration these are layers.
dependencies []distribution.Descriptor
}
// NewManifestBuilder is used to build new manifests for the current schema
// version. It takes a BlobService so it can publish the configuration blob
// as part of the Build process.
func NewManifestBuilder(bs distribution.BlobService, configMediaType string, configJSON []byte) distribution.ManifestBuilder {
mb := &builder{
bs: bs,
configMediaType: configMediaType,
configJSON: make([]byte, len(configJSON)),
}
copy(mb.configJSON, configJSON)
return mb
}
// Build produces a final manifest from the given references.
func (mb *builder) Build(ctx context.Context) (distribution.Manifest, error) {
m := Manifest{
Versioned: SchemaVersion,
Layers: make([]distribution.Descriptor, len(mb.dependencies)),
}
copy(m.Layers, mb.dependencies)
configDigest := digest.FromBytes(mb.configJSON)
var err error
m.Config, err = mb.bs.Stat(ctx, configDigest)
switch err {
case nil:
// Override MediaType, since Put always replaces the specified media
// type with application/octet-stream in the descriptor it returns.
m.Config.MediaType = mb.configMediaType
return FromStruct(m)
case distribution.ErrBlobUnknown:
// nop
default:
return nil, err
}
// Add config to the blob store
m.Config, err = mb.bs.Put(ctx, mb.configMediaType, mb.configJSON)
// Override MediaType, since Put always replaces the specified media
// type with application/octet-stream in the descriptor it returns.
m.Config.MediaType = mb.configMediaType
if err != nil {
return nil, err
}
return FromStruct(m)
}
// AppendReference adds a reference to the current ManifestBuilder.
func (mb *builder) AppendReference(d distribution.Describable) error {
mb.dependencies = append(mb.dependencies, d.Descriptor())
return nil
}
// References returns the current references added to this builder.
func (mb *builder) References() []distribution.Descriptor {
return mb.dependencies
}

View File

@ -1,144 +0,0 @@
package schema2
import (
"encoding/json"
"errors"
"fmt"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest"
"github.com/opencontainers/go-digest"
)
const (
// MediaTypeManifest specifies the mediaType for the current version.
MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json"
// MediaTypeImageConfig specifies the mediaType for the image configuration.
MediaTypeImageConfig = "application/vnd.docker.container.image.v1+json"
// MediaTypePluginConfig specifies the mediaType for plugin configuration.
MediaTypePluginConfig = "application/vnd.docker.plugin.v1+json"
// MediaTypeLayer is the mediaType used for layers referenced by the
// manifest.
MediaTypeLayer = "application/vnd.docker.image.rootfs.diff.tar.gzip"
// MediaTypeForeignLayer is the mediaType used for layers that must be
// downloaded from foreign URLs.
MediaTypeForeignLayer = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
// MediaTypeUncompressedLayer is the mediaType used for layers which
// are not compressed.
MediaTypeUncompressedLayer = "application/vnd.docker.image.rootfs.diff.tar"
)
var (
// SchemaVersion provides a pre-initialized version structure for this
// packages version of the manifest.
SchemaVersion = manifest.Versioned{
SchemaVersion: 2,
MediaType: MediaTypeManifest,
}
)
func init() {
schema2Func := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
m := new(DeserializedManifest)
err := m.UnmarshalJSON(b)
if err != nil {
return nil, distribution.Descriptor{}, err
}
dgst := digest.FromBytes(b)
return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: MediaTypeManifest}, err
}
err := distribution.RegisterManifestSchema(MediaTypeManifest, schema2Func)
if err != nil {
panic(fmt.Sprintf("Unable to register manifest: %s", err))
}
}
// Manifest defines a schema2 manifest.
type Manifest struct {
manifest.Versioned
// Config references the image configuration as a blob.
Config distribution.Descriptor `json:"config"`
// Layers lists descriptors for the layers referenced by the
// configuration.
Layers []distribution.Descriptor `json:"layers"`
}
// References returns the descriptors of this manifests references.
func (m Manifest) References() []distribution.Descriptor {
references := make([]distribution.Descriptor, 0, 1+len(m.Layers))
references = append(references, m.Config)
references = append(references, m.Layers...)
return references
}
// Target returns the target of this manifest.
func (m Manifest) Target() distribution.Descriptor {
return m.Config
}
// DeserializedManifest wraps Manifest with a copy of the original JSON.
// It satisfies the distribution.Manifest interface.
type DeserializedManifest struct {
Manifest
// canonical is the canonical byte representation of the Manifest.
canonical []byte
}
// FromStruct takes a Manifest structure, marshals it to JSON, and returns a
// DeserializedManifest which contains the manifest and its JSON representation.
func FromStruct(m Manifest) (*DeserializedManifest, error) {
var deserialized DeserializedManifest
deserialized.Manifest = m
var err error
deserialized.canonical, err = json.MarshalIndent(&m, "", " ")
return &deserialized, err
}
// UnmarshalJSON populates a new Manifest struct from JSON data.
func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
m.canonical = make([]byte, len(b))
// store manifest in canonical
copy(m.canonical, b)
// Unmarshal canonical JSON into Manifest object
var manifest Manifest
if err := json.Unmarshal(m.canonical, &manifest); err != nil {
return err
}
if manifest.MediaType != MediaTypeManifest {
return fmt.Errorf("mediaType in manifest should be '%s' not '%s'",
MediaTypeManifest, manifest.MediaType)
}
m.Manifest = manifest
return nil
}
// MarshalJSON returns the contents of canonical. If canonical is empty,
// marshals the inner contents.
func (m *DeserializedManifest) MarshalJSON() ([]byte, error) {
if len(m.canonical) > 0 {
return m.canonical, nil
}
return nil, errors.New("JSON representation not initialized in DeserializedManifest")
}
// Payload returns the raw content of the manifest. The contents can be used to
// calculate the content identifier.
func (m DeserializedManifest) Payload() (string, []byte, error) {
return m.MediaType, m.canonical, nil
}

View File

@ -1,12 +0,0 @@
package manifest
// Versioned provides a struct with the manifest schemaVersion and mediaType.
// Incoming content with unknown schema version can be decoded against this
// struct to check the version.
type Versioned struct {
// SchemaVersion is the image manifest schema that this image follows
SchemaVersion int `json:"schemaVersion"`
// MediaType is the media type of this schema.
MediaType string `json:"mediaType,omitempty"`
}

View File

@ -1,58 +0,0 @@
package auth
import (
"net/http"
"strings"
)
// APIVersion represents a version of an API including its
// type and version number.
type APIVersion struct {
// Type refers to the name of a specific API specification
// such as "registry"
Type string
// Version is the version of the API specification implemented,
// This may omit the revision number and only include
// the major and minor version, such as "2.0"
Version string
}
// String returns the string formatted API Version
func (v APIVersion) String() string {
return v.Type + "/" + v.Version
}
// APIVersions gets the API versions out of an HTTP response using the provided
// version header as the key for the HTTP header.
func APIVersions(resp *http.Response, versionHeader string) []APIVersion {
versions := []APIVersion{}
if versionHeader != "" {
for _, supportedVersions := range resp.Header[http.CanonicalHeaderKey(versionHeader)] {
for _, version := range strings.Fields(supportedVersions) {
versions = append(versions, ParseAPIVersion(version))
}
}
}
return versions
}
// ParseAPIVersion parses an API version string into an APIVersion
// Format (Expected, not enforced):
// API version string = <API type> '/' <API version>
// API type = [a-z][a-z0-9]*
// API version = [0-9]+(\.[0-9]+)?
// TODO(dmcgowan): Enforce format, add error condition, remove unknown type
func ParseAPIVersion(versionStr string) APIVersion {
idx := strings.IndexRune(versionStr, '/')
if idx == -1 {
return APIVersion{
Type: "unknown",
Version: versionStr,
}
}
return APIVersion{
Type: strings.ToLower(versionStr[:idx]),
Version: versionStr[idx+1:],
}
}

View File

@ -1,530 +0,0 @@
package auth
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/docker/distribution/registry/client"
"github.com/docker/distribution/registry/client/auth/challenge"
"github.com/docker/distribution/registry/client/transport"
)
var (
// ErrNoBasicAuthCredentials is returned if a request can't be authorized with
// basic auth due to lack of credentials.
ErrNoBasicAuthCredentials = errors.New("no basic auth credentials")
// ErrNoToken is returned if a request is successful but the body does not
// contain an authorization token.
ErrNoToken = errors.New("authorization server did not include a token in the response")
)
const defaultClientID = "registry-client"
// AuthenticationHandler is an interface for authorizing a request from
// params from a "WWW-Authenicate" header for a single scheme.
type AuthenticationHandler interface {
// Scheme returns the scheme as expected from the "WWW-Authenicate" header.
Scheme() string
// AuthorizeRequest adds the authorization header to a request (if needed)
// using the parameters from "WWW-Authenticate" method. The parameters
// values depend on the scheme.
AuthorizeRequest(req *http.Request, params map[string]string) error
}
// CredentialStore is an interface for getting credentials for
// a given URL
type CredentialStore interface {
// Basic returns basic auth for the given URL
Basic(*url.URL) (string, string)
// RefreshToken returns a refresh token for the
// given URL and service
RefreshToken(*url.URL, string) string
// SetRefreshToken sets the refresh token if none
// is provided for the given url and service
SetRefreshToken(realm *url.URL, service, token string)
}
// NewAuthorizer creates an authorizer which can handle multiple authentication
// schemes. The handlers are tried in order, the higher priority authentication
// methods should be first. The challengeMap holds a list of challenges for
// a given root API endpoint (for example "https://registry-1.docker.io/v2/").
func NewAuthorizer(manager challenge.Manager, handlers ...AuthenticationHandler) transport.RequestModifier {
return &endpointAuthorizer{
challenges: manager,
handlers: handlers,
}
}
type endpointAuthorizer struct {
challenges challenge.Manager
handlers []AuthenticationHandler
}
func (ea *endpointAuthorizer) ModifyRequest(req *http.Request) error {
pingPath := req.URL.Path
if v2Root := strings.Index(req.URL.Path, "/v2/"); v2Root != -1 {
pingPath = pingPath[:v2Root+4]
} else if v1Root := strings.Index(req.URL.Path, "/v1/"); v1Root != -1 {
pingPath = pingPath[:v1Root] + "/v2/"
} else {
return nil
}
ping := url.URL{
Host: req.URL.Host,
Scheme: req.URL.Scheme,
Path: pingPath,
}
challenges, err := ea.challenges.GetChallenges(ping)
if err != nil {
return err
}
if len(challenges) > 0 {
for _, handler := range ea.handlers {
for _, c := range challenges {
if c.Scheme != handler.Scheme() {
continue
}
if err := handler.AuthorizeRequest(req, c.Parameters); err != nil {
return err
}
}
}
}
return nil
}
// This is the minimum duration a token can last (in seconds).
// A token must not live less than 60 seconds because older versions
// of the Docker client didn't read their expiration from the token
// response and assumed 60 seconds. So to remain compatible with
// those implementations, a token must live at least this long.
const minimumTokenLifetimeSeconds = 60
// Private interface for time used by this package to enable tests to provide their own implementation.
type clock interface {
Now() time.Time
}
type tokenHandler struct {
creds CredentialStore
transport http.RoundTripper
clock clock
offlineAccess bool
forceOAuth bool
clientID string
scopes []Scope
tokenLock sync.Mutex
tokenCache string
tokenExpiration time.Time
logger Logger
}
// Scope is a type which is serializable to a string
// using the allow scope grammar.
type Scope interface {
String() string
}
// RepositoryScope represents a token scope for access
// to a repository.
type RepositoryScope struct {
Repository string
Class string
Actions []string
}
// String returns the string representation of the repository
// using the scope grammar
func (rs RepositoryScope) String() string {
repoType := "repository"
// Keep existing format for image class to maintain backwards compatibility
// with authorization servers which do not support the expanded grammar.
if rs.Class != "" && rs.Class != "image" {
repoType = fmt.Sprintf("%s(%s)", repoType, rs.Class)
}
return fmt.Sprintf("%s:%s:%s", repoType, rs.Repository, strings.Join(rs.Actions, ","))
}
// RegistryScope represents a token scope for access
// to resources in the registry.
type RegistryScope struct {
Name string
Actions []string
}
// String returns the string representation of the user
// using the scope grammar
func (rs RegistryScope) String() string {
return fmt.Sprintf("registry:%s:%s", rs.Name, strings.Join(rs.Actions, ","))
}
// Logger defines the injectable logging interface, used on TokenHandlers.
type Logger interface {
Debugf(format string, args ...interface{})
}
func logDebugf(logger Logger, format string, args ...interface{}) {
if logger == nil {
return
}
logger.Debugf(format, args...)
}
// TokenHandlerOptions is used to configure a new token handler
type TokenHandlerOptions struct {
Transport http.RoundTripper
Credentials CredentialStore
OfflineAccess bool
ForceOAuth bool
ClientID string
Scopes []Scope
Logger Logger
}
// An implementation of clock for providing real time data.
type realClock struct{}
// Now implements clock
func (realClock) Now() time.Time { return time.Now() }
// NewTokenHandler creates a new AuthenicationHandler which supports
// fetching tokens from a remote token server.
func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler {
// Create options...
return NewTokenHandlerWithOptions(TokenHandlerOptions{
Transport: transport,
Credentials: creds,
Scopes: []Scope{
RepositoryScope{
Repository: scope,
Actions: actions,
},
},
})
}
// NewTokenHandlerWithOptions creates a new token handler using the provided
// options structure.
func NewTokenHandlerWithOptions(options TokenHandlerOptions) AuthenticationHandler {
handler := &tokenHandler{
transport: options.Transport,
creds: options.Credentials,
offlineAccess: options.OfflineAccess,
forceOAuth: options.ForceOAuth,
clientID: options.ClientID,
scopes: options.Scopes,
clock: realClock{},
logger: options.Logger,
}
return handler
}
func (th *tokenHandler) client() *http.Client {
return &http.Client{
Transport: th.transport,
Timeout: 15 * time.Second,
}
}
func (th *tokenHandler) Scheme() string {
return "bearer"
}
func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
var additionalScopes []string
if fromParam := req.URL.Query().Get("from"); fromParam != "" {
additionalScopes = append(additionalScopes, RepositoryScope{
Repository: fromParam,
Actions: []string{"pull"},
}.String())
}
token, err := th.getToken(params, additionalScopes...)
if err != nil {
return err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return nil
}
func (th *tokenHandler) getToken(params map[string]string, additionalScopes ...string) (string, error) {
th.tokenLock.Lock()
defer th.tokenLock.Unlock()
scopes := make([]string, 0, len(th.scopes)+len(additionalScopes))
for _, scope := range th.scopes {
scopes = append(scopes, scope.String())
}
var addedScopes bool
for _, scope := range additionalScopes {
if hasScope(scopes, scope) {
continue
}
scopes = append(scopes, scope)
addedScopes = true
}
now := th.clock.Now()
if now.After(th.tokenExpiration) || addedScopes {
token, expiration, err := th.fetchToken(params, scopes)
if err != nil {
return "", err
}
// do not update cache for added scope tokens
if !addedScopes {
th.tokenCache = token
th.tokenExpiration = expiration
}
return token, nil
}
return th.tokenCache, nil
}
func hasScope(scopes []string, scope string) bool {
for _, s := range scopes {
if s == scope {
return true
}
}
return false
}
type postTokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
IssuedAt time.Time `json:"issued_at"`
Scope string `json:"scope"`
}
func (th *tokenHandler) fetchTokenWithOAuth(realm *url.URL, refreshToken, service string, scopes []string) (token string, expiration time.Time, err error) {
form := url.Values{}
form.Set("scope", strings.Join(scopes, " "))
form.Set("service", service)
clientID := th.clientID
if clientID == "" {
// Use default client, this is a required field
clientID = defaultClientID
}
form.Set("client_id", clientID)
if refreshToken != "" {
form.Set("grant_type", "refresh_token")
form.Set("refresh_token", refreshToken)
} else if th.creds != nil {
form.Set("grant_type", "password")
username, password := th.creds.Basic(realm)
form.Set("username", username)
form.Set("password", password)
// attempt to get a refresh token
form.Set("access_type", "offline")
} else {
// refuse to do oauth without a grant type
return "", time.Time{}, fmt.Errorf("no supported grant type")
}
resp, err := th.client().PostForm(realm.String(), form)
if err != nil {
return "", time.Time{}, err
}
defer resp.Body.Close()
if !client.SuccessStatus(resp.StatusCode) {
err := client.HandleErrorResponse(resp)
return "", time.Time{}, err
}
decoder := json.NewDecoder(resp.Body)
var tr postTokenResponse
if err = decoder.Decode(&tr); err != nil {
return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err)
}
if tr.RefreshToken != "" && tr.RefreshToken != refreshToken {
th.creds.SetRefreshToken(realm, service, tr.RefreshToken)
}
if tr.ExpiresIn < minimumTokenLifetimeSeconds {
// The default/minimum lifetime.
tr.ExpiresIn = minimumTokenLifetimeSeconds
logDebugf(th.logger, "Increasing token expiration to: %d seconds", tr.ExpiresIn)
}
if tr.IssuedAt.IsZero() {
// issued_at is optional in the token response.
tr.IssuedAt = th.clock.Now().UTC()
}
return tr.AccessToken, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil
}
type getTokenResponse struct {
Token string `json:"token"`
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
IssuedAt time.Time `json:"issued_at"`
RefreshToken string `json:"refresh_token"`
}
func (th *tokenHandler) fetchTokenWithBasicAuth(realm *url.URL, service string, scopes []string) (token string, expiration time.Time, err error) {
req, err := http.NewRequest("GET", realm.String(), nil)
if err != nil {
return "", time.Time{}, err
}
reqParams := req.URL.Query()
if service != "" {
reqParams.Add("service", service)
}
for _, scope := range scopes {
reqParams.Add("scope", scope)
}
if th.offlineAccess {
reqParams.Add("offline_token", "true")
clientID := th.clientID
if clientID == "" {
clientID = defaultClientID
}
reqParams.Add("client_id", clientID)
}
if th.creds != nil {
username, password := th.creds.Basic(realm)
if username != "" && password != "" {
reqParams.Add("account", username)
req.SetBasicAuth(username, password)
}
}
req.URL.RawQuery = reqParams.Encode()
resp, err := th.client().Do(req)
if err != nil {
return "", time.Time{}, err
}
defer resp.Body.Close()
if !client.SuccessStatus(resp.StatusCode) {
err := client.HandleErrorResponse(resp)
return "", time.Time{}, err
}
decoder := json.NewDecoder(resp.Body)
var tr getTokenResponse
if err = decoder.Decode(&tr); err != nil {
return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err)
}
if tr.RefreshToken != "" && th.creds != nil {
th.creds.SetRefreshToken(realm, service, tr.RefreshToken)
}
// `access_token` is equivalent to `token` and if both are specified
// the choice is undefined. Canonicalize `access_token` by sticking
// things in `token`.
if tr.AccessToken != "" {
tr.Token = tr.AccessToken
}
if tr.Token == "" {
return "", time.Time{}, ErrNoToken
}
if tr.ExpiresIn < minimumTokenLifetimeSeconds {
// The default/minimum lifetime.
tr.ExpiresIn = minimumTokenLifetimeSeconds
logDebugf(th.logger, "Increasing token expiration to: %d seconds", tr.ExpiresIn)
}
if tr.IssuedAt.IsZero() {
// issued_at is optional in the token response.
tr.IssuedAt = th.clock.Now().UTC()
}
return tr.Token, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil
}
func (th *tokenHandler) fetchToken(params map[string]string, scopes []string) (token string, expiration time.Time, err error) {
realm, ok := params["realm"]
if !ok {
return "", time.Time{}, errors.New("no realm specified for token auth challenge")
}
// TODO(dmcgowan): Handle empty scheme and relative realm
realmURL, err := url.Parse(realm)
if err != nil {
return "", time.Time{}, fmt.Errorf("invalid token auth challenge realm: %s", err)
}
service := params["service"]
var refreshToken string
if th.creds != nil {
refreshToken = th.creds.RefreshToken(realmURL, service)
}
if refreshToken != "" || th.forceOAuth {
return th.fetchTokenWithOAuth(realmURL, refreshToken, service, scopes)
}
return th.fetchTokenWithBasicAuth(realmURL, service, scopes)
}
type basicHandler struct {
creds CredentialStore
}
// NewBasicHandler creaters a new authentiation handler which adds
// basic authentication credentials to a request.
func NewBasicHandler(creds CredentialStore) AuthenticationHandler {
return &basicHandler{
creds: creds,
}
}
func (*basicHandler) Scheme() string {
return "basic"
}
func (bh *basicHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
if bh.creds != nil {
username, password := bh.creds.Basic(req.URL)
if username != "" && password != "" {
req.SetBasicAuth(username, password)
return nil
}
}
return ErrNoBasicAuthCredentials
}

View File

@ -1,126 +0,0 @@
// Package uuid provides simple UUID generation. Only version 4 style UUIDs
// can be generated.
//
// Please see http://tools.ietf.org/html/rfc4122 for details on UUIDs.
package uuid
import (
"crypto/rand"
"fmt"
"io"
"os"
"syscall"
"time"
)
const (
// Bits is the number of bits in a UUID
Bits = 128
// Size is the number of bytes in a UUID
Size = Bits / 8
format = "%08x-%04x-%04x-%04x-%012x"
)
var (
// ErrUUIDInvalid indicates a parsed string is not a valid uuid.
ErrUUIDInvalid = fmt.Errorf("invalid uuid")
// Loggerf can be used to override the default logging destination. Such
// log messages in this library should be logged at warning or higher.
Loggerf = func(format string, args ...interface{}) {}
)
// UUID represents a UUID value. UUIDs can be compared and set to other values
// and accessed by byte.
type UUID [Size]byte
// Generate creates a new, version 4 uuid.
func Generate() (u UUID) {
const (
// ensures we backoff for less than 450ms total. Use the following to
// select new value, in units of 10ms:
// n*(n+1)/2 = d -> n^2 + n - 2d -> n = (sqrt(8d + 1) - 1)/2
maxretries = 9
backoff = time.Millisecond * 10
)
var (
totalBackoff time.Duration
count int
retries int
)
for {
// This should never block but the read may fail. Because of this,
// we just try to read the random number generator until we get
// something. This is a very rare condition but may happen.
b := time.Duration(retries) * backoff
time.Sleep(b)
totalBackoff += b
n, err := io.ReadFull(rand.Reader, u[count:])
if err != nil {
if retryOnError(err) && retries < maxretries {
count += n
retries++
Loggerf("error generating version 4 uuid, retrying: %v", err)
continue
}
// Any other errors represent a system problem. What did someone
// do to /dev/urandom?
panic(fmt.Errorf("error reading random number generator, retried for %v: %v", totalBackoff.String(), err))
}
break
}
u[6] = (u[6] & 0x0f) | 0x40 // set version byte
u[8] = (u[8] & 0x3f) | 0x80 // set high order byte 0b10{8,9,a,b}
return u
}
// Parse attempts to extract a uuid from the string or returns an error.
func Parse(s string) (u UUID, err error) {
if len(s) != 36 {
return UUID{}, ErrUUIDInvalid
}
// create stack addresses for each section of the uuid.
p := make([][]byte, 5)
if _, err := fmt.Sscanf(s, format, &p[0], &p[1], &p[2], &p[3], &p[4]); err != nil {
return u, err
}
copy(u[0:4], p[0])
copy(u[4:6], p[1])
copy(u[6:8], p[2])
copy(u[8:10], p[3])
copy(u[10:16], p[4])
return
}
func (u UUID) String() string {
return fmt.Sprintf(format, u[:4], u[4:6], u[6:8], u[8:10], u[10:])
}
// retryOnError tries to detect whether or not retrying would be fruitful.
func retryOnError(err error) bool {
switch err := err.(type) {
case *os.PathError:
return retryOnError(err.Err) // unpack the target error
case syscall.Errno:
if err == syscall.EPERM {
// EPERM represents an entropy pool exhaustion, a condition under
// which we backoff and retry.
return true
}
}
return false
}

View File

@ -2,6 +2,7 @@
# This file lists all contributors to the repository.
# See hack/generate-authors.sh to make modifications.
17neverends <ionianrise@gmail.com>
7sunarni <710720732@qq.com>
Aanand Prasad <aanand.prasad@gmail.com>
Aarni Koskela <akx@iki.fi>
@ -189,6 +190,7 @@ Anes Hasicic <anes.hasicic@gmail.com>
Angel Velazquez <angelcar@amazon.com>
Anil Belur <askb23@gmail.com>
Anil Madhavapeddy <anil@recoil.org>
Anirudh Aithal <aithal@amazon.com>
Ankit Jain <ajatkj@yahoo.co.in>
Ankush Agarwal <ankushagarwal11@gmail.com>
Anonmily <michelle@michelleliu.io>
@ -227,7 +229,7 @@ Arun Gupta <arun.gupta@gmail.com>
Asad Saeeduddin <masaeedu@gmail.com>
Asbjørn Enge <asbjorn@hanafjedle.net>
Ashly Mathew <ashly.mathew@sap.com>
Austin Vazquez <macedonv@amazon.com>
Austin Vazquez <austin.vazquez.dev@gmail.com>
averagehuman <averagehuman@users.noreply.github.com>
Avi Das <andas222@gmail.com>
Avi Kivity <avi@scylladb.com>
@ -293,6 +295,7 @@ Brandon Liu <bdon@bdon.org>
Brandon Philips <brandon.philips@coreos.com>
Brandon Rhodes <brandon@rhodesmill.org>
Brendan Dixon <brendand@microsoft.com>
Brendon Smith <bws@bws.bio>
Brennan Kinney <5098581+polarathene@users.noreply.github.com>
Brent Salisbury <brent.salisbury@docker.com>
Brett Higgins <brhiggins@arbor.net>
@ -347,6 +350,7 @@ Casey Bisson <casey.bisson@joyent.com>
Catalin Pirvu <pirvu.catalin94@gmail.com>
Ce Gao <ce.gao@outlook.com>
Cedric Davies <cedricda@microsoft.com>
Cesar Talledo <cesar.talledo@docker.com>
Cezar Sa Espinola <cezarsa@gmail.com>
Chad Swenson <chadswen@gmail.com>
Chance Zibolski <chance.zibolski@gmail.com>
@ -375,6 +379,7 @@ Chen Qiu <cheney-90@hotmail.com>
Cheng-mean Liu <soccerl@microsoft.com>
Chengfei Shang <cfshang@alauda.io>
Chengguang Xu <cgxu519@gmx.com>
Chengyu Zhu <hudson@cyzhu.com>
Chentianze <cmoman@126.com>
Chenyang Yan <memory.yancy@gmail.com>
chenyuzhu <chenyuzhi@oschina.cn>
@ -1207,6 +1212,7 @@ K. Heller <pestophagous@gmail.com>
Kai Blin <kai@samba.org>
Kai Qiang Wu (Kennan) <wkq5325@gmail.com>
Kaijie Chen <chen@kaijie.org>
Kaita Nakamura <kaita.nakamura0830@gmail.com>
Kamil Domański <kamil@domanski.co>
Kamjar Gerami <kami.gerami@gmail.com>
Kanstantsin Shautsou <kanstantsin.sha@gmail.com>
@ -1281,6 +1287,7 @@ Krasi Georgiev <krasi@vip-consult.solutions>
Krasimir Georgiev <support@vip-consult.co.uk>
Kris-Mikael Krister <krismikael@protonmail.com>
Kristian Haugene <kristian.haugene@capgemini.com>
Kristian Heljas <kristian@kristian.ee>
Kristina Zabunova <triara.xiii@gmail.com>
Krystian Wojcicki <kwojcicki@sympatico.ca>
Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp>
@ -1482,6 +1489,7 @@ Matthias Kühnle <git.nivoc@neverbox.com>
Matthias Rampke <mr@soundcloud.com>
Matthieu Fronton <m@tthieu.fr>
Matthieu Hauglustaine <matt.hauglustaine@gmail.com>
Matthieu MOREL <matthieu.morel35@gmail.com>
Mattias Jernberg <nostrad@gmail.com>
Mauricio Garavaglia <mauricio@medallia.com>
mauriyouth <mauriyouth@gmail.com>
@ -1712,6 +1720,7 @@ Patrick Hemmer <patrick.hemmer@gmail.com>
Patrick St. laurent <patrick@saint-laurent.us>
Patrick Stapleton <github@gdi2290.com>
Patrik Cyvoct <patrik@ptrk.io>
Patrik Leifert <patrikleifert@hotmail.com>
pattichen <craftsbear@gmail.com>
Paul "TBBle" Hampson <Paul.Hampson@Pobox.com>
Paul <paul9869@gmail.com>
@ -1870,6 +1879,7 @@ Robert Obryk <robryk@gmail.com>
Robert Schneider <mail@shakeme.info>
Robert Shade <robert.shade@gmail.com>
Robert Stern <lexandro2000@gmail.com>
Robert Sturla <robertsturla@outlook.com>
Robert Terhaar <rterhaar@atlanticdynamic.com>
Robert Wallis <smilingrob@gmail.com>
Robert Wang <robert@arctic.tw>

View File

@ -1,9 +1,9 @@
package api // import "github.com/docker/docker/api"
package api
// Common constants for daemon and client.
const (
// DefaultVersion of the current REST API.
DefaultVersion = "1.48"
DefaultVersion = "1.51"
// MinSupportedAPIVersion is the minimum API version that can be supported
// by the API server, specified as "major.minor". Note that the daemon

View File

@ -19,10 +19,10 @@ produces:
consumes:
- "application/json"
- "text/plain"
basePath: "/v1.48"
basePath: "/v1.51"
info:
title: "Docker Engine API"
version: "1.48"
version: "1.51"
x-logo:
url: "https://docs.docker.com/assets/images/logo-docker-main.png"
description: |
@ -55,8 +55,8 @@ info:
the URL is not supported by the daemon, a HTTP `400 Bad Request` error message
is returned.
If you omit the version-prefix, the current version of the API (v1.48) is used.
For example, calling `/info` is the same as calling `/v1.48/info`. Using the
If you omit the version-prefix, the current version of the API (v1.50) is used.
For example, calling `/info` is the same as calling `/v1.51/info`. Using the
API without a version-prefix is deprecated and will be removed in a future release.
Engine releases in the near future should support this version of the API,
@ -1428,63 +1428,10 @@ definitions:
when starting a container from the image.
type: "object"
properties:
Hostname:
description: |
The hostname to use for the container, as a valid RFC 1123 hostname.
<p><br /></p>
> **Deprecated**: this field is not part of the image specification and is
> always empty. It must not be used, and will be removed in API v1.48.
type: "string"
example: ""
Domainname:
description: |
The domain name to use for the container.
<p><br /></p>
> **Deprecated**: this field is not part of the image specification and is
> always empty. It must not be used, and will be removed in API v1.48.
type: "string"
example: ""
User:
description: "The user that commands are run as inside the container."
type: "string"
example: "web:web"
AttachStdin:
description: |
Whether to attach to `stdin`.
<p><br /></p>
> **Deprecated**: this field is not part of the image specification and is
> always false. It must not be used, and will be removed in API v1.48.
type: "boolean"
default: false
example: false
AttachStdout:
description: |
Whether to attach to `stdout`.
<p><br /></p>
> **Deprecated**: this field is not part of the image specification and is
> always false. It must not be used, and will be removed in API v1.48.
type: "boolean"
default: false
example: false
AttachStderr:
description: |
Whether to attach to `stderr`.
<p><br /></p>
> **Deprecated**: this field is not part of the image specification and is
> always false. It must not be used, and will be removed in API v1.48.
type: "boolean"
default: false
example: false
ExposedPorts:
description: |
An object mapping ports to an empty object in the form:
@ -1501,39 +1448,6 @@ definitions:
"80/tcp": {},
"443/tcp": {}
}
Tty:
description: |
Attach standard streams to a TTY, including `stdin` if it is not closed.
<p><br /></p>
> **Deprecated**: this field is not part of the image specification and is
> always false. It must not be used, and will be removed in API v1.48.
type: "boolean"
default: false
example: false
OpenStdin:
description: |
Open `stdin`
<p><br /></p>
> **Deprecated**: this field is not part of the image specification and is
> always false. It must not be used, and will be removed in API v1.48.
type: "boolean"
default: false
example: false
StdinOnce:
description: |
Close `stdin` after one attached client disconnects.
<p><br /></p>
> **Deprecated**: this field is not part of the image specification and is
> always false. It must not be used, and will be removed in API v1.48.
type: "boolean"
default: false
example: false
Env:
description: |
A list of environment variables to set inside the container in the
@ -1559,18 +1473,6 @@ definitions:
default: false
example: false
x-nullable: true
Image:
description: |
The name (or reference) of the image to use when creating the container,
or which was used when the container was created.
<p><br /></p>
> **Deprecated**: this field is not part of the image specification and is
> always empty. It must not be used, and will be removed in API v1.48.
type: "string"
default: ""
example: ""
Volumes:
description: |
An object mapping mount point paths inside the container to empty
@ -1599,30 +1501,6 @@ definitions:
items:
type: "string"
example: []
NetworkDisabled:
description: |
Disable networking for the container.
<p><br /></p>
> **Deprecated**: this field is not part of the image specification and is
> always omitted. It must not be used, and will be removed in API v1.48.
type: "boolean"
default: false
example: false
x-nullable: true
MacAddress:
description: |
MAC address of the container.
<p><br /></p>
> **Deprecated**: this field is not part of the image specification and is
> always omitted. It must not be used, and will be removed in API v1.48.
type: "string"
default: ""
example: ""
x-nullable: true
OnBuild:
description: |
`ONBUILD` metadata that were defined in the image's `Dockerfile`.
@ -1645,17 +1523,6 @@ definitions:
type: "string"
example: "SIGTERM"
x-nullable: true
StopTimeout:
description: |
Timeout to stop a container in seconds.
<p><br /></p>
> **Deprecated**: this field is not part of the image specification and is
> always omitted. It must not be used, and will be removed in API v1.48.
type: "integer"
default: 10
x-nullable: true
Shell:
description: |
Shell for when `RUN`, `CMD`, and `ENTRYPOINT` uses a shell.
@ -1666,19 +1533,11 @@ definitions:
example: ["/bin/sh", "-c"]
# FIXME(thaJeztah): temporarily using a full example to remove some "omitempty" fields. Remove once the fields are removed.
example:
"Hostname": ""
"Domainname": ""
"User": "web:web"
"AttachStdin": false
"AttachStdout": false
"AttachStderr": false
"ExposedPorts": {
"80/tcp": {},
"443/tcp": {}
}
"Tty": false
"OpenStdin": false
"StdinOnce": false
"Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]
"Cmd": ["/bin/sh"]
"Healthcheck": {
@ -1690,7 +1549,6 @@ definitions:
"StartInterval": 0
}
"ArgsEscaped": true
"Image": ""
"Volumes": {
"/app/data": {},
"/app/config": {}
@ -2338,8 +2196,7 @@ definitions:
Number of containers using this image. Includes both stopped and running
containers.
This size is not calculated by default, and depends on which API endpoint
is used. `-1` indicates that the value has not been set / calculated.
`-1` indicates that the value has not been set / calculated.
x-nullable: false
type: "integer"
example: 2
@ -2956,6 +2813,23 @@ definitions:
progressDetail:
$ref: "#/definitions/ProgressDetail"
DeviceInfo:
type: "object"
description: |
DeviceInfo represents a device that can be used by a container.
properties:
Source:
type: "string"
example: "cdi"
description: |
The origin device driver.
ID:
type: "string"
example: "vendor.com/gpu=0"
description: |
The unique identifier for the device within its source driver.
For CDI devices, this would be an FQDN like "vendor.com/gpu=0".
ErrorDetail:
type: "object"
properties:
@ -3039,7 +2913,8 @@ definitions:
be used. If multiple endpoints have the same priority, endpoints are
lexicographically sorted based on their network name, and the one
that sorts first is picked.
type: "number"
type: "integer"
format: "int64"
example:
- 10
@ -5508,8 +5383,11 @@ definitions:
com.example.some-other-label: "some-other-value"
Data:
description: |
Base64-url-safe-encoded ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-5))
data to store as secret.
Data is the data to store as a secret, formatted as a Base64-url-safe-encoded
([RFC 4648](https://tools.ietf.org/html/rfc4648#section-5)) string.
It must be empty if the Driver field is set, in which case the data is
loaded from an external secret store. The maximum allowed size is 500KB,
as defined in [MaxSecretSize](https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/api/validation#MaxSecretSize).
This field is only used to _create_ a secret, and is not returned by
other endpoints.
@ -5560,8 +5438,9 @@ definitions:
type: "string"
Data:
description: |
Base64-url-safe-encoded ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-5))
config data.
Data is the data to store as a config, formatted as a Base64-url-safe-encoded
([RFC 4648](https://tools.ietf.org/html/rfc4648#section-5)) string.
The maximum allowed size is 1000KB, as defined in [MaxConfigSize](https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/manager/controlapi#MaxConfigSize).
type: "string"
Templating:
description: |
@ -5984,7 +5863,7 @@ definitions:
type: "integer"
format: "uint64"
x-nullable: true
example: 18446744073709551615
example: "18446744073709551615"
ContainerThrottlingData:
description: |
@ -6852,6 +6731,17 @@ definitions:
description: "The network pool size"
type: "integer"
example: "24"
FirewallBackend:
$ref: "#/definitions/FirewallInfo"
DiscoveredDevices:
description: |
List of devices discovered by device drivers.
Each device includes information about its source driver, kind, name,
and additional driver-specific attributes.
type: "array"
items:
$ref: "#/definitions/DeviceInfo"
Warnings:
description: |
List of warnings / informational messages about missing features, or
@ -6935,6 +6825,37 @@ definitions:
default: "plugins.moby"
example: "plugins.moby"
FirewallInfo:
description: |
Information about the daemon's firewalling configuration.
This field is currently only used on Linux, and omitted on other platforms.
type: "object"
x-nullable: true
properties:
Driver:
description: |
The name of the firewall backend driver.
type: "string"
example: "nftables"
Info:
description: |
Information about the firewall backend, provided as
"label" / "value" pairs.
<p><br /></p>
> **Note**: The information returned in this field, including the
> formatting of values and labels, should not be considered stable,
> and may change without notice.
type: "array"
items:
type: "array"
items:
type: "string"
example:
- ["ReloadedAt", "2025-01-01T00:00:00Z"]
# PluginsInfo is a temp struct holding Plugins name
# registered with docker daemon. It is used by Info struct
PluginsInfo:
@ -6980,32 +6901,6 @@ definitions:
type: "object"
x-nullable: true
properties:
AllowNondistributableArtifactsCIDRs:
description: |
List of IP ranges to which nondistributable artifacts can be pushed,
using the CIDR syntax [RFC 4632](https://tools.ietf.org/html/4632).
<p><br /></p>
> **Deprecated**: Pushing nondistributable artifacts is now always enabled
> and this field is always `null`. This field will be removed in a API v1.49.
type: "array"
items:
type: "string"
example: []
AllowNondistributableArtifactsHostnames:
description: |
List of registry hostnames to which nondistributable artifacts can be
pushed, using the format `<hostname>[:<port>]` or `<IP address>[:<port>]`.
<p><br /></p>
> **Deprecated**: Pushing nondistributable artifacts is now always enabled
> and this field is always `null`. This field will be removed in a API v1.49.
type: "array"
items:
type: "string"
example: []
InsecureRegistryCIDRs:
description: |
List of IP ranges of insecure registries, using the CIDR syntax
@ -7175,13 +7070,6 @@ definitions:
description: "Actual commit ID of external tool."
type: "string"
example: "cfb82a876ecc11b5ca0977d1733adbe58599088a"
Expected:
description: |
Commit ID of external tool expected by dockerd as set at build time.
**Deprecated**: This field is deprecated and will be omitted in a API v1.49.
type: "string"
example: "2d41c047c83e09a6d61d464906feb2a2f3c52aa4"
SwarmInfo:
description: |
@ -9930,6 +9818,18 @@ paths:
description: "Do not delete untagged parent images"
type: "boolean"
default: false
- name: "platforms"
in: "query"
description: |
Select platform-specific content to delete.
Multiple values are accepted.
Each platform is a OCI platform encoded as a JSON string.
type: "array"
items:
# This should be OCIPlatform
# but $ref is not supported for array in query in Swagger 2.0
# $ref: "#/definitions/OCIPlatform"
type: "string"
tags: ["Image"]
/images/search:
get:
@ -10487,13 +10387,9 @@ paths:
### Image tarball format
An image tarball contains one directory per image layer (named using its long ID), each containing these files:
An image tarball contains [Content as defined in the OCI Image Layout Specification](https://github.com/opencontainers/image-spec/blob/v1.1.1/image-layout.md#content).
- `VERSION`: currently `1.0` - the file format version
- `json`: detailed layer information, similar to `docker inspect layer_id`
- `layer.tar`: A tarfile containing the filesystem changes in this layer
The `layer.tar` file contains `aufs` style `.wh..wh.aufs` files and directories for storing attribute changes and deletions.
Additionally, includes the manifest.json file associated with a backwards compatible docker save format.
If the tarball defines a repository, the tarball should also include a `repositories` file at the root that contains a list of repository and tag names mapped to layer IDs.
@ -10533,6 +10429,7 @@ paths:
If not provided, the full multi-platform image will be saved.
Example: `{"os": "linux", "architecture": "arm", "variant": "v5"}`
tags: ["Image"]
/images/get:
get:
summary: "Export several images"
@ -10567,6 +10464,16 @@ paths:
type: "array"
items:
type: "string"
- name: "platform"
type: "string"
in: "query"
description: |
JSON encoded OCI platform describing a platform which will be used
to select a platform-specific image to be saved if the image is
multi-platform.
If not provided, the full multi-platform image will be saved.
Example: `{"os": "linux", "architecture": "arm", "variant": "v5"}`
tags: ["Image"]
/images/load:
post:

View File

@ -1,4 +1,4 @@
package blkiodev // import "github.com/docker/docker/api/types/blkiodev"
package blkiodev
import "fmt"

View File

@ -0,0 +1,91 @@
package build
import (
"io"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/registry"
)
// BuilderVersion sets the version of underlying builder to use
type BuilderVersion string
const (
// BuilderV1 is the first generation builder in docker daemon
BuilderV1 BuilderVersion = "1"
// BuilderBuildKit is builder based on moby/buildkit project
BuilderBuildKit BuilderVersion = "2"
)
// Result contains the image id of a successful build.
type Result struct {
ID string
}
// ImageBuildOptions holds the information
// necessary to build images.
type ImageBuildOptions struct {
Tags []string
SuppressOutput bool
RemoteContext string
NoCache bool
Remove bool
ForceRemove bool
PullParent bool
Isolation container.Isolation
CPUSetCPUs string
CPUSetMems string
CPUShares int64
CPUQuota int64
CPUPeriod int64
Memory int64
MemorySwap int64
CgroupParent string
NetworkMode string
ShmSize int64
Dockerfile string
Ulimits []*container.Ulimit
// BuildArgs needs to be a *string instead of just a string so that
// we can tell the difference between "" (empty string) and no value
// at all (nil). See the parsing of buildArgs in
// api/server/router/build/build_routes.go for even more info.
BuildArgs map[string]*string
AuthConfigs map[string]registry.AuthConfig
Context io.Reader
Labels map[string]string
// squash the resulting image's layers to the parent
// preserves the original image and creates a new one from the parent with all
// the changes applied to a single layer
Squash bool
// CacheFrom specifies images that are used for matching cache. Images
// specified here do not need to have a valid parent chain to match cache.
CacheFrom []string
SecurityOpt []string
ExtraHosts []string // List of extra hosts
Target string
SessionID string
Platform string
// Version specifies the version of the underlying builder to use
Version BuilderVersion
// BuildID is an optional identifier that can be passed together with the
// build request. The same identifier can be used to gracefully cancel the
// build with the cancel request.
BuildID string
// Outputs defines configurations for exporting build results. Only supported
// in BuildKit mode
Outputs []ImageBuildOutput
}
// ImageBuildOutput defines configuration for exporting a build result
type ImageBuildOutput struct {
Type string
Attrs map[string]string
}
// ImageBuildResponse holds information
// returned by a server after building
// an image.
type ImageBuildResponse struct {
Body io.ReadCloser
OSType string
}

Some files were not shown because too many files have changed in this diff Show More