Compare commits
128 Commits
v26.0.0-rc
...
v26.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 211e74b240 | |||
| 8beff78d85 | |||
| e64914c890 | |||
| c1d70d1fbb | |||
| 53a3f0be18 | |||
| 4add46d686 | |||
| ccea7d8a30 | |||
| 4cf5afaefa | |||
| 6c2b06d535 | |||
| 1c6a8ecf2e | |||
| 6d1c387af2 | |||
| 1e6db5d24b | |||
| 840016ea05 | |||
| 9714adc6c7 | |||
| aec1d364bf | |||
| e397e505d9 | |||
| 43cb06e1ae | |||
| d8fc76ea56 | |||
| 9d8320de9d | |||
| 118d6bafe0 | |||
| 4eeb776247 | |||
| 6ad512068c | |||
| c3243a8cc3 | |||
| f92fcdef1b | |||
| 745704d7b4 | |||
| 1cc2e445af | |||
| 762a85a103 | |||
| b9828336c5 | |||
| 78089c5394 | |||
| b31c9e1e0c | |||
| 5011759056 | |||
| d6796c002f | |||
| c449c1a49d | |||
| 287f482e31 | |||
| c0cc22db58 | |||
| 867061b007 | |||
| 78012b0ee5 | |||
| 249b5a401f | |||
| 718203d50b | |||
| b6c5522128 | |||
| 5515b86514 | |||
| c1053bf9d4 | |||
| 8a3a7b9458 | |||
| 4585809848 | |||
| 870ad7f4b9 | |||
| 5fcbbde4b9 | |||
| 4745b957d2 | |||
| c7a50ebb9f | |||
| 9a2133f2d4 | |||
| c23a404698 | |||
| 0a5bd6c75b | |||
| b2fe82a23e | |||
| cefcba9871 | |||
| 10b9810989 | |||
| 204b324291 | |||
| ee1b2836af | |||
| 8f45f1495c | |||
| 9ca30bd2ac | |||
| 910d5d0247 | |||
| 155dc5e4e4 | |||
| e3f45bf68f | |||
| 2a3b6c03f7 | |||
| 400a8bb4a2 | |||
| efd82e1e31 | |||
| b6e2eca4b8 | |||
| 160f65d9db | |||
| 9a1b0f8bb3 | |||
| 7c722c08d0 | |||
| b8d5454963 | |||
| 7ea10d5ced | |||
| b39bbb4e3b | |||
| 89db01ef97 | |||
| b4d03289a7 | |||
| 799bf52680 | |||
| c5016c6d5b | |||
| ac5421665f | |||
| 3a8f292a3f | |||
| 509123f935 | |||
| 9aae5e4f6b | |||
| fdb48a0664 | |||
| 542e82caeb | |||
| 318911b404 | |||
| d68cc0e8d0 | |||
| 4468148f37 | |||
| d2ea5adfe4 | |||
| ccfd0b2cc2 | |||
| 2ae903e86c | |||
| 5931a2f592 | |||
| ed9dd75575 | |||
| 69575f6175 | |||
| fa84cfd802 | |||
| b70a26deaf | |||
| 23563728bc | |||
| ea3201c575 | |||
| c050bf0909 | |||
| 4eef4afbf4 | |||
| 396a0823f8 | |||
| f96d8e78c4 | |||
| b120b96ac7 | |||
| 24186d8008 | |||
| 48b5efee03 | |||
| 38c3ff67aa | |||
| f8fc5b6bc3 | |||
| a4a79d75c0 | |||
| 115c8d56e5 | |||
| 38fcd1ca63 | |||
| 9392831817 | |||
| 4e9abfecf5 | |||
| dc4163fb1a | |||
| 8adf1ddb86 | |||
| c8e470057a | |||
| 3da26a5e79 | |||
| 4bb2abaa54 | |||
| 90c33dbfd9 | |||
| 800a51d6cd | |||
| d17b3b2d80 | |||
| 9349f58b8a | |||
| adb018084c | |||
| a2f3f40233 | |||
| 46afd26c45 | |||
| d06f137170 | |||
| b55cf2f71f | |||
| 412d6fca9c | |||
| 5c54f75f2a | |||
| 617377c045 | |||
| 860b4f3a7d | |||
| 645b973521 | |||
| 3cf2fe0fff |
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -22,9 +22,13 @@ Please provide the following information:
|
||||
**- Description for the changelog**
|
||||
<!--
|
||||
Write a short (one line) summary that describes the changes in this
|
||||
pull request for inclusion in the changelog:
|
||||
pull request for inclusion in the changelog.
|
||||
It must be placed inside the below triple backticks section:
|
||||
-->
|
||||
```markdown changelog
|
||||
|
||||
|
||||
```
|
||||
|
||||
**- A picture of a cute animal (not mandatory but encouraged)**
|
||||
|
||||
|
||||
12
.github/workflows/codeql.yml
vendored
12
.github/workflows/codeql.yml
vendored
@ -26,6 +26,8 @@ jobs:
|
||||
codeql:
|
||||
runs-on: 'ubuntu-latest'
|
||||
timeout-minutes: 360
|
||||
env:
|
||||
DISABLE_WARN_OUTSIDE_CONTAINER: '1'
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
@ -52,6 +54,16 @@ jobs:
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go
|
||||
# CodeQL 2.16.4's auto-build added support for multi-module repositories,
|
||||
# and is trying to be smart by searching for modules in every directory,
|
||||
# including vendor directories. If no module is found, it's creating one
|
||||
# which is ... not what we want, so let's give it a "go.mod".
|
||||
# see: https://github.com/docker/cli/pull/4944#issuecomment-2002034698
|
||||
-
|
||||
name: Create go.mod
|
||||
run: |
|
||||
ln -s vendor.mod go.mod
|
||||
ln -s vendor.sum go.sum
|
||||
-
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -64,7 +64,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.8
|
||||
go-version: 1.21.10
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
|
||||
62
.github/workflows/validate-pr.yml
vendored
Normal file
62
.github/workflows/validate-pr.yml
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
name: validate-pr
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, labeled, unlabeled]
|
||||
|
||||
jobs:
|
||||
check-area-label:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Missing `area/` label
|
||||
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
|
||||
run: |
|
||||
echo "::error::Every PR with an 'impact/*' label should also have an 'area/*' label"
|
||||
exit 1
|
||||
- name: OK
|
||||
run: exit 0
|
||||
|
||||
check-changelog:
|
||||
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/')
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
PR_BODY: |
|
||||
${{ github.event.pull_request.body }}
|
||||
steps:
|
||||
- name: Check changelog description
|
||||
run: |
|
||||
# Extract the `markdown changelog` note code block
|
||||
block=$(echo -n "$PR_BODY" | tr -d '\r' | awk '/^```markdown changelog$/{flag=1;next}/^```$/{flag=0}flag')
|
||||
|
||||
# Strip empty lines
|
||||
desc=$(echo "$block" | awk NF)
|
||||
|
||||
if [ -z "$desc" ]; then
|
||||
echo "::error::Changelog section is empty. Please provide a description for the changelog."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
len=$(echo -n "$desc" | wc -c)
|
||||
if [[ $len -le 6 ]]; then
|
||||
echo "::error::Description looks too short: $desc"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "This PR will be included in the release notes with the following note:"
|
||||
echo "$desc"
|
||||
|
||||
check-pr-branch:
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
steps:
|
||||
# Backports or PR that target a release branch directly should mention the target branch in the title, for example:
|
||||
# [X.Y backport] Some change that needs backporting to X.Y
|
||||
# [X.Y] Change directly targeting the X.Y branch
|
||||
- name: Get branch from PR title
|
||||
id: title_branch
|
||||
run: echo "$PR_TITLE" | sed -n 's/^\[\([0-9]*\.[0-9]*\)[^]]*\].*/branch=\1/p' >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check release branch
|
||||
if: github.event.pull_request.base.ref != steps.title_branch.outputs.branch && !(github.event.pull_request.base.ref == 'master' && steps.title_branch.outputs.branch == '')
|
||||
run: echo "::error::PR title suggests targetting the ${{ steps.title_branch.outputs.branch }} branch, but is opened against ${{ github.event.pull_request.base.ref }}" && exit 1
|
||||
@ -84,7 +84,7 @@ use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contr
|
||||
<tr>
|
||||
<td>Community Slack</td>
|
||||
<td>
|
||||
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://dockr.ly/slack" target="_blank">with this link</a>.
|
||||
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://dockr.ly/comm-slack" target="_blank">with this link</a>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@ -4,8 +4,8 @@ ARG BASE_VARIANT=alpine
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.21.8
|
||||
ARG XX_VERSION=1.2.1
|
||||
ARG GO_VERSION=1.21.10
|
||||
ARG XX_VERSION=1.4.0
|
||||
ARG GOVERSIONINFO_VERSION=v1.3.0
|
||||
ARG GOTESTSUM_VERSION=v1.10.0
|
||||
ARG BUILDX_VERSION=0.12.1
|
||||
@ -123,8 +123,14 @@ COPY --link . .
|
||||
FROM scratch AS plugins
|
||||
COPY --from=build-plugins /out .
|
||||
|
||||
FROM scratch AS bin-image
|
||||
FROM scratch AS bin-image-linux
|
||||
COPY --from=build /out/docker /docker
|
||||
FROM scratch AS bin-image-darwin
|
||||
COPY --from=build /out/docker /docker
|
||||
FROM scratch AS bin-image-windows
|
||||
COPY --from=build /out/docker /docker.exe
|
||||
|
||||
FROM bin-image-${TARGETOS} AS bin-image
|
||||
|
||||
FROM scratch AS binary
|
||||
COPY --from=build /out .
|
||||
|
||||
2
Makefile
2
Makefile
@ -52,7 +52,7 @@ shellcheck: ## run shellcheck validation
|
||||
.PHONY: fmt
|
||||
fmt: ## run gofumpt (if present) or gofmt
|
||||
@if command -v gofumpt > /dev/null; then \
|
||||
gofumpt -w -d -lang=1.19 . ; \
|
||||
gofumpt -w -d -lang=1.21 . ; \
|
||||
else \
|
||||
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \
|
||||
fi
|
||||
|
||||
18
cli-plugins/hooks/printer.go
Normal file
18
cli-plugins/hooks/printer.go
Normal file
@ -0,0 +1,18 @@
|
||||
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.Fprintf(out, " %s\n", n)
|
||||
}
|
||||
}
|
||||
38
cli-plugins/hooks/printer_test.go
Normal file
38
cli-plugins/hooks/printer_test.go
Normal file
@ -0,0 +1,38 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/morikuni/aec"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestPrintHookMessages(t *testing.T) {
|
||||
testCases := []struct {
|
||||
messages []string
|
||||
expectedOutput string
|
||||
}{
|
||||
{
|
||||
messages: []string{},
|
||||
expectedOutput: "",
|
||||
},
|
||||
{
|
||||
messages: []string{"Bork!"},
|
||||
expectedOutput: aec.Bold.Apply("\nWhat's next:") + "\n" +
|
||||
" Bork!\n",
|
||||
},
|
||||
{
|
||||
messages: []string{"Foo", "bar"},
|
||||
expectedOutput: aec.Bold.Apply("\nWhat's next:") + "\n" +
|
||||
" Foo\n" +
|
||||
" bar\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
w := bytes.Buffer{}
|
||||
PrintNextSteps(&w, tc.messages)
|
||||
assert.Equal(t, w.String(), tc.expectedOutput)
|
||||
}
|
||||
}
|
||||
116
cli-plugins/hooks/template.go
Normal file
116
cli-plugins/hooks/template.go
Normal file
@ -0,0 +1,116 @@
|
||||
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
|
||||
}
|
||||
86
cli-plugins/hooks/template_test.go
Normal file
86
cli-plugins/hooks/template_test.go
Normal file
@ -0,0 +1,86 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestParseTemplate(t *testing.T) {
|
||||
type testFlag struct {
|
||||
name string
|
||||
value string
|
||||
}
|
||||
testCases := []struct {
|
||||
template string
|
||||
flags []testFlag
|
||||
args []string
|
||||
expectedOutput []string
|
||||
}{
|
||||
{
|
||||
template: "",
|
||||
expectedOutput: []string{""},
|
||||
},
|
||||
{
|
||||
template: "a plain template message",
|
||||
expectedOutput: []string{"a plain template message"},
|
||||
},
|
||||
{
|
||||
template: TemplateReplaceFlagValue("tag"),
|
||||
flags: []testFlag{
|
||||
{
|
||||
name: "tag",
|
||||
value: "my-tag",
|
||||
},
|
||||
},
|
||||
expectedOutput: []string{"my-tag"},
|
||||
},
|
||||
{
|
||||
template: TemplateReplaceFlagValue("test-one") + " " + TemplateReplaceFlagValue("test2"),
|
||||
flags: []testFlag{
|
||||
{
|
||||
name: "test-one",
|
||||
value: "value",
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
value: "value2",
|
||||
},
|
||||
},
|
||||
expectedOutput: []string{"value value2"},
|
||||
},
|
||||
{
|
||||
template: TemplateReplaceArg(0) + " " + TemplateReplaceArg(1),
|
||||
args: []string{"zero", "one"},
|
||||
expectedOutput: []string{"zero one"},
|
||||
},
|
||||
{
|
||||
template: "You just pulled " + TemplateReplaceArg(0),
|
||||
args: []string{"alpine"},
|
||||
expectedOutput: []string{"You just pulled alpine"},
|
||||
},
|
||||
{
|
||||
template: "one line\nanother line!",
|
||||
expectedOutput: []string{"one line", "another line!"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
testCmd := &cobra.Command{
|
||||
Use: "pull",
|
||||
Args: cobra.ExactArgs(len(tc.args)),
|
||||
}
|
||||
for _, f := range tc.flags {
|
||||
_ = testCmd.Flags().String(f.name, "", "")
|
||||
err := testCmd.Flag(f.name).Value.Set(f.value)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
err := testCmd.Flags().Parse(tc.args)
|
||||
assert.NilError(t, err)
|
||||
|
||||
out, err := ParseTemplate(tc.template, testCmd)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, out, tc.expectedOutput)
|
||||
}
|
||||
}
|
||||
@ -41,6 +41,9 @@ func (e *pluginError) MarshalText() (text []byte, err error) {
|
||||
// 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)}
|
||||
}
|
||||
|
||||
|
||||
166
cli-plugins/manager/hooks.go
Normal file
166
cli-plugins/manager/hooks.go
Normal file
@ -0,0 +1,166 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/hooks"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"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(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
|
||||
commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
|
||||
flags := getCommandFlags(subCommand)
|
||||
|
||||
runHooks(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(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) {
|
||||
commandName := strings.Join(args, " ")
|
||||
flags := getNaiveFlags(args)
|
||||
|
||||
runHooks(dockerCli, rootCmd, subCommand, commandName, flags, "")
|
||||
}
|
||||
|
||||
func runHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
|
||||
nextSteps := invokeAndCollectHooks(dockerCli, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
|
||||
|
||||
hooks.PrintNextSteps(dockerCli.Err(), nextSteps)
|
||||
}
|
||||
|
||||
func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
|
||||
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(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
|
||||
}
|
||||
nextSteps = append(nextSteps, processedHook...)
|
||||
}
|
||||
return nextSteps
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
110
cli-plugins/manager/hooks_test.go
Normal file
110
cli-plugins/manager/hooks_test.go
Normal file
@ -0,0 +1,110 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestGetNaiveFlags(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
expectedFlags map[string]string
|
||||
}{
|
||||
{
|
||||
args: []string{"docker"},
|
||||
expectedFlags: map[string]string{},
|
||||
},
|
||||
{
|
||||
args: []string{"docker", "build", "-q", "--file", "test.Dockerfile", "."},
|
||||
expectedFlags: map[string]string{
|
||||
"q": "",
|
||||
"file": "",
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"docker", "--context", "a-context", "pull", "-q", "--progress", "auto", "alpine"},
|
||||
expectedFlags: map[string]string{
|
||||
"context": "",
|
||||
"q": "",
|
||||
"progress": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
assert.DeepEqual(t, getNaiveFlags(tc.args), tc.expectedFlags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginMatch(t *testing.T) {
|
||||
testCases := []struct {
|
||||
commandString string
|
||||
pluginConfig map[string]string
|
||||
expectedMatch string
|
||||
expectedOk bool
|
||||
}{
|
||||
{
|
||||
commandString: "image ls",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "image",
|
||||
},
|
||||
expectedMatch: "image",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
commandString: "context ls",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "build",
|
||||
},
|
||||
expectedMatch: "",
|
||||
expectedOk: false,
|
||||
},
|
||||
{
|
||||
commandString: "context ls",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "context ls",
|
||||
},
|
||||
expectedMatch: "context ls",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
commandString: "image ls",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "image ls,image",
|
||||
},
|
||||
expectedMatch: "image ls",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
commandString: "image ls",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "",
|
||||
},
|
||||
expectedMatch: "",
|
||||
expectedOk: false,
|
||||
},
|
||||
{
|
||||
commandString: "image inspect",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "image i",
|
||||
},
|
||||
expectedMatch: "",
|
||||
expectedOk: false,
|
||||
},
|
||||
{
|
||||
commandString: "image inspect",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "image",
|
||||
},
|
||||
expectedMatch: "image",
|
||||
expectedOk: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
match, ok := pluginMatch(tc.pluginConfig, tc.commandString)
|
||||
assert.Equal(t, ok, tc.expectedOk)
|
||||
assert.Equal(t, match, tc.expectedMatch)
|
||||
}
|
||||
}
|
||||
@ -240,8 +240,7 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
|
||||
cmd.Env = append(cmd.Environ(), ReexecEnvvar+"="+os.Args[0])
|
||||
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
|
||||
|
||||
return cmd, nil
|
||||
|
||||
@ -8,6 +8,11 @@ const (
|
||||
// which must be supported by every plugin and returns the
|
||||
// plugin metadata.
|
||||
MetadataSubcommandName = "docker-cli-plugin-metadata"
|
||||
|
||||
// HookSubcommandName is the name of the plugin subcommand
|
||||
// which must be implemented by plugins declaring support
|
||||
// for hooks in their metadata.
|
||||
HookSubcommandName = "docker-cli-plugin-hooks"
|
||||
)
|
||||
|
||||
// Metadata provided by the plugin.
|
||||
|
||||
@ -2,6 +2,8 @@ package manager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
@ -100,3 +102,22 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// RunHook executes the plugin's hooks command
|
||||
// and returns its unprocessed output.
|
||||
func (p *Plugin) RunHook(hookData HookPluginData) ([]byte, error) {
|
||||
hDataBytes, err := json.Marshal(hookData)
|
||||
if err != nil {
|
||||
return nil, wrapAsPluginError(err, "failed to marshall hook data")
|
||||
}
|
||||
|
||||
pCmd := exec.Command(p.Path, p.Name, HookSubcommandName, string(hDataBytes))
|
||||
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
|
||||
}
|
||||
|
||||
@ -12,15 +12,17 @@ import (
|
||||
"github.com/docker/cli/cli-plugins/socket"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/cli/cli/debug"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
// PersistentPreRunE must be called by any plugin command (or
|
||||
// subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins
|
||||
// which do not make use of `PersistentPreRun*` do not need to call
|
||||
// this (although it remains safe to do so). Plugins are recommended
|
||||
// to use `PersistenPreRunE` to enable the error to be
|
||||
// to use `PersistentPreRunE` to enable the error to be
|
||||
// returned. Should not be called outside of a command's
|
||||
// PersistentPreRunE hook and must not be run unless Run has been
|
||||
// called.
|
||||
@ -66,6 +68,8 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager
|
||||
|
||||
// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
|
||||
func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
|
||||
otel.SetErrorHandler(debug.OTELErrorHandler)
|
||||
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
|
||||
@ -7,24 +7,114 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// EnvKey represents the well-known environment variable used to pass the plugin being
|
||||
// executed the socket name it should listen on to coordinate with the host CLI.
|
||||
// EnvKey represents the well-known environment variable used to pass the
|
||||
// plugin being executed the socket name it should listen on to coordinate with
|
||||
// the host CLI.
|
||||
const EnvKey = "DOCKER_CLI_PLUGIN_SOCKET"
|
||||
|
||||
// SetupConn sets up a Unix socket listener, establishes a goroutine to handle connections
|
||||
// and update the conn pointer, and returns the listener for the socket (which the caller
|
||||
// is responsible for closing when it's no longer needed).
|
||||
func SetupConn(conn **net.UnixConn) (*net.UnixListener, error) {
|
||||
listener, err := listen("docker_cli_" + randomID())
|
||||
// NewPluginServer creates a plugin server that listens on a new Unix domain
|
||||
// socket. h is called for each new connection to the socket in a goroutine.
|
||||
func NewPluginServer(h func(net.Conn)) (*PluginServer, error) {
|
||||
// Listen on a Unix socket, with the address being platform-dependent.
|
||||
// When a non-abstract address is used, Go will unlink(2) the socket
|
||||
// for us once the listener is closed, as documented in
|
||||
// [net.UnixListener.SetUnlinkOnClose].
|
||||
l, err := net.ListenUnix("unix", &net.UnixAddr{
|
||||
Name: socketName("docker_cli_" + randomID()),
|
||||
Net: "unix",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accept(listener, conn)
|
||||
if h == nil {
|
||||
h = func(net.Conn) {}
|
||||
}
|
||||
|
||||
return listener, nil
|
||||
pl := &PluginServer{
|
||||
l: l,
|
||||
h: h,
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer pl.Close()
|
||||
for {
|
||||
err := pl.accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return pl, nil
|
||||
}
|
||||
|
||||
type PluginServer struct {
|
||||
mu sync.Mutex
|
||||
conns []net.Conn
|
||||
l *net.UnixListener
|
||||
h func(net.Conn)
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (pl *PluginServer) accept() error {
|
||||
conn, err := pl.l.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pl.mu.Lock()
|
||||
defer pl.mu.Unlock()
|
||||
|
||||
if pl.closed {
|
||||
// Handle potential race between Close and accept.
|
||||
conn.Close()
|
||||
return errors.New("plugin server is closed")
|
||||
}
|
||||
|
||||
pl.conns = append(pl.conns, conn)
|
||||
|
||||
go pl.h(conn)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Addr returns the [net.Addr] of the underlying [net.Listener].
|
||||
func (pl *PluginServer) Addr() net.Addr {
|
||||
return pl.l.Addr()
|
||||
}
|
||||
|
||||
// Close ensures that the server is no longer accepting new connections and
|
||||
// closes all existing connections. Existing connections will receive [io.EOF].
|
||||
//
|
||||
// The error value is that of the underlying [net.Listner.Close] call.
|
||||
func (pl *PluginServer) Close() error {
|
||||
// Close connections first to ensure the connections get io.EOF instead
|
||||
// of a connection reset.
|
||||
pl.closeAllConns()
|
||||
|
||||
// Try to ensure that any active connections have a chance to receive
|
||||
// io.EOF.
|
||||
runtime.Gosched()
|
||||
|
||||
return pl.l.Close()
|
||||
}
|
||||
|
||||
func (pl *PluginServer) closeAllConns() {
|
||||
pl.mu.Lock()
|
||||
defer pl.mu.Unlock()
|
||||
|
||||
// Prevent new connections from being accepted.
|
||||
pl.closed = true
|
||||
|
||||
for _, conn := range pl.conns {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
pl.conns = nil
|
||||
}
|
||||
|
||||
func randomID() string {
|
||||
@ -35,18 +125,6 @@ func randomID() string {
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func accept(listener *net.UnixListener, conn **net.UnixConn) {
|
||||
go func() {
|
||||
for {
|
||||
// ignore error here, if we failed to accept a connection,
|
||||
// conn is nil and we fallback to previous behavior
|
||||
*conn, _ = listener.AcceptUnix()
|
||||
// perform any platform-specific actions on accept (e.g. unlink non-abstract sockets)
|
||||
onAccept(*conn, listener)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ConnectAndWait connects to the socket passed via well-known env var,
|
||||
// if present, and attempts to read from it until it receives an EOF, at which
|
||||
// point cb is called.
|
||||
|
||||
9
cli-plugins/socket/socket_abstract.go
Normal file
9
cli-plugins/socket/socket_abstract.go
Normal file
@ -0,0 +1,9 @@
|
||||
//go:build windows || linux
|
||||
|
||||
package socket
|
||||
|
||||
func socketName(basename string) string {
|
||||
// Address of an abstract socket -- this socket can be opened by name,
|
||||
// but is not present in the filesystem.
|
||||
return "@" + basename
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
package socket
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func listen(socketname string) (*net.UnixListener, error) {
|
||||
return net.ListenUnix("unix", &net.UnixAddr{
|
||||
Name: filepath.Join(os.TempDir(), socketname),
|
||||
Net: "unix",
|
||||
})
|
||||
}
|
||||
|
||||
func onAccept(conn *net.UnixConn, listener *net.UnixListener) {
|
||||
syscall.Unlink(listener.Addr().String())
|
||||
}
|
||||
14
cli-plugins/socket/socket_noabstract.go
Normal file
14
cli-plugins/socket/socket_noabstract.go
Normal file
@ -0,0 +1,14 @@
|
||||
//go:build !windows && !linux
|
||||
|
||||
package socket
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func socketName(basename string) string {
|
||||
// Because abstract sockets are unavailable, use a socket path in the
|
||||
// system temporary directory.
|
||||
return filepath.Join(os.TempDir(), basename)
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
//go:build !darwin && !openbsd
|
||||
|
||||
package socket
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func listen(socketname string) (*net.UnixListener, error) {
|
||||
return net.ListenUnix("unix", &net.UnixAddr{
|
||||
Name: "@" + socketname,
|
||||
Net: "unix",
|
||||
})
|
||||
}
|
||||
|
||||
func onAccept(conn *net.UnixConn, listener *net.UnixListener) {
|
||||
// do nothing
|
||||
// while on darwin and OpenBSD we would unlink here;
|
||||
// on non-darwin the socket is abstract and not present on the filesystem
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
package socket
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func listen(socketname string) (*net.UnixListener, error) {
|
||||
return net.ListenUnix("unix", &net.UnixAddr{
|
||||
Name: filepath.Join(os.TempDir(), socketname),
|
||||
Net: "unix",
|
||||
})
|
||||
}
|
||||
|
||||
func onAccept(conn *net.UnixConn, listener *net.UnixListener) {
|
||||
syscall.Unlink(listener.Addr().String())
|
||||
}
|
||||
@ -1,11 +1,14 @@
|
||||
package socket
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -13,54 +16,110 @@ import (
|
||||
"gotest.tools/v3/poll"
|
||||
)
|
||||
|
||||
func TestSetupConn(t *testing.T) {
|
||||
t.Run("updates conn when connected", func(t *testing.T) {
|
||||
var conn *net.UnixConn
|
||||
listener, err := SetupConn(&conn)
|
||||
func TestPluginServer(t *testing.T) {
|
||||
t.Run("connection closes with EOF when server closes", func(t *testing.T) {
|
||||
called := make(chan struct{})
|
||||
srv, err := NewPluginServer(func(_ net.Conn) { close(called) })
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, listener != nil, "returned nil listener but no error")
|
||||
addr, err := net.ResolveUnixAddr("unix", listener.Addr().String())
|
||||
assert.NilError(t, err, "failed to resolve listener address")
|
||||
assert.Assert(t, srv != nil, "returned nil server but no error")
|
||||
|
||||
_, err = net.DialUnix("unix", nil, addr)
|
||||
assert.NilError(t, err, "failed to dial returned listener")
|
||||
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String())
|
||||
assert.NilError(t, err, "failed to resolve server address")
|
||||
|
||||
pollConnNotNil(t, &conn)
|
||||
conn, err := net.DialUnix("unix", nil, addr)
|
||||
assert.NilError(t, err, "failed to dial returned server")
|
||||
defer conn.Close()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := conn.Read(make([]byte, 1))
|
||||
done <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-called:
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
t.Fatal("handler not called")
|
||||
}
|
||||
|
||||
srv.Close()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if !errors.Is(err, io.EOF) {
|
||||
t.Fatalf("exepcted EOF error, got: %v", err)
|
||||
}
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("allows reconnects", func(t *testing.T) {
|
||||
var conn *net.UnixConn
|
||||
listener, err := SetupConn(&conn)
|
||||
var calls int32
|
||||
h := func(_ net.Conn) {
|
||||
atomic.AddInt32(&calls, 1)
|
||||
}
|
||||
|
||||
srv, err := NewPluginServer(h)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, listener != nil, "returned nil listener but no error")
|
||||
addr, err := net.ResolveUnixAddr("unix", listener.Addr().String())
|
||||
assert.NilError(t, err, "failed to resolve listener address")
|
||||
defer srv.Close()
|
||||
|
||||
assert.Check(t, srv.Addr() != nil, "returned nil addr but no error")
|
||||
|
||||
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String())
|
||||
assert.NilError(t, err, "failed to resolve server address")
|
||||
|
||||
waitForCalls := func(n int) {
|
||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
||||
if atomic.LoadInt32(&calls) == int32(n) {
|
||||
return poll.Success()
|
||||
}
|
||||
return poll.Continue("waiting for handler to be called")
|
||||
})
|
||||
}
|
||||
|
||||
otherConn, err := net.DialUnix("unix", nil, addr)
|
||||
assert.NilError(t, err, "failed to dial returned listener")
|
||||
|
||||
assert.NilError(t, err, "failed to dial returned server")
|
||||
otherConn.Close()
|
||||
waitForCalls(1)
|
||||
|
||||
_, err = net.DialUnix("unix", nil, addr)
|
||||
assert.NilError(t, err, "failed to redial listener")
|
||||
conn, err := net.DialUnix("unix", nil, addr)
|
||||
assert.NilError(t, err, "failed to redial server")
|
||||
defer conn.Close()
|
||||
waitForCalls(2)
|
||||
|
||||
// and again but don't close the existing connection
|
||||
conn2, err := net.DialUnix("unix", nil, addr)
|
||||
assert.NilError(t, err, "failed to redial server")
|
||||
defer conn2.Close()
|
||||
waitForCalls(3)
|
||||
|
||||
srv.Close()
|
||||
|
||||
// now make sure we get EOF on the existing connections
|
||||
buf := make([]byte, 1)
|
||||
_, err = conn.Read(buf)
|
||||
assert.ErrorIs(t, err, io.EOF, "expected EOF error, got: %v", err)
|
||||
|
||||
_, err = conn2.Read(buf)
|
||||
assert.ErrorIs(t, err, io.EOF, "expected EOF error, got: %v", err)
|
||||
})
|
||||
|
||||
t.Run("does not leak sockets to local directory", func(t *testing.T) {
|
||||
var conn *net.UnixConn
|
||||
listener, err := SetupConn(&conn)
|
||||
srv, err := NewPluginServer(nil)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, listener != nil, "returned nil listener but no error")
|
||||
checkDirNoPluginSocket(t)
|
||||
assert.Check(t, srv != nil, "returned nil server but no error")
|
||||
checkDirNoNewPluginServer(t)
|
||||
|
||||
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String())
|
||||
assert.NilError(t, err, "failed to resolve server address")
|
||||
|
||||
addr, err := net.ResolveUnixAddr("unix", listener.Addr().String())
|
||||
assert.NilError(t, err, "failed to resolve listener address")
|
||||
_, err = net.DialUnix("unix", nil, addr)
|
||||
assert.NilError(t, err, "failed to dial returned listener")
|
||||
checkDirNoPluginSocket(t)
|
||||
assert.NilError(t, err, "failed to dial returned server")
|
||||
checkDirNoNewPluginServer(t)
|
||||
})
|
||||
}
|
||||
|
||||
func checkDirNoPluginSocket(t *testing.T) {
|
||||
func checkDirNoNewPluginServer(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
files, err := os.ReadDir(".")
|
||||
@ -78,18 +137,24 @@ func checkDirNoPluginSocket(t *testing.T) {
|
||||
|
||||
func TestConnectAndWait(t *testing.T) {
|
||||
t.Run("calls cancel func on EOF", func(t *testing.T) {
|
||||
var conn *net.UnixConn
|
||||
listener, err := SetupConn(&conn)
|
||||
assert.NilError(t, err, "failed to setup listener")
|
||||
srv, err := NewPluginServer(nil)
|
||||
assert.NilError(t, err, "failed to setup server")
|
||||
defer srv.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
t.Setenv(EnvKey, listener.Addr().String())
|
||||
t.Setenv(EnvKey, srv.Addr().String())
|
||||
cancelFunc := func() {
|
||||
done <- struct{}{}
|
||||
}
|
||||
ConnectAndWait(cancelFunc)
|
||||
pollConnNotNil(t, &conn)
|
||||
conn.Close()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
t.Fatal("unexpectedly done")
|
||||
default:
|
||||
}
|
||||
|
||||
srv.Close()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
@ -101,17 +166,19 @@ func TestConnectAndWait(t *testing.T) {
|
||||
// TODO: this test cannot be executed with `t.Parallel()`, due to
|
||||
// relying on goroutine numbers to ensure correct behaviour
|
||||
t.Run("connect goroutine exits after EOF", func(t *testing.T) {
|
||||
var conn *net.UnixConn
|
||||
listener, err := SetupConn(&conn)
|
||||
assert.NilError(t, err, "failed to setup listener")
|
||||
t.Setenv(EnvKey, listener.Addr().String())
|
||||
srv, err := NewPluginServer(nil)
|
||||
assert.NilError(t, err, "failed to setup server")
|
||||
|
||||
defer srv.Close()
|
||||
|
||||
t.Setenv(EnvKey, srv.Addr().String())
|
||||
numGoroutines := runtime.NumGoroutine()
|
||||
|
||||
ConnectAndWait(func() {})
|
||||
assert.Equal(t, runtime.NumGoroutine(), numGoroutines+1)
|
||||
|
||||
pollConnNotNil(t, &conn)
|
||||
conn.Close()
|
||||
srv.Close()
|
||||
|
||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
||||
if runtime.NumGoroutine() > numGoroutines+1 {
|
||||
return poll.Continue("waiting for connect goroutine to exit")
|
||||
@ -120,14 +187,3 @@ func TestConnectAndWait(t *testing.T) {
|
||||
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(10*time.Millisecond))
|
||||
})
|
||||
}
|
||||
|
||||
func pollConnNotNil(t *testing.T, conn **net.UnixConn) {
|
||||
t.Helper()
|
||||
|
||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
||||
if *conn == nil {
|
||||
return poll.Continue("waiting for conn to not be nil")
|
||||
}
|
||||
return poll.Success()
|
||||
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(10*time.Millisecond))
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@ -10,6 +11,7 @@ import (
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/errdefs"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -67,9 +69,13 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
warning = allCacheWarning
|
||||
}
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
if !r {
|
||||
return 0, "", errdefs.Cancelled(errors.New("builder prune has been cancelled"))
|
||||
}
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().BuildCachePrune(ctx, types.BuildCachePruneOptions{
|
||||
|
||||
@ -5,10 +5,8 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestBuilderPromptTermination(t *testing.T) {
|
||||
@ -21,8 +19,5 @@ func TestBuilderPromptTermination(t *testing.T) {
|
||||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
||||
@ -65,6 +65,7 @@ type Cli interface {
|
||||
ContextStore() store.Store
|
||||
CurrentContext() string
|
||||
DockerEndpoint() docker.Endpoint
|
||||
TelemetryClient
|
||||
}
|
||||
|
||||
// DockerCli is an instance the docker command line client.
|
||||
@ -85,6 +86,7 @@ type DockerCli struct {
|
||||
dockerEndpoint docker.Endpoint
|
||||
contextStoreConfig store.Config
|
||||
initTimeout time.Duration
|
||||
res telemetryResource
|
||||
|
||||
// baseCtx is the base context used for internal operations. In the future
|
||||
// this may be replaced by explicitly passing a context to functions that
|
||||
@ -187,6 +189,36 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
|
||||
return cli.ServerInfo().OSType != "windows", nil
|
||||
}
|
||||
|
||||
// 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 != "" {
|
||||
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 != "" {
|
||||
enabled, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
featuresMap := cli.ConfigFile().Features
|
||||
if v, ok := featuresMap["hooks"]; ok {
|
||||
enabled, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
// default to false
|
||||
return false
|
||||
}
|
||||
|
||||
// ManifestStore returns a store for local manifests
|
||||
func (cli *DockerCli) ManifestStore() manifeststore.Store {
|
||||
// TODO: support override default location from config file
|
||||
|
||||
@ -307,3 +307,56 @@ func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
|
||||
})))
|
||||
assert.Check(t, cli.ContextStore() != nil)
|
||||
}
|
||||
|
||||
func TestHooksEnabled(t *testing.T) {
|
||||
t.Run("disabled by default", func(t *testing.T) {
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Check(t, !cli.HooksEnabled())
|
||||
})
|
||||
|
||||
t.Run("enabled in configFile", func(t *testing.T) {
|
||||
configFile := `{
|
||||
"features": {
|
||||
"hooks": "true"
|
||||
}}`
|
||||
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
|
||||
defer dir.Remove()
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
config.SetDir(dir.Path())
|
||||
|
||||
assert.Check(t, cli.HooksEnabled())
|
||||
})
|
||||
|
||||
t.Run("env var overrides configFile", func(t *testing.T) {
|
||||
configFile := `{
|
||||
"features": {
|
||||
"hooks": "true"
|
||||
}}`
|
||||
t.Setenv("DOCKER_CLI_HOOKS", "false")
|
||||
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
|
||||
defer dir.Remove()
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
config.SetDir(dir.Path())
|
||||
|
||||
assert.Check(t, !cli.HooksEnabled())
|
||||
})
|
||||
|
||||
t.Run("legacy env var overrides configFile", func(t *testing.T) {
|
||||
configFile := `{
|
||||
"features": {
|
||||
"hooks": "true"
|
||||
}}`
|
||||
t.Setenv("DOCKER_CLI_HINTS", "false")
|
||||
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
|
||||
defer dir.Remove()
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
config.SetDir(dir.Path())
|
||||
|
||||
assert.Check(t, !cli.HooksEnabled())
|
||||
})
|
||||
}
|
||||
|
||||
@ -8,7 +8,9 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/errdefs"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -54,9 +56,13 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
||||
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
if !r {
|
||||
return 0, "", errdefs.Cancelled(errors.New("container prune has been cancelled"))
|
||||
}
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().ContainersPrune(ctx, pruneFilters)
|
||||
|
||||
@ -4,12 +4,10 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestContainerPrunePromptTermination(t *testing.T) {
|
||||
@ -22,8 +20,5 @@ func TestContainerPrunePromptTermination(t *testing.T) {
|
||||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
||||
@ -186,7 +186,11 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption
|
||||
defer closeFn()
|
||||
}
|
||||
|
||||
statusChan := waitExitOrRemoved(ctx, apiClient, containerID, copts.autoRemove)
|
||||
// New context here because we don't to cancel waiting on container exit/remove
|
||||
// when we cancel attach, etc.
|
||||
statusCtx, cancelStatusCtx := context.WithCancel(context.WithoutCancel(ctx))
|
||||
defer cancelStatusCtx()
|
||||
statusChan := waitExitOrRemoved(statusCtx, apiClient, containerID, copts.autoRemove)
|
||||
|
||||
// start the container
|
||||
if err := apiClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
|
||||
|
||||
@ -18,6 +18,7 @@ import (
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -106,15 +107,6 @@ var acceptedStatsFilters = map[string]bool{
|
||||
func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
|
||||
// Get the daemonOSType if not set already
|
||||
if daemonOSType == "" {
|
||||
sv, err := apiClient.ServerVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
daemonOSType = sv.Os
|
||||
}
|
||||
|
||||
// waitFirst is a WaitGroup to wait first stat data's reach for each container
|
||||
waitFirst := &sync.WaitGroup{}
|
||||
// closeChan is a non-buffered channel used to collect errors from goroutines.
|
||||
@ -138,9 +130,9 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
return err
|
||||
}
|
||||
|
||||
eh := command.InitEventHandler()
|
||||
eh := newEventHandler()
|
||||
if options.All {
|
||||
eh.Handle(events.ActionCreate, func(e events.Message) {
|
||||
eh.setHandler(events.ActionCreate, func(e events.Message) {
|
||||
s := NewStats(e.Actor.ID[:12])
|
||||
if cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
@ -149,7 +141,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
})
|
||||
}
|
||||
|
||||
eh.Handle(events.ActionStart, func(e events.Message) {
|
||||
eh.setHandler(events.ActionStart, func(e events.Message) {
|
||||
s := NewStats(e.Actor.ID[:12])
|
||||
if cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
@ -158,7 +150,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
})
|
||||
|
||||
if !options.All {
|
||||
eh.Handle(events.ActionDie, func(e events.Message) {
|
||||
eh.setHandler(events.ActionDie, func(e events.Message) {
|
||||
cStats.remove(e.Actor.ID[:12])
|
||||
})
|
||||
}
|
||||
@ -195,7 +187,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
}
|
||||
|
||||
eventChan := make(chan events.Message)
|
||||
go eh.Watch(eventChan)
|
||||
go eh.watch(eventChan)
|
||||
stopped := make(chan struct{})
|
||||
go monitorContainerEvents(started, eventChan, stopped)
|
||||
defer close(stopped)
|
||||
@ -267,6 +259,12 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
if daemonOSType == "" {
|
||||
// Get the daemonOSType if not set already. The daemonOSType variable
|
||||
// should already be set when collecting stats as part of "collect()",
|
||||
// so we unlikely hit this code in practice.
|
||||
daemonOSType = dockerCLI.ServerInfo().OSType
|
||||
}
|
||||
statsCtx := formatter.Context{
|
||||
Output: dockerCLI.Out(),
|
||||
Format: NewStatsFormat(format, daemonOSType),
|
||||
@ -316,3 +314,31 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// newEventHandler initializes and returns an eventHandler
|
||||
func newEventHandler() *eventHandler {
|
||||
return &eventHandler{handlers: make(map[events.Action]func(events.Message))}
|
||||
}
|
||||
|
||||
// eventHandler allows for registering specific events to setHandler.
|
||||
type eventHandler struct {
|
||||
handlers map[events.Action]func(events.Message)
|
||||
}
|
||||
|
||||
func (eh *eventHandler) setHandler(action events.Action, handler func(events.Message)) {
|
||||
eh.handlers[action] = handler
|
||||
}
|
||||
|
||||
// watch ranges over the passed in event chan and processes the events based on the
|
||||
// handlers created for a given action.
|
||||
// To stop watching, close the event chan.
|
||||
func (eh *eventHandler) watch(c <-chan events.Message) {
|
||||
for e := range c {
|
||||
h, exists := eh.handlers[e.Action]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
logrus.Debugf("event handler: received event: %v", e)
|
||||
go h(e)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
@ -35,7 +36,10 @@ func waitExitOrRemoved(ctx context.Context, apiClient client.APIClient, containe
|
||||
|
||||
statusC := make(chan int)
|
||||
go func() {
|
||||
defer close(statusC)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case result := <-resultC:
|
||||
if result.Error != nil {
|
||||
logrus.Errorf("Error waiting for container: %v", result.Error.Message)
|
||||
@ -44,6 +48,9 @@ func waitExitOrRemoved(ctx context.Context, apiClient client.APIClient, containe
|
||||
statusC <- int(result.StatusCode)
|
||||
}
|
||||
case err := <-errC:
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
logrus.Errorf("error waiting for container: %v", err)
|
||||
statusC <- 125
|
||||
}
|
||||
|
||||
@ -9,12 +9,16 @@ import (
|
||||
|
||||
// EventHandler is abstract interface for user to customize
|
||||
// own handle functions of each type of events
|
||||
//
|
||||
// Deprecated: EventHandler is no longer used, and will be removed in the next release.
|
||||
type EventHandler interface {
|
||||
Handle(action events.Action, h func(events.Message))
|
||||
Watch(c <-chan events.Message)
|
||||
}
|
||||
|
||||
// InitEventHandler initializes and returns an EventHandler
|
||||
//
|
||||
// Deprecated: InitEventHandler is no longer used, and will be removed in the next release.
|
||||
func InitEventHandler() EventHandler {
|
||||
return &eventHandler{handlers: make(map[events.Action]func(events.Message))}
|
||||
}
|
||||
|
||||
@ -129,7 +129,6 @@ func TestGetContextFromReaderString(t *testing.T) {
|
||||
tarReader := tar.NewReader(tarArchive)
|
||||
|
||||
_, err = tarReader.Next()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Error when reading tar archive: %s", err)
|
||||
}
|
||||
|
||||
@ -10,7 +10,9 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/errdefs"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -68,9 +70,13 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
warning = allImageWarning
|
||||
}
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
if !r {
|
||||
return 0, "", errdefs.Cancelled(errors.New("image prune has been cancelled"))
|
||||
}
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().ImagesPrune(ctx, pruneFilters)
|
||||
|
||||
@ -4,9 +4,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@ -94,13 +95,18 @@ func TestNewPruneCommandSuccess(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc})
|
||||
cmd := NewPruneCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("prune-command-success.%s.golden", tc.name))
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc})
|
||||
// when prompted, answer "Y" to confirm the prune.
|
||||
// will not be prompted if --force is used.
|
||||
cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader("Y\n"))))
|
||||
cmd := NewPruneCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("prune-command-success.%s.golden", tc.name))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,8 +120,5 @@ func TestPrunePromptTermination(t *testing.T) {
|
||||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
||||
@ -5,8 +5,6 @@
|
||||
"RepoDigests": null,
|
||||
"Parent": "",
|
||||
"Comment": "",
|
||||
"Container": "",
|
||||
"ContainerConfig": null,
|
||||
"DockerVersion": "",
|
||||
"Author": "",
|
||||
"Config": null,
|
||||
@ -28,8 +26,6 @@
|
||||
"RepoDigests": null,
|
||||
"Parent": "",
|
||||
"Comment": "",
|
||||
"Container": "",
|
||||
"ContainerConfig": null,
|
||||
"DockerVersion": "",
|
||||
"Author": "",
|
||||
"Config": null,
|
||||
|
||||
@ -5,8 +5,6 @@
|
||||
"RepoDigests": null,
|
||||
"Parent": "",
|
||||
"Comment": "",
|
||||
"Container": "",
|
||||
"ContainerConfig": null,
|
||||
"DockerVersion": "",
|
||||
"Author": "",
|
||||
"Config": null,
|
||||
|
||||
@ -7,6 +7,8 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -50,9 +52,13 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
||||
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !r {
|
||||
return "", errdefs.Cancelled(errors.New("network prune cancelled has been cancelled"))
|
||||
}
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().NetworksPrune(ctx, pruneFilters)
|
||||
|
||||
@ -4,12 +4,10 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestNetworkPrunePromptTermination(t *testing.T) {
|
||||
@ -22,8 +20,5 @@ func TestNetworkPrunePromptTermination(t *testing.T) {
|
||||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/errdefs"
|
||||
@ -115,8 +114,5 @@ func TestNetworkRemovePromptTermination(t *testing.T) {
|
||||
})
|
||||
cmd := newRemoveCommand(cli)
|
||||
cmd.SetArgs([]string{"existing-network"})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
||||
@ -114,7 +114,6 @@ func runCreate(ctx context.Context, dockerCli command.Cli, options pluginCreateO
|
||||
createCtx, err = archive.TarWithOptions(absContextDir, &archive.TarOptions{
|
||||
Compression: compression,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
Upgrading plugin foo/bar from localhost:5000/foo/bar:v0.1.0 to localhost:5000/foo/bar:v1.0.0
|
||||
Plugin images do not match, are you sure? [y/N]
|
||||
Plugin images do not match, are you sure? [y/N]
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@ -63,11 +64,12 @@ func runUpgrade(ctx context.Context, dockerCli command.Cli, opts pluginOptions)
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, reference.FamiliarString(old), reference.FamiliarString(remote))
|
||||
if !opts.skipRemoteCheck && remote.String() != old.String() {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), "Plugin images do not match, are you sure?"); !r || err != nil {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "canceling upgrade request")
|
||||
}
|
||||
return errors.New("canceling upgrade request")
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), "Plugin images do not match, are you sure?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !r {
|
||||
return errdefs.Cancelled(errors.New("plugin upgrade has been cancelled"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,11 +5,9 @@ import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
@ -34,9 +32,6 @@ func TestUpgradePromptTermination(t *testing.T) {
|
||||
// need to set a remote address that does not match the plugin
|
||||
// reference sent by the `pluginInspectFunc`
|
||||
cmd.SetArgs([]string{"foo/bar", "localhost:5000/foo/bar:v1.0.0"})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
golden.Assert(t, cli.OutBuffer().String(), "plugin-upgrade-terminate.golden")
|
||||
}
|
||||
|
||||
@ -17,8 +17,10 @@ import (
|
||||
"github.com/docker/cli/cli/command/volume"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -75,9 +77,13 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
return fmt.Errorf(`ERROR: The "until" filter is not supported with "--volumes"`)
|
||||
}
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), confirmationMessage(dockerCli, options)); !r || err != nil {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), confirmationMessage(dockerCli, options))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !r {
|
||||
return errdefs.Cancelled(errors.New("system prune has been cancelled"))
|
||||
}
|
||||
}
|
||||
pruneFuncs := []func(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error){
|
||||
container.RunPrune,
|
||||
|
||||
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
@ -18,7 +17,7 @@ func TestPrunePromptPre131DoesNotIncludeBuildCache(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{version: "1.30"})
|
||||
cmd := newPruneCommand(cli)
|
||||
cmd.SetArgs([]string{})
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.ErrorContains(t, cmd.Execute(), "system prune has been cancelled")
|
||||
expected := `WARNING! This will remove:
|
||||
- all stopped containers
|
||||
- all networks not used by at least one container
|
||||
@ -36,7 +35,7 @@ func TestPrunePromptFilters(t *testing.T) {
|
||||
cmd := newPruneCommand(cli)
|
||||
cmd.SetArgs([]string{"--filter", "until=24h", "--filter", "label=hello-world", "--filter", "label!=foo=bar", "--filter", "label=bar=baz"})
|
||||
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.ErrorContains(t, cmd.Execute(), "system prune has been cancelled")
|
||||
expected := `WARNING! This will remove:
|
||||
- all stopped containers
|
||||
- all networks not used by at least one container
|
||||
@ -69,8 +68,5 @@ func TestSystemPrunePromptTermination(t *testing.T) {
|
||||
})
|
||||
|
||||
cmd := newPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
||||
202
cli/command/telemetry.go
Normal file
202
cli/command/telemetry.go
Normal file
@ -0,0 +1,202 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/uuid"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
const exportTimeout = 50 * time.Millisecond
|
||||
|
||||
// TracerProvider is an extension of the trace.TracerProvider interface for CLI programs.
|
||||
type TracerProvider interface {
|
||||
trace.TracerProvider
|
||||
ForceFlush(ctx context.Context) error
|
||||
Shutdown(ctx context.Context) error
|
||||
}
|
||||
|
||||
// MeterProvider is an extension of the metric.MeterProvider interface for CLI programs.
|
||||
type MeterProvider interface {
|
||||
metric.MeterProvider
|
||||
ForceFlush(ctx context.Context) error
|
||||
Shutdown(ctx context.Context) error
|
||||
}
|
||||
|
||||
// TelemetryClient provides the methods for using OTEL tracing or metrics.
|
||||
type TelemetryClient interface {
|
||||
// Resource returns the OTEL Resource configured with this TelemetryClient.
|
||||
// This resource may be created lazily, but the resource should be the same
|
||||
// each time this function is invoked.
|
||||
Resource() *resource.Resource
|
||||
|
||||
// TracerProvider returns a TracerProvider. This TracerProvider will be configured
|
||||
// with the default tracing components for a CLI program along with any options given
|
||||
// for the SDK.
|
||||
TracerProvider(ctx context.Context, opts ...sdktrace.TracerProviderOption) TracerProvider
|
||||
|
||||
// MeterProvider returns a MeterProvider. This MeterProvider will be configured
|
||||
// with the default metric components for a CLI program along with any options given
|
||||
// for the SDK.
|
||||
MeterProvider(ctx context.Context, opts ...sdkmetric.Option) MeterProvider
|
||||
}
|
||||
|
||||
func (cli *DockerCli) Resource() *resource.Resource {
|
||||
return cli.res.Get()
|
||||
}
|
||||
|
||||
func (cli *DockerCli) TracerProvider(ctx context.Context, opts ...sdktrace.TracerProviderOption) TracerProvider {
|
||||
allOpts := make([]sdktrace.TracerProviderOption, 0, len(opts)+2)
|
||||
allOpts = append(allOpts, sdktrace.WithResource(cli.Resource()))
|
||||
allOpts = append(allOpts, dockerSpanExporter(ctx, cli)...)
|
||||
allOpts = append(allOpts, opts...)
|
||||
return sdktrace.NewTracerProvider(allOpts...)
|
||||
}
|
||||
|
||||
func (cli *DockerCli) MeterProvider(ctx context.Context, opts ...sdkmetric.Option) MeterProvider {
|
||||
allOpts := make([]sdkmetric.Option, 0, len(opts)+2)
|
||||
allOpts = append(allOpts, sdkmetric.WithResource(cli.Resource()))
|
||||
allOpts = append(allOpts, dockerMetricExporter(ctx, cli)...)
|
||||
allOpts = append(allOpts, opts...)
|
||||
return sdkmetric.NewMeterProvider(allOpts...)
|
||||
}
|
||||
|
||||
// WithResourceOptions configures additional options for the default resource. The default
|
||||
// resource will continue to include its default options.
|
||||
func WithResourceOptions(opts ...resource.Option) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.res.AppendOptions(opts...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithResource overwrites the default resource and prevents its creation.
|
||||
func WithResource(res *resource.Resource) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.res.Set(res)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type telemetryResource struct {
|
||||
res *resource.Resource
|
||||
opts []resource.Option
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (r *telemetryResource) Set(res *resource.Resource) {
|
||||
r.res = res
|
||||
}
|
||||
|
||||
func (r *telemetryResource) Get() *resource.Resource {
|
||||
r.once.Do(r.init)
|
||||
return r.res
|
||||
}
|
||||
|
||||
func (r *telemetryResource) init() {
|
||||
if r.res != nil {
|
||||
r.opts = nil
|
||||
return
|
||||
}
|
||||
|
||||
opts := append(defaultResourceOptions(), r.opts...)
|
||||
res, err := resource.New(context.Background(), opts...)
|
||||
if err != nil {
|
||||
otel.Handle(err)
|
||||
}
|
||||
r.res = res
|
||||
|
||||
// Clear the resource options since they'll never be used again and to allow
|
||||
// the garbage collector to retrieve that memory.
|
||||
r.opts = nil
|
||||
}
|
||||
|
||||
func defaultResourceOptions() []resource.Option {
|
||||
return []resource.Option{
|
||||
resource.WithDetectors(serviceNameDetector{}),
|
||||
resource.WithAttributes(
|
||||
// Use a unique instance id so OTEL knows that each invocation
|
||||
// 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()),
|
||||
),
|
||||
resource.WithFromEnv(),
|
||||
resource.WithTelemetrySDK(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *telemetryResource) AppendOptions(opts ...resource.Option) {
|
||||
if r.res != nil {
|
||||
return
|
||||
}
|
||||
r.opts = append(r.opts, opts...)
|
||||
}
|
||||
|
||||
type serviceNameDetector struct{}
|
||||
|
||||
func (serviceNameDetector) Detect(ctx context.Context) (*resource.Resource, error) {
|
||||
return resource.StringDetector(
|
||||
semconv.SchemaURL,
|
||||
semconv.ServiceNameKey,
|
||||
func() (string, error) {
|
||||
return filepath.Base(os.Args[0]), nil
|
||||
},
|
||||
).Detect(ctx)
|
||||
}
|
||||
|
||||
// cliReader is an implementation of Reader that will automatically
|
||||
// report to a designated Exporter when Shutdown is called.
|
||||
type cliReader struct {
|
||||
sdkmetric.Reader
|
||||
exporter sdkmetric.Exporter
|
||||
}
|
||||
|
||||
func newCLIReader(exp sdkmetric.Exporter) sdkmetric.Reader {
|
||||
reader := sdkmetric.NewManualReader(
|
||||
sdkmetric.WithTemporalitySelector(deltaTemporality),
|
||||
)
|
||||
return &cliReader{
|
||||
Reader: reader,
|
||||
exporter: exp,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *cliReader) Shutdown(ctx context.Context) error {
|
||||
var rm metricdata.ResourceMetrics
|
||||
if err := r.Reader.Collect(ctx, &rm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Place a pretty tight constraint on the actual reporting.
|
||||
// We don't want CLI metrics to prevent the CLI from exiting
|
||||
// so if there's some kind of issue we need to abort pretty
|
||||
// quickly.
|
||||
ctx, cancel := context.WithTimeout(ctx, exportTimeout)
|
||||
defer cancel()
|
||||
|
||||
return r.exporter.Export(ctx, &rm)
|
||||
}
|
||||
|
||||
// deltaTemporality sets the Temporality of every instrument to delta.
|
||||
//
|
||||
// This isn't really needed since we create a unique resource on each invocation,
|
||||
// but it can help with cardinality concerns for downstream processors since they can
|
||||
// perform aggregation for a time interval and then discard the data once that time
|
||||
// period has passed. Cumulative temporality would imply to the downstream processor
|
||||
// that they might receive a successive point and they may unnecessarily keep state
|
||||
// they really shouldn't.
|
||||
func deltaTemporality(_ sdkmetric.InstrumentKind) metricdata.Temporality {
|
||||
return metricdata.DeltaTemporality
|
||||
}
|
||||
138
cli/command/telemetry_docker.go
Normal file
138
cli/command/telemetry_docker.go
Normal file
@ -0,0 +1,138 @@
|
||||
// FIXME(jsternberg): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.19
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
)
|
||||
|
||||
const (
|
||||
otelContextFieldName string = "otel"
|
||||
otelExporterOTLPEndpoint string = "OTEL_EXPORTER_OTLP_ENDPOINT"
|
||||
debugEnvVarPrefix string = "DOCKER_CLI_"
|
||||
)
|
||||
|
||||
// dockerExporterOTLPEndpoint retrieves the OTLP endpoint used for the docker reporter
|
||||
// from the current context.
|
||||
func dockerExporterOTLPEndpoint(cli Cli) (endpoint string, secure bool) {
|
||||
meta, err := cli.ContextStore().GetMetadata(cli.CurrentContext())
|
||||
if err != nil {
|
||||
otel.Handle(err)
|
||||
return "", false
|
||||
}
|
||||
|
||||
var otelCfg any
|
||||
switch m := meta.Metadata.(type) {
|
||||
case DockerContext:
|
||||
otelCfg = m.AdditionalFields[otelContextFieldName]
|
||||
case map[string]any:
|
||||
otelCfg = m[otelContextFieldName]
|
||||
}
|
||||
|
||||
if otelCfg != nil {
|
||||
otelMap, ok := otelCfg.(map[string]any)
|
||||
if !ok {
|
||||
otel.Handle(errors.Errorf(
|
||||
"unexpected type for field %q: %T (expected: %T)",
|
||||
otelContextFieldName,
|
||||
otelCfg,
|
||||
otelMap,
|
||||
))
|
||||
}
|
||||
// keys from https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/
|
||||
endpoint, _ = otelMap[otelExporterOTLPEndpoint].(string)
|
||||
}
|
||||
|
||||
// Override with env var value if it exists AND IS SET
|
||||
// (ignore otel defaults for this override when the key exists but is empty)
|
||||
if override := os.Getenv(debugEnvVarPrefix + otelExporterOTLPEndpoint); override != "" {
|
||||
endpoint = override
|
||||
}
|
||||
|
||||
if endpoint == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Parse the endpoint. The docker config expects the endpoint to be
|
||||
// in the form of a URL to match the environment variable, but this
|
||||
// option doesn't correspond directly to WithEndpoint.
|
||||
//
|
||||
// We pretend we're the same as the environment reader.
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
otel.Handle(errors.Errorf("docker otel endpoint is invalid: %s", err))
|
||||
return "", false
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "unix":
|
||||
// Unix sockets are a bit weird. OTEL seems to imply they
|
||||
// can be used as an environment variable and are handled properly,
|
||||
// but they don't seem to be as the behavior of the environment variable
|
||||
// is to strip the scheme from the endpoint, but the underlying implementation
|
||||
// needs the scheme to use the correct resolver.
|
||||
//
|
||||
// We'll just handle this in a special way and add the unix:// back to the endpoint.
|
||||
endpoint = fmt.Sprintf("unix://%s", path.Join(u.Host, u.Path))
|
||||
case "https":
|
||||
secure = true
|
||||
fallthrough
|
||||
case "http":
|
||||
endpoint = path.Join(u.Host, u.Path)
|
||||
}
|
||||
return endpoint, secure
|
||||
}
|
||||
|
||||
func dockerSpanExporter(ctx context.Context, cli Cli) []sdktrace.TracerProviderOption {
|
||||
endpoint, secure := dockerExporterOTLPEndpoint(cli)
|
||||
if endpoint == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
opts := []otlptracegrpc.Option{
|
||||
otlptracegrpc.WithEndpoint(endpoint),
|
||||
}
|
||||
if !secure {
|
||||
opts = append(opts, otlptracegrpc.WithInsecure())
|
||||
}
|
||||
|
||||
exp, err := otlptracegrpc.New(ctx, opts...)
|
||||
if err != nil {
|
||||
otel.Handle(err)
|
||||
return nil
|
||||
}
|
||||
return []sdktrace.TracerProviderOption{sdktrace.WithBatcher(exp, sdktrace.WithExportTimeout(exportTimeout))}
|
||||
}
|
||||
|
||||
func dockerMetricExporter(ctx context.Context, cli Cli) []sdkmetric.Option {
|
||||
endpoint, secure := dockerExporterOTLPEndpoint(cli)
|
||||
if endpoint == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
opts := []otlpmetricgrpc.Option{
|
||||
otlpmetricgrpc.WithEndpoint(endpoint),
|
||||
}
|
||||
if !secure {
|
||||
opts = append(opts, otlpmetricgrpc.WithInsecure())
|
||||
}
|
||||
|
||||
exp, err := otlpmetricgrpc.New(ctx, opts...)
|
||||
if err != nil {
|
||||
otel.Handle(err)
|
||||
return nil
|
||||
}
|
||||
return []sdkmetric.Option{sdkmetric.WithReader(newCLIReader(exp))}
|
||||
}
|
||||
167
cli/command/telemetry_utils.go
Normal file
167
cli/command/telemetry_utils.go
Normal file
@ -0,0 +1,167 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/version"
|
||||
"github.com/moby/term"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
|
||||
// BaseCommandAttributes returns an attribute.Set containing attributes to attach to metrics/traces
|
||||
func BaseCommandAttributes(cmd *cobra.Command, streams Streams) []attribute.KeyValue {
|
||||
return append([]attribute.KeyValue{
|
||||
attribute.String("command.name", getCommandName(cmd)),
|
||||
}, stdioAttributes(streams)...)
|
||||
}
|
||||
|
||||
// InstrumentCobraCommands wraps all cobra commands' RunE funcs to set a command duration metric using otel.
|
||||
//
|
||||
// Note: this should be the last func to wrap/modify the PersistentRunE/RunE funcs before command execution.
|
||||
//
|
||||
// can also be used for spans!
|
||||
func (cli *DockerCli) InstrumentCobraCommands(cmd *cobra.Command, mp metric.MeterProvider) {
|
||||
meter := getDefaultMeter(mp)
|
||||
// If PersistentPreRunE is nil, make it execute PersistentPreRun and return nil by default
|
||||
ogPersistentPreRunE := cmd.PersistentPreRunE
|
||||
if ogPersistentPreRunE == nil {
|
||||
ogPersistentPreRun := cmd.PersistentPreRun
|
||||
//nolint:unparam // necessary because error will always be nil here
|
||||
ogPersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
ogPersistentPreRun(cmd, args)
|
||||
return nil
|
||||
}
|
||||
cmd.PersistentPreRun = nil
|
||||
}
|
||||
|
||||
// wrap RunE in PersistentPreRunE so that this operation gets executed on all children commands
|
||||
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
// If RunE is nil, make it execute Run and return nil by default
|
||||
ogRunE := cmd.RunE
|
||||
if ogRunE == nil {
|
||||
ogRun := cmd.Run
|
||||
//nolint:unparam // necessary because error will always be nil here
|
||||
ogRunE = func(cmd *cobra.Command, args []string) error {
|
||||
ogRun(cmd, args)
|
||||
return nil
|
||||
}
|
||||
cmd.Run = nil
|
||||
}
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
// start the timer as the first step of every cobra command
|
||||
baseAttrs := BaseCommandAttributes(cmd, cli)
|
||||
stopCobraCmdTimer := startCobraCommandTimer(cmd, meter, baseAttrs)
|
||||
cmdErr := ogRunE(cmd, args)
|
||||
stopCobraCmdTimer(cmdErr)
|
||||
return cmdErr
|
||||
}
|
||||
|
||||
return ogPersistentPreRunE(cmd, args)
|
||||
}
|
||||
}
|
||||
|
||||
func startCobraCommandTimer(cmd *cobra.Command, meter metric.Meter, attrs []attribute.KeyValue) func(err error) {
|
||||
ctx := cmd.Context()
|
||||
durationCounter, _ := meter.Float64Counter(
|
||||
"command.time",
|
||||
metric.WithDescription("Measures the duration of the cobra command"),
|
||||
metric.WithUnit("ms"),
|
||||
)
|
||||
start := time.Now()
|
||||
|
||||
return func(err error) {
|
||||
duration := float64(time.Since(start)) / float64(time.Millisecond)
|
||||
cmdStatusAttrs := attributesFromError(err)
|
||||
durationCounter.Add(ctx, duration,
|
||||
metric.WithAttributes(attrs...),
|
||||
metric.WithAttributes(cmdStatusAttrs...),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func stdioAttributes(streams Streams) []attribute.KeyValue {
|
||||
// we don't wrap stderr, but we do wrap in/out
|
||||
_, stderrTty := term.GetFdInfo(streams.Err())
|
||||
return []attribute.KeyValue{
|
||||
attribute.Bool("command.stdin.isatty", streams.In().IsTerminal()),
|
||||
attribute.Bool("command.stdout.isatty", streams.Out().IsTerminal()),
|
||||
attribute.Bool("command.stderr.isatty", stderrTty),
|
||||
}
|
||||
}
|
||||
|
||||
func attributesFromError(err error) []attribute.KeyValue {
|
||||
attrs := []attribute.KeyValue{}
|
||||
exitCode := 0
|
||||
if err != nil {
|
||||
exitCode = 1
|
||||
if stderr, ok := err.(statusError); ok {
|
||||
// StatusError should only be used for errors, and all errors should
|
||||
// have a non-zero exit status, so only set this here if this value isn't 0
|
||||
if stderr.StatusCode != 0 {
|
||||
exitCode = stderr.StatusCode
|
||||
}
|
||||
}
|
||||
attrs = append(attrs, attribute.String("command.error.type", otelErrorType(err)))
|
||||
}
|
||||
attrs = append(attrs, attribute.Int("command.status.code", exitCode))
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
// otelErrorType returns an attribute for the error type based on the error category.
|
||||
func otelErrorType(err error) string {
|
||||
name := "generic"
|
||||
if errors.Is(err, context.Canceled) {
|
||||
name = "canceled"
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// statusError reports an unsuccessful exit by a command.
|
||||
type statusError struct {
|
||||
Status string
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func (e statusError) Error() string {
|
||||
return fmt.Sprintf("Status: %s, Code: %d", e.Status, e.StatusCode)
|
||||
}
|
||||
|
||||
// getCommandName gets the cobra command name in the format
|
||||
// `... parentCommandName commandName` by traversing it's parent commands recursively.
|
||||
// until the root command is reached.
|
||||
//
|
||||
// Note: The root command's name is excluded. If cmd is the root cmd, return ""
|
||||
func getCommandName(cmd *cobra.Command) string {
|
||||
fullCmdName := getFullCommandName(cmd)
|
||||
i := strings.Index(fullCmdName, " ")
|
||||
if i == -1 {
|
||||
return ""
|
||||
}
|
||||
return fullCmdName[i+1:]
|
||||
}
|
||||
|
||||
// getFullCommandName gets the full cobra command name in the format
|
||||
// `... parentCommandName commandName` by traversing it's parent commands recursively
|
||||
// until the root command is reached.
|
||||
func getFullCommandName(cmd *cobra.Command) string {
|
||||
if cmd.HasParent() {
|
||||
return fmt.Sprintf("%s %s", getFullCommandName(cmd.Parent()), cmd.Name())
|
||||
}
|
||||
return cmd.Name()
|
||||
}
|
||||
|
||||
// getDefaultMeter gets the default metric.Meter for the application
|
||||
// using the given metric.MeterProvider
|
||||
func getDefaultMeter(mp metric.MeterProvider) metric.Meter {
|
||||
return mp.Meter(
|
||||
"github.com/docker/cli",
|
||||
metric.WithInstrumentationVersion(version.Version),
|
||||
)
|
||||
}
|
||||
189
cli/command/telemetry_utils_test.go
Normal file
189
cli/command/telemetry_utils_test.go
Normal file
@ -0,0 +1,189 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func setupCobraCommands() (*cobra.Command, *cobra.Command, *cobra.Command) {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "root [OPTIONS] COMMAND [ARG...]",
|
||||
}
|
||||
childCmd := &cobra.Command{
|
||||
Use: "child [OPTIONS] COMMAND [ARG...]",
|
||||
}
|
||||
grandchildCmd := &cobra.Command{
|
||||
Use: "grandchild [OPTIONS] COMMAND [ARG...]",
|
||||
}
|
||||
childCmd.AddCommand(grandchildCmd)
|
||||
rootCmd.AddCommand(childCmd)
|
||||
|
||||
return rootCmd, childCmd, grandchildCmd
|
||||
}
|
||||
|
||||
func TestGetFullCommandName(t *testing.T) {
|
||||
rootCmd, childCmd, grandchildCmd := setupCobraCommands()
|
||||
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
testName string
|
||||
cmd *cobra.Command
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
testName: "rootCmd",
|
||||
cmd: rootCmd,
|
||||
expected: "root",
|
||||
},
|
||||
{
|
||||
testName: "childCmd",
|
||||
cmd: childCmd,
|
||||
expected: "root child",
|
||||
},
|
||||
{
|
||||
testName: "grandChild",
|
||||
cmd: grandchildCmd,
|
||||
expected: "root child grandchild",
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actual := getFullCommandName(tc.cmd)
|
||||
assert.Equal(t, actual, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCommandName(t *testing.T) {
|
||||
rootCmd, childCmd, grandchildCmd := setupCobraCommands()
|
||||
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
testName string
|
||||
cmd *cobra.Command
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
testName: "rootCmd",
|
||||
cmd: rootCmd,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
testName: "childCmd",
|
||||
cmd: childCmd,
|
||||
expected: "child",
|
||||
},
|
||||
{
|
||||
testName: "grandchildCmd",
|
||||
cmd: grandchildCmd,
|
||||
expected: "child grandchild",
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actual := getCommandName(tc.cmd)
|
||||
assert.Equal(t, actual, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStdioAttributes(t *testing.T) {
|
||||
outBuffer := new(bytes.Buffer)
|
||||
errBuffer := new(bytes.Buffer)
|
||||
t.Parallel()
|
||||
for _, tc := range []struct {
|
||||
test string
|
||||
stdinTty bool
|
||||
stdoutTty bool
|
||||
// TODO(laurazard): test stderr
|
||||
expected []attribute.KeyValue
|
||||
}{
|
||||
{
|
||||
test: "",
|
||||
expected: []attribute.KeyValue{
|
||||
attribute.Bool("command.stdin.isatty", false),
|
||||
attribute.Bool("command.stdout.isatty", false),
|
||||
attribute.Bool("command.stderr.isatty", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
test: "",
|
||||
stdinTty: true,
|
||||
stdoutTty: true,
|
||||
expected: []attribute.KeyValue{
|
||||
attribute.Bool("command.stdin.isatty", true),
|
||||
attribute.Bool("command.stdout.isatty", true),
|
||||
attribute.Bool("command.stderr.isatty", false),
|
||||
},
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.test, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cli := &DockerCli{
|
||||
in: streams.NewIn(io.NopCloser(strings.NewReader(""))),
|
||||
out: streams.NewOut(outBuffer),
|
||||
err: errBuffer,
|
||||
}
|
||||
cli.In().SetIsTerminal(tc.stdinTty)
|
||||
cli.Out().SetIsTerminal(tc.stdoutTty)
|
||||
actual := stdioAttributes(cli)
|
||||
|
||||
assert.Check(t, reflect.DeepEqual(actual, tc.expected))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttributesFromError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
testName string
|
||||
err error
|
||||
expected []attribute.KeyValue
|
||||
}{
|
||||
{
|
||||
testName: "no error",
|
||||
err: nil,
|
||||
expected: []attribute.KeyValue{
|
||||
attribute.Int("command.status.code", 0),
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "non-0 exit code",
|
||||
err: statusError{StatusCode: 127},
|
||||
expected: []attribute.KeyValue{
|
||||
attribute.String("command.error.type", "generic"),
|
||||
attribute.Int("command.status.code", 127),
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "canceled",
|
||||
err: context.Canceled,
|
||||
expected: []attribute.KeyValue{
|
||||
attribute.String("command.error.type", "canceled"),
|
||||
attribute.Int("command.status.code", 1),
|
||||
},
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actual := attributesFromError(tc.err)
|
||||
assert.Check(t, reflect.DeepEqual(actual, tc.expected))
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/theupdateframework/notary/client"
|
||||
@ -45,12 +46,10 @@ func revokeTrust(ctx context.Context, dockerCLI command.Cli, remote string, opti
|
||||
if imgRefAndAuth.Tag() == "" && !options.forceYes {
|
||||
deleteRemote, err := command.PromptForConfirmation(ctx, dockerCLI.In(), dockerCLI.Out(), fmt.Sprintf("Please confirm you would like to delete all signature data for %s?", remote))
|
||||
if err != nil {
|
||||
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
|
||||
return errors.Wrap(err, "aborting action")
|
||||
return err
|
||||
}
|
||||
if !deleteRemote {
|
||||
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
|
||||
return nil
|
||||
return errdefs.Cancelled(errors.New("trust revoke has been cancelled"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/notary"
|
||||
@ -58,6 +57,8 @@ func TestTrustRevokeCommandErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTrustRevokeCommand(t *testing.T) {
|
||||
revokeCancelledError := "trust revoke has been cancelled"
|
||||
|
||||
testCases := []struct {
|
||||
doc string
|
||||
notaryRepository func(trust.ImageRefAndAuth, []string) (client.Repository, error)
|
||||
@ -69,7 +70,8 @@ func TestTrustRevokeCommand(t *testing.T) {
|
||||
doc: "OfflineErrors_Confirm",
|
||||
notaryRepository: notary.GetOfflineNotaryRepository,
|
||||
args: []string{"reg-name.io/image"},
|
||||
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.",
|
||||
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] ",
|
||||
expectedErr: revokeCancelledError,
|
||||
},
|
||||
{
|
||||
doc: "OfflineErrors_Offline",
|
||||
@ -87,7 +89,8 @@ func TestTrustRevokeCommand(t *testing.T) {
|
||||
doc: "UninitializedErrors_Confirm",
|
||||
notaryRepository: notary.GetUninitializedNotaryRepository,
|
||||
args: []string{"reg-name.io/image"},
|
||||
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.",
|
||||
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] ",
|
||||
expectedErr: revokeCancelledError,
|
||||
},
|
||||
{
|
||||
doc: "UninitializedErrors_NoTrustData",
|
||||
@ -105,7 +108,8 @@ func TestTrustRevokeCommand(t *testing.T) {
|
||||
doc: "EmptyNotaryRepo_Confirm",
|
||||
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
|
||||
args: []string{"reg-name.io/image"},
|
||||
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.",
|
||||
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] ",
|
||||
expectedErr: revokeCancelledError,
|
||||
},
|
||||
{
|
||||
doc: "EmptyNotaryRepo_NoSignedTags",
|
||||
@ -123,7 +127,8 @@ func TestTrustRevokeCommand(t *testing.T) {
|
||||
doc: "AllSigConfirmation",
|
||||
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
|
||||
args: []string{"alpine"},
|
||||
expectedMessage: "Please confirm you would like to delete all signature data for alpine? [y/N] \nAborting action.",
|
||||
expectedMessage: "Please confirm you would like to delete all signature data for alpine? [y/N] ",
|
||||
expectedErr: revokeCancelledError,
|
||||
},
|
||||
}
|
||||
|
||||
@ -136,9 +141,9 @@ func TestTrustRevokeCommand(t *testing.T) {
|
||||
cmd.SetOut(io.Discard)
|
||||
if tc.expectedErr != "" {
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedErr)
|
||||
return
|
||||
} else {
|
||||
assert.NilError(t, cmd.Execute())
|
||||
}
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Contains(cli.OutBuffer().String(), tc.expectedMessage))
|
||||
})
|
||||
}
|
||||
@ -159,10 +164,6 @@ func TestRevokeTrustPromptTermination(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cmd := newRevokeCommand(cli)
|
||||
cmd.SetArgs([]string{"example/trust-demo"})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
assert.Equal(t, cli.ErrBuffer().String(), "")
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
golden.Assert(t, cli.OutBuffer().String(), "trust-revoke-prompt-termination.golden")
|
||||
}
|
||||
|
||||
@ -132,10 +132,12 @@ func removeSingleSigner(ctx context.Context, dockerCLI command.Cli, repoName, si
|
||||
}
|
||||
|
||||
ok, err := maybePromptForSignerRemoval(ctx, dockerCLI, repoName, signerName, isLastSigner, forceYes)
|
||||
if err != nil || !ok {
|
||||
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := notaryRepo.RemoveDelegationKeys(releasesRoleTUFName, role.KeyIDs); err != nil {
|
||||
return false, err
|
||||
|
||||
@ -1,2 +1 @@
|
||||
Please confirm you would like to delete all signature data for example/trust-demo? [y/N]
|
||||
Aborting action.
|
||||
|
||||
@ -19,6 +19,7 @@ import (
|
||||
"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/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
@ -75,9 +76,7 @@ func PrettyPrint(i any) string {
|
||||
}
|
||||
}
|
||||
|
||||
type PromptError error
|
||||
|
||||
var ErrPromptTerminated = PromptError(errors.New("prompt terminated"))
|
||||
var ErrPromptTerminated = errdefs.Cancelled(errors.New("prompt terminated"))
|
||||
|
||||
// PromptForConfirmation requests and checks confirmation from the user.
|
||||
// This will display the provided message followed by ' [y/N] '. If the user
|
||||
@ -123,6 +122,8 @@ func PromptForConfirmation(ctx context.Context, ins io.Reader, outs io.Writer, m
|
||||
|
||||
select {
|
||||
case <-notifyCtx.Done():
|
||||
// print a newline on termination
|
||||
_, _ = fmt.Fprintln(outs, "")
|
||||
return false, ErrPromptTerminated
|
||||
case r := <-result:
|
||||
return r, nil
|
||||
|
||||
@ -106,120 +106,68 @@ func TestPromptForConfirmation(t *testing.T) {
|
||||
}()
|
||||
|
||||
for _, tc := range []struct {
|
||||
desc string
|
||||
f func(*testing.T, context.Context, chan promptResult)
|
||||
desc string
|
||||
f func() error
|
||||
expected promptResult
|
||||
}{
|
||||
{"SIGINT", func(t *testing.T, ctx context.Context, c chan promptResult) {
|
||||
t.Helper()
|
||||
|
||||
{"SIGINT", func() error {
|
||||
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("PromptForConfirmation did not return after SIGINT")
|
||||
case r := <-c:
|
||||
assert.Check(t, !r.result)
|
||||
assert.ErrorContains(t, r.err, "prompt terminated")
|
||||
}
|
||||
}},
|
||||
{"no", func(t *testing.T, ctx context.Context, c chan promptResult) {
|
||||
t.Helper()
|
||||
|
||||
return nil
|
||||
}, promptResult{false, command.ErrPromptTerminated}},
|
||||
{"no", func() error {
|
||||
_, err := fmt.Fprint(promptWriter, "n\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("PromptForConfirmation did not return after user input `n`")
|
||||
case r := <-c:
|
||||
assert.Check(t, !r.result)
|
||||
assert.NilError(t, r.err)
|
||||
}
|
||||
}},
|
||||
{"yes", func(t *testing.T, ctx context.Context, c chan promptResult) {
|
||||
t.Helper()
|
||||
|
||||
return err
|
||||
}, promptResult{false, nil}},
|
||||
{"yes", func() error {
|
||||
_, err := fmt.Fprint(promptWriter, "y\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("PromptForConfirmation did not return after user input `y`")
|
||||
case r := <-c:
|
||||
assert.Check(t, r.result)
|
||||
assert.NilError(t, r.err)
|
||||
}
|
||||
}},
|
||||
{"any", func(t *testing.T, ctx context.Context, c chan promptResult) {
|
||||
t.Helper()
|
||||
|
||||
return err
|
||||
}, promptResult{true, nil}},
|
||||
{"any", func() error {
|
||||
_, err := fmt.Fprint(promptWriter, "a\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("PromptForConfirmation did not return after user input `a`")
|
||||
case r := <-c:
|
||||
assert.Check(t, !r.result)
|
||||
assert.NilError(t, r.err)
|
||||
}
|
||||
}},
|
||||
{"with space", func(t *testing.T, ctx context.Context, c chan promptResult) {
|
||||
t.Helper()
|
||||
|
||||
return err
|
||||
}, promptResult{false, nil}},
|
||||
{"with space", func() error {
|
||||
_, err := fmt.Fprint(promptWriter, " y\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("PromptForConfirmation did not return after user input ` y`")
|
||||
case r := <-c:
|
||||
assert.Check(t, r.result)
|
||||
assert.NilError(t, r.err)
|
||||
}
|
||||
}},
|
||||
{"reader closed", func(t *testing.T, ctx context.Context, c chan promptResult) {
|
||||
t.Helper()
|
||||
|
||||
assert.NilError(t, promptReader.Close())
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("PromptForConfirmation did not return after promptReader was closed")
|
||||
case r := <-c:
|
||||
assert.Check(t, !r.result)
|
||||
assert.NilError(t, r.err)
|
||||
}
|
||||
}},
|
||||
return err
|
||||
}, promptResult{true, nil}},
|
||||
{"reader closed", func() error {
|
||||
return promptReader.Close()
|
||||
}, promptResult{false, nil}},
|
||||
} {
|
||||
t.Run("case="+tc.desc, func(t *testing.T) {
|
||||
buf.Reset()
|
||||
promptReader, promptWriter = io.Pipe()
|
||||
|
||||
wroteHook := make(chan struct{}, 1)
|
||||
defer close(wroteHook)
|
||||
promptOut := test.NewWriterWithHook(bufioWriter, func(p []byte) {
|
||||
wroteHook <- struct{}{}
|
||||
})
|
||||
|
||||
result := make(chan promptResult, 1)
|
||||
defer close(result)
|
||||
go func() {
|
||||
r, err := command.PromptForConfirmation(ctx, promptReader, promptOut, "")
|
||||
result <- promptResult{r, err}
|
||||
}()
|
||||
|
||||
// wait for the Prompt to write to the buffer
|
||||
pollForPromptOutput(ctx, t, wroteHook)
|
||||
drainChannel(ctx, wroteHook)
|
||||
select {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
case <-wroteHook:
|
||||
}
|
||||
|
||||
assert.NilError(t, bufioWriter.Flush())
|
||||
assert.Equal(t, strings.TrimSpace(buf.String()), "Are you sure you want to proceed? [y/N]")
|
||||
|
||||
resultCtx, resultCancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||
defer resultCancel()
|
||||
// wait for the Prompt to write to the buffer
|
||||
drainChannel(ctx, wroteHook)
|
||||
|
||||
tc.f(t, resultCtx, result)
|
||||
assert.NilError(t, tc.f())
|
||||
|
||||
select {
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
t.Fatal("timeout waiting for prompt result")
|
||||
case r := <-result:
|
||||
assert.Equal(t, r, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -235,20 +183,3 @@ func drainChannel(ctx context.Context, ch <-chan struct{}) {
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func pollForPromptOutput(ctx context.Context, t *testing.T, wroteHook <-chan struct{}) {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Prompt output was not written to before ctx was cancelled")
|
||||
return
|
||||
case <-wroteHook:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,10 +32,6 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCli, options)
|
||||
if err != nil {
|
||||
if errdefs.IsCancelled(err) {
|
||||
fmt.Fprintln(dockerCli.Out(), output)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if output != "" {
|
||||
@ -81,8 +77,12 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
warning = allVolumesWarning
|
||||
}
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
||||
return 0, "", errdefs.Cancelled(errors.New("user cancelled operation"))
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
if !r {
|
||||
return 0, "", errdefs.Cancelled(errors.New("volume prune has been cancelled"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -155,6 +155,7 @@ func TestVolumePrunePromptYes(t *testing.T) {
|
||||
|
||||
cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(input))))
|
||||
cmd := NewPruneCommand(cli)
|
||||
cmd.SetArgs([]string{})
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "volume-prune-yes.golden")
|
||||
}
|
||||
@ -171,7 +172,8 @@ func TestVolumePrunePromptNo(t *testing.T) {
|
||||
|
||||
cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(input))))
|
||||
cmd := NewPruneCommand(cli)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
cmd.SetArgs([]string{})
|
||||
assert.ErrorContains(t, cmd.Execute(), "volume prune has been cancelled")
|
||||
golden.Assert(t, cli.OutBuffer().String(), "volume-prune-no.golden")
|
||||
}
|
||||
}
|
||||
@ -196,7 +198,7 @@ func TestVolumePrunePromptTerminate(t *testing.T) {
|
||||
})
|
||||
|
||||
cmd := NewPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, nil)
|
||||
|
||||
cmd.SetArgs([]string{})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
golden.Assert(t, cli.OutBuffer().String(), "volume-prune-terminate.golden")
|
||||
}
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
WARNING! This will remove anonymous local volumes not used by at least one container.
|
||||
Are you sure you want to continue? [y/N]
|
||||
Are you sure you want to continue? [y/N]
|
||||
@ -16,6 +16,16 @@ func TestNamespaceScope(t *testing.T) {
|
||||
assert.Check(t, is.Equal("foo_bar", scoped))
|
||||
}
|
||||
|
||||
func TestNamespaceDescope(t *testing.T) {
|
||||
descoped := Namespace{name: "foo"}.Descope("foo_bar")
|
||||
assert.Check(t, is.Equal("bar", descoped))
|
||||
}
|
||||
|
||||
func TestNamespaceName(t *testing.T) {
|
||||
namespaceName := Namespace{name: "foo"}.Name()
|
||||
assert.Check(t, is.Equal("foo", namespaceName))
|
||||
}
|
||||
|
||||
func TestAddStackLabel(t *testing.T) {
|
||||
labels := map[string]string{
|
||||
"something": "labeled",
|
||||
|
||||
@ -61,6 +61,15 @@ func TestConvertEnvironment(t *testing.T) {
|
||||
assert.Check(t, is.DeepEqual([]string{"foo=bar", "key=value"}, env))
|
||||
}
|
||||
|
||||
func TestConvertEnvironmentWhenNilValueExists(t *testing.T) {
|
||||
source := map[string]*string{
|
||||
"key": strPtr("value"),
|
||||
"keyWithNoValue": nil,
|
||||
}
|
||||
env := convertEnvironment(source)
|
||||
assert.Check(t, is.DeepEqual([]string{"key=value", "keyWithNoValue"}, env))
|
||||
}
|
||||
|
||||
func TestConvertExtraHosts(t *testing.T) {
|
||||
source := composetypes.HostsList{
|
||||
"zulu:127.0.0.2",
|
||||
|
||||
@ -9,6 +9,55 @@ import (
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestVolumesWithMultipleServiceVolumeConfigs(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
serviceVolumes := []composetypes.ServiceVolumeConfig{
|
||||
{
|
||||
Type: "volume",
|
||||
Target: "/foo",
|
||||
},
|
||||
{
|
||||
Type: "volume",
|
||||
Target: "/foo/bar",
|
||||
},
|
||||
}
|
||||
|
||||
expected := []mount.Mount{
|
||||
{
|
||||
Type: "volume",
|
||||
Target: "/foo",
|
||||
},
|
||||
{
|
||||
Type: "volume",
|
||||
Target: "/foo/bar",
|
||||
},
|
||||
}
|
||||
|
||||
mnt, err := Volumes(serviceVolumes, volumes{}, namespace)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, mnt))
|
||||
}
|
||||
|
||||
func TestVolumesWithMultipleServiceVolumeConfigsWithUndefinedVolumeConfig(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
serviceVolumes := []composetypes.ServiceVolumeConfig{
|
||||
{
|
||||
Type: "volume",
|
||||
Source: "foo",
|
||||
Target: "/foo",
|
||||
},
|
||||
{
|
||||
Type: "volume",
|
||||
Target: "/foo/bar",
|
||||
},
|
||||
}
|
||||
|
||||
_, err := Volumes(serviceVolumes, volumes{}, namespace)
|
||||
assert.Error(t, err, "undefined volume \"foo\"")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountAnonymousVolume(t *testing.T) {
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "volume",
|
||||
@ -104,6 +153,49 @@ func TestConvertVolumeToMountConflictingOptionsTmpfsInBind(t *testing.T) {
|
||||
assert.Error(t, err, "tmpfs options are incompatible with type bind")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsClusterInVolume(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "volume",
|
||||
Target: "/target",
|
||||
Cluster: &composetypes.ServiceVolumeCluster{},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "cluster options are incompatible with type volume")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsClusterInBind(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "bind",
|
||||
Source: "/foo",
|
||||
Target: "/target",
|
||||
Bind: &composetypes.ServiceVolumeBind{
|
||||
Propagation: "slave",
|
||||
},
|
||||
Cluster: &composetypes.ServiceVolumeCluster{},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "cluster options are incompatible with type bind")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsClusterInTmpfs(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "tmpfs",
|
||||
Target: "/target",
|
||||
Tmpfs: &composetypes.ServiceVolumeTmpfs{
|
||||
Size: 1000,
|
||||
},
|
||||
Cluster: &composetypes.ServiceVolumeCluster{},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "cluster options are incompatible with type tmpfs")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsBindInTmpfs(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
@ -132,6 +224,50 @@ func TestConvertVolumeToMountConflictingOptionsVolumeInTmpfs(t *testing.T) {
|
||||
assert.Error(t, err, "volume options are incompatible with type tmpfs")
|
||||
}
|
||||
|
||||
func TestHandleNpipeToMountAnonymousNpipe(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "npipe",
|
||||
Target: "/target",
|
||||
Volume: &composetypes.ServiceVolumeVolume{
|
||||
NoCopy: true,
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "invalid npipe source, source cannot be empty")
|
||||
}
|
||||
|
||||
func TestHandleNpipeToMountConflictingOptionsTmpfsInNpipe(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "npipe",
|
||||
Source: "/foo",
|
||||
Target: "/target",
|
||||
Tmpfs: &composetypes.ServiceVolumeTmpfs{
|
||||
Size: 1000,
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "tmpfs options are incompatible with type npipe")
|
||||
}
|
||||
|
||||
func TestHandleNpipeToMountConflictingOptionsVolumeInNpipe(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "npipe",
|
||||
Source: "/foo",
|
||||
Target: "/target",
|
||||
Volume: &composetypes.ServiceVolumeVolume{
|
||||
NoCopy: true,
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "volume options are incompatible with type npipe")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountNamedVolume(t *testing.T) {
|
||||
stackVolumes := volumes{
|
||||
"normal": composetypes.VolumeConfig{
|
||||
@ -344,6 +480,27 @@ func TestConvertTmpfsToMountVolumeWithSource(t *testing.T) {
|
||||
assert.Error(t, err, "invalid tmpfs source, source must be empty")
|
||||
}
|
||||
|
||||
func TestHandleNpipeToMountBind(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
expected := mount.Mount{
|
||||
Type: mount.TypeNamedPipe,
|
||||
Source: "/bar",
|
||||
Target: "/foo",
|
||||
ReadOnly: true,
|
||||
BindOptions: &mount.BindOptions{Propagation: mount.PropagationShared},
|
||||
}
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "npipe",
|
||||
Source: "/bar",
|
||||
Target: "/foo",
|
||||
ReadOnly: true,
|
||||
Bind: &composetypes.ServiceVolumeBind{Propagation: "shared"},
|
||||
}
|
||||
mnt, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, mnt))
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountAnonymousNpipe(t *testing.T) {
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "npipe",
|
||||
@ -427,3 +584,98 @@ func TestConvertVolumeMountClusterGroup(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, mnt))
|
||||
}
|
||||
|
||||
func TestHandleClusterToMountAnonymousCluster(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "cluster",
|
||||
Target: "/target",
|
||||
Volume: &composetypes.ServiceVolumeVolume{
|
||||
NoCopy: true,
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "invalid cluster source, source cannot be empty")
|
||||
}
|
||||
|
||||
func TestHandleClusterToMountConflictingOptionsTmpfsInCluster(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "cluster",
|
||||
Source: "/foo",
|
||||
Target: "/target",
|
||||
Tmpfs: &composetypes.ServiceVolumeTmpfs{
|
||||
Size: 1000,
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "tmpfs options are incompatible with type cluster")
|
||||
}
|
||||
|
||||
func TestHandleClusterToMountConflictingOptionsVolumeInCluster(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "cluster",
|
||||
Source: "/foo",
|
||||
Target: "/target",
|
||||
Volume: &composetypes.ServiceVolumeVolume{
|
||||
NoCopy: true,
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "volume options are incompatible with type cluster")
|
||||
}
|
||||
|
||||
func TestHandleClusterToMountWithUndefinedVolumeConfig(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "cluster",
|
||||
Source: "foo",
|
||||
Target: "/srv",
|
||||
}
|
||||
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "undefined volume \"foo\"")
|
||||
}
|
||||
|
||||
func TestHandleClusterToMountWithVolumeConfigName(t *testing.T) {
|
||||
stackVolumes := volumes{
|
||||
"foo": composetypes.VolumeConfig{
|
||||
Name: "bar",
|
||||
},
|
||||
}
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "cluster",
|
||||
Source: "foo",
|
||||
Target: "/srv",
|
||||
}
|
||||
|
||||
expected := mount.Mount{
|
||||
Type: mount.TypeCluster,
|
||||
Source: "bar",
|
||||
Target: "/srv",
|
||||
ClusterOptions: &mount.ClusterOptions{},
|
||||
}
|
||||
|
||||
mnt, err := convertVolumeToMount(config, stackVolumes, NewNamespace("foo"))
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, mnt))
|
||||
}
|
||||
|
||||
func TestHandleClusterToMountBind(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "cluster",
|
||||
Source: "/bar",
|
||||
Target: "/foo",
|
||||
ReadOnly: true,
|
||||
Bind: &composetypes.ServiceVolumeBind{Propagation: "shared"},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "bind options are incompatible with type cluster")
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ type ConfigFile struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
// ProxyConfig contains proxy configuration settings
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
// Enable sets the DEBUG env var to true
|
||||
@ -24,3 +25,13 @@ func Disable() {
|
||||
func IsEnabled() bool {
|
||||
return os.Getenv("DEBUG") != ""
|
||||
}
|
||||
|
||||
// OTELErrorHandler is an error handler for OTEL that
|
||||
// uses the CLI debug package to log messages when an error
|
||||
// occurs.
|
||||
//
|
||||
// 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) {
|
||||
logrus.WithError(err).Debug("otel error")
|
||||
})
|
||||
|
||||
@ -69,7 +69,7 @@ func processBuilder(dockerCli command.Cli, cmd *cobra.Command, args, osargs []st
|
||||
}
|
||||
|
||||
// is this a build that should be forwarded to the builder?
|
||||
fwargs, fwosargs, alias, forwarded := forwardBuilder(builderAlias, args, osargs)
|
||||
fwargs, fwosargs, fwcmdpath, forwarded := forwardBuilder(builderAlias, args, osargs)
|
||||
if !forwarded {
|
||||
return args, osargs, nil, nil
|
||||
}
|
||||
@ -117,33 +117,37 @@ func processBuilder(dockerCli command.Cli, cmd *cobra.Command, args, osargs []st
|
||||
}
|
||||
|
||||
// overwrite the command path for this plugin using the alias name.
|
||||
cmd.Annotations[pluginmanager.CommandAnnotationPluginCommandPath] = fmt.Sprintf("%s %s", cmd.CommandPath(), alias)
|
||||
cmd.Annotations[pluginmanager.CommandAnnotationPluginCommandPath] = strings.Join(append([]string{cmd.CommandPath()}, fwcmdpath...), " ")
|
||||
|
||||
return fwargs, fwosargs, envs, nil
|
||||
}
|
||||
|
||||
func forwardBuilder(alias string, args, osargs []string) ([]string, []string, string, bool) {
|
||||
aliases := [][2][]string{
|
||||
func forwardBuilder(alias string, args, osargs []string) ([]string, []string, []string, bool) {
|
||||
aliases := [][3][]string{
|
||||
{
|
||||
{"builder"},
|
||||
{alias},
|
||||
{"builder"},
|
||||
},
|
||||
{
|
||||
{"build"},
|
||||
{alias, "build"},
|
||||
{},
|
||||
},
|
||||
{
|
||||
{"image", "build"},
|
||||
{alias, "build"},
|
||||
{"image"},
|
||||
},
|
||||
}
|
||||
for _, al := range aliases {
|
||||
if fwargs, changed := command.StringSliceReplaceAt(args, al[0], al[1], 0); changed {
|
||||
fwosargs, _ := command.StringSliceReplaceAt(osargs, al[0], al[1], -1)
|
||||
return fwargs, fwosargs, al[0][0], true
|
||||
fwcmdpath := al[2]
|
||||
return fwargs, fwosargs, fwcmdpath, true
|
||||
}
|
||||
}
|
||||
return args, osargs, "", false
|
||||
return args, osargs, nil, false
|
||||
}
|
||||
|
||||
// hasBuilderName checks if a builder name is defined in args or env vars
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
@ -14,25 +14,30 @@ import (
|
||||
"github.com/docker/cli/cli-plugins/socket"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/commands"
|
||||
"github.com/docker/cli/cli/debug"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/cli/version"
|
||||
platformsignals "github.com/docker/cli/cmd/docker/internal/signals"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
ctx := context.Background()
|
||||
dockerCli, err := command.NewDockerCli(command.WithBaseContext(ctx))
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logrus.SetOutput(dockerCli.Err())
|
||||
otel.SetErrorHandler(debug.OTELErrorHandler)
|
||||
|
||||
if err := runDocker(dockerCli); err != nil {
|
||||
if err := runDocker(ctx, dockerCli); err != nil {
|
||||
if sterr, ok := err.(cli.StatusError); ok {
|
||||
if sterr.Status != "" {
|
||||
fmt.Fprintln(dockerCli.Err(), sterr.Status)
|
||||
@ -44,6 +49,9 @@ func main() {
|
||||
}
|
||||
os.Exit(sterr.StatusCode)
|
||||
}
|
||||
if errdefs.IsCancelled(err) {
|
||||
os.Exit(0)
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Err(), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@ -221,38 +229,46 @@ func tryPluginRun(dockerCli command.Cli, cmd *cobra.Command, subcommand string,
|
||||
return err
|
||||
}
|
||||
|
||||
// Establish the plugin socket, adding it to the environment under a well-known key if successful.
|
||||
var conn *net.UnixConn
|
||||
listener, err := socket.SetupConn(&conn)
|
||||
// Establish the plugin socket, adding it to the environment under a
|
||||
// well-known key if successful.
|
||||
srv, err := socket.NewPluginServer(nil)
|
||||
if err == nil {
|
||||
envs = append(envs, socket.EnvKey+"="+listener.Addr().String())
|
||||
defer listener.Close()
|
||||
plugincmd.Env = append(plugincmd.Env, socket.EnvKey+"="+srv.Addr().String())
|
||||
}
|
||||
|
||||
plugincmd.Env = append(envs, plugincmd.Env...)
|
||||
// Set additional environment variables specified by the caller.
|
||||
plugincmd.Env = append(plugincmd.Env, envs...)
|
||||
|
||||
// Background signal handling logic: block on the signals channel, and
|
||||
// notify the plugin via the PluginServer (or signal) as appropriate.
|
||||
const exitLimit = 3
|
||||
|
||||
signals := make(chan os.Signal, exitLimit)
|
||||
signal.Notify(signals, platformsignals.TerminationSignals...)
|
||||
// signal handling goroutine: listen on signals channel, and if conn is
|
||||
// non-nil, attempt to close it to let the plugin know to exit. Regardless
|
||||
// of whether we successfully signal the plugin or not, after 3 SIGINTs,
|
||||
// we send a SIGKILL to the plugin process and exit
|
||||
go func() {
|
||||
retries := 0
|
||||
for range signals {
|
||||
// If stdin is a TTY, the kernel will forward
|
||||
// signals to the subprocess because the shared
|
||||
// pgid makes the TTY a controlling terminal.
|
||||
//
|
||||
// The plugin should have it's own copy of this
|
||||
// termination logic, and exit after 3 retries
|
||||
// on it's own.
|
||||
if dockerCli.Out().IsTerminal() {
|
||||
// running attached to a terminal, so the plugin will already
|
||||
// receive signals due to sharing a pgid with the parent CLI
|
||||
continue
|
||||
}
|
||||
if conn != nil {
|
||||
if err := conn.Close(); err != nil {
|
||||
_, _ = fmt.Fprintf(dockerCli.Err(), "failed to signal plugin to close: %v\n", err)
|
||||
}
|
||||
conn = nil
|
||||
}
|
||||
|
||||
// Terminate the plugin server, which will
|
||||
// close all connections with plugin
|
||||
// subprocesses, and signal them to exit.
|
||||
//
|
||||
// Repeated invocations will result in EINVAL,
|
||||
// or EBADF; but that is fine for our purposes.
|
||||
_ = srv.Close()
|
||||
|
||||
// If we're still running after 3 interruptions
|
||||
// (SIGINT/SIGTERM), send a SIGKILL to the plugin as a
|
||||
// final attempt to terminate, and exit.
|
||||
retries++
|
||||
if retries >= exitLimit {
|
||||
_, _ = fmt.Fprintf(dockerCli.Err(), "got %d SIGTERM/SIGINTs, forcefully exiting\n", retries)
|
||||
@ -278,7 +294,8 @@ func tryPluginRun(dockerCli command.Cli, cmd *cobra.Command, subcommand string,
|
||||
return nil
|
||||
}
|
||||
|
||||
func runDocker(dockerCli *command.DockerCli) error {
|
||||
//nolint:gocyclo
|
||||
func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
|
||||
tcmd := newDockerCommand(dockerCli)
|
||||
|
||||
cmd, args, err := tcmd.HandleGlobalFlags()
|
||||
@ -290,6 +307,11 @@ func runDocker(dockerCli *command.DockerCli) error {
|
||||
return err
|
||||
}
|
||||
|
||||
mp := dockerCli.MeterProvider(ctx)
|
||||
defer mp.Shutdown(ctx)
|
||||
otel.SetMeterProvider(mp)
|
||||
dockerCli.InstrumentCobraCommands(cmd, mp)
|
||||
|
||||
var envs []string
|
||||
args, os.Args, envs, err = processAliases(dockerCli, cmd, args, os.Args)
|
||||
if err != nil {
|
||||
@ -306,23 +328,43 @@ func runDocker(dockerCli *command.DockerCli) error {
|
||||
}
|
||||
}
|
||||
|
||||
var subCommand *cobra.Command
|
||||
if len(args) > 0 {
|
||||
ccmd, _, err := cmd.Find(args)
|
||||
subCommand = ccmd
|
||||
if err != nil || pluginmanager.IsPluginCommand(ccmd) {
|
||||
err := tryPluginRun(dockerCli, cmd, args[0], envs)
|
||||
if err == nil {
|
||||
if dockerCli.HooksEnabled() && dockerCli.Out().IsTerminal() && ccmd != nil {
|
||||
pluginmanager.RunPluginHooks(dockerCli, cmd, ccmd, args)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !pluginmanager.IsNotFound(err) {
|
||||
// For plugin not found we fall through to
|
||||
// cmd.Execute() which deals with reporting
|
||||
// "command not found" in a consistent way.
|
||||
return err
|
||||
}
|
||||
// For plugin not found we fall through to
|
||||
// cmd.Execute() which deals with reporting
|
||||
// "command not found" in a consistent way.
|
||||
}
|
||||
}
|
||||
|
||||
// We've parsed global args already, so reset args to those
|
||||
// which remain.
|
||||
cmd.SetArgs(args)
|
||||
return cmd.Execute()
|
||||
err = cmd.Execute()
|
||||
|
||||
// If the command is being executed in an interactive terminal
|
||||
// and hook are enabled, run the plugin hooks.
|
||||
if dockerCli.HooksEnabled() && dockerCli.Out().IsTerminal() && subCommand != nil {
|
||||
var errMessage string
|
||||
if err != nil {
|
||||
errMessage = err.Error()
|
||||
}
|
||||
pluginmanager.RunCLICommandHooks(dockerCli, cmd, subCommand, errMessage)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type versionDetails interface {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
variable "GO_VERSION" {
|
||||
default = "1.21.8"
|
||||
default = "1.21.10"
|
||||
}
|
||||
variable "VERSION" {
|
||||
default = ""
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.21.8
|
||||
ARG GO_VERSION=1.21.10
|
||||
ARG ALPINE_VERSION=3.18
|
||||
|
||||
ARG BUILDX_VERSION=0.12.1
|
||||
@ -11,7 +11,7 @@ ENV GOTOOLCHAIN=local
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
FROM golang AS gofumpt
|
||||
ARG GOFUMPT_VERSION=v0.4.0
|
||||
ARG GOFUMPT_VERSION=v0.6.0
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=tmpfs,target=/go/src/ \
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.21.8
|
||||
ARG GO_VERSION=1.21.10
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG GOLANGCI_LINT_VERSION=v1.55.2
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.21.8
|
||||
ARG GO_VERSION=1.21.10
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG MODOUTDATED_VERSION=v0.8.0
|
||||
|
||||
|
||||
@ -50,8 +50,10 @@ The table below provides an overview of the current status of deprecated feature
|
||||
|
||||
| Status | Feature | Deprecated | Remove |
|
||||
|------------|------------------------------------------------------------------------------------------------------------------------------------|------------|--------|
|
||||
| Deprecated | [Unauthenticated TCP connections](#unauthenticated-tcp-connections) | v26.0 | v27.0 |
|
||||
| Deprecated | [`Container` and `ContainerConfig` fields in Image inspect](#container-and-containerconfig-fields-in-image-inspect) | v25.0 | v26.0 |
|
||||
| Deprecated | [Deprecate legacy API versions](#deprecate-legacy-api-versions) | v25.0 | v26.0 |
|
||||
| Deprecated | [Container short ID in network Aliases field](#container-short-id-in-network-aliases-field) | v25.0 | v26.0 |
|
||||
| Removed | [Container short ID in network Aliases field](#container-short-id-in-network-aliases-field) | v25.0 | v26.0 |
|
||||
| Deprecated | [IsAutomated field, and "is-automated" filter on docker search](#isautomated-field-and-is-automated-filter-on-docker-search) | v25.0 | v26.0 |
|
||||
| Removed | [logentries logging driver](#logentries-logging-driver) | v24.0 | v25.0 |
|
||||
| Removed | [OOM-score adjust for the daemon](#oom-score-adjust-for-the-daemon) | v24.0 | v25.0 |
|
||||
@ -72,7 +74,7 @@ The table below provides an overview of the current status of deprecated feature
|
||||
| Removed | [`docker build --stream` flag (experimental)](#docker-build---stream-flag-experimental) | v20.10 | v20.10 |
|
||||
| Deprecated | [`fluentd-async-connect` log opt](#fluentd-async-connect-log-opt) | v20.10 | - |
|
||||
| Removed | [Configuration options for experimental CLI features](#configuration-options-for-experimental-cli-features) | v19.03 | v23.0 |
|
||||
| Deprecated | [Pushing and pulling with image manifest v2 schema 1](#pushing-and-pulling-with-image-manifest-v2-schema-1) | v19.03 | v20.10 |
|
||||
| Deprecated | [Pushing and pulling with image manifest v2 schema 1](#pushing-and-pulling-with-image-manifest-v2-schema-1) | v19.03 | v27.0 |
|
||||
| Removed | [`docker engine` subcommands](#docker-engine-subcommands) | v19.03 | v20.10 |
|
||||
| Removed | [Top-level `docker deploy` subcommand (experimental)](#top-level-docker-deploy-subcommand-experimental) | v19.03 | v20.10 |
|
||||
| Removed | [`docker stack deploy` using "dab" files (experimental)](#docker-stack-deploy-using-dab-files-experimental) | v19.03 | v20.10 |
|
||||
@ -110,6 +112,46 @@ The table below provides an overview of the current status of deprecated feature
|
||||
| Removed | [`--run` flag on `docker commit`](#--run-flag-on-docker-commit) | v0.10 | v1.13 |
|
||||
| Removed | [Three arguments form in `docker import`](#three-arguments-form-in-docker-import) | v0.6.7 | v1.12 |
|
||||
|
||||
### Unauthenticated TCP connections
|
||||
|
||||
**Deprecated in Release: v26.0**
|
||||
**Target For Removal In Release: v27.0**
|
||||
|
||||
Configuring the Docker daemon to listen on a TCP address will require mandatory
|
||||
TLS verification. This change aims to ensure secure communication by preventing
|
||||
unauthorized access to the Docker daemon over potentially insecure networks.
|
||||
This mandatory TLS requirement applies to all TCP addresses except `tcp://localhost`.
|
||||
|
||||
In version 27.0 and later, specifying `--tls=false` or `--tlsverify=false` CLI flags
|
||||
causes the daemon to fail to start if it's also configured to accept remote connections over TCP.
|
||||
This also applies to the equivalent configuration options in `daemon.json`.
|
||||
|
||||
To facilitate remote access to the Docker daemon over TCP, you'll need to
|
||||
implement TLS verification. This secures the connection by encrypting data in
|
||||
transit and providing a mechanism for mutual authentication.
|
||||
|
||||
For environments remote daemon access isn't required,
|
||||
we recommend binding the Docker daemon to a Unix socket.
|
||||
For daemon's where remote access is required and where TLS encryption is not feasible,
|
||||
you may want to consider using SSH as an alternative solution.
|
||||
|
||||
For further information, assistance, and step-by-step instructions on
|
||||
configuring TLS (or SSH) for the Docker daemon, refer to
|
||||
[Protect the Docker daemon socket](https://docs.docker.com/engine/security/protect-access/).
|
||||
|
||||
### `Container` and `ContainerConfig` fields in Image inspect
|
||||
|
||||
**Deprecated in Release: v25.0**
|
||||
**Target For Removal In Release: v26.0**
|
||||
|
||||
The `Container` and `ContainerConfig` fields returned by `docker inspect` are
|
||||
mostly an implementation detail of the classic (non-BuildKit) image builder.
|
||||
These fields are not portable and are empty when using the
|
||||
BuildKit-based builder (enabled by default since v23.0).
|
||||
These fields are deprecated in v25.0 and will be omitted starting from v26.0.
|
||||
If image configuration of an image is needed, you can obtain it from the
|
||||
`Config` field.
|
||||
|
||||
### Deprecate legacy API versions
|
||||
|
||||
**Deprecated in Release: v25.0**
|
||||
@ -167,7 +209,7 @@ old clients, and those clients must be supported.
|
||||
### Container short ID in network Aliases field
|
||||
|
||||
**Deprecated in Release: v25.0**
|
||||
**Target For Remove In Release: v26.0**
|
||||
**Removed In Release: v26.0**
|
||||
|
||||
The `Aliases` field returned by `docker inspect` contains the container short
|
||||
ID once the container is started. This behavior is deprecated in v25.0 but
|
||||
@ -558,15 +600,35 @@ for the old option will be removed in a future release.
|
||||
|
||||
**Deprecated in Release: v19.03**
|
||||
|
||||
**Target For Removal In Release: v20.10**
|
||||
**Disabled by default in Release: v26.0**
|
||||
|
||||
The image manifest
|
||||
[v2 schema 1](https://github.com/docker/distribution/blob/fda42e5ef908bdba722d435ff1f330d40dfcd56c/docs/spec/manifest-v2-1.md)
|
||||
format is deprecated in favor of the
|
||||
[v2 schema 2](https://github.com/docker/distribution/blob/fda42e5ef908bdba722d435ff1f330d40dfcd56c/docs/spec/manifest-v2-2.md) format.
|
||||
**Target For Removal In Release: v27.0**
|
||||
|
||||
If the registry you are using still supports v2 schema 1, urge their administrators to move to v2 schema 2.
|
||||
The image manifest [v2 schema 1](https://distribution.github.io/distribution/spec/deprecated-schema-v1/)
|
||||
and "Docker Image v1" formats were deprecated in favor of the
|
||||
[v2 schema 2](https://distribution.github.io/distribution/spec/manifest-v2-2/)
|
||||
and [OCI image spec](https://github.com/opencontainers/image-spec/tree/v1.1.0)
|
||||
formats.
|
||||
|
||||
These legacy formats should no longer be used, and users are recommended to
|
||||
update images to use current formats, or to upgrade to more current images.
|
||||
Starting with Docker v26.0, pulling these images is disabled by default, and
|
||||
produces an error when attempting to pull the image:
|
||||
|
||||
```console
|
||||
$ docker pull ubuntu:10.04
|
||||
Error response from daemon:
|
||||
[DEPRECATION NOTICE] Docker Image Format v1 and Docker Image manifest version 2, schema 1 support is disabled by default and will be removed in an upcoming release.
|
||||
Suggest the author of docker.io/library/ubuntu:10.04 to upgrade the image to the OCI Format or Docker Image manifest v2, schema 2.
|
||||
More information at https://docs.docker.com/go/deprecated-image-specs/
|
||||
```
|
||||
|
||||
An environment variable (`DOCKER_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE`) is
|
||||
added in Docker v26.0 that allows re-enabling support for these image formats
|
||||
in the daemon. This environment variable must be set to a non-empty value in
|
||||
the daemon's environment (for example, through a [systemd override file](https://docs.docker.com/config/daemon/systemd/)).
|
||||
Support for the `DOCKER_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE` environment-variable
|
||||
will be removed in Docker v27.0 after which this functionality is removed permanently.
|
||||
|
||||
### `docker engine` subcommands
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ Execute a command in a running container
|
||||
| [`-e`](#env), [`--env`](#env) | `list` | | Set environment variables |
|
||||
| `--env-file` | `list` | | Read in a file of environment variables |
|
||||
| `-i`, `--interactive` | | | Keep STDIN open even if not attached |
|
||||
| `--privileged` | | | Give extended privileges to the command |
|
||||
| [`--privileged`](#privileged) | | | Give extended privileges to the command |
|
||||
| `-t`, `--tty` | | | Allocate a pseudo-TTY |
|
||||
| `-u`, `--user` | `string` | | Username or UID (format: `<name\|uid>[:<group\|gid>]`) |
|
||||
| [`-w`](#workdir), [`--workdir`](#workdir) | `string` | | Working directory inside the container |
|
||||
@ -96,6 +96,10 @@ VAR_B=2
|
||||
HOME=/root
|
||||
```
|
||||
|
||||
### <a name="privileged"></a> Escalate container privileges (--privileged)
|
||||
|
||||
See [`docker run --privileged`](container_run.md#privileged).
|
||||
|
||||
### <a name="workdir"></a> Set the working directory for the exec process (--workdir, -w)
|
||||
|
||||
By default `docker exec` command runs in the same working directory set when
|
||||
|
||||
@ -108,7 +108,7 @@ Create and run a new container from an image
|
||||
| [`-t`](#tty), [`--tty`](#tty) | | | Allocate a pseudo-TTY |
|
||||
| [`--ulimit`](#ulimit) | `ulimit` | | Ulimit options |
|
||||
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
|
||||
| `--userns` | `string` | | User namespace to use |
|
||||
| [`--userns`](#userns) | `string` | | User namespace to use |
|
||||
| [`--uts`](#uts) | `string` | | UTS namespace to use |
|
||||
| [`-v`](#volume), [`--volume`](#volume) | `list` | | Bind mount a volume |
|
||||
| `--volume-driver` | `string` | | Optional volume driver for the container |
|
||||
@ -271,6 +271,21 @@ container.
|
||||
strace: Process 1 attached
|
||||
```
|
||||
|
||||
### <a name="userns"></a> Disable namespace remapping for a container (--userns)
|
||||
|
||||
If you enable user namespaces on the daemon,
|
||||
all containers are started with user namespaces enabled by default.
|
||||
To disable user namespace remapping for a specific container,
|
||||
you can set the `--userns` flag to `host`.
|
||||
|
||||
```console
|
||||
docker run --userns=host hello-world
|
||||
```
|
||||
|
||||
`host` is the only valid value for the `--userns` flag.
|
||||
|
||||
For more information, refer to [Isolate containers with a user namespace](https://docs.docker.com/engine/security/userns-remap/).
|
||||
|
||||
### <a name="uts"></a> UTS settings (--uts)
|
||||
|
||||
```text
|
||||
@ -326,7 +341,37 @@ are broken into multiple containers, you might need to share the IPC mechanisms
|
||||
of the containers, using `"shareable"` mode for the main (i.e. "donor")
|
||||
container, and `"container:<donor-name-or-ID>"` for other containers.
|
||||
|
||||
### <a name="privileged"></a> Full container capabilities (--privileged)
|
||||
### <a name="privileged"></a> Escalate container privileges (--privileged)
|
||||
|
||||
The `--privileged` flag gives the following capabilities to a container:
|
||||
|
||||
- Enables all Linux kernel capabilities
|
||||
- Disables the default seccomp profile
|
||||
- Disables the default AppArmor profile
|
||||
- Disables the SELinux process label
|
||||
- Grants access to all host devices
|
||||
- Makes `/sys` read-write
|
||||
- Makes cgroups mounts read-write
|
||||
|
||||
In other words, the container can then do almost everything that the host can
|
||||
do. This flag exists to allow special use-cases, like running Docker within
|
||||
Docker.
|
||||
|
||||
> **Warning**
|
||||
>
|
||||
> Use the `--privileged` flag with caution.
|
||||
> A container with `--privileged` is not a securely sandboxed process.
|
||||
> Containers in this mode can get a root shell on the host
|
||||
> and take control over the system.
|
||||
>
|
||||
> For most use cases, this flag should not be the preferred solution.
|
||||
> If your container requires escalated privileges,
|
||||
> you should prefer to explicitly grant the necessary permissions,
|
||||
> for example by adding individual kernel capabilities with `--cap-add`.
|
||||
>
|
||||
> For more information, see
|
||||
> [Runtime privilege and Linux capabilities](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities)
|
||||
{ .warning }
|
||||
|
||||
The following example doesn't work, because by default, Docker drops most
|
||||
potentially dangerous kernel capabilities, including `CAP_SYS_ADMIN ` (which is
|
||||
@ -348,11 +393,6 @@ Filesystem Size Used Avail Use% Mounted on
|
||||
none 1.9G 0 1.9G 0% /mnt
|
||||
```
|
||||
|
||||
The `--privileged` flag gives all capabilities to the container, and it also
|
||||
lifts all the limitations enforced by the `device` cgroup controller. In other
|
||||
words, the container can then do almost everything that the host can do. This
|
||||
flag exists to allow special use-cases, like running Docker within Docker.
|
||||
|
||||
### <a name="workdir"></a> Set working directory (-w, --workdir)
|
||||
|
||||
```console
|
||||
@ -1323,6 +1363,7 @@ in the image, or `SIGTERM` if the image has no `STOPSIGNAL` defined.
|
||||
| `--security-opt="seccomp=unconfined"` | Turn off seccomp confinement for the container |
|
||||
| `--security-opt="seccomp=builtin"` | Use the default (built-in) seccomp profile for the container. This can be used to enable seccomp for a container running on a daemon with a custom default profile set, or with seccomp disabled ("unconfined"). |
|
||||
| `--security-opt="seccomp=profile.json"` | White-listed syscalls seccomp Json file to be used as a seccomp filter |
|
||||
| `--security-opt="systempaths=unconfined"` | Turn off confinement for system paths (masked paths, read-only paths) for the container |
|
||||
|
||||
The `--security-opt` flag lets you override the default labeling scheme for a
|
||||
container. Specifying the level in the following command allows you to share
|
||||
|
||||
@ -69,7 +69,7 @@ user credentials, VPNs, and so forth.
|
||||
> **Note**
|
||||
>
|
||||
> If the `URL` parameter contains a fragment the system recursively clones
|
||||
> the repository and its submodules using a `git clone --recursive` command.
|
||||
> the repository and its submodules.
|
||||
|
||||
Git URLs accept context configuration in their fragment section, separated by a
|
||||
colon (`:`). The first part represents the reference that Git checks out,
|
||||
|
||||
@ -162,7 +162,8 @@ equivalent Docker daemon flags used for docker0 bridge:
|
||||
| `com.docker.network.container_iface_prefix` | - | Set a custom prefix for container interfaces |
|
||||
|
||||
The following arguments can be passed to `docker network create` for any
|
||||
network driver, again with their approximate equivalents to `docker daemon`.
|
||||
network driver, again with their approximate equivalents to Docker daemon
|
||||
flags used for the docker0 bridge:
|
||||
|
||||
| Argument | Equivalent | Description |
|
||||
|--------------|----------------|--------------------------------------------|
|
||||
@ -183,6 +184,12 @@ $ docker network create \
|
||||
|
||||
### <a name="internal"></a> Network internal mode (--internal)
|
||||
|
||||
Containers on an internal network may communicate between each other, but not
|
||||
with any other network, as no default route is configured and firewall rules
|
||||
are set up to drop all traffic to or from other networks. Communication with
|
||||
the gateway IP address (and thus appropriately configured host services) is
|
||||
possible, and the host may communicate with any container IP directly.
|
||||
|
||||
By default, when you connect a container to an `overlay` network, Docker also
|
||||
connects a bridge network to it to provide external connectivity. If you want
|
||||
to create an externally isolated `overlay` network, you can specify the
|
||||
|
||||
@ -1236,6 +1236,15 @@ The list of feature options include:
|
||||
snapshotters instead of the classic storage drivers for storing image and
|
||||
container data. For more information, see
|
||||
[containerd storage](https://docs.docker.com/storage/containerd/).
|
||||
- `windows-dns-proxy`: when set to `true`, the daemon's internal DNS resolver
|
||||
will forward requests to external servers. Without this, most applications
|
||||
running in the container will still be able to use secondary DNS servers
|
||||
configured in the container itself, but `nslookup` won't be able to resolve
|
||||
external names. The current default is `false`, it will change to `true` in
|
||||
a future release. This option is only allowed on Windows.
|
||||
|
||||
> **Warning**
|
||||
> The `windows-dns-proxy` feature flag will be removed in a future release.
|
||||
|
||||
#### Configuration reload behavior
|
||||
|
||||
@ -1347,7 +1356,7 @@ using the `daemon.json` file.
|
||||
"default-network-opts": {
|
||||
"bridge": {
|
||||
"com.docker.network.bridge.host_binding_ipv4": "127.0.0.1",
|
||||
"com.docker.network.bridge.mtu": "1234"
|
||||
"com.docker.network.driver.mtu": "1234"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1363,7 +1372,7 @@ you create use these option configurations as defaults.
|
||||
```console
|
||||
$ docker network create mynet
|
||||
$ docker network inspect mynet --format "{{json .Options}}"
|
||||
{"com.docker.network.bridge.host_binding_ipv4":"127.0.0.1","com.docker.network.bridge.mtu":"1234"}
|
||||
{"com.docker.network.bridge.host_binding_ipv4":"127.0.0.1","com.docker.network.driver.mtu":"1234"}
|
||||
```
|
||||
|
||||
Note that changing this daemon configuration doesn't affect pre-existing
|
||||
@ -1377,5 +1386,5 @@ daemon configuration. The CLI flag expects a value with the following format:
|
||||
```console
|
||||
$ sudo dockerd \
|
||||
--default-network-opt bridge=com.docker.network.bridge.host_binding_ipv4=127.0.0.1 \
|
||||
--default-network-opt bridge=com.docker.network.bridge.mtu=1234
|
||||
--default-network-opt bridge=com.docker.network.driver.mtu=1234
|
||||
```
|
||||
|
||||
@ -7,7 +7,7 @@ title: Running containers
|
||||
---
|
||||
|
||||
Docker runs processes in isolated containers. A container is a process
|
||||
which runs on a host. The host may be local or remote. When an you
|
||||
which runs on a host. The host may be local or remote. When you
|
||||
execute `docker run`, the container process that runs is isolated in
|
||||
that it has its own file system, its own networking, and its own
|
||||
isolated process tree separate from the host.
|
||||
@ -813,11 +813,12 @@ by default a container is not allowed to access any devices, but a
|
||||
the documentation on [cgroups devices](https://www.kernel.org/doc/Documentation/cgroup-v1/devices.txt)).
|
||||
|
||||
The `--privileged` flag gives all capabilities to the container. When the operator
|
||||
executes `docker run --privileged`, Docker will enable access to all devices on
|
||||
the host as well as set some configuration in AppArmor or SELinux to allow the
|
||||
container nearly all the same access to the host as processes running outside
|
||||
containers on the host. Additional information about running with `--privileged`
|
||||
is available on the [Docker Blog](https://www.docker.com/blog/docker-can-now-run-within-docker/).
|
||||
executes `docker run --privileged`, Docker enables access to all devices on
|
||||
the host, and reconfigures AppArmor or SELinux to allow the container
|
||||
nearly all the same access to the host as processes running outside
|
||||
containers on the host. Use this flag with caution.
|
||||
For more information about the `--privileged` flag, see the
|
||||
[`docker run` reference](https://docs.docker.com/reference/cli/docker/container/run/#privileged).
|
||||
|
||||
If you want to limit access to a specific device or devices you can use
|
||||
the `--device` flag. It allows you to specify one or more devices that
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/e2e/internal/fixtures"
|
||||
"github.com/docker/cli/internal/test/environment"
|
||||
@ -34,6 +35,22 @@ func TestRunAttachedFromRemoteImageAndRemove(t *testing.T) {
|
||||
golden.Assert(t, result.Stderr(), "run-attached-from-remote-and-remove.golden")
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/docker/cli/issues/5053
|
||||
func TestRunInvalidEntrypointWithAutoremove(t *testing.T) {
|
||||
environment.SkipIfDaemonNotLinux(t)
|
||||
|
||||
result := make(chan *icmd.Result)
|
||||
go func() {
|
||||
result <- icmd.RunCommand("docker", "run", "--rm", fixtures.AlpineImage, "invalidcommand")
|
||||
}()
|
||||
select {
|
||||
case r := <-result:
|
||||
r.Assert(t, icmd.Expected{ExitCode: 127})
|
||||
case <-time.After(4 * time.Second):
|
||||
t.Fatal("test took too long, shouldn't hang")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithContentTrust(t *testing.T) {
|
||||
skip.If(t, environment.RemoteDaemon())
|
||||
|
||||
|
||||
@ -1,14 +1,25 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/e2e/internal/fixtures"
|
||||
"github.com/docker/cli/e2e/testutils"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/environment"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/icmd"
|
||||
"gotest.tools/v3/poll"
|
||||
"gotest.tools/v3/skip"
|
||||
)
|
||||
|
||||
@ -65,3 +76,185 @@ func TestTCPSchemeUsesHTTPProxyEnv(t *testing.T) {
|
||||
assert.Equal(t, strings.TrimSpace(result.Stdout()), "99.99.9")
|
||||
assert.Equal(t, received, "docker.acme.example.com:2376")
|
||||
}
|
||||
|
||||
// Test that the prompt command exits with 0
|
||||
// when the user sends SIGINT/SIGTERM to the process
|
||||
func TestPromptExitCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
dir := fixtures.SetupConfigFile(t)
|
||||
t.Cleanup(dir.Remove)
|
||||
|
||||
defaultCmdOpts := []icmd.CmdOp{
|
||||
fixtures.WithConfig(dir.Path()),
|
||||
fixtures.WithNotary,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
run func(t *testing.T) icmd.Cmd
|
||||
}{
|
||||
{
|
||||
name: "volume prune",
|
||||
run: func(t *testing.T) icmd.Cmd {
|
||||
t.Helper()
|
||||
return icmd.Command("docker", "volume", "prune")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "network prune",
|
||||
run: func(t *testing.T) icmd.Cmd {
|
||||
t.Helper()
|
||||
return icmd.Command("docker", "network", "prune")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "container prune",
|
||||
run: func(t *testing.T) icmd.Cmd {
|
||||
t.Helper()
|
||||
return icmd.Command("docker", "container", "prune")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "image prune",
|
||||
run: func(t *testing.T) icmd.Cmd {
|
||||
t.Helper()
|
||||
return icmd.Command("docker", "image", "prune")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "system prune",
|
||||
run: func(t *testing.T) icmd.Cmd {
|
||||
t.Helper()
|
||||
return icmd.Command("docker", "system", "prune")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "revoke trust",
|
||||
run: func(t *testing.T) icmd.Cmd {
|
||||
t.Helper()
|
||||
return icmd.Command("docker", "trust", "revoke", "example/trust-demo")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "plugin install",
|
||||
run: func(t *testing.T) icmd.Cmd {
|
||||
t.Helper()
|
||||
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.44"))
|
||||
|
||||
pluginDir := testutils.SetupPlugin(t, ctx)
|
||||
t.Cleanup(pluginDir.Remove)
|
||||
|
||||
plugin := "registry:5000/plugin-content-trust-install:latest"
|
||||
|
||||
icmd.RunCommand("docker", "plugin", "create", plugin, pluginDir.Path()).Assert(t, icmd.Success)
|
||||
icmd.RunCmd(icmd.Command("docker", "plugin", "push", plugin), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||
icmd.RunCmd(icmd.Command("docker", "plugin", "rm", "-f", plugin), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||
return icmd.Command("docker", "plugin", "install", plugin)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "plugin upgrade",
|
||||
run: func(t *testing.T) icmd.Cmd {
|
||||
t.Helper()
|
||||
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.44"))
|
||||
|
||||
pluginLatestDir := testutils.SetupPlugin(t, ctx)
|
||||
t.Cleanup(pluginLatestDir.Remove)
|
||||
pluginNextDir := testutils.SetupPlugin(t, ctx)
|
||||
t.Cleanup(pluginNextDir.Remove)
|
||||
|
||||
plugin := "registry:5000/plugin-content-trust-upgrade"
|
||||
|
||||
icmd.RunCommand("docker", "plugin", "create", plugin+":latest", pluginLatestDir.Path()).Assert(t, icmd.Success)
|
||||
icmd.RunCommand("docker", "plugin", "create", plugin+":next", pluginNextDir.Path()).Assert(t, icmd.Success)
|
||||
icmd.RunCmd(icmd.Command("docker", "plugin", "push", plugin+":latest"), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||
icmd.RunCmd(icmd.Command("docker", "plugin", "push", plugin+":next"), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||
icmd.RunCmd(icmd.Command("docker", "plugin", "rm", "-f", plugin+":latest"), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||
icmd.RunCmd(icmd.Command("docker", "plugin", "rm", "-f", plugin+":next"), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||
icmd.RunCmd(icmd.Command("docker", "plugin", "install", "--disable", "--grant-all-permissions", plugin+":latest"), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||
return icmd.Command("docker", "plugin", "upgrade", plugin+":latest", plugin+":next")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run("case="+tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
bufioWriter := bufio.NewWriter(buf)
|
||||
|
||||
writeDone := make(chan struct{})
|
||||
w := test.NewWriterWithHook(bufioWriter, func(p []byte) {
|
||||
writeDone <- struct{}{}
|
||||
})
|
||||
|
||||
drainChCtx, drainChCtxCancel := context.WithCancel(ctx)
|
||||
t.Cleanup(drainChCtxCancel)
|
||||
|
||||
drainChannel(drainChCtx, writeDone)
|
||||
|
||||
r, _ := io.Pipe()
|
||||
defer r.Close()
|
||||
result := icmd.StartCmd(tc.run(t),
|
||||
append(defaultCmdOpts,
|
||||
icmd.WithStdout(w),
|
||||
icmd.WithStderr(w),
|
||||
icmd.WithStdin(r))...)
|
||||
|
||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return poll.Error(ctx.Err())
|
||||
default:
|
||||
|
||||
if err := bufioWriter.Flush(); err != nil {
|
||||
return poll.Continue(err.Error())
|
||||
}
|
||||
if strings.Contains(buf.String(), "[y/N]") {
|
||||
return poll.Success()
|
||||
}
|
||||
|
||||
return poll.Continue("command did not prompt for confirmation, instead prompted:\n%s\n", buf.String())
|
||||
}
|
||||
}, poll.WithDelay(100*time.Millisecond), poll.WithTimeout(1*time.Second))
|
||||
|
||||
drainChCtxCancel()
|
||||
|
||||
assert.NilError(t, result.Cmd.Process.Signal(syscall.SIGINT))
|
||||
|
||||
proc, err := result.Cmd.Process.Wait()
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, proc.ExitCode(), 0, "expected exit code to be 0, got %d", proc.ExitCode())
|
||||
|
||||
processCtx, processCtxCancel := context.WithTimeout(ctx, time.Second)
|
||||
t.Cleanup(processCtxCancel)
|
||||
|
||||
select {
|
||||
case <-processCtx.Done():
|
||||
t.Fatal("timed out waiting for new line after process exit")
|
||||
case <-writeDone:
|
||||
buf.Reset()
|
||||
assert.NilError(t, bufioWriter.Flush())
|
||||
assert.Equal(t, buf.String(), "\n", "expected a new line after the process exits from SIGINT")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func drainChannel(ctx context.Context, ch <-chan struct{}) {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ch:
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@ -1,20 +1,14 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/e2e/internal/fixtures"
|
||||
"github.com/docker/cli/e2e/testutils"
|
||||
"github.com/docker/cli/internal/test/environment"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/fs"
|
||||
"gotest.tools/v3/icmd"
|
||||
"gotest.tools/v3/skip"
|
||||
)
|
||||
@ -31,8 +25,11 @@ func TestInstallWithContentTrust(t *testing.T) {
|
||||
dir := fixtures.SetupConfigFile(t)
|
||||
defer dir.Remove()
|
||||
|
||||
pluginDir := preparePluginDir(t)
|
||||
defer pluginDir.Remove()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
pluginDir := testutils.SetupPlugin(t, ctx)
|
||||
t.Cleanup(pluginDir.Remove)
|
||||
|
||||
icmd.RunCommand("docker", "plugin", "create", pluginName, pluginDir.Path()).Assert(t, icmd.Success)
|
||||
result := icmd.RunCmd(icmd.Command("docker", "plugin", "push", pluginName),
|
||||
@ -73,46 +70,3 @@ func TestInstallWithContentTrustUntrusted(t *testing.T) {
|
||||
Err: "Error: remote trust data does not exist",
|
||||
})
|
||||
}
|
||||
|
||||
func preparePluginDir(t *testing.T) *fs.Dir {
|
||||
t.Helper()
|
||||
p := &types.PluginConfig{
|
||||
Interface: types.PluginConfigInterface{
|
||||
Socket: "basic.sock",
|
||||
Types: []types.PluginInterfaceType{{Capability: "docker.dummy/1.0"}},
|
||||
},
|
||||
Entrypoint: []string{"/basic"},
|
||||
}
|
||||
configJSON, err := json.Marshal(p)
|
||||
assert.NilError(t, err)
|
||||
|
||||
binPath, err := ensureBasicPluginBin()
|
||||
assert.NilError(t, err)
|
||||
|
||||
dir := fs.NewDir(t, "plugin_test",
|
||||
fs.WithFile("config.json", string(configJSON), fs.WithMode(0o644)),
|
||||
fs.WithDir("rootfs", fs.WithMode(0o755)),
|
||||
)
|
||||
icmd.RunCommand("/bin/cp", binPath, dir.Join("rootfs", p.Entrypoint[0])).Assert(t, icmd.Success)
|
||||
return dir
|
||||
}
|
||||
|
||||
func ensureBasicPluginBin() (string, error) {
|
||||
name := "docker-basic-plugin"
|
||||
p, err := exec.LookPath(name)
|
||||
if err == nil {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
goBin, err := exec.LookPath("/usr/local/go/bin/go")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
installPath := filepath.Join(os.Getenv("GOPATH"), "bin", name)
|
||||
cmd := exec.Command(goBin, "build", "-o", installPath, "./basic")
|
||||
cmd.Env = append(os.Environ(), "CGO_ENABLED=0")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return "", errors.Wrapf(err, "error building basic plugin bin: %s", string(out))
|
||||
}
|
||||
return installPath, nil
|
||||
}
|
||||
|
||||
2
e2e/testdata/Dockerfile.gencerts
vendored
2
e2e/testdata/Dockerfile.gencerts
vendored
@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.21.8
|
||||
ARG GO_VERSION=1.21.10
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine AS generated
|
||||
ENV GOTOOLCHAIN=local
|
||||
|
||||
102
e2e/testutils/plugins.go
Normal file
102
e2e/testutils/plugins.go
Normal file
@ -0,0 +1,102 @@
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/fs"
|
||||
"gotest.tools/v3/icmd"
|
||||
)
|
||||
|
||||
//go:embed plugins/*
|
||||
var plugins embed.FS
|
||||
|
||||
// SetupPlugin builds a plugin and creates a temporary
|
||||
// directory with the plugin's config.json and rootfs.
|
||||
func SetupPlugin(t *testing.T, ctx context.Context) *fs.Dir {
|
||||
t.Helper()
|
||||
|
||||
p := &types.PluginConfig{
|
||||
Linux: types.PluginConfigLinux{
|
||||
Capabilities: []string{"CAP_SYS_ADMIN"},
|
||||
},
|
||||
Interface: types.PluginConfigInterface{
|
||||
Socket: "basic.sock",
|
||||
Types: []types.PluginInterfaceType{{Capability: "docker.dummy/1.0"}},
|
||||
},
|
||||
Entrypoint: []string{"/basic"},
|
||||
}
|
||||
configJSON, err := json.Marshal(p)
|
||||
assert.NilError(t, err)
|
||||
|
||||
binPath, err := buildPlugin(t, ctx)
|
||||
assert.NilError(t, err)
|
||||
|
||||
dir := fs.NewDir(t, "plugin_test",
|
||||
fs.WithFile("config.json", string(configJSON), fs.WithMode(0o644)),
|
||||
fs.WithDir("rootfs", fs.WithMode(0o755)),
|
||||
)
|
||||
|
||||
icmd.RunCommand("/bin/cp", binPath, dir.Join("rootfs", p.Entrypoint[0])).Assert(t, icmd.Success)
|
||||
return dir
|
||||
}
|
||||
|
||||
// buildPlugin uses Go to build a plugin from one of the source files in the plugins directory.
|
||||
// It returns the path to the built plugin binary.
|
||||
// To allow for multiple plugins to be built in parallel, the plugin is compiled with a unique
|
||||
// identifier in the binary. This is done by setting a linker flag with the -ldflags option.
|
||||
func buildPlugin(t *testing.T, ctx context.Context) (string, error) {
|
||||
t.Helper()
|
||||
|
||||
randomName, err := randomString()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
goBin, err := exec.LookPath("/usr/local/go/bin/go")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
installPath := filepath.Join(os.Getenv("GOPATH"), "bin", randomName)
|
||||
|
||||
pluginContent, err := plugins.ReadFile("plugins/basic.go")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dir := fs.NewDir(t, "plugin_build")
|
||||
if err := os.WriteFile(dir.Join("main.go"), pluginContent, 0o644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer dir.Remove()
|
||||
|
||||
cmd := exec.CommandContext(ctx, goBin, "build", "-ldflags",
|
||||
fmt.Sprintf("-X 'main.UNIQUEME=%s'", randomName),
|
||||
"-o", installPath, dir.Join("main.go"))
|
||||
|
||||
cmd.Env = append(os.Environ(), "CGO_ENABLED=0")
|
||||
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return "", errors.Wrapf(err, "error building basic plugin bin: %s", string(out))
|
||||
}
|
||||
|
||||
return installPath, nil
|
||||
}
|
||||
|
||||
func randomString() (string, error) {
|
||||
b := make([]byte, 8)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(b), nil
|
||||
}
|
||||
@ -9,6 +9,11 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// set by the compile flags to get around content sha being the same
|
||||
var (
|
||||
UNIQUEME string
|
||||
)
|
||||
|
||||
func main() {
|
||||
p, err := filepath.Abs(filepath.Join("run", "docker", "plugins"))
|
||||
if err != nil {
|
||||
@ -7,12 +7,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli *FakeCli, assertFunc func(*testing.T, error)) {
|
||||
func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli *FakeCli) {
|
||||
t.Helper()
|
||||
|
||||
errChan := make(chan error)
|
||||
@ -73,11 +74,6 @@ func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli
|
||||
t.Logf("command stderr:\n%s\n", cli.ErrBuffer().String())
|
||||
t.Fatalf("command %s did not return after SIGINT", cmd.Name())
|
||||
case err := <-errChan:
|
||||
if assertFunc != nil {
|
||||
assertFunc(t, err)
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, cli.ErrBuffer().String(), "")
|
||||
}
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,27 +4,22 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// WriterWithHook is an io.Writer that calls a hook function
|
||||
// after every write.
|
||||
// This is useful in testing to wait for a write to complete,
|
||||
// or to check what was written.
|
||||
// To create a WriterWithHook use the NewWriterWithHook function.
|
||||
type WriterWithHook struct {
|
||||
type writerWithHook struct {
|
||||
actualWriter io.Writer
|
||||
hook func([]byte)
|
||||
}
|
||||
|
||||
// Write writes p to the actual writer and then calls the hook function.
|
||||
func (w *WriterWithHook) Write(p []byte) (n int, err error) {
|
||||
func (w *writerWithHook) Write(p []byte) (n int, err error) {
|
||||
defer w.hook(p)
|
||||
return w.actualWriter.Write(p)
|
||||
}
|
||||
|
||||
var _ io.Writer = (*WriterWithHook)(nil)
|
||||
var _ io.Writer = (*writerWithHook)(nil)
|
||||
|
||||
// NewWriterWithHook returns a new WriterWithHook that still writes to the actualWriter
|
||||
// but also calls the hook function after every write.
|
||||
// The hook function is useful for testing, or waiting for a write to complete.
|
||||
func NewWriterWithHook(actualWriter io.Writer, hook func([]byte)) *WriterWithHook {
|
||||
return &WriterWithHook{actualWriter: actualWriter, hook: hook}
|
||||
// NewWriterWithHook returns a io.Writer that still
|
||||
// writes to the actualWriter but also calls the hook function
|
||||
// after every write. It is useful to use this function when
|
||||
// you need to wait for a writer to complete writing inside a test.
|
||||
func NewWriterWithHook(actualWriter io.Writer, hook func([]byte)) *writerWithHook {
|
||||
return &writerWithHook{actualWriter: actualWriter, hook: hook}
|
||||
}
|
||||
|
||||
@ -135,3 +135,18 @@ func TestNetworkOptAdvancedSyntaxInvalid(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetworkOptStringNetOptString(t *testing.T) {
|
||||
networkOpt := &NetworkOpt{}
|
||||
result := networkOpt.String()
|
||||
assert.Check(t, is.Equal("", result))
|
||||
if result != "" {
|
||||
t.Errorf("Expected an empty string, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetworkOptTypeNetOptType(t *testing.T) {
|
||||
networkOpt := &NetworkOpt{}
|
||||
result := networkOpt.Type()
|
||||
assert.Check(t, is.Equal("network", result))
|
||||
}
|
||||
|
||||
@ -465,3 +465,18 @@ func TestParseLink(t *testing.T) {
|
||||
t.Fatalf("Expected error 'bad format for links: link:alias:wrong' but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAllOrEmptyReturnsNilOrValue(t *testing.T) {
|
||||
opts := NewListOpts(nil)
|
||||
assert.Check(t, is.DeepEqual(opts.GetAllOrEmpty(), []string{}))
|
||||
opts.Set("foo")
|
||||
assert.Check(t, is.DeepEqual(opts.GetAllOrEmpty(), []string{"foo"}))
|
||||
}
|
||||
|
||||
func TestParseCPUsReturnZeroOnInvalidValues(t *testing.T) {
|
||||
resValue, _ := ParseCPUs("foo")
|
||||
var z1 int64 = 0
|
||||
assert.Equal(t, z1, resValue)
|
||||
resValue, _ = ParseCPUs("1e-32")
|
||||
assert.Equal(t, z1, resValue)
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ if [ -z "$CGO_ENABLED" ]; then
|
||||
case "$(go env GOOS)" in
|
||||
linux)
|
||||
case "$(go env GOARCH)" in
|
||||
amd64|arm64|arm|s390x)
|
||||
amd64|arm64|arm|s390x|riscv64)
|
||||
CGO_ENABLED=1
|
||||
;;
|
||||
*)
|
||||
|
||||
@ -23,7 +23,7 @@ EOL
|
||||
}
|
||||
|
||||
update() {
|
||||
(set -x ; go mod tidy -compat=1.19 -modfile=vendor.mod; go mod vendor -modfile=vendor.mod)
|
||||
(set -x ; go mod tidy -modfile=vendor.mod; go mod vendor -modfile=vendor.mod)
|
||||
}
|
||||
|
||||
validate() {
|
||||
|
||||
37
vendor.mod
37
vendor.mod
@ -4,15 +4,15 @@ module github.com/docker/cli
|
||||
// There is no 'go.mod' file, as that would imply opting in for all the rules
|
||||
// around SemVer, which this repo cannot abide by as it uses CalVer.
|
||||
|
||||
go 1.19
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0
|
||||
github.com/containerd/containerd v1.7.13
|
||||
github.com/containerd/containerd v1.7.15
|
||||
github.com/creack/pty v1.1.21
|
||||
github.com/distribution/reference v0.5.0
|
||||
github.com/docker/distribution v2.8.3+incompatible
|
||||
github.com/docker/docker v26.0.0-rc1.0.20240307174919-f4c696eef17d+incompatible // 26.0.0-rc2
|
||||
github.com/docker/docker v26.1.2-0.20240508085902-ef1912d8b6ae+incompatible
|
||||
github.com/docker/docker-credential-helpers v0.8.1
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/docker/go-units v0.5.0
|
||||
@ -23,7 +23,7 @@ require (
|
||||
github.com/mattn/go-runewidth v0.0.15
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/moby/patternmatcher v0.6.0
|
||||
github.com/moby/swarmkit/v2 v2.0.0-20240125134710-dcda100a8261
|
||||
github.com/moby/swarmkit/v2 v2.0.0-20240227173239-911c97650f2e
|
||||
github.com/moby/sys/sequential v0.5.0
|
||||
github.com/moby/sys/signal v0.7.0
|
||||
github.com/moby/term v0.5.0
|
||||
@ -38,9 +38,15 @@ require (
|
||||
github.com/tonistiigi/go-rosetta v0.0.0-20200727161949-f79598599c5d
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
go.opentelemetry.io/otel v1.21.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0
|
||||
go.opentelemetry.io/otel/metric v1.21.0
|
||||
go.opentelemetry.io/otel/sdk v1.21.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.21.0
|
||||
go.opentelemetry.io/otel/trace v1.21.0
|
||||
golang.org/x/sync v0.6.0
|
||||
golang.org/x/sys v0.16.0
|
||||
golang.org/x/term v0.15.0
|
||||
golang.org/x/sys v0.18.0
|
||||
golang.org/x/term v0.18.0
|
||||
golang.org/x/text v0.14.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gotest.tools/v3 v3.5.1
|
||||
@ -52,16 +58,18 @@ require (
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.4 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.3.0 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
@ -78,14 +86,15 @@ require (
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
go.etcd.io/etcd/raft/v3 v3.5.6 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.21.0 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/net v0.23.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.16.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
google.golang.org/grpc v1.60.1 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
)
|
||||
|
||||
82
vendor.sum
82
vendor.sum
@ -1,6 +1,7 @@
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
@ -27,17 +28,19 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq
|
||||
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
|
||||
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||
github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8=
|
||||
github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=
|
||||
github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
|
||||
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
|
||||
github.com/containerd/containerd v1.7.13 h1:wPYKIeGMN8vaggSKuV1X0wZulpMz4CrgEsZdaCyB6Is=
|
||||
github.com/containerd/containerd v1.7.13/go.mod h1:zT3up6yTRfEUa6+GsITYIJNgSVL9NQ4x4h1RPzk0Wu4=
|
||||
github.com/containerd/containerd v1.7.15 h1:afEHXdil9iAm03BmhjzKyXnnEBtjaLJefdU7DV0IFes=
|
||||
github.com/containerd/containerd v1.7.15/go.mod h1:ISzRRTMF8EXNpJlTzyr2XMhN+j9K302C21/+cr3kUnY=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
@ -54,8 +57,8 @@ github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v26.0.0-rc1.0.20240307174919-f4c696eef17d+incompatible h1:3KKLLQTG8GiV/4+OMpBuXm35zK75vbzwUxbp7AZuxdA=
|
||||
github.com/docker/docker v26.0.0-rc1.0.20240307174919-f4c696eef17d+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v26.1.2-0.20240508085902-ef1912d8b6ae+incompatible h1:PcRQNw8eAMTjdnD7+y3IeJcsCKqxHmlT0MmqfNs9Jc4=
|
||||
github.com/docker/docker v26.1.2-0.20240508085902-ef1912d8b6ae+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
|
||||
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
|
||||
@ -84,8 +87,8 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
@ -98,16 +101,19 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
|
||||
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
||||
github.com/google/certificate-transparency-go v1.1.4 h1:hCyXHDbtqlr/lMXU0D4WgbalXL0Zk4dSWWMbPV8VrqY=
|
||||
github.com/google/certificate-transparency-go v1.1.4/go.mod h1:D6lvbfwckhNrbM9WVl1EVeMOyzC19mpIjMOI4nxBHtQ=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
@ -119,6 +125,7 @@ github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
@ -131,6 +138,7 @@ github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVt
|
||||
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk=
|
||||
github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
|
||||
@ -144,6 +152,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@ -167,8 +176,8 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/swarmkit/v2 v2.0.0-20240125134710-dcda100a8261 h1:mjLf2jYrqtIS4LvLzg0gNyJR4rMXS4X5Bg1A4hOhVMs=
|
||||
github.com/moby/swarmkit/v2 v2.0.0-20240125134710-dcda100a8261/go.mod h1:oRJU1d0hrkkwCtouwfQGcIAKcVEkclMYoLWocqrg6gI=
|
||||
github.com/moby/swarmkit/v2 v2.0.0-20240227173239-911c97650f2e h1:4FRRm/5kOaCc+ssRBPmmcQM7b0KHdOgqKob93VnvHPs=
|
||||
github.com/moby/swarmkit/v2 v2.0.0-20240227173239-911c97650f2e/go.mod h1:kNy225f/gWAnF8wPftteMc5nbAHhrH+HUfvyjmhFjeQ=
|
||||
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
|
||||
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
|
||||
github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI=
|
||||
@ -228,7 +237,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
@ -256,11 +266,13 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a h1:tlJ7tGUHvcvL1v3yR6NcCc9nOqh2L+CG6HWrYQtwzQ0=
|
||||
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a/go.mod h1:Y94A6rPp2OwNfP/7vmf8O2xx2IykP8pPXQ1DLouGnEw=
|
||||
github.com/tonistiigi/go-rosetta v0.0.0-20200727161949-f79598599c5d h1:wvQZpqy8p0D/FUia6ipKDhXrzPzBVJE4PZyPc5+5Ay0=
|
||||
github.com/tonistiigi/go-rosetta v0.0.0-20200727161949-f79598599c5d/go.mod h1:xKQhd7snlzKFuUi1taTGWjpRE8iFTA06DeacYi3CVFQ=
|
||||
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b h1:FsyNrX12e5BkplJq7wKOLk0+C6LZ+KGXvuEcKUYm5ss=
|
||||
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
@ -271,7 +283,9 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc h1:zkGwegkOW709y0oiAraH/3D8njopUR/pARHv4tZZ6pw=
|
||||
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc/go.mod h1:FM4U1E3NzlNMRnSUTU3P1UdukWhYGifqEsjk9fn7BCk=
|
||||
github.com/zmap/zlint/v3 v3.1.0 h1:WjVytZo79m/L1+/Mlphl09WBob6YTGljN5IGWZFpAv0=
|
||||
github.com/zmap/zlint/v3 v3.1.0/go.mod h1:L7t8s3sEKkb0A2BxGy1IWrxt1ZATa1R4QfJZaQOD3zU=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.6/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ=
|
||||
go.etcd.io/etcd/raft/v3 v3.5.6 h1:tOmx6Ym6rn2GpZOrvTGJZciJHek6RnC3U/zNInzIN50=
|
||||
go.etcd.io/etcd/raft/v3 v3.5.6/go.mod h1:wL8kkRGx1Hp8FmZUuHfL3K2/OaGIDaXGr1N7i2G07J0=
|
||||
@ -279,15 +293,27 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJ
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
|
||||
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
|
||||
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
|
||||
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
|
||||
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
|
||||
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
|
||||
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q=
|
||||
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
|
||||
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@ -297,8 +323,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
@ -310,8 +336,8 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -333,11 +359,11 @@ golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
@ -356,16 +382,18 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20231012201019-e917dd12ba7a h1:fwgW9j3vHirt4ObdHoYNwuO24BEZjSzbh+zPaNWoiY8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q=
|
||||
google.golang.org/genproto v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:EMfReVxb80Dq1hhioy0sOsY9jCE46YDgHlJ7fWVUWRE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 h1:W18sezcAYs+3tDZX4F80yctqa12jcP1PUS2gQu1zTPU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97/go.mod h1:iargEX0SFPm3xcfMI0d1domjg0ZF4Aa0p2awqyxhvF0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc=
|
||||
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
|
||||
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
|
||||
@ -373,6 +401,7 @@ gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM=
|
||||
@ -391,5 +420,6 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
|
||||
k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
|
||||
tags.cncf.io/container-device-interface v0.6.2 h1:dThE6dtp/93ZDGhqaED2Pu374SOeUkBfuvkLuiTdwzg=
|
||||
tags.cncf.io/container-device-interface v0.6.2/go.mod h1:Shusyhjs1A5Na/kqPVLL0KqnHQHuunol9LFeUNkuGVE=
|
||||
|
||||
25
vendor/github.com/cenkalti/backoff/v4/.gitignore
generated
vendored
Normal file
25
vendor/github.com/cenkalti/backoff/v4/.gitignore
generated
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
|
||||
# IDEs
|
||||
.idea/
|
||||
20
vendor/github.com/cenkalti/backoff/v4/LICENSE
generated
vendored
Normal file
20
vendor/github.com/cenkalti/backoff/v4/LICENSE
generated
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Cenk Altı
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
32
vendor/github.com/cenkalti/backoff/v4/README.md
generated
vendored
Normal file
32
vendor/github.com/cenkalti/backoff/v4/README.md
generated
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
# Exponential Backoff [![GoDoc][godoc image]][godoc] [![Build Status][travis image]][travis] [![Coverage Status][coveralls image]][coveralls]
|
||||
|
||||
This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client].
|
||||
|
||||
[Exponential backoff][exponential backoff wiki]
|
||||
is an algorithm that uses feedback to multiplicatively decrease the rate of some process,
|
||||
in order to gradually find an acceptable rate.
|
||||
The retries exponentially increase and stop increasing when a certain threshold is met.
|
||||
|
||||
## Usage
|
||||
|
||||
Import path is `github.com/cenkalti/backoff/v4`. Please note the version part at the end.
|
||||
|
||||
Use https://pkg.go.dev/github.com/cenkalti/backoff/v4 to view the documentation.
|
||||
|
||||
## Contributing
|
||||
|
||||
* I would like to keep this library as small as possible.
|
||||
* Please don't send a PR without opening an issue and discussing it first.
|
||||
* If proposed change is not a common use case, I will probably not accept it.
|
||||
|
||||
[godoc]: https://pkg.go.dev/github.com/cenkalti/backoff/v4
|
||||
[godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png
|
||||
[travis]: https://travis-ci.org/cenkalti/backoff
|
||||
[travis image]: https://travis-ci.org/cenkalti/backoff.png?branch=master
|
||||
[coveralls]: https://coveralls.io/github/cenkalti/backoff?branch=master
|
||||
[coveralls image]: https://coveralls.io/repos/github/cenkalti/backoff/badge.svg?branch=master
|
||||
|
||||
[google-http-java-client]: https://github.com/google/google-http-java-client/blob/da1aa993e90285ec18579f1553339b00e19b3ab5/google-http-client/src/main/java/com/google/api/client/util/ExponentialBackOff.java
|
||||
[exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff
|
||||
|
||||
[advanced example]: https://pkg.go.dev/github.com/cenkalti/backoff/v4?tab=doc#pkg-examples
|
||||
66
vendor/github.com/cenkalti/backoff/v4/backoff.go
generated
vendored
Normal file
66
vendor/github.com/cenkalti/backoff/v4/backoff.go
generated
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
// Package backoff implements backoff algorithms for retrying operations.
|
||||
//
|
||||
// Use Retry function for retrying operations that may fail.
|
||||
// If Retry does not meet your needs,
|
||||
// copy/paste the function into your project and modify as you wish.
|
||||
//
|
||||
// There is also Ticker type similar to time.Ticker.
|
||||
// You can use it if you need to work with channels.
|
||||
//
|
||||
// See Examples section below for usage examples.
|
||||
package backoff
|
||||
|
||||
import "time"
|
||||
|
||||
// BackOff is a backoff policy for retrying an operation.
|
||||
type BackOff interface {
|
||||
// NextBackOff returns the duration to wait before retrying the operation,
|
||||
// or backoff. Stop to indicate that no more retries should be made.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// duration := backoff.NextBackOff();
|
||||
// if (duration == backoff.Stop) {
|
||||
// // Do not retry operation.
|
||||
// } else {
|
||||
// // Sleep for duration and retry operation.
|
||||
// }
|
||||
//
|
||||
NextBackOff() time.Duration
|
||||
|
||||
// Reset to initial state.
|
||||
Reset()
|
||||
}
|
||||
|
||||
// Stop indicates that no more retries should be made for use in NextBackOff().
|
||||
const Stop time.Duration = -1
|
||||
|
||||
// ZeroBackOff is a fixed backoff policy whose backoff time is always zero,
|
||||
// meaning that the operation is retried immediately without waiting, indefinitely.
|
||||
type ZeroBackOff struct{}
|
||||
|
||||
func (b *ZeroBackOff) Reset() {}
|
||||
|
||||
func (b *ZeroBackOff) NextBackOff() time.Duration { return 0 }
|
||||
|
||||
// StopBackOff is a fixed backoff policy that always returns backoff.Stop for
|
||||
// NextBackOff(), meaning that the operation should never be retried.
|
||||
type StopBackOff struct{}
|
||||
|
||||
func (b *StopBackOff) Reset() {}
|
||||
|
||||
func (b *StopBackOff) NextBackOff() time.Duration { return Stop }
|
||||
|
||||
// ConstantBackOff is a backoff policy that always returns the same backoff delay.
|
||||
// This is in contrast to an exponential backoff policy,
|
||||
// which returns a delay that grows longer as you call NextBackOff() over and over again.
|
||||
type ConstantBackOff struct {
|
||||
Interval time.Duration
|
||||
}
|
||||
|
||||
func (b *ConstantBackOff) Reset() {}
|
||||
func (b *ConstantBackOff) NextBackOff() time.Duration { return b.Interval }
|
||||
|
||||
func NewConstantBackOff(d time.Duration) *ConstantBackOff {
|
||||
return &ConstantBackOff{Interval: d}
|
||||
}
|
||||
62
vendor/github.com/cenkalti/backoff/v4/context.go
generated
vendored
Normal file
62
vendor/github.com/cenkalti/backoff/v4/context.go
generated
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
package backoff
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BackOffContext is a backoff policy that stops retrying after the context
|
||||
// is canceled.
|
||||
type BackOffContext interface { // nolint: golint
|
||||
BackOff
|
||||
Context() context.Context
|
||||
}
|
||||
|
||||
type backOffContext struct {
|
||||
BackOff
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// WithContext returns a BackOffContext with context ctx
|
||||
//
|
||||
// ctx must not be nil
|
||||
func WithContext(b BackOff, ctx context.Context) BackOffContext { // nolint: golint
|
||||
if ctx == nil {
|
||||
panic("nil context")
|
||||
}
|
||||
|
||||
if b, ok := b.(*backOffContext); ok {
|
||||
return &backOffContext{
|
||||
BackOff: b.BackOff,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
return &backOffContext{
|
||||
BackOff: b,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
func getContext(b BackOff) context.Context {
|
||||
if cb, ok := b.(BackOffContext); ok {
|
||||
return cb.Context()
|
||||
}
|
||||
if tb, ok := b.(*backOffTries); ok {
|
||||
return getContext(tb.delegate)
|
||||
}
|
||||
return context.Background()
|
||||
}
|
||||
|
||||
func (b *backOffContext) Context() context.Context {
|
||||
return b.ctx
|
||||
}
|
||||
|
||||
func (b *backOffContext) NextBackOff() time.Duration {
|
||||
select {
|
||||
case <-b.ctx.Done():
|
||||
return Stop
|
||||
default:
|
||||
return b.BackOff.NextBackOff()
|
||||
}
|
||||
}
|
||||
161
vendor/github.com/cenkalti/backoff/v4/exponential.go
generated
vendored
Normal file
161
vendor/github.com/cenkalti/backoff/v4/exponential.go
generated
vendored
Normal file
@ -0,0 +1,161 @@
|
||||
package backoff
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
ExponentialBackOff is a backoff implementation that increases the backoff
|
||||
period for each retry attempt using a randomization function that grows exponentially.
|
||||
|
||||
NextBackOff() is calculated using the following formula:
|
||||
|
||||
randomized interval =
|
||||
RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor])
|
||||
|
||||
In other words NextBackOff() will range between the randomization factor
|
||||
percentage below and above the retry interval.
|
||||
|
||||
For example, given the following parameters:
|
||||
|
||||
RetryInterval = 2
|
||||
RandomizationFactor = 0.5
|
||||
Multiplier = 2
|
||||
|
||||
the actual backoff period used in the next retry attempt will range between 1 and 3 seconds,
|
||||
multiplied by the exponential, that is, between 2 and 6 seconds.
|
||||
|
||||
Note: MaxInterval caps the RetryInterval and not the randomized interval.
|
||||
|
||||
If the time elapsed since an ExponentialBackOff instance is created goes past the
|
||||
MaxElapsedTime, then the method NextBackOff() starts returning backoff.Stop.
|
||||
|
||||
The elapsed time can be reset by calling Reset().
|
||||
|
||||
Example: Given the following default arguments, for 10 tries the sequence will be,
|
||||
and assuming we go over the MaxElapsedTime on the 10th try:
|
||||
|
||||
Request # RetryInterval (seconds) Randomized Interval (seconds)
|
||||
|
||||
1 0.5 [0.25, 0.75]
|
||||
2 0.75 [0.375, 1.125]
|
||||
3 1.125 [0.562, 1.687]
|
||||
4 1.687 [0.8435, 2.53]
|
||||
5 2.53 [1.265, 3.795]
|
||||
6 3.795 [1.897, 5.692]
|
||||
7 5.692 [2.846, 8.538]
|
||||
8 8.538 [4.269, 12.807]
|
||||
9 12.807 [6.403, 19.210]
|
||||
10 19.210 backoff.Stop
|
||||
|
||||
Note: Implementation is not thread-safe.
|
||||
*/
|
||||
type ExponentialBackOff struct {
|
||||
InitialInterval time.Duration
|
||||
RandomizationFactor float64
|
||||
Multiplier float64
|
||||
MaxInterval time.Duration
|
||||
// After MaxElapsedTime the ExponentialBackOff returns Stop.
|
||||
// It never stops if MaxElapsedTime == 0.
|
||||
MaxElapsedTime time.Duration
|
||||
Stop time.Duration
|
||||
Clock Clock
|
||||
|
||||
currentInterval time.Duration
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// Clock is an interface that returns current time for BackOff.
|
||||
type Clock interface {
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
// Default values for ExponentialBackOff.
|
||||
const (
|
||||
DefaultInitialInterval = 500 * time.Millisecond
|
||||
DefaultRandomizationFactor = 0.5
|
||||
DefaultMultiplier = 1.5
|
||||
DefaultMaxInterval = 60 * time.Second
|
||||
DefaultMaxElapsedTime = 15 * time.Minute
|
||||
)
|
||||
|
||||
// NewExponentialBackOff creates an instance of ExponentialBackOff using default values.
|
||||
func NewExponentialBackOff() *ExponentialBackOff {
|
||||
b := &ExponentialBackOff{
|
||||
InitialInterval: DefaultInitialInterval,
|
||||
RandomizationFactor: DefaultRandomizationFactor,
|
||||
Multiplier: DefaultMultiplier,
|
||||
MaxInterval: DefaultMaxInterval,
|
||||
MaxElapsedTime: DefaultMaxElapsedTime,
|
||||
Stop: Stop,
|
||||
Clock: SystemClock,
|
||||
}
|
||||
b.Reset()
|
||||
return b
|
||||
}
|
||||
|
||||
type systemClock struct{}
|
||||
|
||||
func (t systemClock) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// SystemClock implements Clock interface that uses time.Now().
|
||||
var SystemClock = systemClock{}
|
||||
|
||||
// Reset the interval back to the initial retry interval and restarts the timer.
|
||||
// Reset must be called before using b.
|
||||
func (b *ExponentialBackOff) Reset() {
|
||||
b.currentInterval = b.InitialInterval
|
||||
b.startTime = b.Clock.Now()
|
||||
}
|
||||
|
||||
// NextBackOff calculates the next backoff interval using the formula:
|
||||
// Randomized interval = RetryInterval * (1 ± RandomizationFactor)
|
||||
func (b *ExponentialBackOff) NextBackOff() time.Duration {
|
||||
// Make sure we have not gone over the maximum elapsed time.
|
||||
elapsed := b.GetElapsedTime()
|
||||
next := getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval)
|
||||
b.incrementCurrentInterval()
|
||||
if b.MaxElapsedTime != 0 && elapsed+next > b.MaxElapsedTime {
|
||||
return b.Stop
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
// GetElapsedTime returns the elapsed time since an ExponentialBackOff instance
|
||||
// is created and is reset when Reset() is called.
|
||||
//
|
||||
// The elapsed time is computed using time.Now().UnixNano(). It is
|
||||
// safe to call even while the backoff policy is used by a running
|
||||
// ticker.
|
||||
func (b *ExponentialBackOff) GetElapsedTime() time.Duration {
|
||||
return b.Clock.Now().Sub(b.startTime)
|
||||
}
|
||||
|
||||
// Increments the current interval by multiplying it with the multiplier.
|
||||
func (b *ExponentialBackOff) incrementCurrentInterval() {
|
||||
// Check for overflow, if overflow is detected set the current interval to the max interval.
|
||||
if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier {
|
||||
b.currentInterval = b.MaxInterval
|
||||
} else {
|
||||
b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a random value from the following interval:
|
||||
// [currentInterval - randomizationFactor * currentInterval, currentInterval + randomizationFactor * currentInterval].
|
||||
func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration {
|
||||
if randomizationFactor == 0 {
|
||||
return currentInterval // make sure no randomness is used when randomizationFactor is 0.
|
||||
}
|
||||
var delta = randomizationFactor * float64(currentInterval)
|
||||
var minInterval = float64(currentInterval) - delta
|
||||
var maxInterval = float64(currentInterval) + delta
|
||||
|
||||
// Get a random value from the range [minInterval, maxInterval].
|
||||
// The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then
|
||||
// we want a 33% chance for selecting either 1, 2 or 3.
|
||||
return time.Duration(minInterval + (random * (maxInterval - minInterval + 1)))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user