Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5650f9b102 | |||
| e8bc27d4ca | |||
| 2acb9c2e8f | |||
| 230d4d0bab | |||
| 6d47c06b0d | |||
| a6d757c0e3 | |||
| c463d968fc | |||
| a61a0c3ac9 | |||
| 0576b3dad7 | |||
| 60b13f1f80 | |||
| 5dab29765f | |||
| b600054c51 | |||
| 42912d4d68 | |||
| 28638aab95 | |||
| e89982aea9 | |||
| dafe63b085 | |||
| 7dfc99c17e | |||
| 079107e1bc | |||
| 14d7485d98 | |||
| 50bb59277a | |||
| c4767b3141 | |||
| 3f807ede91 | |||
| 836fc57e2a | |||
| 258e27438f | |||
| 29cd8591ee | |||
| e23a63eb4c | |||
| 1e52a86d42 | |||
| e382a5f225 | |||
| b72abbb6f0 | |||
| 26e9ff49c8 | |||
| 4021ee92fe | |||
| 326c7138bb | |||
| ee359a394b | |||
| 004e2925d7 | |||
| 6691085012 | |||
| 60f2d38d53 | |||
| 81b482ea5e | |||
| 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 | |||
| 800a51d6cd |
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)**
|
||||
|
||||
|
||||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@ -77,13 +77,13 @@ jobs:
|
||||
platformPair=${platform//\//-}
|
||||
tar -cvzf "/tmp/out/docker-${platformPair}.tar.gz" .
|
||||
if [ -z "${{ matrix.use_glibc }}" ]; then
|
||||
echo "ARTIFACT_NAME=${{ matrix.target }}" >> $GITHUB_ENV
|
||||
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "ARTIFACT_NAME=${{ matrix.target }}-glibc" >> $GITHUB_ENV
|
||||
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}-glibc" >> $GITHUB_ENV
|
||||
fi
|
||||
-
|
||||
name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: /tmp/out/*
|
||||
|
||||
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.11
|
||||
-
|
||||
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
@ -1,10 +1,10 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG BASE_VARIANT=alpine
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.21.8
|
||||
ARG GO_VERSION=1.21.11
|
||||
ARG XX_VERSION=1.4.0
|
||||
ARG GOVERSIONINFO_VERSION=v1.3.0
|
||||
ARG GOTESTSUM_VERSION=v1.10.0
|
||||
@ -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 .
|
||||
|
||||
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)}
|
||||
}
|
||||
|
||||
|
||||
191
cli-plugins/manager/hooks.go
Normal file
191
cli-plugins/manager/hooks.go
Normal file
@ -0,0 +1,191 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/hooks"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// HookPluginData is the type representing the information
|
||||
// that plugins declaring support for hooks get passed when
|
||||
// being invoked following a CLI command execution.
|
||||
type HookPluginData struct {
|
||||
// RootCmd is a string representing the matching hook configuration
|
||||
// which is currently being invoked. If a hook for `docker context` is
|
||||
// configured and the user executes `docker context ls`, the plugin will
|
||||
// be invoked with `context`.
|
||||
RootCmd string
|
||||
Flags map[string]string
|
||||
CommandError string
|
||||
}
|
||||
|
||||
// RunCLICommandHooks is the entrypoint into the hooks execution flow after
|
||||
// a main CLI command was executed. It calls the hook subcommand for all
|
||||
// present CLI plugins that declare support for hooks in their metadata and
|
||||
// parses/prints their responses.
|
||||
func RunCLICommandHooks(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
|
||||
}
|
||||
|
||||
var appended bool
|
||||
nextSteps, appended = appendNextSteps(nextSteps, processedHook)
|
||||
if !appended {
|
||||
logrus.Debugf("Plugin %s responded with an empty hook message %q. Ignoring.", pluginName, string(hookReturn))
|
||||
}
|
||||
}
|
||||
return nextSteps
|
||||
}
|
||||
|
||||
// appendNextSteps appends the processed hook output to the nextSteps slice.
|
||||
// If the processed hook output is empty, it is not appended.
|
||||
// Empty lines are not stripped if there's at least one non-empty line.
|
||||
func appendNextSteps(nextSteps []string, processed []string) ([]string, bool) {
|
||||
empty := true
|
||||
for _, l := range processed {
|
||||
if strings.TrimSpace(l) != "" {
|
||||
empty = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if empty {
|
||||
return nextSteps, false
|
||||
}
|
||||
|
||||
return append(nextSteps, processed...), true
|
||||
}
|
||||
|
||||
// pluginMatch takes a plugin configuration and a string representing the
|
||||
// command being executed (such as 'image ls' – the root 'docker' is omitted)
|
||||
// and, if the configuration includes a hook for the invoked command, returns
|
||||
// the configured hook string.
|
||||
func pluginMatch(pluginCfg map[string]string, subCmd string) (string, bool) {
|
||||
configuredPluginHooks, ok := pluginCfg["hooks"]
|
||||
if !ok || configuredPluginHooks == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
commands := strings.Split(configuredPluginHooks, ",")
|
||||
for _, hookCmd := range commands {
|
||||
if hookMatch(hookCmd, subCmd) {
|
||||
return hookCmd, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func hookMatch(hookCmd, subCmd string) bool {
|
||||
hookCmdTokens := strings.Split(hookCmd, " ")
|
||||
subCmdTokens := strings.Split(subCmd, " ")
|
||||
|
||||
if len(hookCmdTokens) > len(subCmdTokens) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, v := range hookCmdTokens {
|
||||
if v != subCmdTokens[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func getCommandFlags(cmd *cobra.Command) map[string]string {
|
||||
flags := make(map[string]string)
|
||||
cmd.Flags().Visit(func(f *pflag.Flag) {
|
||||
var fValue string
|
||||
if f.Value.Type() == "bool" {
|
||||
fValue = f.Value.String()
|
||||
}
|
||||
flags[f.Name] = fValue
|
||||
})
|
||||
return flags
|
||||
}
|
||||
|
||||
// getNaiveFlags string-matches argv and parses them into a map.
|
||||
// This is used when calling hooks after a plugin command, since
|
||||
// in this case we can't rely on the cobra command tree to parse
|
||||
// flags in this case. In this case, no values are ever passed,
|
||||
// since we don't have enough information to process them.
|
||||
func getNaiveFlags(args []string) map[string]string {
|
||||
flags := make(map[string]string)
|
||||
for _, arg := range args {
|
||||
if strings.HasPrefix(arg, "--") {
|
||||
flags[arg[2:]] = ""
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
flags[arg[1:]] = ""
|
||||
}
|
||||
}
|
||||
return flags
|
||||
}
|
||||
143
cli-plugins/manager/hooks_test.go
Normal file
143
cli-plugins/manager/hooks_test.go
Normal file
@ -0,0 +1,143 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendNextSteps(t *testing.T) {
|
||||
testCases := []struct {
|
||||
processed []string
|
||||
expectedOut []string
|
||||
}{
|
||||
{
|
||||
processed: []string{},
|
||||
expectedOut: []string{},
|
||||
},
|
||||
{
|
||||
processed: []string{"", ""},
|
||||
expectedOut: []string{},
|
||||
},
|
||||
{
|
||||
processed: []string{"Some hint", "", "Some other hint"},
|
||||
expectedOut: []string{"Some hint", "", "Some other hint"},
|
||||
},
|
||||
{
|
||||
processed: []string{"Hint 1", "Hint 2"},
|
||||
expectedOut: []string{"Hint 1", "Hint 2"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
got, appended := appendNextSteps([]string{}, tc.processed)
|
||||
assert.Check(t, is.DeepEqual(got, tc.expectedOut))
|
||||
assert.Check(t, is.Equal(appended, len(got) > 0))
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
@ -50,6 +52,24 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager
|
||||
opts = append(opts, withPluginClientConn(plugin.Name()))
|
||||
}
|
||||
err = tcmd.Initialize(opts...)
|
||||
ogRunE := cmd.RunE
|
||||
if ogRunE == nil {
|
||||
ogRun := cmd.Run
|
||||
// necessary because error will always be nil here
|
||||
// see: https://github.com/golangci/golangci-lint/issues/1379
|
||||
//nolint:unparam
|
||||
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 {
|
||||
stopInstrumentation := dockerCli.StartInstrumentation(cmd)
|
||||
err := ogRunE(cmd, args)
|
||||
stopInstrumentation(err)
|
||||
return err
|
||||
}
|
||||
})
|
||||
return err
|
||||
}
|
||||
@ -66,6 +86,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
|
||||
@ -241,6 +273,11 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
|
||||
return ResolveDefaultContext(cli.options, cli.contextStoreConfig)
|
||||
},
|
||||
}
|
||||
|
||||
// TODO(krissetto): pass ctx to the funcs instead of using this
|
||||
cli.createGlobalMeterProvider(cli.baseCtx)
|
||||
cli.createGlobalTracerProvider(cli.baseCtx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -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())
|
||||
})
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/containerd/containerd/platforms"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
@ -239,7 +239,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
if options.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") {
|
||||
p, err := platforms.Parse(options.platform)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error parsing specified platform")
|
||||
return "", errors.Wrap(errdefs.InvalidParameter(err), "error parsing specified platform")
|
||||
}
|
||||
platform = &p
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -24,6 +24,10 @@ type CreateOptions struct {
|
||||
Description string
|
||||
Docker map[string]string
|
||||
From string
|
||||
|
||||
// Additional Metadata to store in the context. This option is not
|
||||
// currently exposed to the user.
|
||||
metaData map[string]any
|
||||
}
|
||||
|
||||
func longCreateDescription() string {
|
||||
@ -94,7 +98,8 @@ func createNewContext(contextStore store.ReaderWriter, o *CreateOptions) error {
|
||||
docker.DockerEndpoint: dockerEP,
|
||||
},
|
||||
Metadata: command.DockerContext{
|
||||
Description: o.Description,
|
||||
Description: o.Description,
|
||||
AdditionalFields: o.metaData,
|
||||
},
|
||||
Name: o.Name,
|
||||
}
|
||||
|
||||
@ -8,14 +8,18 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestExportImportWithFile(t *testing.T) {
|
||||
contextFile := filepath.Join(t.TempDir(), "exported")
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "test")
|
||||
createTestContext(t, cli, "test", map[string]any{
|
||||
"MyCustomMetadata": t.Name(),
|
||||
})
|
||||
cli.ErrBuffer().Reset()
|
||||
assert.NilError(t, RunExport(cli, &ExportOptions{
|
||||
ContextName: "test",
|
||||
@ -29,18 +33,26 @@ func TestExportImportWithFile(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
context2, err := cli.ContextStore().GetMetadata("test2")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, context1.Endpoints, context2.Endpoints)
|
||||
assert.DeepEqual(t, context1.Metadata, context2.Metadata)
|
||||
assert.Equal(t, "test", context1.Name)
|
||||
assert.Equal(t, "test2", context2.Name)
|
||||
|
||||
assert.Equal(t, "test2\n", cli.OutBuffer().String())
|
||||
assert.Equal(t, "Successfully imported context \"test2\"\n", cli.ErrBuffer().String())
|
||||
assert.Check(t, is.DeepEqual(context1.Metadata, command.DockerContext{
|
||||
Description: "description of test",
|
||||
AdditionalFields: map[string]any{"MyCustomMetadata": t.Name()},
|
||||
}))
|
||||
|
||||
assert.Check(t, is.DeepEqual(context1.Endpoints, context2.Endpoints))
|
||||
assert.Check(t, is.DeepEqual(context1.Metadata, context2.Metadata))
|
||||
assert.Check(t, is.Equal("test", context1.Name))
|
||||
assert.Check(t, is.Equal("test2", context2.Name))
|
||||
|
||||
assert.Check(t, is.Equal("test2\n", cli.OutBuffer().String()))
|
||||
assert.Check(t, is.Equal("Successfully imported context \"test2\"\n", cli.ErrBuffer().String()))
|
||||
}
|
||||
|
||||
func TestExportImportPipe(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "test")
|
||||
createTestContext(t, cli, "test", map[string]any{
|
||||
"MyCustomMetadata": t.Name(),
|
||||
})
|
||||
cli.ErrBuffer().Reset()
|
||||
cli.OutBuffer().Reset()
|
||||
assert.NilError(t, RunExport(cli, &ExportOptions{
|
||||
@ -56,13 +68,19 @@ func TestExportImportPipe(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
context2, err := cli.ContextStore().GetMetadata("test2")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, context1.Endpoints, context2.Endpoints)
|
||||
assert.DeepEqual(t, context1.Metadata, context2.Metadata)
|
||||
assert.Equal(t, "test", context1.Name)
|
||||
assert.Equal(t, "test2", context2.Name)
|
||||
|
||||
assert.Equal(t, "test2\n", cli.OutBuffer().String())
|
||||
assert.Equal(t, "Successfully imported context \"test2\"\n", cli.ErrBuffer().String())
|
||||
assert.Check(t, is.DeepEqual(context1.Metadata, command.DockerContext{
|
||||
Description: "description of test",
|
||||
AdditionalFields: map[string]any{"MyCustomMetadata": t.Name()},
|
||||
}))
|
||||
|
||||
assert.Check(t, is.DeepEqual(context1.Endpoints, context2.Endpoints))
|
||||
assert.Check(t, is.DeepEqual(context1.Metadata, context2.Metadata))
|
||||
assert.Check(t, is.Equal("test", context1.Name))
|
||||
assert.Check(t, is.Equal("test2", context2.Name))
|
||||
|
||||
assert.Check(t, is.Equal("test2\n", cli.OutBuffer().String()))
|
||||
assert.Check(t, is.Equal("Successfully imported context \"test2\"\n", cli.ErrBuffer().String()))
|
||||
}
|
||||
|
||||
func TestExportExistingFile(t *testing.T) {
|
||||
|
||||
@ -10,7 +10,9 @@ import (
|
||||
|
||||
func TestInspect(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "current")
|
||||
createTestContext(t, cli, "current", map[string]any{
|
||||
"MyCustomMetadata": "MyCustomMetadataValue",
|
||||
})
|
||||
cli.OutBuffer().Reset()
|
||||
assert.NilError(t, runInspect(cli, inspectOptions{
|
||||
refs: []string{"current"},
|
||||
|
||||
@ -66,6 +66,8 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
|
||||
Name: rawMeta.Name,
|
||||
Current: isCurrent,
|
||||
Error: err.Error(),
|
||||
|
||||
ContextType: getContextType(nil, opts.format),
|
||||
})
|
||||
continue
|
||||
}
|
||||
@ -80,6 +82,8 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
|
||||
Description: meta.Description,
|
||||
DockerEndpoint: dockerEndpoint.Host,
|
||||
Error: errMsg,
|
||||
|
||||
ContextType: getContextType(meta.AdditionalFields, opts.format),
|
||||
}
|
||||
contexts = append(contexts, &desc)
|
||||
}
|
||||
@ -96,6 +100,8 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
|
||||
Name: curContext,
|
||||
Current: true,
|
||||
Error: errMsg,
|
||||
|
||||
ContextType: getContextType(nil, opts.format),
|
||||
})
|
||||
}
|
||||
sort.Slice(contexts, func(i, j int) bool {
|
||||
@ -111,6 +117,30 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getContextType sets the LegacyContextType field for compatibility with
|
||||
// Visual Studio, which depends on this field from the "cloud integration"
|
||||
// wrapper.
|
||||
//
|
||||
// https://github.com/docker/compose-cli/blob/c156ce6da4c2b317174d42daf1b019efa87e9f92/api/context/store/contextmetadata.go#L28-L34
|
||||
// https://github.com/docker/compose-cli/blob/c156ce6da4c2b317174d42daf1b019efa87e9f92/api/context/store/store.go#L34-L51
|
||||
//
|
||||
// TODO(thaJeztah): remove this and [ClientContext.ContextType] once Visual Studio is updated to no longer depend on this.
|
||||
func getContextType(meta map[string]any, format string) string {
|
||||
if format != formatter.JSONFormat && format != formatter.JSONFormatKey {
|
||||
// We only need the ContextType field when formatting as JSON,
|
||||
// which is the format-string used by Visual Studio to detect the
|
||||
// context-type.
|
||||
return ""
|
||||
}
|
||||
if ct, ok := meta["Type"]; ok {
|
||||
// If the context on-disk has a context-type (ecs, aci), return it.
|
||||
return ct.(string)
|
||||
}
|
||||
|
||||
// Use the default context-type.
|
||||
return "moby"
|
||||
}
|
||||
|
||||
func format(dockerCli command.Cli, opts *listOptions, contexts []*formatter.ClientContext) error {
|
||||
contextCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
|
||||
@ -4,36 +4,70 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
func createTestContext(t *testing.T, cli command.Cli, name string) {
|
||||
func createTestContexts(t *testing.T, cli command.Cli, name ...string) {
|
||||
t.Helper()
|
||||
for _, n := range name {
|
||||
createTestContext(t, cli, n, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func createTestContext(t *testing.T, cli command.Cli, name string, metaData map[string]any) {
|
||||
t.Helper()
|
||||
|
||||
err := RunCreate(cli, &CreateOptions{
|
||||
Name: name,
|
||||
Description: "description of " + name,
|
||||
Docker: map[string]string{keyHost: "https://someswarmserver.example.com"},
|
||||
|
||||
metaData: metaData,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "current")
|
||||
createTestContext(t, cli, "other")
|
||||
createTestContext(t, cli, "unset")
|
||||
createTestContexts(t, cli, "current", "other", "unset")
|
||||
cli.SetCurrentContext("current")
|
||||
cli.OutBuffer().Reset()
|
||||
assert.NilError(t, runList(cli, &listOptions{}))
|
||||
golden.Assert(t, cli.OutBuffer().String(), "list.golden")
|
||||
}
|
||||
|
||||
func TestListJSON(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "current", nil)
|
||||
createTestContext(t, cli, "context1", map[string]any{"Type": "aci"})
|
||||
createTestContext(t, cli, "context2", map[string]any{"Type": "ecs"})
|
||||
createTestContext(t, cli, "context3", map[string]any{"Type": "moby"})
|
||||
cli.SetCurrentContext("current")
|
||||
|
||||
t.Run("format={{json .}}", func(t *testing.T) {
|
||||
cli.OutBuffer().Reset()
|
||||
assert.NilError(t, runList(cli, &listOptions{format: formatter.JSONFormat}))
|
||||
golden.Assert(t, cli.OutBuffer().String(), "list-json.golden")
|
||||
})
|
||||
|
||||
t.Run("format=json", func(t *testing.T) {
|
||||
cli.OutBuffer().Reset()
|
||||
assert.NilError(t, runList(cli, &listOptions{format: formatter.JSONFormatKey}))
|
||||
golden.Assert(t, cli.OutBuffer().String(), "list-json.golden")
|
||||
})
|
||||
|
||||
t.Run("format={{ json .Name }}", func(t *testing.T) {
|
||||
cli.OutBuffer().Reset()
|
||||
assert.NilError(t, runList(cli, &listOptions{format: `{{ json .Name }}`}))
|
||||
golden.Assert(t, cli.OutBuffer().String(), "list-json-name.golden")
|
||||
})
|
||||
}
|
||||
|
||||
func TestListQuiet(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "current")
|
||||
createTestContext(t, cli, "other")
|
||||
createTestContexts(t, cli, "current", "other")
|
||||
cli.SetCurrentContext("current")
|
||||
cli.OutBuffer().Reset()
|
||||
assert.NilError(t, runList(cli, &listOptions{quiet: true}))
|
||||
|
||||
@ -13,8 +13,7 @@ import (
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "current")
|
||||
createTestContext(t, cli, "other")
|
||||
createTestContexts(t, cli, "current", "other")
|
||||
assert.NilError(t, RunRemove(cli, RemoveOptions{}, []string{"other"}))
|
||||
_, err := cli.ContextStore().GetMetadata("current")
|
||||
assert.NilError(t, err)
|
||||
@ -24,8 +23,7 @@ func TestRemove(t *testing.T) {
|
||||
|
||||
func TestRemoveNotAContext(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "current")
|
||||
createTestContext(t, cli, "other")
|
||||
createTestContexts(t, cli, "current", "other")
|
||||
err := RunRemove(cli, RemoveOptions{}, []string{"not-a-context"})
|
||||
assert.ErrorContains(t, err, `context "not-a-context" does not exist`)
|
||||
|
||||
@ -35,8 +33,7 @@ func TestRemoveNotAContext(t *testing.T) {
|
||||
|
||||
func TestRemoveCurrent(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "current")
|
||||
createTestContext(t, cli, "other")
|
||||
createTestContexts(t, cli, "current", "other")
|
||||
cli.SetCurrentContext("current")
|
||||
err := RunRemove(cli, RemoveOptions{}, []string{"current"})
|
||||
assert.ErrorContains(t, err, `context "current" is in use, set -f flag to force remove`)
|
||||
@ -50,8 +47,7 @@ func TestRemoveCurrentForce(t *testing.T) {
|
||||
assert.NilError(t, testCfg.Save())
|
||||
|
||||
cli := makeFakeCli(t, withCliConfig(testCfg))
|
||||
createTestContext(t, cli, "current")
|
||||
createTestContext(t, cli, "other")
|
||||
createTestContexts(t, cli, "current", "other")
|
||||
cli.SetCurrentContext("current")
|
||||
assert.NilError(t, RunRemove(cli, RemoveOptions{Force: true}, []string{"current"}))
|
||||
reloadedConfig, err := config.Load(configDir)
|
||||
@ -61,7 +57,7 @@ func TestRemoveCurrentForce(t *testing.T) {
|
||||
|
||||
func TestRemoveDefault(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "other")
|
||||
createTestContext(t, cli, "other", nil)
|
||||
cli.SetCurrentContext("current")
|
||||
err := RunRemove(cli, RemoveOptions{}, []string{"default"})
|
||||
assert.ErrorContains(t, err, `default: context "default" cannot be removed`)
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
|
||||
func TestShow(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "current")
|
||||
createTestContext(t, cli, "current", nil)
|
||||
cli.SetCurrentContext("current")
|
||||
|
||||
cli.OutBuffer().Reset()
|
||||
|
||||
3
cli/command/context/testdata/inspect.golden
vendored
3
cli/command/context/testdata/inspect.golden
vendored
@ -2,7 +2,8 @@
|
||||
{
|
||||
"Name": "current",
|
||||
"Metadata": {
|
||||
"Description": "description of current"
|
||||
"Description": "description of current",
|
||||
"MyCustomMetadata": "MyCustomMetadataValue"
|
||||
},
|
||||
"Endpoints": {
|
||||
"docker": {
|
||||
|
||||
5
cli/command/context/testdata/list-json-name.golden
vendored
Normal file
5
cli/command/context/testdata/list-json-name.golden
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
"context1"
|
||||
"context2"
|
||||
"context3"
|
||||
"current"
|
||||
"default"
|
||||
5
cli/command/context/testdata/list-json.golden
vendored
Normal file
5
cli/command/context/testdata/list-json.golden
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{"Name":"context1","Description":"description of context1","DockerEndpoint":"https://someswarmserver.example.com","Current":false,"Error":"","ContextType":"aci"}
|
||||
{"Name":"context2","Description":"description of context2","DockerEndpoint":"https://someswarmserver.example.com","Current":false,"Error":"","ContextType":"ecs"}
|
||||
{"Name":"context3","Description":"description of context3","DockerEndpoint":"https://someswarmserver.example.com","Current":false,"Error":"","ContextType":"moby"}
|
||||
{"Name":"current","Description":"description of current","DockerEndpoint":"https://someswarmserver.example.com","Current":true,"Error":"","ContextType":"moby"}
|
||||
{"Name":"default","Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"unix:///var/run/docker.sock","Current":false,"Error":"","ContextType":"moby"}
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/context/docker"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/assert/cmp"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestUpdateDescriptionOnly(t *testing.T) {
|
||||
@ -34,7 +34,7 @@ func TestUpdateDescriptionOnly(t *testing.T) {
|
||||
|
||||
func TestUpdateDockerOnly(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "test")
|
||||
createTestContext(t, cli, "test", nil)
|
||||
assert.NilError(t, RunUpdate(cli, &UpdateOptions{
|
||||
Name: "test",
|
||||
Docker: map[string]string{
|
||||
@ -46,7 +46,7 @@ func TestUpdateDockerOnly(t *testing.T) {
|
||||
dc, err := command.GetDockerContext(c)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, dc.Description, "description of test")
|
||||
assert.Check(t, cmp.Contains(c.Endpoints, docker.DockerEndpoint))
|
||||
assert.Check(t, is.Contains(c.Endpoints, docker.DockerEndpoint))
|
||||
assert.Equal(t, c.Endpoints[docker.DockerEndpoint].(docker.EndpointMeta).Host, "tcp://some-host")
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package formatter
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
// ClientContextTableFormat is the default client context format.
|
||||
ClientContextTableFormat = "table {{.Name}}{{if .Current}} *{{end}}\t{{.Description}}\t{{.DockerEndpoint}}\t{{.Error}}"
|
||||
@ -28,6 +30,13 @@ type ClientContext struct {
|
||||
DockerEndpoint string
|
||||
Current bool
|
||||
Error string
|
||||
|
||||
// ContextType is a temporary field for compatibility with
|
||||
// Visual Studio, which depends on this from the "cloud integration"
|
||||
// wrapper.
|
||||
//
|
||||
// Deprecated: this type is only for backward-compatibility. Do not use.
|
||||
ContextType string `json:"ContextType,omitempty"`
|
||||
}
|
||||
|
||||
// ClientContextWrite writes formatted contexts using the Context
|
||||
@ -60,6 +69,13 @@ func newClientContextContext() *clientContextContext {
|
||||
}
|
||||
|
||||
func (c *clientContextContext) MarshalJSON() ([]byte, error) {
|
||||
if c.c.ContextType != "" {
|
||||
// We only have ContextType set for plain "json" or "{{json .}}" formatting,
|
||||
// so we should be able to just use the default json.Marshal with no
|
||||
// special handling.
|
||||
return json.Marshal(c.c)
|
||||
}
|
||||
// FIXME(thaJeztah): why do we need a special marshal function here?
|
||||
return MarshalJSON(c)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
218
cli/command/telemetry.go
Normal file
218
cli/command/telemetry.go
Normal file
@ -0,0 +1,218 @@
|
||||
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 the currently initialized TracerProvider. This TracerProvider will be configured
|
||||
// with the default tracing components for a CLI program
|
||||
TracerProvider() trace.TracerProvider
|
||||
|
||||
// MeterProvider returns the currently initialized MeterProvider. This MeterProvider will be configured
|
||||
// with the default metric components for a CLI program
|
||||
MeterProvider() metric.MeterProvider
|
||||
}
|
||||
|
||||
func (cli *DockerCli) Resource() *resource.Resource {
|
||||
return cli.res.Get()
|
||||
}
|
||||
|
||||
func (cli *DockerCli) TracerProvider() trace.TracerProvider {
|
||||
return otel.GetTracerProvider()
|
||||
}
|
||||
|
||||
func (cli *DockerCli) MeterProvider() metric.MeterProvider {
|
||||
return otel.GetMeterProvider()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// createGlobalMeterProvider creates a new MeterProvider from the initialized DockerCli struct
|
||||
// with the given options and sets it as the global meter provider
|
||||
func (cli *DockerCli) createGlobalMeterProvider(ctx context.Context, opts ...sdkmetric.Option) {
|
||||
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...)
|
||||
mp := sdkmetric.NewMeterProvider(allOpts...)
|
||||
otel.SetMeterProvider(mp)
|
||||
}
|
||||
|
||||
// createGlobalTracerProvider creates a new TracerProvider from the initialized DockerCli struct
|
||||
// with the given options and sets it as the global tracer provider
|
||||
func (cli *DockerCli) createGlobalTracerProvider(ctx context.Context, opts ...sdktrace.TracerProviderOption) {
|
||||
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...)
|
||||
tp := sdktrace.NewTracerProvider(allOpts...)
|
||||
otel.SetTracerProvider(tp)
|
||||
}
|
||||
|
||||
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 {
|
||||
// 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.ForceFlush(ctx)
|
||||
}
|
||||
|
||||
func (r *cliReader) ForceFlush(ctx context.Context) error {
|
||||
var rm metricdata.ResourceMetrics
|
||||
if err := r.Reader.Collect(ctx, &rm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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))}
|
||||
}
|
||||
182
cli/command/telemetry_utils.go
Normal file
182
cli/command/telemetry_utils.go
Normal file
@ -0,0 +1,182 @@
|
||||
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(ctx context.Context, cmd *cobra.Command) {
|
||||
// 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
|
||||
stopInstrumentation := cli.StartInstrumentation(cmd)
|
||||
cmdErr := ogRunE(cmd, args)
|
||||
stopInstrumentation(cmdErr)
|
||||
return cmdErr
|
||||
}
|
||||
|
||||
return ogPersistentPreRunE(cmd, args)
|
||||
}
|
||||
}
|
||||
|
||||
// StartInstrumentation instruments CLI commands with the individual metrics and spans configured.
|
||||
// It's the main command OTel utility, and new command-related metrics should be added to it.
|
||||
// It should be called immediately before command execution, and returns a stopInstrumentation function
|
||||
// that must be called with the error resulting from the command execution.
|
||||
func (cli *DockerCli) StartInstrumentation(cmd *cobra.Command) (stopInstrumentation func(error)) {
|
||||
baseAttrs := BaseCommandAttributes(cmd, cli)
|
||||
return startCobraCommandTimer(cli.MeterProvider(), baseAttrs)
|
||||
}
|
||||
|
||||
func startCobraCommandTimer(mp metric.MeterProvider, attrs []attribute.KeyValue) func(err error) {
|
||||
meter := getDefaultMeter(mp)
|
||||
durationCounter, _ := meter.Float64Counter(
|
||||
"command.time",
|
||||
metric.WithDescription("Measures the duration of the cobra command"),
|
||||
metric.WithUnit("ms"),
|
||||
)
|
||||
start := time.Now()
|
||||
|
||||
return func(err error) {
|
||||
// Use a new context for the export so that the command being cancelled
|
||||
// doesn't affect the metrics, and we get metrics for cancelled commands.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), exportTimeout)
|
||||
defer cancel()
|
||||
|
||||
duration := float64(time.Since(start)) / float64(time.Millisecond)
|
||||
cmdStatusAttrs := attributesFromError(err)
|
||||
durationCounter.Add(ctx, duration,
|
||||
metric.WithAttributes(attrs...),
|
||||
metric.WithAttributes(cmdStatusAttrs...),
|
||||
)
|
||||
if mp, ok := mp.(MeterProvider); ok {
|
||||
mp.ForceFlush(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
})
|
||||
|
||||
@ -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,15 @@ func runDocker(dockerCli *command.DockerCli) error {
|
||||
return err
|
||||
}
|
||||
|
||||
mp := dockerCli.MeterProvider()
|
||||
if mp, ok := mp.(command.MeterProvider); ok {
|
||||
defer mp.Shutdown(ctx)
|
||||
} else {
|
||||
fmt.Fprint(dockerCli.Err(), "Warning: Unexpected OTEL error, metrics may not be flushed")
|
||||
}
|
||||
|
||||
dockerCli.InstrumentCobraCommands(ctx, cmd)
|
||||
|
||||
var envs []string
|
||||
args, os.Args, envs, err = processAliases(dockerCli, cmd, args, os.Args)
|
||||
if err != nil {
|
||||
@ -306,23 +332,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.11"
|
||||
}
|
||||
variable "VERSION" {
|
||||
default = ""
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG ALPINE_VERSION=3.20
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS gen
|
||||
RUN apk add --no-cache bash git
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.21.8
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG GO_VERSION=1.21.11
|
||||
ARG ALPINE_VERSION=3.20
|
||||
|
||||
ARG BUILDX_VERSION=0.12.1
|
||||
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.21.8
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG GO_VERSION=1.21.11
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG GOLANGCI_LINT_VERSION=v1.55.2
|
||||
|
||||
FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.21.8
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG GO_VERSION=1.21.11
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG MODOUTDATED_VERSION=v0.8.0
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
|
||||
|
||||
@ -902,17 +902,19 @@ PS C:\> docker run --device=class/86E0D1E0-8089-11D0-9CE4-08003E301F73 mcr.micro
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> This is experimental feature and as such doesn't represent a stable API.
|
||||
> The CDI feature is experimental, and potentially subject to change.
|
||||
> CDI is currently only supported for Linux containers.
|
||||
|
||||
Container Device Interface (CDI) is a
|
||||
[standardized](https://github.com/cncf-tags/container-device-interface/blob/main/SPEC.md)
|
||||
mechanism for container runtimes to create containers which are able to
|
||||
interact with third party devices.
|
||||
[Container Device Interface
|
||||
(CDI)](https://github.com/cncf-tags/container-device-interface/blob/main/SPEC.md)
|
||||
is a standardized mechanism for container runtimes to create containers which
|
||||
are able to interact with third party devices.
|
||||
|
||||
With CDI, device configurations are defined using a JSON file. In addition to
|
||||
enabling the container to interact with the device node, it also lets you
|
||||
specify additional configuration for the device, such as kernel modules, host
|
||||
libraries, and environment variables.
|
||||
With CDI, device configurations are declaratively defined using a JSON or YAML
|
||||
file. In addition to enabling the container to interact with the device node,
|
||||
it also lets you specify additional configuration for the device, such as
|
||||
environment variables, host mounts (such as shared objects), and executable
|
||||
hooks.
|
||||
|
||||
You can reference a CDI device with the `--device` flag using the
|
||||
fully-qualified name of the device, as shown in the following example:
|
||||
@ -924,10 +926,10 @@ $ docker run --device=vendor.com/class=device-name --rm -it ubuntu
|
||||
This starts an `ubuntu` container with access to the specified CDI device,
|
||||
`vendor.com/class=device-name`, assuming that:
|
||||
|
||||
- A valid CDI specification (JSON file) for the requested device is available
|
||||
on the system running the daemon, in one of the configured CDI specification
|
||||
directories.
|
||||
- The CDI feature has been enabled on the daemon side, see [Enable CDI
|
||||
- A valid CDI specification (JSON or YAML file) for the requested device is
|
||||
available on the system running the daemon, in one of the configured CDI
|
||||
specification directories.
|
||||
- The CDI feature has been enabled in the daemon; see [Enable CDI
|
||||
devices](https://docs.docker.com/reference/cli/dockerd/#enable-cdi-devices).
|
||||
|
||||
### <a name="attach"></a> Attach to STDIN/STDOUT/STDERR (-a, --attach)
|
||||
@ -1363,6 +1365,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
|
||||
|
||||
@ -117,6 +117,12 @@ data traffic from the management traffic of the cluster.
|
||||
|
||||
If unspecified, the IP address or interface of the advertise address is used.
|
||||
|
||||
Setting `--data-path-addr` does not restrict which interfaces or source IP
|
||||
addresses the VXLAN socket is bound to. Similar to `--advertise-addr`, the
|
||||
purpose of this flag is to inform other members of the swarm about which
|
||||
address to use for control plane traffic. To restrict access to the VXLAN port
|
||||
of the node, use firewall rules.
|
||||
|
||||
### <a name="data-path-port"></a> Configure port number for data traffic (--data-path-port)
|
||||
|
||||
The `--data-path-port` flag allows you to configure the UDP port number to use
|
||||
|
||||
@ -530,7 +530,7 @@ For example:
|
||||
"runtimeType": "io.containerd.runsc.v1",
|
||||
"options": {
|
||||
"TypeUrl": "io.containerd.runsc.v1.options",
|
||||
"ConfigPath": "/etc/containerd/runsc.toml",
|
||||
"ConfigPath": "/etc/containerd/runsc.toml"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.11
|
||||
|
||||
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
|
||||
;;
|
||||
*)
|
||||
|
||||
42
vendor.mod
42
vendor.mod
@ -8,11 +8,11 @@ go 1.21
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0
|
||||
github.com/containerd/containerd v1.7.14
|
||||
github.com/containerd/platforms v0.2.0
|
||||
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-rc3.0.20240320143339-8b79278316b5+incompatible // master (v26.0.0-dev)
|
||||
github.com/docker/docker v26.1.4-0.20240605103321-de5c9cf0b96e+incompatible // 26.1 branch (v26.1.4-dev)
|
||||
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,13 +23,13 @@ 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
|
||||
github.com/morikuni/aec v1.0.0
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.0
|
||||
@ -38,30 +38,39 @@ 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
|
||||
tags.cncf.io/container-device-interface v0.6.2
|
||||
tags.cncf.io/container-device-interface v0.7.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.4 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.5 // 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/containerd v1.7.18 // 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.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 +87,13 @@ 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
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.19.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/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/grpc v1.60.1 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
)
|
||||
|
||||
85
vendor.sum
85
vendor.sum
@ -6,10 +6,10 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
|
||||
github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Microsoft/hcsshim v0.11.5 h1:haEcLNpj9Ka1gd3B3tAEs9CpE0c+1IhoL59w/exYU38=
|
||||
github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d h1:hi6J4K6DKrR4/ljxn6SF6nURyu785wKMuQcjt7H3VCQ=
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
@ -39,10 +39,12 @@ github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmD
|
||||
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.14 h1:H/XLzbnGuenZEGK+v0RkwTdv2u1QFAruMe5N0GNPJwA=
|
||||
github.com/containerd/containerd v1.7.14/go.mod h1:YMC9Qt5yzNqXx/fO4j/5yYVIHXSRrlB3H7sxkUTvspg=
|
||||
github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao=
|
||||
github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v0.2.0 h1:clGNvVIcY3k39VJSYdFGohI1b3bP/eeBUVR5+XA28oo=
|
||||
github.com/containerd/platforms v0.2.0/go.mod h1:XOM2BS6kN6gXafPLg80V6y/QUib+xoLyC3qVmHzibko=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
@ -57,8 +59,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-rc3.0.20240320143339-8b79278316b5+incompatible h1:Bp8IgclMsG8nlne6WQNkPE49qZHddTWm/LXx61DHNN4=
|
||||
github.com/docker/docker v26.0.0-rc3.0.20240320143339-8b79278316b5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v26.1.4-0.20240605103321-de5c9cf0b96e+incompatible h1:Zx3pZjoBZhEwxP38O4F6/NtfbhUIxzsz3H1N15dmZw8=
|
||||
github.com/docker/docker v26.1.4-0.20240605103321-de5c9cf0b96e+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=
|
||||
@ -87,8 +89,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=
|
||||
@ -101,6 +103,8 @@ 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=
|
||||
@ -174,8 +178,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=
|
||||
@ -201,8 +205,8 @@ github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoT
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@ -235,8 +239,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.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
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=
|
||||
@ -291,19 +295,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/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
|
||||
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.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
|
||||
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=
|
||||
@ -313,12 +325,10 @@ 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=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@ -326,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=
|
||||
@ -349,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=
|
||||
@ -365,20 +375,19 @@ golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
|
||||
golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
|
||||
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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
@ -410,5 +419,5 @@ 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=
|
||||
tags.cncf.io/container-device-interface v0.7.2 h1:MLqGnWfOr1wB7m08ieI4YJ3IoLKKozEnnNYBtacDPQU=
|
||||
tags.cncf.io/container-device-interface v0.7.2/go.mod h1:Xb1PvXv2BhfNb3tla4r9JL129ck1Lxv9KuU6eVOfKto=
|
||||
|
||||
10
vendor/github.com/Microsoft/go-winio/.golangci.yml
generated
vendored
10
vendor/github.com/Microsoft/go-winio/.golangci.yml
generated
vendored
@ -1,7 +1,3 @@
|
||||
run:
|
||||
skip-dirs:
|
||||
- pkg/etw/sample
|
||||
|
||||
linters:
|
||||
enable:
|
||||
# style
|
||||
@ -20,9 +16,13 @@ linters:
|
||||
- gofmt # files are gofmt'ed
|
||||
- gosec # security
|
||||
- nilerr # returns nil even with non-nil error
|
||||
- thelper # test helpers without t.Helper()
|
||||
- unparam # unused function params
|
||||
|
||||
issues:
|
||||
exclude-dirs:
|
||||
- pkg/etw/sample
|
||||
|
||||
exclude-rules:
|
||||
# err is very often shadowed in nested scopes
|
||||
- linters:
|
||||
@ -69,9 +69,7 @@ linters-settings:
|
||||
# struct order is often for Win32 compat
|
||||
# also, ignore pointer bytes/GC issues for now until performance becomes an issue
|
||||
- fieldalignment
|
||||
check-shadowing: true
|
||||
nolintlint:
|
||||
allow-leading-space: false
|
||||
require-explanation: true
|
||||
require-specific: true
|
||||
revive:
|
||||
|
||||
33
vendor/github.com/Microsoft/go-winio/backup.go
generated
vendored
33
vendor/github.com/Microsoft/go-winio/backup.go
generated
vendored
@ -10,14 +10,14 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/Microsoft/go-winio/internal/fs"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//sys backupRead(h syscall.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupRead
|
||||
//sys backupWrite(h syscall.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupWrite
|
||||
//sys backupRead(h windows.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupRead
|
||||
//sys backupWrite(h windows.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupWrite
|
||||
|
||||
const (
|
||||
BackupData = uint32(iota + 1)
|
||||
@ -104,7 +104,7 @@ func (r *BackupStreamReader) Next() (*BackupHeader, error) {
|
||||
if err := binary.Read(r.r, binary.LittleEndian, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hdr.Name = syscall.UTF16ToString(name)
|
||||
hdr.Name = windows.UTF16ToString(name)
|
||||
}
|
||||
if wsi.StreamID == BackupSparseBlock {
|
||||
if err := binary.Read(r.r, binary.LittleEndian, &hdr.Offset); err != nil {
|
||||
@ -205,7 +205,7 @@ func NewBackupFileReader(f *os.File, includeSecurity bool) *BackupFileReader {
|
||||
// Read reads a backup stream from the file by calling the Win32 API BackupRead().
|
||||
func (r *BackupFileReader) Read(b []byte) (int, error) {
|
||||
var bytesRead uint32
|
||||
err := backupRead(syscall.Handle(r.f.Fd()), b, &bytesRead, false, r.includeSecurity, &r.ctx)
|
||||
err := backupRead(windows.Handle(r.f.Fd()), b, &bytesRead, false, r.includeSecurity, &r.ctx)
|
||||
if err != nil {
|
||||
return 0, &os.PathError{Op: "BackupRead", Path: r.f.Name(), Err: err}
|
||||
}
|
||||
@ -220,7 +220,7 @@ func (r *BackupFileReader) Read(b []byte) (int, error) {
|
||||
// the underlying file.
|
||||
func (r *BackupFileReader) Close() error {
|
||||
if r.ctx != 0 {
|
||||
_ = backupRead(syscall.Handle(r.f.Fd()), nil, nil, true, false, &r.ctx)
|
||||
_ = backupRead(windows.Handle(r.f.Fd()), nil, nil, true, false, &r.ctx)
|
||||
runtime.KeepAlive(r.f)
|
||||
r.ctx = 0
|
||||
}
|
||||
@ -244,7 +244,7 @@ func NewBackupFileWriter(f *os.File, includeSecurity bool) *BackupFileWriter {
|
||||
// Write restores a portion of the file using the provided backup stream.
|
||||
func (w *BackupFileWriter) Write(b []byte) (int, error) {
|
||||
var bytesWritten uint32
|
||||
err := backupWrite(syscall.Handle(w.f.Fd()), b, &bytesWritten, false, w.includeSecurity, &w.ctx)
|
||||
err := backupWrite(windows.Handle(w.f.Fd()), b, &bytesWritten, false, w.includeSecurity, &w.ctx)
|
||||
if err != nil {
|
||||
return 0, &os.PathError{Op: "BackupWrite", Path: w.f.Name(), Err: err}
|
||||
}
|
||||
@ -259,7 +259,7 @@ func (w *BackupFileWriter) Write(b []byte) (int, error) {
|
||||
// close the underlying file.
|
||||
func (w *BackupFileWriter) Close() error {
|
||||
if w.ctx != 0 {
|
||||
_ = backupWrite(syscall.Handle(w.f.Fd()), nil, nil, true, false, &w.ctx)
|
||||
_ = backupWrite(windows.Handle(w.f.Fd()), nil, nil, true, false, &w.ctx)
|
||||
runtime.KeepAlive(w.f)
|
||||
w.ctx = 0
|
||||
}
|
||||
@ -271,17 +271,14 @@ func (w *BackupFileWriter) Close() error {
|
||||
//
|
||||
// If the file opened was a directory, it cannot be used with Readdir().
|
||||
func OpenForBackup(path string, access uint32, share uint32, createmode uint32) (*os.File, error) {
|
||||
winPath, err := syscall.UTF16FromString(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h, err := syscall.CreateFile(&winPath[0],
|
||||
access,
|
||||
share,
|
||||
h, err := fs.CreateFile(path,
|
||||
fs.AccessMask(access),
|
||||
fs.FileShareMode(share),
|
||||
nil,
|
||||
createmode,
|
||||
syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OPEN_REPARSE_POINT,
|
||||
0)
|
||||
fs.FileCreationDisposition(createmode),
|
||||
fs.FILE_FLAG_BACKUP_SEMANTICS|fs.FILE_FLAG_OPEN_REPARSE_POINT,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
err = &os.PathError{Op: "open", Path: path, Err: err}
|
||||
return nil, err
|
||||
|
||||
85
vendor/github.com/Microsoft/go-winio/file.go
generated
vendored
85
vendor/github.com/Microsoft/go-winio/file.go
generated
vendored
@ -15,26 +15,11 @@ import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//sys cancelIoEx(file syscall.Handle, o *syscall.Overlapped) (err error) = CancelIoEx
|
||||
//sys createIoCompletionPort(file syscall.Handle, port syscall.Handle, key uintptr, threadCount uint32) (newport syscall.Handle, err error) = CreateIoCompletionPort
|
||||
//sys getQueuedCompletionStatus(port syscall.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) = GetQueuedCompletionStatus
|
||||
//sys setFileCompletionNotificationModes(h syscall.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes
|
||||
//sys wsaGetOverlappedResult(h syscall.Handle, o *syscall.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
|
||||
|
||||
type atomicBool int32
|
||||
|
||||
func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 }
|
||||
func (b *atomicBool) setFalse() { atomic.StoreInt32((*int32)(b), 0) }
|
||||
func (b *atomicBool) setTrue() { atomic.StoreInt32((*int32)(b), 1) }
|
||||
|
||||
//revive:disable-next-line:predeclared Keep "new" to maintain consistency with "atomic" pkg
|
||||
func (b *atomicBool) swap(new bool) bool {
|
||||
var newInt int32
|
||||
if new {
|
||||
newInt = 1
|
||||
}
|
||||
return atomic.SwapInt32((*int32)(b), newInt) == 1
|
||||
}
|
||||
//sys cancelIoEx(file windows.Handle, o *windows.Overlapped) (err error) = CancelIoEx
|
||||
//sys createIoCompletionPort(file windows.Handle, port windows.Handle, key uintptr, threadCount uint32) (newport windows.Handle, err error) = CreateIoCompletionPort
|
||||
//sys getQueuedCompletionStatus(port windows.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) = GetQueuedCompletionStatus
|
||||
//sys setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes
|
||||
//sys wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
|
||||
|
||||
var (
|
||||
ErrFileClosed = errors.New("file has already been closed")
|
||||
@ -50,7 +35,7 @@ func (*timeoutError) Temporary() bool { return true }
|
||||
type timeoutChan chan struct{}
|
||||
|
||||
var ioInitOnce sync.Once
|
||||
var ioCompletionPort syscall.Handle
|
||||
var ioCompletionPort windows.Handle
|
||||
|
||||
// ioResult contains the result of an asynchronous IO operation.
|
||||
type ioResult struct {
|
||||
@ -60,12 +45,12 @@ type ioResult struct {
|
||||
|
||||
// ioOperation represents an outstanding asynchronous Win32 IO.
|
||||
type ioOperation struct {
|
||||
o syscall.Overlapped
|
||||
o windows.Overlapped
|
||||
ch chan ioResult
|
||||
}
|
||||
|
||||
func initIO() {
|
||||
h, err := createIoCompletionPort(syscall.InvalidHandle, 0, 0, 0xffffffff)
|
||||
h, err := createIoCompletionPort(windows.InvalidHandle, 0, 0, 0xffffffff)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -76,10 +61,10 @@ func initIO() {
|
||||
// win32File implements Reader, Writer, and Closer on a Win32 handle without blocking in a syscall.
|
||||
// It takes ownership of this handle and will close it if it is garbage collected.
|
||||
type win32File struct {
|
||||
handle syscall.Handle
|
||||
handle windows.Handle
|
||||
wg sync.WaitGroup
|
||||
wgLock sync.RWMutex
|
||||
closing atomicBool
|
||||
closing atomic.Bool
|
||||
socket bool
|
||||
readDeadline deadlineHandler
|
||||
writeDeadline deadlineHandler
|
||||
@ -90,11 +75,11 @@ type deadlineHandler struct {
|
||||
channel timeoutChan
|
||||
channelLock sync.RWMutex
|
||||
timer *time.Timer
|
||||
timedout atomicBool
|
||||
timedout atomic.Bool
|
||||
}
|
||||
|
||||
// makeWin32File makes a new win32File from an existing file handle.
|
||||
func makeWin32File(h syscall.Handle) (*win32File, error) {
|
||||
func makeWin32File(h windows.Handle) (*win32File, error) {
|
||||
f := &win32File{handle: h}
|
||||
ioInitOnce.Do(initIO)
|
||||
_, err := createIoCompletionPort(h, ioCompletionPort, 0, 0xffffffff)
|
||||
@ -110,7 +95,12 @@ func makeWin32File(h syscall.Handle) (*win32File, error) {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Deprecated: use NewOpenFile instead.
|
||||
func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) {
|
||||
return NewOpenFile(windows.Handle(h))
|
||||
}
|
||||
|
||||
func NewOpenFile(h windows.Handle) (io.ReadWriteCloser, error) {
|
||||
// If we return the result of makeWin32File directly, it can result in an
|
||||
// interface-wrapped nil, rather than a nil interface value.
|
||||
f, err := makeWin32File(h)
|
||||
@ -124,13 +114,13 @@ func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) {
|
||||
func (f *win32File) closeHandle() {
|
||||
f.wgLock.Lock()
|
||||
// Atomically set that we are closing, releasing the resources only once.
|
||||
if !f.closing.swap(true) {
|
||||
if !f.closing.Swap(true) {
|
||||
f.wgLock.Unlock()
|
||||
// cancel all IO and wait for it to complete
|
||||
_ = cancelIoEx(f.handle, nil)
|
||||
f.wg.Wait()
|
||||
// at this point, no new IO can start
|
||||
syscall.Close(f.handle)
|
||||
windows.Close(f.handle)
|
||||
f.handle = 0
|
||||
} else {
|
||||
f.wgLock.Unlock()
|
||||
@ -145,14 +135,14 @@ func (f *win32File) Close() error {
|
||||
|
||||
// IsClosed checks if the file has been closed.
|
||||
func (f *win32File) IsClosed() bool {
|
||||
return f.closing.isSet()
|
||||
return f.closing.Load()
|
||||
}
|
||||
|
||||
// prepareIO prepares for a new IO operation.
|
||||
// The caller must call f.wg.Done() when the IO is finished, prior to Close() returning.
|
||||
func (f *win32File) prepareIO() (*ioOperation, error) {
|
||||
f.wgLock.RLock()
|
||||
if f.closing.isSet() {
|
||||
if f.closing.Load() {
|
||||
f.wgLock.RUnlock()
|
||||
return nil, ErrFileClosed
|
||||
}
|
||||
@ -164,12 +154,12 @@ func (f *win32File) prepareIO() (*ioOperation, error) {
|
||||
}
|
||||
|
||||
// ioCompletionProcessor processes completed async IOs forever.
|
||||
func ioCompletionProcessor(h syscall.Handle) {
|
||||
func ioCompletionProcessor(h windows.Handle) {
|
||||
for {
|
||||
var bytes uint32
|
||||
var key uintptr
|
||||
var op *ioOperation
|
||||
err := getQueuedCompletionStatus(h, &bytes, &key, &op, syscall.INFINITE)
|
||||
err := getQueuedCompletionStatus(h, &bytes, &key, &op, windows.INFINITE)
|
||||
if op == nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -182,11 +172,11 @@ func ioCompletionProcessor(h syscall.Handle) {
|
||||
// asyncIO processes the return value from ReadFile or WriteFile, blocking until
|
||||
// the operation has actually completed.
|
||||
func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, err error) (int, error) {
|
||||
if err != syscall.ERROR_IO_PENDING { //nolint:errorlint // err is Errno
|
||||
if err != windows.ERROR_IO_PENDING { //nolint:errorlint // err is Errno
|
||||
return int(bytes), err
|
||||
}
|
||||
|
||||
if f.closing.isSet() {
|
||||
if f.closing.Load() {
|
||||
_ = cancelIoEx(f.handle, &c.o)
|
||||
}
|
||||
|
||||
@ -201,8 +191,8 @@ func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, er
|
||||
select {
|
||||
case r = <-c.ch:
|
||||
err = r.err
|
||||
if err == syscall.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
||||
if f.closing.isSet() {
|
||||
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
||||
if f.closing.Load() {
|
||||
err = ErrFileClosed
|
||||
}
|
||||
} else if err != nil && f.socket {
|
||||
@ -214,7 +204,7 @@ func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, er
|
||||
_ = cancelIoEx(f.handle, &c.o)
|
||||
r = <-c.ch
|
||||
err = r.err
|
||||
if err == syscall.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
||||
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
||||
err = ErrTimeout
|
||||
}
|
||||
}
|
||||
@ -235,23 +225,22 @@ func (f *win32File) Read(b []byte) (int, error) {
|
||||
}
|
||||
defer f.wg.Done()
|
||||
|
||||
if f.readDeadline.timedout.isSet() {
|
||||
if f.readDeadline.timedout.Load() {
|
||||
return 0, ErrTimeout
|
||||
}
|
||||
|
||||
var bytes uint32
|
||||
err = syscall.ReadFile(f.handle, b, &bytes, &c.o)
|
||||
err = windows.ReadFile(f.handle, b, &bytes, &c.o)
|
||||
n, err := f.asyncIO(c, &f.readDeadline, bytes, err)
|
||||
runtime.KeepAlive(b)
|
||||
|
||||
// Handle EOF conditions.
|
||||
if err == nil && n == 0 && len(b) != 0 {
|
||||
return 0, io.EOF
|
||||
} else if err == syscall.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno
|
||||
} else if err == windows.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno
|
||||
return 0, io.EOF
|
||||
} else {
|
||||
return n, err
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Write writes to a file handle.
|
||||
@ -262,12 +251,12 @@ func (f *win32File) Write(b []byte) (int, error) {
|
||||
}
|
||||
defer f.wg.Done()
|
||||
|
||||
if f.writeDeadline.timedout.isSet() {
|
||||
if f.writeDeadline.timedout.Load() {
|
||||
return 0, ErrTimeout
|
||||
}
|
||||
|
||||
var bytes uint32
|
||||
err = syscall.WriteFile(f.handle, b, &bytes, &c.o)
|
||||
err = windows.WriteFile(f.handle, b, &bytes, &c.o)
|
||||
n, err := f.asyncIO(c, &f.writeDeadline, bytes, err)
|
||||
runtime.KeepAlive(b)
|
||||
return n, err
|
||||
@ -282,7 +271,7 @@ func (f *win32File) SetWriteDeadline(deadline time.Time) error {
|
||||
}
|
||||
|
||||
func (f *win32File) Flush() error {
|
||||
return syscall.FlushFileBuffers(f.handle)
|
||||
return windows.FlushFileBuffers(f.handle)
|
||||
}
|
||||
|
||||
func (f *win32File) Fd() uintptr {
|
||||
@ -299,7 +288,7 @@ func (d *deadlineHandler) set(deadline time.Time) error {
|
||||
}
|
||||
d.timer = nil
|
||||
}
|
||||
d.timedout.setFalse()
|
||||
d.timedout.Store(false)
|
||||
|
||||
select {
|
||||
case <-d.channel:
|
||||
@ -314,7 +303,7 @@ func (d *deadlineHandler) set(deadline time.Time) error {
|
||||
}
|
||||
|
||||
timeoutIO := func() {
|
||||
d.timedout.setTrue()
|
||||
d.timedout.Store(true)
|
||||
close(d.channel)
|
||||
}
|
||||
|
||||
|
||||
22
vendor/github.com/Microsoft/go-winio/fileinfo.go
generated
vendored
22
vendor/github.com/Microsoft/go-winio/fileinfo.go
generated
vendored
@ -18,9 +18,18 @@ type FileBasicInfo struct {
|
||||
_ uint32 // padding
|
||||
}
|
||||
|
||||
// alignedFileBasicInfo is a FileBasicInfo, but aligned to uint64 by containing
|
||||
// uint64 rather than windows.Filetime. Filetime contains two uint32s. uint64
|
||||
// alignment is necessary to pass this as FILE_BASIC_INFO.
|
||||
type alignedFileBasicInfo struct {
|
||||
CreationTime, LastAccessTime, LastWriteTime, ChangeTime uint64
|
||||
FileAttributes uint32
|
||||
_ uint32 // padding
|
||||
}
|
||||
|
||||
// GetFileBasicInfo retrieves times and attributes for a file.
|
||||
func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) {
|
||||
bi := &FileBasicInfo{}
|
||||
bi := &alignedFileBasicInfo{}
|
||||
if err := windows.GetFileInformationByHandleEx(
|
||||
windows.Handle(f.Fd()),
|
||||
windows.FileBasicInfo,
|
||||
@ -30,16 +39,21 @@ func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) {
|
||||
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(f)
|
||||
return bi, nil
|
||||
// Reinterpret the alignedFileBasicInfo as a FileBasicInfo so it matches the
|
||||
// public API of this module. The data may be unnecessarily aligned.
|
||||
return (*FileBasicInfo)(unsafe.Pointer(bi)), nil
|
||||
}
|
||||
|
||||
// SetFileBasicInfo sets times and attributes for a file.
|
||||
func SetFileBasicInfo(f *os.File, bi *FileBasicInfo) error {
|
||||
// Create an alignedFileBasicInfo based on a FileBasicInfo. The copy is
|
||||
// suitable to pass to GetFileInformationByHandleEx.
|
||||
biAligned := *(*alignedFileBasicInfo)(unsafe.Pointer(bi))
|
||||
if err := windows.SetFileInformationByHandle(
|
||||
windows.Handle(f.Fd()),
|
||||
windows.FileBasicInfo,
|
||||
(*byte)(unsafe.Pointer(bi)),
|
||||
uint32(unsafe.Sizeof(*bi)),
|
||||
(*byte)(unsafe.Pointer(&biAligned)),
|
||||
uint32(unsafe.Sizeof(biAligned)),
|
||||
); err != nil {
|
||||
return &os.PathError{Op: "SetFileInformationByHandle", Path: f.Name(), Err: err}
|
||||
}
|
||||
|
||||
47
vendor/github.com/Microsoft/go-winio/hvsock.go
generated
vendored
47
vendor/github.com/Microsoft/go-winio/hvsock.go
generated
vendored
@ -10,7 +10,6 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
@ -181,13 +180,13 @@ type HvsockConn struct {
|
||||
var _ net.Conn = &HvsockConn{}
|
||||
|
||||
func newHVSocket() (*win32File, error) {
|
||||
fd, err := syscall.Socket(afHVSock, syscall.SOCK_STREAM, 1)
|
||||
fd, err := windows.Socket(afHVSock, windows.SOCK_STREAM, 1)
|
||||
if err != nil {
|
||||
return nil, os.NewSyscallError("socket", err)
|
||||
}
|
||||
f, err := makeWin32File(fd)
|
||||
if err != nil {
|
||||
syscall.Close(fd)
|
||||
windows.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
f.socket = true
|
||||
@ -197,16 +196,24 @@ func newHVSocket() (*win32File, error) {
|
||||
// ListenHvsock listens for connections on the specified hvsock address.
|
||||
func ListenHvsock(addr *HvsockAddr) (_ *HvsockListener, err error) {
|
||||
l := &HvsockListener{addr: *addr}
|
||||
sock, err := newHVSocket()
|
||||
|
||||
var sock *win32File
|
||||
sock, err = newHVSocket()
|
||||
if err != nil {
|
||||
return nil, l.opErr("listen", err)
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = sock.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
sa := addr.raw()
|
||||
err = socket.Bind(windows.Handle(sock.handle), &sa)
|
||||
err = socket.Bind(sock.handle, &sa)
|
||||
if err != nil {
|
||||
return nil, l.opErr("listen", os.NewSyscallError("socket", err))
|
||||
}
|
||||
err = syscall.Listen(sock.handle, 16)
|
||||
err = windows.Listen(sock.handle, 16)
|
||||
if err != nil {
|
||||
return nil, l.opErr("listen", os.NewSyscallError("listen", err))
|
||||
}
|
||||
@ -246,7 +253,7 @@ func (l *HvsockListener) Accept() (_ net.Conn, err error) {
|
||||
var addrbuf [addrlen * 2]byte
|
||||
|
||||
var bytes uint32
|
||||
err = syscall.AcceptEx(l.sock.handle, sock.handle, &addrbuf[0], 0 /* rxdatalen */, addrlen, addrlen, &bytes, &c.o)
|
||||
err = windows.AcceptEx(l.sock.handle, sock.handle, &addrbuf[0], 0 /* rxdatalen */, addrlen, addrlen, &bytes, &c.o)
|
||||
if _, err = l.sock.asyncIO(c, nil, bytes, err); err != nil {
|
||||
return nil, l.opErr("accept", os.NewSyscallError("acceptex", err))
|
||||
}
|
||||
@ -263,7 +270,7 @@ func (l *HvsockListener) Accept() (_ net.Conn, err error) {
|
||||
conn.remote.fromRaw((*rawHvsockAddr)(unsafe.Pointer(&addrbuf[addrlen])))
|
||||
|
||||
// initialize the accepted socket and update its properties with those of the listening socket
|
||||
if err = windows.Setsockopt(windows.Handle(sock.handle),
|
||||
if err = windows.Setsockopt(sock.handle,
|
||||
windows.SOL_SOCKET, windows.SO_UPDATE_ACCEPT_CONTEXT,
|
||||
(*byte)(unsafe.Pointer(&l.sock.handle)), int32(unsafe.Sizeof(l.sock.handle))); err != nil {
|
||||
return nil, conn.opErr("accept", os.NewSyscallError("setsockopt", err))
|
||||
@ -334,7 +341,7 @@ func (d *HvsockDialer) Dial(ctx context.Context, addr *HvsockAddr) (conn *Hvsock
|
||||
}()
|
||||
|
||||
sa := addr.raw()
|
||||
err = socket.Bind(windows.Handle(sock.handle), &sa)
|
||||
err = socket.Bind(sock.handle, &sa)
|
||||
if err != nil {
|
||||
return nil, conn.opErr(op, os.NewSyscallError("bind", err))
|
||||
}
|
||||
@ -347,7 +354,7 @@ func (d *HvsockDialer) Dial(ctx context.Context, addr *HvsockAddr) (conn *Hvsock
|
||||
var bytes uint32
|
||||
for i := uint(0); i <= d.Retries; i++ {
|
||||
err = socket.ConnectEx(
|
||||
windows.Handle(sock.handle),
|
||||
sock.handle,
|
||||
&sa,
|
||||
nil, // sendBuf
|
||||
0, // sendDataLen
|
||||
@ -367,7 +374,7 @@ func (d *HvsockDialer) Dial(ctx context.Context, addr *HvsockAddr) (conn *Hvsock
|
||||
|
||||
// update the connection properties, so shutdown can be used
|
||||
if err = windows.Setsockopt(
|
||||
windows.Handle(sock.handle),
|
||||
sock.handle,
|
||||
windows.SOL_SOCKET,
|
||||
windows.SO_UPDATE_CONNECT_CONTEXT,
|
||||
nil, // optvalue
|
||||
@ -378,7 +385,7 @@ func (d *HvsockDialer) Dial(ctx context.Context, addr *HvsockAddr) (conn *Hvsock
|
||||
|
||||
// get the local name
|
||||
var sal rawHvsockAddr
|
||||
err = socket.GetSockName(windows.Handle(sock.handle), &sal)
|
||||
err = socket.GetSockName(sock.handle, &sal)
|
||||
if err != nil {
|
||||
return nil, conn.opErr(op, os.NewSyscallError("getsockname", err))
|
||||
}
|
||||
@ -421,7 +428,7 @@ func (d *HvsockDialer) redialWait(ctx context.Context) (err error) {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// assumes error is a plain, unwrapped syscall.Errno provided by direct syscall.
|
||||
// assumes error is a plain, unwrapped windows.Errno provided by direct syscall.
|
||||
func canRedial(err error) bool {
|
||||
//nolint:errorlint // guaranteed to be an Errno
|
||||
switch err {
|
||||
@ -447,9 +454,9 @@ func (conn *HvsockConn) Read(b []byte) (int, error) {
|
||||
return 0, conn.opErr("read", err)
|
||||
}
|
||||
defer conn.sock.wg.Done()
|
||||
buf := syscall.WSABuf{Buf: &b[0], Len: uint32(len(b))}
|
||||
buf := windows.WSABuf{Buf: &b[0], Len: uint32(len(b))}
|
||||
var flags, bytes uint32
|
||||
err = syscall.WSARecv(conn.sock.handle, &buf, 1, &bytes, &flags, &c.o, nil)
|
||||
err = windows.WSARecv(conn.sock.handle, &buf, 1, &bytes, &flags, &c.o, nil)
|
||||
n, err := conn.sock.asyncIO(c, &conn.sock.readDeadline, bytes, err)
|
||||
if err != nil {
|
||||
var eno windows.Errno
|
||||
@ -482,9 +489,9 @@ func (conn *HvsockConn) write(b []byte) (int, error) {
|
||||
return 0, conn.opErr("write", err)
|
||||
}
|
||||
defer conn.sock.wg.Done()
|
||||
buf := syscall.WSABuf{Buf: &b[0], Len: uint32(len(b))}
|
||||
buf := windows.WSABuf{Buf: &b[0], Len: uint32(len(b))}
|
||||
var bytes uint32
|
||||
err = syscall.WSASend(conn.sock.handle, &buf, 1, &bytes, 0, &c.o, nil)
|
||||
err = windows.WSASend(conn.sock.handle, &buf, 1, &bytes, 0, &c.o, nil)
|
||||
n, err := conn.sock.asyncIO(c, &conn.sock.writeDeadline, bytes, err)
|
||||
if err != nil {
|
||||
var eno windows.Errno
|
||||
@ -511,7 +518,7 @@ func (conn *HvsockConn) shutdown(how int) error {
|
||||
return socket.ErrSocketClosed
|
||||
}
|
||||
|
||||
err := syscall.Shutdown(conn.sock.handle, how)
|
||||
err := windows.Shutdown(conn.sock.handle, how)
|
||||
if err != nil {
|
||||
// If the connection was closed, shutdowns fail with "not connected"
|
||||
if errors.Is(err, windows.WSAENOTCONN) ||
|
||||
@ -525,7 +532,7 @@ func (conn *HvsockConn) shutdown(how int) error {
|
||||
|
||||
// CloseRead shuts down the read end of the socket, preventing future read operations.
|
||||
func (conn *HvsockConn) CloseRead() error {
|
||||
err := conn.shutdown(syscall.SHUT_RD)
|
||||
err := conn.shutdown(windows.SHUT_RD)
|
||||
if err != nil {
|
||||
return conn.opErr("closeread", err)
|
||||
}
|
||||
@ -535,7 +542,7 @@ func (conn *HvsockConn) CloseRead() error {
|
||||
// CloseWrite shuts down the write end of the socket, preventing future write operations and
|
||||
// notifying the other endpoint that no more data will be written.
|
||||
func (conn *HvsockConn) CloseWrite() error {
|
||||
err := conn.shutdown(syscall.SHUT_WR)
|
||||
err := conn.shutdown(windows.SHUT_WR)
|
||||
if err != nil {
|
||||
return conn.opErr("closewrite", err)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user