Compare commits
183 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b0631f45e | |||
| 84828b0eb8 | |||
| b5ca7e8e6b | |||
| cfaaeb0982 | |||
| 5a8120c809 | |||
| c27751fcfe | |||
| ff5fdfae35 | |||
| 9f19820f88 | |||
| 7607c3f945 | |||
| 61cd986723 | |||
| 30cac75693 | |||
| 255a5f630e | |||
| 535bb6c85c | |||
| 47775a8fa0 | |||
| 4a80c6da83 | |||
| b199ece92a | |||
| 48741f72ff | |||
| 4541df21e5 | |||
| eaf98b2202 | |||
| 98d0b0cc14 | |||
| 5ea072d936 | |||
| 08f86507b4 | |||
| 66eb27a487 | |||
| e002576821 | |||
| a9ac6fa376 | |||
| 23eadcd950 | |||
| 3b45f3c09a | |||
| 79141ce5eb | |||
| 0442a7378f | |||
| 082dfb7360 | |||
| f519a8648d | |||
| bb0e9adbc0 | |||
| e0979b3adf | |||
| cab5164877 | |||
| 888716aa59 | |||
| 667fa7bc92 | |||
| 63f5930c17 | |||
| 0f75059e9f | |||
| 0ce8989a78 | |||
| 2f795987d6 | |||
| 5185ab89fe | |||
| 344a85eae6 | |||
| c81f38feac | |||
| 966b44183f | |||
| ecfdf74115 | |||
| d6d8ca6ebe | |||
| 3a35b16669 | |||
| 091421f13f | |||
| d1a19d4476 | |||
| 40725aea3c | |||
| fdcfd229aa | |||
| abd02b6a23 | |||
| 2d74733942 | |||
| d421dea843 | |||
| 4bdfd3b684 | |||
| 09caaa312d | |||
| 4dfe7ad85e | |||
| 3cdc44568d | |||
| 19ce7f2eaf | |||
| 1673cd88a8 | |||
| a9e6180cd8 | |||
| 3d3f78028a | |||
| 64b56179b5 | |||
| 29c1ababd7 | |||
| 2cd4786630 | |||
| 571124d4b0 | |||
| 60ae1bb1fc | |||
| 879acd15ff | |||
| a07391c65d | |||
| 650b45a42a | |||
| bc57a035c4 | |||
| a390a32da1 | |||
| 70bf6cb7c5 | |||
| e9cf371b56 | |||
| c26090bd3e | |||
| 7ec69def79 | |||
| d2b751ce58 | |||
| a5ec6c2963 | |||
| ce3090ccc4 | |||
| 802d8e801a | |||
| 6bd6b3e8ac | |||
| 2c0f9f476d | |||
| e73fb7d2f6 | |||
| 1bd58b0936 | |||
| 44e5100232 | |||
| 79c9c7e3e4 | |||
| 5f13d0f2b5 | |||
| d0d91bb0cd | |||
| ad21055bac | |||
| 4c882e0f6c | |||
| bc90bb6855 | |||
| 58a35692d6 | |||
| f6d49e9ca4 | |||
| 46caf5697c | |||
| 2eec74659e | |||
| 8fc0c74f9a | |||
| e201b4e8a5 | |||
| 292713c887 | |||
| 4321293972 | |||
| aa66f07a3e | |||
| ceef542046 | |||
| f9b3c8ce10 | |||
| b2a669fb56 | |||
| e37d814ce9 | |||
| d80436021c | |||
| c6f456bc90 | |||
| e558b915c2 | |||
| a9e530999e | |||
| a89a15a85c | |||
| 4be2ddedd3 | |||
| 8dcde50b6e | |||
| 6d551e0a5a | |||
| a2d78071c1 | |||
| df209212cf | |||
| ea1f10b440 | |||
| 7bcbe0837b | |||
| 0b985e74f1 | |||
| 95ac11e714 | |||
| 8ad07217dc | |||
| e32d5d56f5 | |||
| 985b58e7e1 | |||
| 3b5dff2783 | |||
| c775585e6c | |||
| 9bc16bbde0 | |||
| 2793731977 | |||
| 539f6de682 | |||
| cdc2cdc2a8 | |||
| d962a90517 | |||
| 6f46cd2f4b | |||
| 1a165fd535 | |||
| 293bbb44a0 | |||
| 8bedb69f2c | |||
| 9dc175d6ef | |||
| 43a2fcf5d7 | |||
| e3da0cc584 | |||
| 076ec3b56e | |||
| 124716ba6b | |||
| fda7da2303 | |||
| 3f154adf70 | |||
| c7072a885d | |||
| 7a6270d190 | |||
| d95385057f | |||
| e6382db10e | |||
| 55bc30a784 | |||
| 049f84c94d | |||
| 791bdf7b3c | |||
| 1d8f87a2fb | |||
| d4217eb205 | |||
| dd617b1464 | |||
| eae4c38023 | |||
| eb82fe87a5 | |||
| 55a83aff23 | |||
| fe0a8d2791 | |||
| b414752ef8 | |||
| 7b78eabcab | |||
| 9e997a57fa | |||
| 068a01ea94 | |||
| d75f8d83d3 | |||
| ffdfc5f94d | |||
| 6bd9908388 | |||
| 75595836f2 | |||
| 41277f53d5 | |||
| 4e7497e9cf | |||
| be669099cb | |||
| 111468ccd6 | |||
| 427c1361c5 | |||
| 656523e20d | |||
| aad2ae50e8 | |||
| 8a1b096e76 | |||
| c99d3312eb | |||
| 77a8a8c6ca | |||
| 0cff340983 | |||
| eb48cad302 | |||
| 2493a96027 | |||
| 8f55738579 | |||
| 768d10767f | |||
| d5e6e2ec6e | |||
| da4b6275ba | |||
| 7e71782ba6 | |||
| 762d59359e | |||
| 8890a1c929 | |||
| cfe0605616 | |||
| 48dbdc6f2d |
@ -1,6 +1,6 @@
|
||||
/build/
|
||||
/cli/winresources/versioninfo.json
|
||||
/cli/winresources/*.syso
|
||||
/cmd/docker/winresources/versioninfo.json
|
||||
/cmd/docker/winresources/*.syso
|
||||
/man/man*/
|
||||
/man/vendor/
|
||||
/man/go.sum
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@ -63,7 +63,7 @@ jobs:
|
||||
name: Update Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23.6"
|
||||
go-version: "1.23.7"
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
||||
14
.github/workflows/e2e.yml
vendored
14
.github/workflows/e2e.yml
vendored
@ -25,24 +25,22 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
tests:
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- non-experimental
|
||||
- experimental
|
||||
- local
|
||||
- connhelper-ssh
|
||||
base:
|
||||
- alpine
|
||||
- debian
|
||||
engine-version:
|
||||
- 27.0 # latest
|
||||
- 26.1 # latest - 1
|
||||
- 23.0 # mirantis lts
|
||||
# TODO(krissetto) 19.03 needs a look, doesn't work ubuntu 22.04 (cgroup errors).
|
||||
# we could have a separate job that tests it against ubuntu 20.04
|
||||
- 28 # latest
|
||||
- 27 # latest - 1
|
||||
- 26 # github actions default
|
||||
- 23 # mirantis lts
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -66,7 +66,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23.6"
|
||||
go-version: "1.23.7"
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
|
||||
6
.github/workflows/validate-pr.yml
vendored
6
.github/workflows/validate-pr.yml
vendored
@ -15,7 +15,7 @@ on:
|
||||
|
||||
jobs:
|
||||
check-area-label:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
steps:
|
||||
- name: Missing `area/` label
|
||||
@ -27,7 +27,7 @@ jobs:
|
||||
run: exit 0
|
||||
|
||||
check-changelog:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
env:
|
||||
HAS_IMPACT_LABEL: ${{ contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') }}
|
||||
@ -65,7 +65,7 @@ jobs:
|
||||
echo "$desc"
|
||||
|
||||
check-pr-branch:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -8,8 +8,8 @@
|
||||
Thumbs.db
|
||||
.editorconfig
|
||||
/build/
|
||||
/cli/winresources/versioninfo.json
|
||||
/cli/winresources/*.syso
|
||||
/cmd/docker/winresources/versioninfo.json
|
||||
/cmd/docker/winresources/*.syso
|
||||
profile.out
|
||||
|
||||
# top-level go.mod is not meant to be checked in
|
||||
|
||||
@ -44,7 +44,7 @@ run:
|
||||
# which causes it to fallback to go1.17 semantics.
|
||||
#
|
||||
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
|
||||
go: "1.23.6"
|
||||
go: "1.23.7"
|
||||
timeout: 5m
|
||||
|
||||
linters-settings:
|
||||
@ -62,6 +62,8 @@ linters-settings:
|
||||
desc: The containerd platforms package was migrated to a separate module. Use github.com/containerd/platforms instead.
|
||||
- pkg: "github.com/docker/docker/pkg/system"
|
||||
desc: This package should not be used unless strictly necessary.
|
||||
- pkg: "github.com/docker/distribution/uuid"
|
||||
desc: Use github.com/google/uuid instead.
|
||||
- pkg: "io/ioutil"
|
||||
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
|
||||
gocyclo:
|
||||
|
||||
@ -4,7 +4,7 @@ ARG BASE_VARIANT=alpine
|
||||
ARG ALPINE_VERSION=3.21
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.23.6
|
||||
ARG GO_VERSION=1.23.7
|
||||
ARG XX_VERSION=1.6.1
|
||||
ARG GOVERSIONINFO_VERSION=v1.4.1
|
||||
ARG GOTESTSUM_VERSION=v1.12.0
|
||||
@ -67,7 +67,7 @@ ARG PACKAGER_NAME
|
||||
COPY --link --from=goversioninfo /out/goversioninfo /usr/bin/goversioninfo
|
||||
RUN --mount=type=bind,target=.,ro \
|
||||
--mount=type=cache,target=/root/.cache \
|
||||
--mount=type=tmpfs,target=cli/winresources \
|
||||
--mount=type=tmpfs,target=cmd/docker/winresources \
|
||||
# override the default behavior of go with xx-go
|
||||
xx-go --wrap && \
|
||||
# export GOCACHE=$(go env GOCACHE)/$(xx-info)$([ -f /etc/alpine-release ] && echo "alpine") && \
|
||||
|
||||
14
Makefile
14
Makefile
@ -67,20 +67,20 @@ dynbinary: ## build dynamically linked binary
|
||||
|
||||
.PHONY: plugins
|
||||
plugins: ## build example CLI plugins
|
||||
./scripts/build/plugins
|
||||
scripts/build/plugins
|
||||
|
||||
.PHONY: vendor
|
||||
vendor: ## update vendor with go modules
|
||||
rm -rf vendor
|
||||
./scripts/vendor update
|
||||
scripts/with-go-mod.sh scripts/vendor update
|
||||
|
||||
.PHONY: validate-vendor
|
||||
validate-vendor: ## validate vendor
|
||||
./scripts/vendor validate
|
||||
scripts/with-go-mod.sh scripts/vendor validate
|
||||
|
||||
.PHONY: mod-outdated
|
||||
mod-outdated: ## check outdated dependencies
|
||||
./scripts/vendor outdated
|
||||
scripts/with-go-mod.sh scripts/vendor outdated
|
||||
|
||||
.PHONY: authors
|
||||
authors: ## generate AUTHORS file from git history
|
||||
@ -115,15 +115,15 @@ shell-completion: ## generate shell-completion scripts
|
||||
|
||||
.PHONY: manpages
|
||||
manpages: ## generate man pages from go source and markdown
|
||||
scripts/docs/generate-man.sh
|
||||
scripts/with-go-mod.sh scripts/docs/generate-man.sh
|
||||
|
||||
.PHONY: mddocs
|
||||
mddocs: ## generate markdown files from go source
|
||||
scripts/docs/generate-md.sh
|
||||
scripts/with-go-mod.sh scripts/docs/generate-md.sh
|
||||
|
||||
.PHONY: yamldocs
|
||||
yamldocs: ## generate documentation YAML files consumed by docs repo
|
||||
scripts/docs/generate-yaml.sh
|
||||
scripts/with-go-mod.sh scripts/docs/generate-yaml.sh
|
||||
|
||||
.PHONY: help
|
||||
help: ## print this help
|
||||
|
||||
@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli-plugins/plugin"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
@ -97,7 +97,7 @@ func main() {
|
||||
cmd.AddCommand(goodbye, apiversion, exitStatus2)
|
||||
return cmd
|
||||
},
|
||||
manager.Metadata{
|
||||
metadata.Metadata{
|
||||
SchemaVersion: "0.1.0",
|
||||
Vendor: "Docker Inc.",
|
||||
Version: "testing",
|
||||
|
||||
30
cli-plugins/manager/annotations.go
Normal file
30
cli-plugins/manager/annotations.go
Normal file
@ -0,0 +1,30 @@
|
||||
package manager
|
||||
|
||||
import "github.com/docker/cli/cli-plugins/metadata"
|
||||
|
||||
const (
|
||||
// CommandAnnotationPlugin is added to every stub command added by
|
||||
// AddPluginCommandStubs with the value "true" and so can be
|
||||
// used to distinguish plugin stubs from regular commands.
|
||||
CommandAnnotationPlugin = metadata.CommandAnnotationPlugin
|
||||
|
||||
// CommandAnnotationPluginVendor is added to every stub command
|
||||
// added by AddPluginCommandStubs and contains the vendor of
|
||||
// that plugin.
|
||||
CommandAnnotationPluginVendor = metadata.CommandAnnotationPluginVendor
|
||||
|
||||
// CommandAnnotationPluginVersion is added to every stub command
|
||||
// added by AddPluginCommandStubs and contains the version of
|
||||
// that plugin.
|
||||
CommandAnnotationPluginVersion = metadata.CommandAnnotationPluginVersion
|
||||
|
||||
// CommandAnnotationPluginInvalid is added to any stub command
|
||||
// added by AddPluginCommandStubs for an invalid command (that
|
||||
// is, one which failed it's candidate test) and contains the
|
||||
// reason for the failure.
|
||||
CommandAnnotationPluginInvalid = metadata.CommandAnnotationPluginInvalid
|
||||
|
||||
// CommandAnnotationPluginCommandPath is added to overwrite the
|
||||
// command path for a plugin invocation.
|
||||
CommandAnnotationPluginCommandPath = metadata.CommandAnnotationPluginCommandPath
|
||||
)
|
||||
@ -1,6 +1,10 @@
|
||||
package manager
|
||||
|
||||
import "os/exec"
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
)
|
||||
|
||||
// Candidate represents a possible plugin candidate, for mocking purposes
|
||||
type Candidate interface {
|
||||
@ -17,5 +21,5 @@ func (c *candidate) Path() string {
|
||||
}
|
||||
|
||||
func (c *candidate) Metadata() ([]byte, error) {
|
||||
return exec.Command(c.path, MetadataSubcommandName).Output() // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
||||
return exec.Command(c.path, metadata.MetadataSubcommandName).Output() // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/assert/cmp"
|
||||
@ -30,10 +31,10 @@ func (c *fakeCandidate) Metadata() ([]byte, error) {
|
||||
|
||||
func TestValidateCandidate(t *testing.T) {
|
||||
const (
|
||||
goodPluginName = NamePrefix + "goodplugin"
|
||||
goodPluginName = metadata.NamePrefix + "goodplugin"
|
||||
|
||||
builtinName = NamePrefix + "builtin"
|
||||
builtinAlias = NamePrefix + "alias"
|
||||
builtinName = metadata.NamePrefix + "builtin"
|
||||
builtinAlias = metadata.NamePrefix + "alias"
|
||||
|
||||
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
|
||||
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
|
||||
@ -43,9 +44,9 @@ func TestValidateCandidate(t *testing.T) {
|
||||
|
||||
fakeroot := &cobra.Command{Use: "docker"}
|
||||
fakeroot.AddCommand(&cobra.Command{
|
||||
Use: strings.TrimPrefix(builtinName, NamePrefix),
|
||||
Use: strings.TrimPrefix(builtinName, metadata.NamePrefix),
|
||||
Aliases: []string{
|
||||
strings.TrimPrefix(builtinAlias, NamePrefix),
|
||||
strings.TrimPrefix(builtinAlias, metadata.NamePrefix),
|
||||
},
|
||||
})
|
||||
|
||||
@ -59,7 +60,7 @@ func TestValidateCandidate(t *testing.T) {
|
||||
}{
|
||||
/* Each failing one of the tests */
|
||||
{name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
|
||||
{name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", NamePrefix)},
|
||||
{name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix)},
|
||||
{name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
|
||||
{name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`},
|
||||
{name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`},
|
||||
@ -84,7 +85,7 @@ func TestValidateCandidate(t *testing.T) {
|
||||
assert.ErrorContains(t, p.Err, tc.invalid)
|
||||
default:
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, NamePrefix+p.Name, goodPluginName)
|
||||
assert.Equal(t, metadata.NamePrefix+p.Name, goodPluginName)
|
||||
assert.Equal(t, p.SchemaVersion, "0.1.0")
|
||||
assert.Equal(t, p.Vendor, "e2e-testing")
|
||||
}
|
||||
|
||||
@ -2,41 +2,12 @@ package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
const (
|
||||
// CommandAnnotationPlugin is added to every stub command added by
|
||||
// AddPluginCommandStubs with the value "true" and so can be
|
||||
// used to distinguish plugin stubs from regular commands.
|
||||
CommandAnnotationPlugin = "com.docker.cli.plugin"
|
||||
|
||||
// CommandAnnotationPluginVendor is added to every stub command
|
||||
// added by AddPluginCommandStubs and contains the vendor of
|
||||
// that plugin.
|
||||
CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor"
|
||||
|
||||
// CommandAnnotationPluginVersion is added to every stub command
|
||||
// added by AddPluginCommandStubs and contains the version of
|
||||
// that plugin.
|
||||
CommandAnnotationPluginVersion = "com.docker.cli.plugin.version"
|
||||
|
||||
// CommandAnnotationPluginInvalid is added to any stub command
|
||||
// added by AddPluginCommandStubs for an invalid command (that
|
||||
// is, one which failed it's candidate test) and contains the
|
||||
// reason for the failure.
|
||||
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
|
||||
|
||||
// CommandAnnotationPluginCommandPath is added to overwrite the
|
||||
// command path for a plugin invocation.
|
||||
CommandAnnotationPluginCommandPath = "com.docker.cli.plugin.command_path"
|
||||
)
|
||||
|
||||
var pluginCommandStubsOnce sync.Once
|
||||
@ -44,10 +15,10 @@ var pluginCommandStubsOnce sync.Once
|
||||
// AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid
|
||||
// plugin. The command stubs will have several annotations added, see
|
||||
// `CommandAnnotationPlugin*`.
|
||||
func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err error) {
|
||||
func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (err error) {
|
||||
pluginCommandStubsOnce.Do(func() {
|
||||
var plugins []Plugin
|
||||
plugins, err = ListPlugins(dockerCli, rootCmd)
|
||||
plugins, err = ListPlugins(dockerCLI, rootCmd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -57,12 +28,12 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e
|
||||
vendor = "unknown"
|
||||
}
|
||||
annotations := map[string]string{
|
||||
CommandAnnotationPlugin: "true",
|
||||
CommandAnnotationPluginVendor: vendor,
|
||||
CommandAnnotationPluginVersion: p.Version,
|
||||
metadata.CommandAnnotationPlugin: "true",
|
||||
metadata.CommandAnnotationPluginVendor: vendor,
|
||||
metadata.CommandAnnotationPluginVersion: p.Version,
|
||||
}
|
||||
if p.Err != nil {
|
||||
annotations[CommandAnnotationPluginInvalid] = p.Err.Error()
|
||||
annotations[metadata.CommandAnnotationPluginInvalid] = p.Err.Error()
|
||||
}
|
||||
rootCmd.AddCommand(&cobra.Command{
|
||||
Use: p.Name,
|
||||
@ -89,7 +60,7 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e
|
||||
cargs = append(cargs, args...)
|
||||
cargs = append(cargs, toComplete)
|
||||
os.Args = cargs
|
||||
runCommand, runErr := PluginRunCommand(dockerCli, p.Name, cmd)
|
||||
runCommand, runErr := PluginRunCommand(dockerCLI, p.Name, cmd)
|
||||
if runErr != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
@ -104,44 +75,3 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
const (
|
||||
dockerCliAttributePrefix = attribute.Key("docker.cli")
|
||||
|
||||
cobraCommandPath = attribute.Key("cobra.command_path")
|
||||
)
|
||||
|
||||
func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
|
||||
commandPath := cmd.Annotations[CommandAnnotationPluginCommandPath]
|
||||
if commandPath == "" {
|
||||
commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name)
|
||||
}
|
||||
|
||||
attrSet := attribute.NewSet(
|
||||
cobraCommandPath.String(commandPath),
|
||||
)
|
||||
|
||||
kvs := make([]attribute.KeyValue, 0, attrSet.Len())
|
||||
for iter := attrSet.Iter(); iter.Next(); {
|
||||
attr := iter.Attribute()
|
||||
kvs = append(kvs, attribute.KeyValue{
|
||||
Key: dockerCliAttributePrefix + "." + attr.Key,
|
||||
Value: attr.Value,
|
||||
})
|
||||
}
|
||||
return attribute.NewSet(kvs...)
|
||||
}
|
||||
|
||||
func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string {
|
||||
if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 {
|
||||
// values in environment variables need to be in baggage format
|
||||
// otel/baggage package can be used after update to v1.22, currently it encodes incorrectly
|
||||
attrsSlice := make([]string, attrs.Len())
|
||||
for iter := attrs.Iter(); iter.Next(); {
|
||||
i, v := iter.IndexedAttribute()
|
||||
attrsSlice[i] = string(v.Key) + "=" + url.PathEscape(v.Value.AsString())
|
||||
}
|
||||
env = append(env, ResourceAttributesEnvvar+"="+strings.Join(attrsSlice, ","))
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
26
cli-plugins/manager/cobra_test.go
Normal file
26
cli-plugins/manager/cobra_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestPluginResourceAttributesEnvvar(t *testing.T) {
|
||||
cmd := &cobra.Command{
|
||||
Annotations: map[string]string{
|
||||
cobra.CommandDisplayNameAnnotation: "docker",
|
||||
},
|
||||
}
|
||||
|
||||
// Ensure basic usage is fine.
|
||||
env := appendPluginResourceAttributesEnvvar(nil, cmd, Plugin{Name: "compose"})
|
||||
assert.DeepEqual(t, []string{"OTEL_RESOURCE_ATTRIBUTES=docker.cli.cobra.command_path=docker%20compose"}, env)
|
||||
|
||||
// Add a user-based environment variable to OTEL_RESOURCE_ATTRIBUTES.
|
||||
t.Setenv("OTEL_RESOURCE_ATTRIBUTES", "a.b.c=foo")
|
||||
|
||||
env = appendPluginResourceAttributesEnvvar(nil, cmd, Plugin{Name: "compose"})
|
||||
assert.DeepEqual(t, []string{"OTEL_RESOURCE_ATTRIBUTES=a.b.c=foo,docker.cli.cobra.command_path=docker%20compose"}, env)
|
||||
}
|
||||
@ -4,7 +4,7 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// pluginError is set as Plugin.Err by NewPlugin if the plugin
|
||||
@ -39,16 +39,16 @@ func (e *pluginError) MarshalText() (text []byte, err error) {
|
||||
}
|
||||
|
||||
// wrapAsPluginError wraps an error in a pluginError with an
|
||||
// additional message, analogous to errors.Wrapf.
|
||||
// additional message.
|
||||
func wrapAsPluginError(err error, msg string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &pluginError{cause: errors.Wrap(err, msg)}
|
||||
return &pluginError{cause: fmt.Errorf("%s: %w", msg, err)}
|
||||
}
|
||||
|
||||
// NewPluginError creates a new pluginError, analogous to
|
||||
// errors.Errorf.
|
||||
func NewPluginError(msg string, args ...any) error {
|
||||
return &pluginError{cause: errors.Errorf(msg, args...)}
|
||||
return &pluginError{cause: fmt.Errorf(msg, args...)}
|
||||
}
|
||||
|
||||
@ -6,7 +6,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/hooks"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@ -29,29 +30,28 @@ type HookPluginData struct {
|
||||
// a main CLI command was executed. It calls the hook subcommand for all
|
||||
// present CLI plugins that declare support for hooks in their metadata and
|
||||
// parses/prints their responses.
|
||||
func RunCLICommandHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
|
||||
func RunCLICommandHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
|
||||
commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
|
||||
flags := getCommandFlags(subCommand)
|
||||
|
||||
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, cmdErrorMessage)
|
||||
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, cmdErrorMessage)
|
||||
}
|
||||
|
||||
// RunPluginHooks is the entrypoint for the hooks execution flow
|
||||
// after a plugin command was just executed by the CLI.
|
||||
func RunPluginHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) {
|
||||
func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, args []string) {
|
||||
commandName := strings.Join(args, " ")
|
||||
flags := getNaiveFlags(args)
|
||||
|
||||
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, "")
|
||||
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, "")
|
||||
}
|
||||
|
||||
func runHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
|
||||
nextSteps := invokeAndCollectHooks(ctx, dockerCli, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
|
||||
|
||||
hooks.PrintNextSteps(dockerCli.Err(), nextSteps)
|
||||
func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
|
||||
nextSteps := invokeAndCollectHooks(ctx, cfg, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
|
||||
hooks.PrintNextSteps(subCommand.ErrOrStderr(), nextSteps)
|
||||
}
|
||||
|
||||
func invokeAndCollectHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
|
||||
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
|
||||
// check if the context was cancelled before invoking hooks
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@ -59,19 +59,20 @@ func invokeAndCollectHooks(ctx context.Context, dockerCli command.Cli, rootCmd,
|
||||
default:
|
||||
}
|
||||
|
||||
pluginsCfg := dockerCli.ConfigFile().Plugins
|
||||
pluginsCfg := cfg.Plugins
|
||||
if pluginsCfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
pluginDirs := getPluginDirs(cfg)
|
||||
nextSteps := make([]string, 0, len(pluginsCfg))
|
||||
for pluginName, cfg := range pluginsCfg {
|
||||
match, ok := pluginMatch(cfg, subCmdStr)
|
||||
for pluginName, pluginCfg := range pluginsCfg {
|
||||
match, ok := pluginMatch(pluginCfg, subCmdStr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
p, err := GetPlugin(pluginName, dockerCli, rootCmd)
|
||||
p, err := getPlugin(pluginName, pluginDirs, rootCmd)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/fvbommel/sortorder"
|
||||
@ -22,10 +22,12 @@ const (
|
||||
// used to originally invoke the docker CLI when executing a
|
||||
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
|
||||
// the plugin to re-execute the original CLI.
|
||||
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
|
||||
ReexecEnvvar = metadata.ReexecEnvvar
|
||||
|
||||
// ResourceAttributesEnvvar is the name of the envvar that includes additional
|
||||
// resource attributes for OTEL.
|
||||
//
|
||||
// Deprecated: The "OTEL_RESOURCE_ATTRIBUTES" env-var is part of the OpenTelemetry specification; users should define their own const for this. This const will be removed in the next release.
|
||||
ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES"
|
||||
)
|
||||
|
||||
@ -59,20 +61,16 @@ func IsNotFound(err error) bool {
|
||||
// 3. Platform-specific defaultSystemPluginDirs.
|
||||
//
|
||||
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
|
||||
func getPluginDirs(cfg *configfile.ConfigFile) ([]string, error) {
|
||||
func getPluginDirs(cfg *configfile.ConfigFile) []string {
|
||||
var pluginDirs []string
|
||||
|
||||
if cfg != nil {
|
||||
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
|
||||
}
|
||||
pluginDir, err := config.Path("cli-plugins")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
|
||||
pluginDirs = append(pluginDirs, pluginDir)
|
||||
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
|
||||
return pluginDirs, nil
|
||||
return pluginDirs
|
||||
}
|
||||
|
||||
func addPluginCandidatesFromDir(res map[string][]string, d string) {
|
||||
@ -91,10 +89,10 @@ func addPluginCandidatesFromDir(res map[string][]string, d string) {
|
||||
continue
|
||||
}
|
||||
name := dentry.Name()
|
||||
if !strings.HasPrefix(name, NamePrefix) {
|
||||
if !strings.HasPrefix(name, metadata.NamePrefix) {
|
||||
continue
|
||||
}
|
||||
name = strings.TrimPrefix(name, NamePrefix)
|
||||
name = strings.TrimPrefix(name, metadata.NamePrefix)
|
||||
var err error
|
||||
if name, err = trimExeSuffix(name); err != nil {
|
||||
continue
|
||||
@ -113,12 +111,12 @@ func listPluginCandidates(dirs []string) map[string][]string {
|
||||
}
|
||||
|
||||
// GetPlugin returns a plugin on the system by its name
|
||||
func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plugin, error) {
|
||||
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func GetPlugin(name string, dockerCLI config.Provider, rootcmd *cobra.Command) (*Plugin, error) {
|
||||
pluginDirs := getPluginDirs(dockerCLI.ConfigFile())
|
||||
return getPlugin(name, pluginDirs, rootcmd)
|
||||
}
|
||||
|
||||
func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugin, error) {
|
||||
candidates := listPluginCandidates(pluginDirs)
|
||||
if paths, ok := candidates[name]; ok {
|
||||
if len(paths) == 0 {
|
||||
@ -139,17 +137,21 @@ func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plu
|
||||
}
|
||||
|
||||
// ListPlugins produces a list of the plugins available on the system
|
||||
func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
|
||||
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, error) {
|
||||
pluginDirs := getPluginDirs(dockerCli.ConfigFile())
|
||||
candidates := listPluginCandidates(pluginDirs)
|
||||
if len(candidates) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var plugins []Plugin
|
||||
var mu sync.Mutex
|
||||
eg, _ := errgroup.WithContext(context.TODO())
|
||||
ctx := rootcmd.Context()
|
||||
if ctx == nil {
|
||||
// Fallback, mostly for tests that pass a bare cobra.command
|
||||
ctx = context.Background()
|
||||
}
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
cmds := rootcmd.Commands()
|
||||
for _, paths := range candidates {
|
||||
func(paths []string) {
|
||||
@ -186,7 +188,7 @@ func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error
|
||||
// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
|
||||
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
|
||||
// The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
|
||||
func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
|
||||
func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
|
||||
// This uses the full original args, not the args which may
|
||||
// have been provided by cobra to our caller. This is because
|
||||
// they lack e.g. global options which we must propagate here.
|
||||
@ -196,11 +198,8 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
|
||||
// fallback to their "invalid" command path.
|
||||
return nil, errPluginNotFound(name)
|
||||
}
|
||||
exename := addExeSuffix(NamePrefix + name)
|
||||
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exename := addExeSuffix(metadata.NamePrefix + name)
|
||||
pluginDirs := getPluginDirs(dockerCli.ConfigFile())
|
||||
|
||||
for _, d := range pluginDirs {
|
||||
path := filepath.Join(d, exename)
|
||||
@ -233,7 +232,7 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
cmd.Env = append(cmd.Environ(), ReexecEnvvar+"="+os.Args[0])
|
||||
cmd.Env = append(cmd.Environ(), metadata.ReexecEnvvar+"="+os.Args[0])
|
||||
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
|
||||
|
||||
return cmd, nil
|
||||
@ -243,5 +242,5 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
|
||||
|
||||
// IsPluginCommand checks if the given cmd is a plugin-stub.
|
||||
func IsPluginCommand(cmd *cobra.Command) bool {
|
||||
return cmd.Annotations[CommandAnnotationPlugin] == "true"
|
||||
return cmd.Annotations[metadata.CommandAnnotationPlugin] == "true"
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -81,6 +82,12 @@ func TestListPluginCandidates(t *testing.T) {
|
||||
assert.DeepEqual(t, candidates, exp)
|
||||
}
|
||||
|
||||
func TestListPluginCandidatesEmpty(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
candidates := listPluginCandidates([]string{tmpDir, filepath.Join(tmpDir, "no-such-dir")})
|
||||
assert.Assert(t, len(candidates) == 0)
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/docker/cli/issues/5643.
|
||||
// Check that inaccessible directories that come before accessible ones are ignored
|
||||
// and do not prevent the latter from being processed.
|
||||
@ -166,14 +173,11 @@ func TestErrPluginNotFound(t *testing.T) {
|
||||
func TestGetPluginDirs(t *testing.T) {
|
||||
cli := test.NewFakeCli(nil)
|
||||
|
||||
pluginDir, err := config.Path("cli-plugins")
|
||||
assert.NilError(t, err)
|
||||
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
|
||||
expected := append([]string{pluginDir}, defaultSystemPluginDirs...)
|
||||
|
||||
var pluginDirs []string
|
||||
pluginDirs, err = getPluginDirs(cli.ConfigFile())
|
||||
pluginDirs := getPluginDirs(cli.ConfigFile())
|
||||
assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))
|
||||
assert.NilError(t, err)
|
||||
|
||||
extras := []string{
|
||||
"foo", "bar", "baz",
|
||||
@ -182,7 +186,6 @@ func TestGetPluginDirs(t *testing.T) {
|
||||
cli.SetConfigFile(&configfile.ConfigFile{
|
||||
CLIPluginsExtraDirs: extras,
|
||||
})
|
||||
pluginDirs, err = getPluginDirs(cli.ConfigFile())
|
||||
pluginDirs = getPluginDirs(cli.ConfigFile())
|
||||
assert.DeepEqual(t, expected, pluginDirs)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
@ -1,30 +1,23 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
// NamePrefix is the prefix required on all plugin binary names
|
||||
NamePrefix = "docker-"
|
||||
NamePrefix = metadata.NamePrefix
|
||||
|
||||
// MetadataSubcommandName is the name of the plugin subcommand
|
||||
// which must be supported by every plugin and returns the
|
||||
// plugin metadata.
|
||||
MetadataSubcommandName = "docker-cli-plugin-metadata"
|
||||
MetadataSubcommandName = metadata.MetadataSubcommandName
|
||||
|
||||
// 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"
|
||||
HookSubcommandName = metadata.HookSubcommandName
|
||||
)
|
||||
|
||||
// Metadata provided by the plugin.
|
||||
type Metadata struct {
|
||||
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
|
||||
SchemaVersion string `json:",omitempty"`
|
||||
// Vendor is the name of the plugin vendor. Mandatory
|
||||
Vendor string `json:",omitempty"`
|
||||
// Version is the optional version of this plugin.
|
||||
Version string `json:",omitempty"`
|
||||
// ShortDescription should be suitable for a single line help message.
|
||||
ShortDescription string `json:",omitempty"`
|
||||
// URL is a pointer to the plugin's homepage.
|
||||
URL string `json:",omitempty"`
|
||||
}
|
||||
type Metadata = metadata.Metadata
|
||||
|
||||
@ -3,13 +3,15 @@ package manager
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -17,7 +19,7 @@ var pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
|
||||
|
||||
// Plugin represents a potential plugin with all it's metadata.
|
||||
type Plugin struct {
|
||||
Metadata
|
||||
metadata.Metadata
|
||||
|
||||
Name string `json:",omitempty"`
|
||||
Path string `json:",omitempty"`
|
||||
@ -44,18 +46,18 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
|
||||
// which would fail here, so there are all real errors.
|
||||
fullname := filepath.Base(path)
|
||||
if fullname == "." {
|
||||
return Plugin{}, errors.Errorf("unable to determine basename of plugin candidate %q", path)
|
||||
return Plugin{}, fmt.Errorf("unable to determine basename of plugin candidate %q", path)
|
||||
}
|
||||
var err error
|
||||
if fullname, err = trimExeSuffix(fullname); err != nil {
|
||||
return Plugin{}, errors.Wrapf(err, "plugin candidate %q", path)
|
||||
return Plugin{}, fmt.Errorf("plugin candidate %q: %w", path, err)
|
||||
}
|
||||
if !strings.HasPrefix(fullname, NamePrefix) {
|
||||
return Plugin{}, errors.Errorf("plugin candidate %q: does not have %q prefix", path, NamePrefix)
|
||||
if !strings.HasPrefix(fullname, metadata.NamePrefix) {
|
||||
return Plugin{}, fmt.Errorf("plugin candidate %q: does not have %q prefix", path, metadata.NamePrefix)
|
||||
}
|
||||
|
||||
p := Plugin{
|
||||
Name: strings.TrimPrefix(fullname, NamePrefix),
|
||||
Name: strings.TrimPrefix(fullname, metadata.NamePrefix),
|
||||
Path: path,
|
||||
}
|
||||
|
||||
@ -112,9 +114,9 @@ func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte,
|
||||
return nil, wrapAsPluginError(err, "failed to marshall hook data")
|
||||
}
|
||||
|
||||
pCmd := exec.CommandContext(ctx, p.Path, p.Name, HookSubcommandName, string(hDataBytes)) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
||||
pCmd := exec.CommandContext(ctx, p.Path, p.Name, metadata.HookSubcommandName, string(hDataBytes)) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
||||
pCmd.Env = os.Environ()
|
||||
pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0])
|
||||
pCmd.Env = append(pCmd.Env, metadata.ReexecEnvvar+"="+os.Args[0])
|
||||
hookCmdOutput, err := pCmd.Output()
|
||||
if err != nil {
|
||||
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")
|
||||
|
||||
@ -1,22 +1,16 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// This is made slightly more complex due to needing to be case insensitive.
|
||||
// This is made slightly more complex due to needing to be case-insensitive.
|
||||
func trimExeSuffix(s string) (string, error) {
|
||||
ext := filepath.Ext(s)
|
||||
if ext == "" {
|
||||
return "", errors.Errorf("path %q lacks required file extension", s)
|
||||
}
|
||||
|
||||
exe := ".exe"
|
||||
if !strings.EqualFold(ext, exe) {
|
||||
return "", errors.Errorf("path %q lacks required %q suffix", s, exe)
|
||||
if ext == "" || !strings.EqualFold(ext, ".exe") {
|
||||
return "", fmt.Errorf("path %q lacks required file extension (.exe)", s)
|
||||
}
|
||||
return strings.TrimSuffix(s, ext), nil
|
||||
}
|
||||
|
||||
85
cli-plugins/manager/telemetry.go
Normal file
85
cli-plugins/manager/telemetry.go
Normal file
@ -0,0 +1,85 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/baggage"
|
||||
)
|
||||
|
||||
const (
|
||||
// resourceAttributesEnvVar is the name of the envvar that includes additional
|
||||
// resource attributes for OTEL as defined in the [OpenTelemetry specification].
|
||||
//
|
||||
// [OpenTelemetry specification]: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration
|
||||
resourceAttributesEnvVar = "OTEL_RESOURCE_ATTRIBUTES"
|
||||
|
||||
// dockerCLIAttributePrefix is the prefix for any docker cli OTEL attributes.
|
||||
//
|
||||
// It is a copy of the const defined in [command.dockerCLIAttributePrefix].
|
||||
dockerCLIAttributePrefix = "docker.cli."
|
||||
cobraCommandPath = attribute.Key("cobra.command_path")
|
||||
)
|
||||
|
||||
func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
|
||||
commandPath := cmd.Annotations[metadata.CommandAnnotationPluginCommandPath]
|
||||
if commandPath == "" {
|
||||
commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name)
|
||||
}
|
||||
|
||||
attrSet := attribute.NewSet(
|
||||
cobraCommandPath.String(commandPath),
|
||||
)
|
||||
|
||||
kvs := make([]attribute.KeyValue, 0, attrSet.Len())
|
||||
for iter := attrSet.Iter(); iter.Next(); {
|
||||
attr := iter.Attribute()
|
||||
kvs = append(kvs, attribute.KeyValue{
|
||||
Key: dockerCLIAttributePrefix + attr.Key,
|
||||
Value: attr.Value,
|
||||
})
|
||||
}
|
||||
return attribute.NewSet(kvs...)
|
||||
}
|
||||
|
||||
func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string {
|
||||
if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 {
|
||||
// Construct baggage members for each of the attributes.
|
||||
// Ignore any failures as these aren't significant and
|
||||
// represent an internal issue.
|
||||
members := make([]baggage.Member, 0, attrs.Len())
|
||||
for iter := attrs.Iter(); iter.Next(); {
|
||||
attr := iter.Attribute()
|
||||
m, err := baggage.NewMemberRaw(string(attr.Key), attr.Value.AsString())
|
||||
if err != nil {
|
||||
otel.Handle(err)
|
||||
continue
|
||||
}
|
||||
members = append(members, m)
|
||||
}
|
||||
|
||||
// Combine plugin added resource attributes with ones found in the environment
|
||||
// variable. Our own attributes should be namespaced so there shouldn't be a
|
||||
// conflict. We do not parse the environment variable because we do not want
|
||||
// to handle errors in user configuration.
|
||||
attrsSlice := make([]string, 0, 2)
|
||||
if v := strings.TrimSpace(os.Getenv(resourceAttributesEnvVar)); v != "" {
|
||||
attrsSlice = append(attrsSlice, v)
|
||||
}
|
||||
if b, err := baggage.New(members...); err != nil {
|
||||
otel.Handle(err)
|
||||
} else if b.Len() > 0 {
|
||||
attrsSlice = append(attrsSlice, b.String())
|
||||
}
|
||||
|
||||
if len(attrsSlice) > 0 {
|
||||
env = append(env, resourceAttributesEnvVar+"="+strings.Join(attrsSlice, ","))
|
||||
}
|
||||
}
|
||||
return env
|
||||
}
|
||||
28
cli-plugins/metadata/annotations.go
Normal file
28
cli-plugins/metadata/annotations.go
Normal file
@ -0,0 +1,28 @@
|
||||
package metadata
|
||||
|
||||
const (
|
||||
// CommandAnnotationPlugin is added to every stub command added by
|
||||
// AddPluginCommandStubs with the value "true" and so can be
|
||||
// used to distinguish plugin stubs from regular commands.
|
||||
CommandAnnotationPlugin = "com.docker.cli.plugin"
|
||||
|
||||
// CommandAnnotationPluginVendor is added to every stub command
|
||||
// added by AddPluginCommandStubs and contains the vendor of
|
||||
// that plugin.
|
||||
CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor"
|
||||
|
||||
// CommandAnnotationPluginVersion is added to every stub command
|
||||
// added by AddPluginCommandStubs and contains the version of
|
||||
// that plugin.
|
||||
CommandAnnotationPluginVersion = "com.docker.cli.plugin.version"
|
||||
|
||||
// CommandAnnotationPluginInvalid is added to any stub command
|
||||
// added by AddPluginCommandStubs for an invalid command (that
|
||||
// is, one which failed it's candidate test) and contains the
|
||||
// reason for the failure.
|
||||
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
|
||||
|
||||
// CommandAnnotationPluginCommandPath is added to overwrite the
|
||||
// command path for a plugin invocation.
|
||||
CommandAnnotationPluginCommandPath = "com.docker.cli.plugin.command_path"
|
||||
)
|
||||
36
cli-plugins/metadata/metadata.go
Normal file
36
cli-plugins/metadata/metadata.go
Normal file
@ -0,0 +1,36 @@
|
||||
package metadata
|
||||
|
||||
const (
|
||||
// NamePrefix is the prefix required on all plugin binary names
|
||||
NamePrefix = "docker-"
|
||||
|
||||
// MetadataSubcommandName is the name of the plugin subcommand
|
||||
// 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"
|
||||
|
||||
// ReexecEnvvar is the name of an ennvar which is set to the command
|
||||
// used to originally invoke the docker CLI when executing a
|
||||
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
|
||||
// the plugin to re-execute the original CLI.
|
||||
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
|
||||
)
|
||||
|
||||
// Metadata provided by the plugin.
|
||||
type Metadata struct {
|
||||
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
|
||||
SchemaVersion string `json:",omitempty"`
|
||||
// Vendor is the name of the plugin vendor. Mandatory
|
||||
Vendor string `json:",omitempty"`
|
||||
// Version is the optional version of this plugin.
|
||||
Version string `json:",omitempty"`
|
||||
// ShortDescription should be suitable for a single line help message.
|
||||
ShortDescription string `json:",omitempty"`
|
||||
// URL is a pointer to the plugin's homepage.
|
||||
URL string `json:",omitempty"`
|
||||
}
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli-plugins/socket"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
@ -30,7 +30,7 @@ import (
|
||||
var PersistentPreRunE func(*cobra.Command, []string) error
|
||||
|
||||
// RunPlugin executes the specified plugin command
|
||||
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) error {
|
||||
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) error {
|
||||
tcmd := newPluginCommand(dockerCli, plugin, meta)
|
||||
|
||||
var persistentPreRunOnce sync.Once
|
||||
@ -81,7 +81,7 @@ 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) {
|
||||
func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) {
|
||||
otel.SetErrorHandler(debug.OTELErrorHandler)
|
||||
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
@ -111,7 +111,7 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
|
||||
func withPluginClientConn(name string) command.CLIOption {
|
||||
return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) {
|
||||
cmd := "docker"
|
||||
if x := os.Getenv(manager.ReexecEnvvar); x != "" {
|
||||
if x := os.Getenv(metadata.ReexecEnvvar); x != "" {
|
||||
cmd = x
|
||||
}
|
||||
var flags []string
|
||||
@ -140,9 +140,9 @@ func withPluginClientConn(name string) command.CLIOption {
|
||||
})
|
||||
}
|
||||
|
||||
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cli.TopLevelCommand {
|
||||
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) *cli.TopLevelCommand {
|
||||
name := plugin.Name()
|
||||
fullname := manager.NamePrefix + name
|
||||
fullname := metadata.NamePrefix + name
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name),
|
||||
@ -177,12 +177,12 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
|
||||
return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags())
|
||||
}
|
||||
|
||||
func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
|
||||
func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra.Command {
|
||||
if meta.ShortDescription == "" {
|
||||
meta.ShortDescription = plugin.Short
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: manager.MetadataSubcommandName,
|
||||
Use: metadata.MetadataSubcommandName,
|
||||
Hidden: true,
|
||||
// Suppress the global/parent PersistentPreRunE, which
|
||||
// needlessly initializes the client and tries to
|
||||
@ -200,8 +200,8 @@ func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.
|
||||
|
||||
// RunningStandalone tells a CLI plugin it is run standalone by direct execution
|
||||
func RunningStandalone() bool {
|
||||
if os.Getenv(manager.ReexecEnvvar) != "" {
|
||||
if os.Getenv(metadata.ReexecEnvvar) != "" {
|
||||
return false
|
||||
}
|
||||
return len(os.Args) < 2 || os.Args[1] != manager.MetadataSubcommandName
|
||||
return len(os.Args) < 2 || os.Args[1] != metadata.MetadataSubcommandName
|
||||
}
|
||||
|
||||
@ -178,24 +178,39 @@ 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) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
srv, err := NewPluginServer(nil)
|
||||
assert.NilError(t, err, "failed to setup server")
|
||||
|
||||
defer srv.Close()
|
||||
|
||||
t.Setenv(EnvKey, srv.Addr().String())
|
||||
|
||||
runtime.Gosched()
|
||||
numGoroutines := runtime.NumGoroutine()
|
||||
|
||||
ConnectAndWait(func() {})
|
||||
assert.Equal(t, runtime.NumGoroutine(), numGoroutines+1)
|
||||
|
||||
runtime.Gosched()
|
||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
||||
// +1 goroutine for the poll.WaitOn
|
||||
// +1 goroutine for the connect goroutine
|
||||
if runtime.NumGoroutine() < numGoroutines+1+1 {
|
||||
return poll.Continue("waiting for connect goroutine to spawn")
|
||||
}
|
||||
return poll.Success()
|
||||
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(500*time.Millisecond))
|
||||
|
||||
srv.Close()
|
||||
|
||||
runtime.Gosched()
|
||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
||||
// +1 goroutine for the poll.WaitOn
|
||||
if runtime.NumGoroutine() > numGoroutines+1 {
|
||||
return poll.Continue("waiting for connect goroutine to exit")
|
||||
}
|
||||
return poll.Success()
|
||||
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(10*time.Millisecond))
|
||||
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(500*time.Millisecond))
|
||||
})
|
||||
}
|
||||
|
||||
20
cli/cobra.go
20
cli/cobra.go
@ -3,15 +3,12 @@ package cli
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli/command"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/moby/term"
|
||||
"github.com/morikuni/aec"
|
||||
@ -62,13 +59,6 @@ func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *c
|
||||
"docs.code-delimiter": `"`, // https://github.com/docker/cli-docs-tool/blob/77abede22166eaea4af7335096bdcedd043f5b19/annotation/annotation.go#L20-L22
|
||||
}
|
||||
|
||||
// Configure registry.CertsDir() when running in rootless-mode
|
||||
if os.Getenv("ROOTLESSKIT_STATE_DIR") != "" {
|
||||
if configHome, err := homedir.GetConfigHome(); err == nil {
|
||||
registry.SetCertsDir(filepath.Join(configHome, "docker/certs.d"))
|
||||
}
|
||||
}
|
||||
|
||||
return opts, helpCommand
|
||||
}
|
||||
|
||||
@ -252,7 +242,7 @@ func hasAdditionalHelp(cmd *cobra.Command) bool {
|
||||
}
|
||||
|
||||
func isPlugin(cmd *cobra.Command) bool {
|
||||
return pluginmanager.IsPluginCommand(cmd)
|
||||
return cmd.Annotations[metadata.CommandAnnotationPlugin] == "true"
|
||||
}
|
||||
|
||||
func hasAliases(cmd *cobra.Command) bool {
|
||||
@ -356,9 +346,9 @@ func decoratedName(cmd *cobra.Command) string {
|
||||
}
|
||||
|
||||
func vendorAndVersion(cmd *cobra.Command) string {
|
||||
if vendor, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) {
|
||||
if vendor, ok := cmd.Annotations[metadata.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) {
|
||||
version := ""
|
||||
if v, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVersion]; ok && v != "" {
|
||||
if v, ok := cmd.Annotations[metadata.CommandAnnotationPluginVersion]; ok && v != "" {
|
||||
version = ", " + v
|
||||
}
|
||||
return fmt.Sprintf("(%s%s)", vendor, version)
|
||||
@ -417,7 +407,7 @@ func invalidPlugins(cmd *cobra.Command) []*cobra.Command {
|
||||
}
|
||||
|
||||
func invalidPluginReason(cmd *cobra.Command) string {
|
||||
return cmd.Annotations[pluginmanager.CommandAnnotationPluginInvalid]
|
||||
return cmd.Annotations[metadata.CommandAnnotationPluginInvalid]
|
||||
}
|
||||
|
||||
const usageTemplate = `Usage:
|
||||
|
||||
@ -3,7 +3,7 @@ package cli
|
||||
import (
|
||||
"testing"
|
||||
|
||||
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
@ -49,9 +49,9 @@ func TestVendorAndVersion(t *testing.T) {
|
||||
cmd := &cobra.Command{
|
||||
Use: "test",
|
||||
Annotations: map[string]string{
|
||||
pluginmanager.CommandAnnotationPlugin: "true",
|
||||
pluginmanager.CommandAnnotationPluginVendor: tc.vendor,
|
||||
pluginmanager.CommandAnnotationPluginVersion: tc.version,
|
||||
metadata.CommandAnnotationPlugin: "true",
|
||||
metadata.CommandAnnotationPluginVendor: tc.vendor,
|
||||
metadata.CommandAnnotationPluginVersion: tc.version,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, vendorAndVersion(cmd), tc.expected)
|
||||
@ -69,8 +69,8 @@ func TestInvalidPlugin(t *testing.T) {
|
||||
assert.Assert(t, is.Len(invalidPlugins(root), 0))
|
||||
|
||||
sub1.Annotations = map[string]string{
|
||||
pluginmanager.CommandAnnotationPlugin: "true",
|
||||
pluginmanager.CommandAnnotationPluginInvalid: "foo",
|
||||
metadata.CommandAnnotationPlugin: "true",
|
||||
metadata.CommandAnnotationPluginInvalid: "foo",
|
||||
}
|
||||
root.AddCommand(sub1, sub2)
|
||||
sub1.AddCommand(sub1sub1, sub1sub2)
|
||||
@ -100,6 +100,6 @@ func TestDecoratedName(t *testing.T) {
|
||||
topLevelCommand := &cobra.Command{Use: "pluginTopLevelCommand"}
|
||||
root.AddCommand(topLevelCommand)
|
||||
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand ")
|
||||
topLevelCommand.Annotations = map[string]string{pluginmanager.CommandAnnotationPlugin: "true"}
|
||||
topLevelCommand.Annotations = map[string]string{metadata.CommandAnnotationPlugin: "true"}
|
||||
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand*")
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
@ -21,21 +20,15 @@ import (
|
||||
"github.com/docker/cli/cli/context/store"
|
||||
"github.com/docker/cli/cli/debug"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
manifeststore "github.com/docker/cli/cli/manifest/store"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/cli/version"
|
||||
dopts "github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
notaryclient "github.com/theupdateframework/notary/client"
|
||||
)
|
||||
|
||||
const defaultInitTimeout = 2 * time.Second
|
||||
@ -53,13 +46,10 @@ type Cli interface {
|
||||
Streams
|
||||
SetIn(in *streams.In)
|
||||
Apply(ops ...CLIOption) error
|
||||
ConfigFile() *configfile.ConfigFile
|
||||
config.Provider
|
||||
ServerInfo() ServerInfo
|
||||
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||
DefaultVersion() string
|
||||
CurrentVersion() string
|
||||
ManifestStore() manifeststore.Store
|
||||
RegistryClient(bool) registryclient.RegistryClient
|
||||
ContentTrustEnabled() bool
|
||||
BuildKitEnabled() (bool, error)
|
||||
ContextStore() store.Store
|
||||
@ -96,7 +86,7 @@ type DockerCli struct {
|
||||
enableGlobalMeter, enableGlobalTracer bool
|
||||
}
|
||||
|
||||
// DefaultVersion returns api.defaultVersion.
|
||||
// DefaultVersion returns [api.DefaultVersion].
|
||||
func (*DockerCli) DefaultVersion() string {
|
||||
return api.DefaultVersion
|
||||
}
|
||||
@ -202,16 +192,16 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
|
||||
|
||||
// HooksEnabled returns whether plugin hooks are enabled.
|
||||
func (cli *DockerCli) HooksEnabled() bool {
|
||||
// legacy support DOCKER_CLI_HINTS env var
|
||||
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
|
||||
// use DOCKER_CLI_HOOKS env var value if set and not empty
|
||||
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
|
||||
enabled, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
// use DOCKER_CLI_HOOKS env var value if set and not empty
|
||||
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
|
||||
// legacy support DOCKER_CLI_HINTS env var
|
||||
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
|
||||
enabled, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return false
|
||||
@ -230,21 +220,6 @@ func (cli *DockerCli) HooksEnabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ManifestStore returns a store for local manifests
|
||||
func (*DockerCli) ManifestStore() manifeststore.Store {
|
||||
// TODO: support override default location from config file
|
||||
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
|
||||
}
|
||||
|
||||
// RegistryClient returns a client for communicating with a Docker distribution
|
||||
// registry
|
||||
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
|
||||
resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig {
|
||||
return ResolveAuthConfig(cli.ConfigFile(), index)
|
||||
}
|
||||
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
|
||||
}
|
||||
|
||||
// WithInitializeClient is passed to DockerCli.Initialize by callers who wish to set a particular API Client for use by the CLI.
|
||||
func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) CLIOption {
|
||||
return func(dockerCli *DockerCli) error {
|
||||
@ -292,6 +267,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
|
||||
if cli.enableGlobalTracer {
|
||||
cli.createGlobalTracerProvider(cli.baseCtx)
|
||||
}
|
||||
filterResourceAttributesEnvvar()
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -345,7 +321,10 @@ func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint,
|
||||
|
||||
// Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
|
||||
func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) {
|
||||
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
|
||||
// defaultToTLS determines whether we should use a TLS host as default
|
||||
// if nothing was configured by the user.
|
||||
defaultToTLS := opts.TLSOptions != nil
|
||||
host, err := getServerHost(opts.Hosts, defaultToTLS)
|
||||
if err != nil {
|
||||
return docker.Endpoint{}, err
|
||||
}
|
||||
@ -403,11 +382,6 @@ func (cli *DockerCli) initializeFromClient() {
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
}
|
||||
|
||||
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
|
||||
func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
|
||||
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
|
||||
}
|
||||
|
||||
// ContextStore returns the ContextStore
|
||||
func (cli *DockerCli) ContextStore() store.Store {
|
||||
return cli.contextStore
|
||||
@ -553,18 +527,15 @@ func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
|
||||
var host string
|
||||
func getServerHost(hosts []string, defaultToTLS bool) (string, error) {
|
||||
switch len(hosts) {
|
||||
case 0:
|
||||
host = os.Getenv(client.EnvOverrideHost)
|
||||
return dopts.ParseHost(defaultToTLS, os.Getenv(client.EnvOverrideHost))
|
||||
case 1:
|
||||
host = hosts[0]
|
||||
return dopts.ParseHost(defaultToTLS, hosts[0])
|
||||
default:
|
||||
return "", errors.New("Specify only one -H")
|
||||
}
|
||||
|
||||
return dopts.ParseHost(tlsOptions != nil, host)
|
||||
}
|
||||
|
||||
// UserAgent returns the user agent string used for making API requests
|
||||
|
||||
@ -13,8 +13,10 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ValidArgsFn a function to be used by cobra command as `ValidArgsFunction` to offer command line completion
|
||||
type ValidArgsFn func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
|
||||
// ValidArgsFn a function to be used by cobra command as `ValidArgsFunction` to offer command line completion.
|
||||
//
|
||||
// Deprecated: use [cobra.CompletionFunc].
|
||||
type ValidArgsFn = cobra.CompletionFunc
|
||||
|
||||
// APIClientProvider provides a method to get an [client.APIClient], initializing
|
||||
// it if needed.
|
||||
@ -27,7 +29,7 @@ type APIClientProvider interface {
|
||||
}
|
||||
|
||||
// ImageNames offers completion for images present within the local store
|
||||
func ImageNames(dockerCLI APIClientProvider, limit int) ValidArgsFn {
|
||||
func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if limit > 0 && len(args) >= limit {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
@ -47,7 +49,7 @@ func ImageNames(dockerCLI APIClientProvider, limit int) ValidArgsFn {
|
||||
// ContainerNames offers completion for container names and IDs
|
||||
// By default, only names are returned.
|
||||
// Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs.
|
||||
func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(container.Summary) bool) ValidArgsFn {
|
||||
func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(container.Summary) bool) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().ContainerList(cmd.Context(), container.ListOptions{
|
||||
All: all,
|
||||
@ -80,7 +82,7 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
|
||||
}
|
||||
|
||||
// VolumeNames offers completion for volumes
|
||||
func VolumeNames(dockerCLI APIClientProvider) ValidArgsFn {
|
||||
func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{})
|
||||
if err != nil {
|
||||
@ -95,7 +97,7 @@ func VolumeNames(dockerCLI APIClientProvider) ValidArgsFn {
|
||||
}
|
||||
|
||||
// NetworkNames offers completion for networks
|
||||
func NetworkNames(dockerCLI APIClientProvider) ValidArgsFn {
|
||||
func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{})
|
||||
if err != nil {
|
||||
@ -133,7 +135,7 @@ func EnvVarNames(_ *cobra.Command, _ []string, _ string) (names []string, _ cobr
|
||||
}
|
||||
|
||||
// FromList offers completion for the given list of options.
|
||||
func FromList(options ...string) ValidArgsFn {
|
||||
func FromList(options ...string) cobra.CompletionFunc {
|
||||
return cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp)
|
||||
}
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
// completeNames offers completion for swarm configs
|
||||
func completeNames(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||
func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().ConfigList(cmd.Context(), types.ConfigListOptions{})
|
||||
if err != nil {
|
||||
|
||||
@ -51,17 +51,7 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
|
||||
var in io.Reader = dockerCLI.In()
|
||||
if options.File != "-" {
|
||||
file, err := sequential.Open(options.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
in = file
|
||||
defer file.Close()
|
||||
}
|
||||
|
||||
configData, err := io.ReadAll(in)
|
||||
configData, err := readConfigData(dockerCLI.In(), options.File)
|
||||
if err != nil {
|
||||
return errors.Errorf("Error reading content from %q: %v", options.File, err)
|
||||
}
|
||||
@ -83,6 +73,54 @@ func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateO
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(dockerCLI.Out(), r.ID)
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), r.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// maxConfigSize is the maximum byte length of the [swarm.ConfigSpec.Data] field,
|
||||
// as defined by [MaxConfigSize] in SwarmKit.
|
||||
//
|
||||
// [MaxConfigSize]: https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/manager/controlapi#MaxConfigSize
|
||||
const maxConfigSize = 1000 * 1024 // 1000KB
|
||||
|
||||
// readConfigData reads the config from either stdin or the given fileName.
|
||||
//
|
||||
// It reads up to twice the maximum size of the config ([maxConfigSize]),
|
||||
// just in case swarm's limit changes; this is only a safeguard to prevent
|
||||
// reading arbitrary files into memory.
|
||||
func readConfigData(in io.Reader, fileName string) ([]byte, error) {
|
||||
switch fileName {
|
||||
case "-":
|
||||
data, err := io.ReadAll(io.LimitReader(in, 2*maxConfigSize))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading from STDIN: %w", err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, errors.New("error reading from STDIN: data is empty")
|
||||
}
|
||||
return data, nil
|
||||
case "":
|
||||
return nil, errors.New("config file is required")
|
||||
default:
|
||||
// Open file with [FILE_FLAG_SEQUENTIAL_SCAN] on Windows, which
|
||||
// prevents Windows from aggressively caching it. We expect this
|
||||
// file to be only read once. Given that this is expected to be
|
||||
// a small file, this may not be a significant optimization, so
|
||||
// we could choose to omit this, and use a regular [os.Open].
|
||||
//
|
||||
// [FILE_FLAG_SEQUENTIAL_SCAN]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
|
||||
f, err := sequential.Open(fileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
|
||||
}
|
||||
defer f.Close()
|
||||
data, err := io.ReadAll(io.LimitReader(f, 2*maxConfigSize))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("error reading from %s: data is empty", fileName)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ func TestConfigCreateErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigCreateWithName(t *testing.T) {
|
||||
name := "foo"
|
||||
const name = "config-with-name"
|
||||
var actual []byte
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
@ -87,7 +87,7 @@ func TestConfigCreateWithLabels(t *testing.T) {
|
||||
"lbl1": "Label-foo",
|
||||
"lbl2": "Label-bar",
|
||||
}
|
||||
name := "foo"
|
||||
const name = "config-with-labels"
|
||||
|
||||
data, err := os.ReadFile(filepath.Join("testdata", configDataFile))
|
||||
assert.NilError(t, err)
|
||||
@ -124,7 +124,7 @@ func TestConfigCreateWithTemplatingDriver(t *testing.T) {
|
||||
expectedDriver := &swarm.Driver{
|
||||
Name: "template-driver",
|
||||
}
|
||||
name := "foo"
|
||||
const name = "config-with-template-driver"
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
|
||||
@ -5,7 +5,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
@ -157,11 +156,11 @@ func (ctx *configInspectContext) Labels() map[string]string {
|
||||
}
|
||||
|
||||
func (ctx *configInspectContext) CreatedAt() string {
|
||||
return command.PrettyPrint(ctx.Config.CreatedAt)
|
||||
return formatter.PrettyPrint(ctx.Config.CreatedAt)
|
||||
}
|
||||
|
||||
func (ctx *configInspectContext) UpdatedAt() string {
|
||||
return command.PrettyPrint(ctx.Config.UpdatedAt)
|
||||
return formatter.PrettyPrint(ctx.Config.UpdatedAt)
|
||||
}
|
||||
|
||||
func (ctx *configInspectContext) Data() string {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.22
|
||||
// +build go1.22
|
||||
|
||||
package container
|
||||
|
||||
@ -145,7 +144,7 @@ func addCompletions(cmd *cobra.Command, dockerCLI completion.APIClientProvider)
|
||||
}
|
||||
|
||||
// completeCgroupns implements shell completion for the `--cgroupns` option of `run` and `create`.
|
||||
func completeCgroupns() completion.ValidArgsFn {
|
||||
func completeCgroupns() cobra.CompletionFunc {
|
||||
return completion.FromList(string(container.CgroupnsModeHost), string(container.CgroupnsModePrivate))
|
||||
}
|
||||
|
||||
@ -156,7 +155,7 @@ func completeDetachKeys(_ *cobra.Command, _ []string, _ string) ([]string, cobra
|
||||
|
||||
// completeIpc implements shell completion for the `--ipc` option of `run` and `create`.
|
||||
// The completion is partly composite.
|
||||
func completeIpc(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
func completeIpc(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container"
|
||||
return []string{"container:"}, cobra.ShellCompDirectiveNoSpace
|
||||
@ -176,7 +175,7 @@ func completeIpc(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command
|
||||
}
|
||||
|
||||
// completeLink implements shell completion for the `--link` option of `run` and `create`.
|
||||
func completeLink(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
func completeLink(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return postfixWith(":", containerNames(dockerCLI, cmd, args, toComplete)), cobra.ShellCompDirectiveNoSpace
|
||||
}
|
||||
@ -185,7 +184,7 @@ func completeLink(dockerCLI completion.APIClientProvider) func(cmd *cobra.Comman
|
||||
// completeLogDriver implements shell completion for the `--log-driver` option of `run` and `create`.
|
||||
// The log drivers are collected from a call to the Info endpoint with a fallback to a hard-coded list
|
||||
// of the build-in log drivers.
|
||||
func completeLogDriver(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||
func completeLogDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
info, err := dockerCLI.Client().Info(cmd.Context())
|
||||
if err != nil {
|
||||
@ -207,7 +206,7 @@ func completeLogOpt(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.S
|
||||
}
|
||||
|
||||
// completePid implements shell completion for the `--pid` option of `run` and `create`.
|
||||
func completePid(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
func completePid(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container"
|
||||
return []string{"container:"}, cobra.ShellCompDirectiveNoSpace
|
||||
@ -278,7 +277,7 @@ func completeUlimit(_ *cobra.Command, _ []string, _ string) ([]string, cobra.She
|
||||
}
|
||||
|
||||
// completeVolumeDriver contacts the API to get the built-in and installed volume drivers.
|
||||
func completeVolumeDriver(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||
func completeVolumeDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
info, err := dockerCLI.Client().Info(cmd.Context())
|
||||
if err != nil {
|
||||
|
||||
@ -4,8 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
@ -15,6 +15,7 @@ import (
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
imagetypes "github.com/docker/docker/api/types/image"
|
||||
@ -114,12 +115,6 @@ func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet,
|
||||
StatusCode: 125,
|
||||
}
|
||||
}
|
||||
if err = validateAPIVersion(containerCfg, dockerCli.Client().ClientVersion()); err != nil {
|
||||
return cli.StatusError{
|
||||
Status: withHelp(err, "create").Error(),
|
||||
StatusCode: 125,
|
||||
}
|
||||
}
|
||||
id, err := createContainer(ctx, dockerCli, containerCfg, options)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -206,9 +201,6 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
hostConfig := containerCfg.HostConfig
|
||||
networkingConfig := containerCfg.NetworkingConfig
|
||||
|
||||
warnOnOomKillDisable(*hostConfig, dockerCli.Err())
|
||||
warnOnLocalhostDNS(*hostConfig, dockerCli.Err())
|
||||
|
||||
var (
|
||||
trustedRef reference.Canonical
|
||||
namedRef reference.Named
|
||||
@ -242,7 +234,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
return err
|
||||
}
|
||||
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
|
||||
return image.TagTrusted(ctx, dockerCli, trustedRef, taggedRef)
|
||||
return trust.TagTrusted(ctx, dockerCli.Client(), dockerCli.Err(), trustedRef, taggedRef)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -291,6 +283,9 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
}
|
||||
}
|
||||
|
||||
if warn := localhostDNSWarning(*hostConfig); warn != "" {
|
||||
response.Warnings = append(response.Warnings, warn)
|
||||
}
|
||||
for _, w := range response.Warnings {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), "WARNING:", w)
|
||||
}
|
||||
@ -298,33 +293,17 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
return response.ID, err
|
||||
}
|
||||
|
||||
func warnOnOomKillDisable(hostConfig container.HostConfig, stderr io.Writer) {
|
||||
if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 {
|
||||
_, _ = fmt.Fprintln(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.")
|
||||
}
|
||||
}
|
||||
|
||||
// check the DNS settings passed via --dns against localhost regexp to warn if
|
||||
// they are trying to set a DNS to a localhost address
|
||||
func warnOnLocalhostDNS(hostConfig container.HostConfig, stderr io.Writer) {
|
||||
// they are trying to set a DNS to a localhost address.
|
||||
//
|
||||
// TODO(thaJeztah): move this to the daemon, which can make a better call if it will work or not (depending on networking mode).
|
||||
func localhostDNSWarning(hostConfig container.HostConfig) string {
|
||||
for _, dnsIP := range hostConfig.DNS {
|
||||
if isLocalhost(dnsIP) {
|
||||
_, _ = fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP)
|
||||
return
|
||||
if addr, err := netip.ParseAddr(dnsIP); err == nil && addr.IsLoopback() {
|
||||
return fmt.Sprintf("Localhost DNS (%s) may fail in containers.", addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IPLocalhost is a regex pattern for IPv4 or IPv6 loopback range.
|
||||
const ipLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)`
|
||||
|
||||
var localhostIPRegexp = regexp.MustCompile(ipLocalhost)
|
||||
|
||||
// IsLocalhost returns true if ip matches the localhost IP regular expression.
|
||||
// Used for determining if nameserver settings are being passed which are
|
||||
// localhost addresses
|
||||
func isLocalhost(ip string) bool {
|
||||
return localhostIPRegexp.MatchString(ip)
|
||||
return ""
|
||||
}
|
||||
|
||||
func validatePullOpt(val string) error {
|
||||
|
||||
@ -270,31 +270,24 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
|
||||
|
||||
func TestNewCreateCommandWithWarnings(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
warning bool
|
||||
name string
|
||||
args []string
|
||||
warnings []string
|
||||
warning bool
|
||||
}{
|
||||
{
|
||||
name: "container-create-without-oom-kill-disable",
|
||||
name: "container-create-no-warnings",
|
||||
args: []string{"image:tag"},
|
||||
},
|
||||
{
|
||||
name: "container-create-oom-kill-disable-false",
|
||||
args: []string{"--oom-kill-disable=false", "image:tag"},
|
||||
name: "container-create-daemon-single-warning",
|
||||
args: []string{"image:tag"},
|
||||
warnings: []string{"warning from daemon"},
|
||||
},
|
||||
{
|
||||
name: "container-create-oom-kill-without-memory-limit",
|
||||
args: []string{"--oom-kill-disable", "image:tag"},
|
||||
warning: true,
|
||||
},
|
||||
{
|
||||
name: "container-create-oom-kill-true-without-memory-limit",
|
||||
args: []string{"--oom-kill-disable=true", "image:tag"},
|
||||
warning: true,
|
||||
},
|
||||
{
|
||||
name: "container-create-oom-kill-true-with-memory-limit",
|
||||
args: []string{"--oom-kill-disable=true", "--memory=100M", "image:tag"},
|
||||
name: "container-create-daemon-multiple-warnings",
|
||||
args: []string{"image:tag"},
|
||||
warnings: []string{"warning from daemon", "another warning from daemon"},
|
||||
},
|
||||
{
|
||||
name: "container-create-localhost-dns",
|
||||
@ -316,7 +309,7 @@ func TestNewCreateCommandWithWarnings(t *testing.T) {
|
||||
platform *specs.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{}, nil
|
||||
return container.CreateResponse{Warnings: tc.warnings}, nil
|
||||
},
|
||||
})
|
||||
cmd := NewCreateCommand(fakeCLI)
|
||||
@ -324,7 +317,7 @@ func TestNewCreateCommandWithWarnings(t *testing.T) {
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
if tc.warning {
|
||||
if tc.warning || len(tc.warnings) > 0 {
|
||||
golden.Assert(t, fakeCLI.ErrBuffer().String(), tc.name+".golden")
|
||||
} else {
|
||||
assert.Equal(t, fakeCLI.ErrBuffer().String(), "")
|
||||
|
||||
@ -13,7 +13,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
@ -1135,12 +1134,3 @@ func validateAttach(val string) (string, error) {
|
||||
}
|
||||
return val, errors.Errorf("valid streams are STDIN, STDOUT and STDERR")
|
||||
}
|
||||
|
||||
func validateAPIVersion(c *containerConfig, serverAPIVersion string) error {
|
||||
for _, m := range c.HostConfig.Mounts {
|
||||
if err := command.ValidateMountWithAPIVersion(m, serverAPIVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -107,12 +107,6 @@ func runRun(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, ro
|
||||
StatusCode: 125,
|
||||
}
|
||||
}
|
||||
if err = validateAPIVersion(containerCfg, dockerCli.CurrentVersion()); err != nil {
|
||||
return cli.StatusError{
|
||||
Status: withHelp(err, "run").Error(),
|
||||
StatusCode: 125,
|
||||
}
|
||||
}
|
||||
return runContainer(ctx, dockerCli, ropts, copts, containerCfg)
|
||||
}
|
||||
|
||||
@ -244,10 +238,16 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
case status := <-statusChan:
|
||||
// notify hijackedIOStreamer that we're exiting and wait
|
||||
// so that the terminal can be restored.
|
||||
cancelFun()
|
||||
<-errCh
|
||||
// If container exits, output stream processing may not be finished yet,
|
||||
// we need to keep the streamer running until all output is read.
|
||||
// However, if stdout or stderr is not attached, we can just exit.
|
||||
if !config.AttachStdout && !config.AttachStderr {
|
||||
// Notify hijackedIOStreamer that we're exiting and wait
|
||||
// so that the terminal can be restored.
|
||||
cancelFun()
|
||||
}
|
||||
<-errCh // Drain channel but don't care about result
|
||||
|
||||
if status != 0 {
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
|
||||
@ -156,8 +156,10 @@ func TestRunAttachTermination(t *testing.T) {
|
||||
ID: "id",
|
||||
}, nil
|
||||
},
|
||||
containerKillFunc: func(ctx context.Context, containerID, signal string) error {
|
||||
killCh <- struct{}{}
|
||||
containerKillFunc: func(ctx context.Context, containerID, sig string) error {
|
||||
if sig == "TERM" {
|
||||
close(killCh)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
containerAttachFunc: func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
|
||||
@ -172,7 +174,7 @@ func TestRunAttachTermination(t *testing.T) {
|
||||
waitFunc: func(_ string) (<-chan container.WaitResponse, <-chan error) {
|
||||
responseChan := make(chan container.WaitResponse, 1)
|
||||
errChan := make(chan error)
|
||||
|
||||
<-killCh
|
||||
responseChan <- container.WaitResponse{
|
||||
StatusCode: 130,
|
||||
}
|
||||
@ -201,8 +203,7 @@ func TestRunAttachTermination(t *testing.T) {
|
||||
case <-attachCh:
|
||||
}
|
||||
|
||||
assert.NilError(t, syscall.Kill(syscall.Getpid(), syscall.SIGINT))
|
||||
// end stream from "container" so that we'll detach
|
||||
assert.NilError(t, syscall.Kill(syscall.Getpid(), syscall.SIGTERM))
|
||||
conn.Close()
|
||||
|
||||
select {
|
||||
|
||||
2
cli/command/container/testdata/container-create-daemon-multiple-warnings.golden
vendored
Normal file
2
cli/command/container/testdata/container-create-daemon-multiple-warnings.golden
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
WARNING: warning from daemon
|
||||
WARNING: another warning from daemon
|
||||
1
cli/command/container/testdata/container-create-daemon-single-warning.golden
vendored
Normal file
1
cli/command/container/testdata/container-create-daemon-single-warning.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
WARNING: warning from daemon
|
||||
@ -1 +1 @@
|
||||
WARNING: Localhost DNS setting (--dns=::1) may fail in containers.
|
||||
WARNING: Localhost DNS (::1) may fail in containers.
|
||||
|
||||
@ -1 +1 @@
|
||||
WARNING: Localhost DNS setting (--dns=127.0.0.11) may fail in containers.
|
||||
WARNING: Localhost DNS (127.0.0.11) may fail in containers.
|
||||
|
||||
@ -1 +0,0 @@
|
||||
WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.
|
||||
@ -1 +0,0 @@
|
||||
WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.
|
||||
@ -59,42 +59,87 @@ func TestCreate(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
assert.NilError(t, cli.ContextStore().CreateOrUpdate(store.Metadata{Name: "existing-context"}))
|
||||
tests := []struct {
|
||||
doc string
|
||||
options CreateOptions
|
||||
expecterErr string
|
||||
}{
|
||||
{
|
||||
doc: "empty name",
|
||||
expecterErr: `context name cannot be empty`,
|
||||
},
|
||||
{
|
||||
doc: "reserved name",
|
||||
options: CreateOptions{
|
||||
Name: "default",
|
||||
},
|
||||
expecterErr: `"default" is a reserved context name`,
|
||||
},
|
||||
{
|
||||
doc: "whitespace-only name",
|
||||
options: CreateOptions{
|
||||
Name: " ",
|
||||
},
|
||||
expecterErr: `context name " " is invalid`,
|
||||
},
|
||||
{
|
||||
doc: "existing context",
|
||||
options: CreateOptions{
|
||||
Name: "existing-context",
|
||||
},
|
||||
expecterErr: `context "existing-context" already exists`,
|
||||
},
|
||||
{
|
||||
doc: "invalid docker host",
|
||||
options: CreateOptions{
|
||||
Name: "invalid-docker-host",
|
||||
Docker: map[string]string{
|
||||
keyHost: "some///invalid/host",
|
||||
"host": "some///invalid/host",
|
||||
},
|
||||
},
|
||||
expecterErr: `unable to parse docker host`,
|
||||
},
|
||||
{
|
||||
doc: "ssh host with skip-tls-verify=false",
|
||||
options: CreateOptions{
|
||||
Name: "skip-tls-verify-false",
|
||||
Docker: map[string]string{
|
||||
"host": "ssh://example.com,skip-tls-verify=false",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "ssh host with skip-tls-verify=true",
|
||||
options: CreateOptions{
|
||||
Name: "skip-tls-verify-true",
|
||||
Docker: map[string]string{
|
||||
"host": "ssh://example.com,skip-tls-verify=true",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "ssh host with skip-tls-verify=INVALID",
|
||||
options: CreateOptions{
|
||||
Name: "skip-tls-verify-invalid",
|
||||
Docker: map[string]string{
|
||||
"host": "ssh://example.com",
|
||||
"skip-tls-verify": "INVALID",
|
||||
},
|
||||
},
|
||||
expecterErr: `unable to create docker endpoint config: skip-tls-verify: parsing "INVALID": invalid syntax`,
|
||||
},
|
||||
{
|
||||
doc: "unknown option",
|
||||
options: CreateOptions{
|
||||
Name: "unknown-option",
|
||||
Docker: map[string]string{
|
||||
"UNKNOWN": "value",
|
||||
},
|
||||
},
|
||||
expecterErr: `unable to create docker endpoint config: unrecognized config key: UNKNOWN`,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.options.Name, func(t *testing.T) {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
err := RunCreate(cli, &tc.options)
|
||||
if tc.expecterErr == "" {
|
||||
assert.NilError(t, err)
|
||||
|
||||
@ -68,7 +68,14 @@ func parseBool(config map[string]string, name string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
res, err := strconv.ParseBool(strVal)
|
||||
return res, fmt.Errorf("name: %w", err)
|
||||
if err != nil {
|
||||
var nErr *strconv.NumError
|
||||
if errors.As(err, &nErr) {
|
||||
return res, fmt.Errorf("%s: parsing %q: %w", name, nErr.Num, nErr.Err)
|
||||
}
|
||||
return res, fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func validateConfig(config map[string]string, allowedKeys map[string]struct{}) error {
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.22
|
||||
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/width"
|
||||
@ -59,3 +64,27 @@ func Ellipsis(s string, maxDisplayWidth int) string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// capitalizeFirst capitalizes the first character of string
|
||||
func capitalizeFirst(s string) string {
|
||||
switch l := len(s); l {
|
||||
case 0:
|
||||
return s
|
||||
case 1:
|
||||
return strings.ToLower(s)
|
||||
default:
|
||||
return strings.ToUpper(string(s[0])) + strings.ToLower(s[1:])
|
||||
}
|
||||
}
|
||||
|
||||
// PrettyPrint outputs arbitrary data for human formatted output by uppercasing the first letter.
|
||||
func PrettyPrint(i any) string {
|
||||
switch t := i.(type) {
|
||||
case nil:
|
||||
return "None"
|
||||
case string:
|
||||
return capitalizeFirst(t)
|
||||
default:
|
||||
return capitalizeFirst(fmt.Sprintf("%s", t))
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,9 +76,9 @@ func (c *Context) preFormat() {
|
||||
func (c *Context) parseFormat() (*template.Template, error) {
|
||||
tmpl, err := templates.Parse(c.finalFormat)
|
||||
if err != nil {
|
||||
return tmpl, errors.Wrap(err, "template parsing error")
|
||||
return nil, errors.Wrap(err, "template parsing error")
|
||||
}
|
||||
return tmpl, err
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
func (c *Context) postFormat(tmpl *template.Template, subContext SubContext) {
|
||||
|
||||
@ -22,6 +22,7 @@ import (
|
||||
"github.com/docker/cli/cli/command/image/build"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
@ -241,7 +242,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
|
||||
|
||||
if err != nil {
|
||||
if options.quiet && urlutil.IsURL(specifiedContext) {
|
||||
fmt.Fprintln(dockerCli.Err(), progBuff)
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), progBuff)
|
||||
}
|
||||
return errors.Errorf("unable to prepare context: %s", err)
|
||||
}
|
||||
@ -336,16 +337,16 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
|
||||
for k, auth := range creds {
|
||||
authConfigs[k] = registrytypes.AuthConfig(auth)
|
||||
}
|
||||
buildOptions := imageBuildOptions(dockerCli, options)
|
||||
buildOptions.Version = types.BuilderV1
|
||||
buildOptions.Dockerfile = relDockerfile
|
||||
buildOptions.AuthConfigs = authConfigs
|
||||
buildOptions.RemoteContext = remote
|
||||
buildOpts := imageBuildOptions(dockerCli, options)
|
||||
buildOpts.Version = types.BuilderV1
|
||||
buildOpts.Dockerfile = relDockerfile
|
||||
buildOpts.AuthConfigs = authConfigs
|
||||
buildOpts.RemoteContext = remote
|
||||
|
||||
response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
|
||||
response, err := dockerCli.Client().ImageBuild(ctx, body, buildOpts)
|
||||
if err != nil {
|
||||
if options.quiet {
|
||||
fmt.Fprintf(dockerCli.Err(), "%s", progBuff)
|
||||
_, _ = fmt.Fprintf(dockerCli.Err(), "%s", progBuff)
|
||||
}
|
||||
cancel()
|
||||
return err
|
||||
@ -356,7 +357,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
|
||||
aux := func(msg jsonstream.JSONMessage) {
|
||||
var result types.BuildResult
|
||||
if err := json.Unmarshal(*msg.Aux, &result); err != nil {
|
||||
fmt.Fprintf(dockerCli.Err(), "Failed to parse aux message: %s", err)
|
||||
_, _ = fmt.Fprintf(dockerCli.Err(), "Failed to parse aux message: %s", err)
|
||||
} else {
|
||||
imageID = result.ID
|
||||
}
|
||||
@ -370,7 +371,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
|
||||
jerr.Code = 1
|
||||
}
|
||||
if options.quiet {
|
||||
fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff)
|
||||
_, _ = fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff)
|
||||
}
|
||||
return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code}
|
||||
}
|
||||
@ -380,7 +381,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
|
||||
// Windows: show error message about modified file permissions if the
|
||||
// daemon isn't running Windows.
|
||||
if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet {
|
||||
fmt.Fprintln(dockerCli.Out(), "SECURITY WARNING: You are building a Docker "+
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), "SECURITY WARNING: You are building a Docker "+
|
||||
"image from Windows against a non-Windows Docker host. All files and "+
|
||||
"directories added to build context will have '-rwxr-xr-x' permissions. "+
|
||||
"It is recommended to double check and reset permissions for sensitive "+
|
||||
@ -406,7 +407,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
|
||||
// Since the build was successful, now we must tag any of the resolved
|
||||
// images from the above Dockerfile rewrite.
|
||||
for _, resolved := range resolvedTags {
|
||||
if err := TagTrusted(ctx, dockerCli, resolved.digestRef, resolved.tagRef); err != nil {
|
||||
if err := trust.TagTrusted(ctx, dockerCli.Client(), dockerCli.Err(), resolved.digestRef, resolved.tagRef); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -501,12 +502,12 @@ func replaceDockerfileForContentTrust(ctx context.Context, inputTarStream io.Rea
|
||||
hdr, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
// Signals end of archive.
|
||||
tarWriter.Close()
|
||||
pipeWriter.Close()
|
||||
_ = tarWriter.Close()
|
||||
_ = pipeWriter.Close()
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -518,7 +519,7 @@ func replaceDockerfileForContentTrust(ctx context.Context, inputTarStream io.Rea
|
||||
var newDockerfile []byte
|
||||
newDockerfile, *resolvedTags, err = rewriteDockerfileFromForContentTrust(ctx, content, translator)
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
hdr.Size = int64(len(newDockerfile))
|
||||
@ -526,12 +527,12 @@ func replaceDockerfileForContentTrust(ctx context.Context, inputTarStream io.Rea
|
||||
}
|
||||
|
||||
if err := tarWriter.WriteHeader(hdr); err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := io.Copy(tarWriter, content); err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +51,16 @@ func NewLoadCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
func runLoad(ctx context.Context, dockerCli command.Cli, opts loadOptions) error {
|
||||
var input io.Reader = dockerCli.In()
|
||||
if opts.input != "" {
|
||||
|
||||
// TODO(thaJeztah): add support for "-" as STDIN to match other commands, possibly making it a required positional argument.
|
||||
switch opts.input {
|
||||
case "":
|
||||
// To avoid getting stuck, verify that a tar file is given either in
|
||||
// the input flag or through stdin and if not display an error message and exit.
|
||||
if dockerCli.In().IsTerminal() {
|
||||
return errors.Errorf("requested load from stdin, but stdin is empty")
|
||||
}
|
||||
default:
|
||||
// We use sequential.Open to use sequential file access on Windows, avoiding
|
||||
// depleting the standby list un-necessarily. On Linux, this equates to a regular os.Open.
|
||||
file, err := sequential.Open(opts.input)
|
||||
@ -62,12 +71,6 @@ func runLoad(ctx context.Context, dockerCli command.Cli, opts loadOptions) error
|
||||
input = file
|
||||
}
|
||||
|
||||
// To avoid getting stuck, verify that a tar file is given either in
|
||||
// the input flag or through stdin and if not display an error message and exit.
|
||||
if opts.input == "" && dockerCli.In().IsTerminal() {
|
||||
return errors.Errorf("requested load from stdin, but stdin is empty")
|
||||
}
|
||||
|
||||
var options []client.ImageLoadOption
|
||||
if opts.quiet || !dockerCli.Out().IsTerminal() {
|
||||
options = append(options, client.ImageLoadWithQuiet(true))
|
||||
|
||||
@ -74,8 +74,6 @@ Image index won't be pushed, meaning that other manifests, including attestation
|
||||
}
|
||||
|
||||
// RunPush performs a push against the engine based on the specified options
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func RunPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error {
|
||||
var platform *ocispec.Platform
|
||||
out := tui.NewOutput(dockerCli.Out())
|
||||
@ -107,10 +105,7 @@ To push the complete multi-platform image, remove the --platform flag.
|
||||
}
|
||||
|
||||
// Resolve the Repository name from fqn to RepositoryInfo
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repoInfo, _ := registry.ParseRepositoryInfo(ref)
|
||||
|
||||
// Resolve the Auth config relevant for this server
|
||||
authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index)
|
||||
@ -139,8 +134,8 @@ To push the complete multi-platform image, remove the --platform flag.
|
||||
|
||||
defer responseBody.Close()
|
||||
if !opts.untrusted {
|
||||
// TODO PushTrustedReference currently doesn't respect `--quiet`
|
||||
return PushTrustedReference(ctx, dockerCli, repoInfo, ref, authConfig, responseBody)
|
||||
// TODO pushTrustedReference currently doesn't respect `--quiet`
|
||||
return pushTrustedReference(ctx, dockerCli, repoInfo, ref, authConfig, responseBody)
|
||||
}
|
||||
|
||||
if opts.quiet {
|
||||
|
||||
@ -3,17 +3,14 @@ package image
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
@ -30,142 +27,23 @@ type target struct {
|
||||
size int64
|
||||
}
|
||||
|
||||
// TrustedPush handles content trust pushing of an image
|
||||
func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, options image.PushOptions) error {
|
||||
responseBody, err := cli.Client().ImagePush(ctx, reference.FamiliarString(ref), options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer responseBody.Close()
|
||||
|
||||
return PushTrustedReference(ctx, cli, repoInfo, ref, authConfig, responseBody)
|
||||
// notaryClientProvider is used in tests to provide a dummy notary client.
|
||||
type notaryClientProvider interface {
|
||||
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error)
|
||||
}
|
||||
|
||||
// PushTrustedReference pushes a canonical reference to the trust server.
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func PushTrustedReference(ctx context.Context, ioStreams command.Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader) error {
|
||||
// If it is a trusted push we would like to find the target entry which match the
|
||||
// tag provided in the function and then do an AddTarget later.
|
||||
target := &client.Target{}
|
||||
// Count the times of calling for handleTarget,
|
||||
// if it is called more that once, that should be considered an error in a trusted push.
|
||||
cnt := 0
|
||||
handleTarget := func(msg jsonstream.JSONMessage) {
|
||||
cnt++
|
||||
if cnt > 1 {
|
||||
// handleTarget should only be called once. This will be treated as an error.
|
||||
return
|
||||
}
|
||||
|
||||
var pushResult types.PushResult
|
||||
err := json.Unmarshal(*msg.Aux, &pushResult)
|
||||
if err == nil && pushResult.Tag != "" {
|
||||
if dgst, err := digest.Parse(pushResult.Digest); err == nil {
|
||||
h, err := hex.DecodeString(dgst.Hex())
|
||||
if err != nil {
|
||||
target = nil
|
||||
return
|
||||
}
|
||||
target.Name = pushResult.Tag
|
||||
target.Hashes = data.Hashes{string(dgst.Algorithm()): h}
|
||||
target.Length = int64(pushResult.Size)
|
||||
}
|
||||
}
|
||||
// newNotaryClient provides a Notary Repository to interact with signed metadata for an image.
|
||||
func newNotaryClient(cli command.Streams, imgRefAndAuth trust.ImageRefAndAuth) (client.Repository, error) {
|
||||
if ncp, ok := cli.(notaryClientProvider); ok {
|
||||
// notaryClientProvider is used in tests to provide a dummy notary client.
|
||||
return ncp.NotaryClient(imgRefAndAuth, []string{"pull"})
|
||||
}
|
||||
|
||||
var tag string
|
||||
switch x := ref.(type) {
|
||||
case reference.Canonical:
|
||||
return errors.New("cannot push a digest reference")
|
||||
case reference.NamedTagged:
|
||||
tag = x.Tag()
|
||||
default:
|
||||
// We want trust signatures to always take an explicit tag,
|
||||
// otherwise it will act as an untrusted push.
|
||||
if err := jsonstream.Display(ctx, in, ioStreams.Out()); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintln(ioStreams.Err(), "No tag specified, skipping trust metadata push")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := jsonstream.Display(ctx, in, ioStreams.Out(), jsonstream.WithAuxCallback(handleTarget)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt > 1 {
|
||||
return errors.Errorf("internal error: only one call to handleTarget expected")
|
||||
}
|
||||
|
||||
if target == nil {
|
||||
return errors.Errorf("no targets found, provide a specific tag in order to sign it")
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(ioStreams.Out(), "Signing and pushing trust metadata")
|
||||
|
||||
repo, err := trust.GetNotaryRepository(ioStreams.In(), ioStreams.Out(), command.UserAgent(), repoInfo, &authConfig, "push", "pull")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error establishing connection to trust repository")
|
||||
}
|
||||
|
||||
// get the latest repository metadata so we can figure out which roles to sign
|
||||
_, err = repo.ListTargets()
|
||||
|
||||
switch err.(type) {
|
||||
case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
|
||||
keys := repo.GetCryptoService().ListKeys(data.CanonicalRootRole)
|
||||
var rootKeyID string
|
||||
// always select the first root key
|
||||
if len(keys) > 0 {
|
||||
sort.Strings(keys)
|
||||
rootKeyID = keys[0]
|
||||
} else {
|
||||
rootPublicKey, err := repo.GetCryptoService().Create(data.CanonicalRootRole, "", data.ECDSAKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rootKeyID = rootPublicKey.ID()
|
||||
}
|
||||
|
||||
// Initialize the notary repository with a remotely managed snapshot key
|
||||
if err := repo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil {
|
||||
return trust.NotaryError(repoInfo.Name.Name(), err)
|
||||
}
|
||||
_, _ = fmt.Fprintf(ioStreams.Out(), "Finished initializing %q\n", repoInfo.Name.Name())
|
||||
err = repo.AddTarget(target, data.CanonicalTargetsRole)
|
||||
case nil:
|
||||
// already initialized and we have successfully downloaded the latest metadata
|
||||
err = AddTargetToAllSignableRoles(repo, target)
|
||||
default:
|
||||
return trust.NotaryError(repoInfo.Name.Name(), err)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
err = repo.Publish()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "failed to sign %s:%s", repoInfo.Name.Name(), tag)
|
||||
return trust.NotaryError(repoInfo.Name.Name(), err)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(ioStreams.Out(), "Successfully signed %s:%s\n", repoInfo.Name.Name(), tag)
|
||||
return nil
|
||||
return trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), "pull")
|
||||
}
|
||||
|
||||
// AddTargetToAllSignableRoles attempts to add the image target to all the top level delegation roles we can
|
||||
// (based on whether we have the signing key and whether the role's path allows
|
||||
// us to).
|
||||
// If there are no delegation roles, we add to the targets role.
|
||||
func AddTargetToAllSignableRoles(repo client.Repository, target *client.Target) error {
|
||||
signableRoles, err := trust.GetSignableRoles(repo, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return repo.AddTarget(target, signableRoles...)
|
||||
// pushTrustedReference pushes a canonical reference to the trust server.
|
||||
func pushTrustedReference(ctx context.Context, ioStreams command.Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader) error {
|
||||
return trust.PushTrustedReference(ctx, ioStreams, repoInfo, ref, authConfig, in, command.UserAgent())
|
||||
}
|
||||
|
||||
// trustedPull handles content trust pulling of an image
|
||||
@ -205,7 +83,11 @@ func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.Image
|
||||
return err
|
||||
}
|
||||
|
||||
if err := TagTrusted(ctx, cli, trustedRef, tagged); err != nil {
|
||||
// Use familiar references when interacting with client and output
|
||||
familiarRef := reference.FamiliarString(tagged)
|
||||
trustedFamiliarRef := reference.FamiliarString(trustedRef)
|
||||
_, _ = fmt.Fprintf(cli.Err(), "Tagging %s as %s\n", trustedFamiliarRef, familiarRef)
|
||||
if err := cli.Client().ImageTag(ctx, trustedFamiliarRef, familiarRef); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -213,7 +95,7 @@ func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.Image
|
||||
}
|
||||
|
||||
func getTrustedPullTargets(cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth) ([]target, error) {
|
||||
notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPullOnly)
|
||||
notaryRepo, err := newNotaryClient(cli, imgRefAndAuth)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error establishing connection to trust repository")
|
||||
}
|
||||
@ -293,7 +175,7 @@ func TrustedReference(ctx context.Context, cli command.Cli, ref reference.NamedT
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notaryRepo, err := cli.NotaryClient(imgRefAndAuth, []string{"pull"})
|
||||
notaryRepo, err := newNotaryClient(cli, imgRefAndAuth)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error establishing connection to trust repository")
|
||||
}
|
||||
@ -326,17 +208,6 @@ func convertTarget(t client.Target) (target, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TagTrusted tags a trusted ref
|
||||
func TagTrusted(ctx context.Context, cli command.Cli, trustedRef reference.Canonical, ref reference.NamedTagged) error {
|
||||
// Use familiar references when interacting with client and output
|
||||
familiarRef := reference.FamiliarString(ref)
|
||||
trustedFamiliarRef := reference.FamiliarString(trustedRef)
|
||||
|
||||
_, _ = fmt.Fprintf(cli.Err(), "Tagging %s as %s\n", trustedFamiliarRef, familiarRef)
|
||||
|
||||
return cli.Client().ImageTag(ctx, trustedFamiliarRef, familiarRef)
|
||||
}
|
||||
|
||||
// AuthResolver returns an auth resolver function from a command.Cli
|
||||
func AuthResolver(cli command.Cli) func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig {
|
||||
return func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig {
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/trust"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/theupdateframework/notary/client"
|
||||
"github.com/theupdateframework/notary/passphrase"
|
||||
"github.com/theupdateframework/notary/trustpinning"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/env"
|
||||
)
|
||||
|
||||
func TestENVTrustServer(t *testing.T) {
|
||||
env.PatchAll(t, map[string]string{"DOCKER_CONTENT_TRUST_SERVER": "https://notary-test.example.com:5000"})
|
||||
indexInfo := ®istrytypes.IndexInfo{Name: "testserver"}
|
||||
output, err := trust.Server(indexInfo)
|
||||
expectedStr := "https://notary-test.example.com:5000"
|
||||
if err != nil || output != expectedStr {
|
||||
t.Fatalf("Expected server to be %s, got %s", expectedStr, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPENVTrustServer(t *testing.T) {
|
||||
env.PatchAll(t, map[string]string{"DOCKER_CONTENT_TRUST_SERVER": "http://notary-test.example.com:5000"})
|
||||
indexInfo := ®istrytypes.IndexInfo{Name: "testserver"}
|
||||
_, err := trust.Server(indexInfo)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error with invalid scheme")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOfficialTrustServer(t *testing.T) {
|
||||
indexInfo := ®istrytypes.IndexInfo{Name: "testserver", Official: true}
|
||||
output, err := trust.Server(indexInfo)
|
||||
if err != nil || output != trust.NotaryServer {
|
||||
t.Fatalf("Expected server to be %s, got %s", trust.NotaryServer, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonOfficialTrustServer(t *testing.T) {
|
||||
indexInfo := ®istrytypes.IndexInfo{Name: "testserver", Official: false}
|
||||
output, err := trust.Server(indexInfo)
|
||||
expectedStr := "https://" + indexInfo.Name
|
||||
if err != nil || output != expectedStr {
|
||||
t.Fatalf("Expected server to be %s, got %s", expectedStr, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddTargetToAllSignableRolesError(t *testing.T) {
|
||||
notaryRepo, err := client.NewFileCachedRepository(t.TempDir(), "gun", "https://localhost", nil, passphrase.ConstantRetriever("password"), trustpinning.TrustPinConfig{})
|
||||
assert.NilError(t, err)
|
||||
target := client.Target{}
|
||||
err = AddTargetToAllSignableRoles(notaryRepo, &target)
|
||||
assert.Error(t, err, "client is offline")
|
||||
}
|
||||
@ -1,11 +1,16 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/manifest/store"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@ -21,6 +26,37 @@ type annotateOptions struct {
|
||||
osVersion string
|
||||
}
|
||||
|
||||
// manifestStoreProvider is used in tests to provide a dummy store.
|
||||
type manifestStoreProvider interface {
|
||||
// ManifestStore returns a store for local manifests
|
||||
ManifestStore() store.Store
|
||||
RegistryClient(bool) registryclient.RegistryClient
|
||||
}
|
||||
|
||||
// newManifestStore returns a store for local manifests
|
||||
func newManifestStore(dockerCLI command.Cli) store.Store {
|
||||
if msp, ok := dockerCLI.(manifestStoreProvider); ok {
|
||||
// manifestStoreProvider is used in tests to provide a dummy store.
|
||||
return msp.ManifestStore()
|
||||
}
|
||||
|
||||
// TODO: support override default location from config file
|
||||
return store.NewStore(filepath.Join(config.Dir(), "manifests"))
|
||||
}
|
||||
|
||||
// newRegistryClient returns a client for communicating with a Docker distribution
|
||||
// registry
|
||||
func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient.RegistryClient {
|
||||
if msp, ok := dockerCLI.(manifestStoreProvider); ok {
|
||||
// manifestStoreProvider is used in tests to provide a dummy store.
|
||||
return msp.RegistryClient(allowInsecure)
|
||||
}
|
||||
resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig {
|
||||
return command.ResolveAuthConfig(dockerCLI.ConfigFile(), index)
|
||||
}
|
||||
return registryclient.NewRegistryClient(resolver, command.UserAgent(), allowInsecure)
|
||||
}
|
||||
|
||||
// NewAnnotateCommand creates a new `docker manifest annotate` command
|
||||
func newAnnotateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts annotateOptions
|
||||
@ -47,7 +83,7 @@ func newAnnotateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runManifestAnnotate(dockerCli command.Cli, opts annotateOptions) error {
|
||||
func runManifestAnnotate(dockerCLI command.Cli, opts annotateOptions) error {
|
||||
targetRef, err := normalizeReference(opts.target)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "annotate: error parsing name for manifest list %s", opts.target)
|
||||
@ -57,7 +93,7 @@ func runManifestAnnotate(dockerCli command.Cli, opts annotateOptions) error {
|
||||
return errors.Wrapf(err, "annotate: error parsing name for manifest %s", opts.image)
|
||||
}
|
||||
|
||||
manifestStore := dockerCli.ManifestStore()
|
||||
manifestStore := newManifestStore(dockerCLI)
|
||||
imageManifest, err := manifestStore.Get(targetRef, imgRef)
|
||||
switch {
|
||||
case store.IsNotFound(err):
|
||||
|
||||
@ -7,7 +7,6 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/store"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -42,12 +41,7 @@ func createManifestList(ctx context.Context, dockerCLI command.Cli, args []strin
|
||||
return errors.Wrapf(err, "error parsing name for manifest list %s", newRef)
|
||||
}
|
||||
|
||||
_, err = registry.ParseRepositoryInfo(targetRef)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error parsing repository name for manifest list %s", newRef)
|
||||
}
|
||||
|
||||
manifestStore := dockerCLI.ManifestStore()
|
||||
manifestStore := newManifestStore(dockerCLI)
|
||||
_, err = manifestStore.GetList(targetRef)
|
||||
switch {
|
||||
case store.IsNotFound(err):
|
||||
|
||||
@ -11,7 +11,6 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -62,7 +61,7 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions)
|
||||
return err
|
||||
}
|
||||
|
||||
imageManifest, err := dockerCli.ManifestStore().Get(listRef, namedRef)
|
||||
imageManifest, err := newManifestStore(dockerCli).Get(listRef, namedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -70,13 +69,13 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions)
|
||||
}
|
||||
|
||||
// Try a local manifest list first
|
||||
localManifestList, err := dockerCli.ManifestStore().GetList(namedRef)
|
||||
localManifestList, err := newManifestStore(dockerCli).GetList(namedRef)
|
||||
if err == nil {
|
||||
return printManifestList(dockerCli, namedRef, localManifestList, opts)
|
||||
}
|
||||
|
||||
// Next try a remote manifest
|
||||
registryClient := dockerCli.RegistryClient(opts.insecure)
|
||||
registryClient := newRegistryClient(dockerCli, opts.insecure)
|
||||
imageManifest, err := registryClient.GetManifest(ctx, namedRef)
|
||||
if err == nil {
|
||||
return printManifest(dockerCli, imageManifest, opts)
|
||||
@ -113,10 +112,7 @@ func printManifest(dockerCli command.Cli, manifest types.ImageManifest, opts ins
|
||||
|
||||
func printManifestList(dockerCli command.Cli, namedRef reference.Named, list []types.ImageManifest, opts inspectOptions) error {
|
||||
if !opts.verbose {
|
||||
targetRepo, err := registry.ParseRepositoryInfo(namedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetRepo := reference.TrimNamed(namedRef)
|
||||
|
||||
manifests := []manifestlist.ManifestDescriptor{}
|
||||
// More than one response. This is a manifest list.
|
||||
|
||||
@ -15,7 +15,6 @@ import (
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/manifest/ocischema"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -69,7 +68,7 @@ func runPush(ctx context.Context, dockerCli command.Cli, opts pushOpts) error {
|
||||
return err
|
||||
}
|
||||
|
||||
manifests, err := dockerCli.ManifestStore().GetList(targetRef)
|
||||
manifests, err := newManifestStore(dockerCli).GetList(targetRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -86,7 +85,7 @@ func runPush(ctx context.Context, dockerCli command.Cli, opts pushOpts) error {
|
||||
return err
|
||||
}
|
||||
if opts.purge {
|
||||
return dockerCli.ManifestStore().Remove(targetRef)
|
||||
return newManifestStore(dockerCli).Remove(targetRef)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -100,21 +99,10 @@ func buildPushRequest(manifests []types.ImageManifest, targetRef reference.Named
|
||||
return req, err
|
||||
}
|
||||
|
||||
targetRepo, err := registry.ParseRepositoryInfo(targetRef)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
targetRepoName, err := registryclient.RepoNameForReference(targetRepo.Name)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
targetRepoName := reference.Path(reference.TrimNamed(targetRef))
|
||||
|
||||
for _, imageManifest := range manifests {
|
||||
manifestRepoName, err := registryclient.RepoNameForReference(imageManifest.Ref)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
manifestRepoName := reference.Path(reference.TrimNamed(imageManifest.Ref))
|
||||
repoName, _ := reference.WithName(manifestRepoName)
|
||||
if repoName.Name() != targetRepoName {
|
||||
blobs, err := buildBlobRequestList(imageManifest, repoName)
|
||||
@ -134,11 +122,7 @@ func buildPushRequest(manifests []types.ImageManifest, targetRef reference.Named
|
||||
}
|
||||
|
||||
func buildManifestList(manifests []types.ImageManifest, targetRef reference.Named) (*manifestlist.DeserializedManifestList, error) {
|
||||
targetRepoInfo, err := registry.ParseRepositoryInfo(targetRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
targetRepo := reference.TrimNamed(targetRef)
|
||||
descriptors := []manifestlist.ManifestDescriptor{}
|
||||
for _, imageManifest := range manifests {
|
||||
if imageManifest.Descriptor.Platform == nil ||
|
||||
@ -147,7 +131,7 @@ func buildManifestList(manifests []types.ImageManifest, targetRef reference.Name
|
||||
return nil, errors.Errorf(
|
||||
"manifest %s must have an OS and Architecture to be pushed to a registry", imageManifest.Ref)
|
||||
}
|
||||
descriptor, err := buildManifestDescriptor(targetRepoInfo, imageManifest)
|
||||
descriptor, err := buildManifestDescriptor(targetRepo, imageManifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -157,14 +141,9 @@ func buildManifestList(manifests []types.ImageManifest, targetRef reference.Name
|
||||
return manifestlist.FromDescriptors(descriptors)
|
||||
}
|
||||
|
||||
func buildManifestDescriptor(targetRepo *registry.RepositoryInfo, imageManifest types.ImageManifest) (manifestlist.ManifestDescriptor, error) {
|
||||
repoInfo, err := registry.ParseRepositoryInfo(imageManifest.Ref)
|
||||
if err != nil {
|
||||
return manifestlist.ManifestDescriptor{}, err
|
||||
}
|
||||
|
||||
manifestRepoHostname := reference.Domain(repoInfo.Name)
|
||||
targetRepoHostname := reference.Domain(targetRepo.Name)
|
||||
func buildManifestDescriptor(targetRepo reference.Named, imageManifest types.ImageManifest) (manifestlist.ManifestDescriptor, error) {
|
||||
manifestRepoHostname := reference.Domain(reference.TrimNamed(imageManifest.Ref))
|
||||
targetRepoHostname := reference.Domain(reference.TrimNamed(targetRepo))
|
||||
if manifestRepoHostname != targetRepoHostname {
|
||||
return manifestlist.ManifestDescriptor{}, errors.Errorf("cannot use source images from a different registry than the target image: %s != %s", manifestRepoHostname, targetRepoHostname)
|
||||
}
|
||||
@ -182,7 +161,7 @@ func buildManifestDescriptor(targetRepo *registry.RepositoryInfo, imageManifest
|
||||
manifest.Platform = *platform
|
||||
}
|
||||
|
||||
if err = manifest.Descriptor.Digest.Validate(); err != nil {
|
||||
if err := manifest.Descriptor.Digest.Validate(); err != nil {
|
||||
return manifestlist.ManifestDescriptor{}, errors.Wrapf(err,
|
||||
"digest parse of image %q failed", imageManifest.Ref)
|
||||
}
|
||||
@ -269,7 +248,7 @@ func buildPutManifestRequest(imageManifest types.ImageManifest, targetRef refere
|
||||
}
|
||||
|
||||
func pushList(ctx context.Context, dockerCLI command.Cli, req pushRequest) error {
|
||||
rclient := dockerCLI.RegistryClient(req.insecure)
|
||||
rclient := newRegistryClient(dockerCLI, req.insecure)
|
||||
|
||||
if err := mountBlobs(ctx, rclient, req.targetRef, req.manifestBlobs); err != nil {
|
||||
return err
|
||||
|
||||
@ -16,7 +16,7 @@ func newRmManifestListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Short: "Delete one or more manifest lists from local storage",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRemove(cmd.Context(), dockerCLI.ManifestStore(), args)
|
||||
return runRemove(cmd.Context(), newManifestStore(dockerCLI), args)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -69,15 +69,15 @@ func normalizeReference(ref string) (reference.Named, error) {
|
||||
|
||||
// getManifest from the local store, and fallback to the remote registry if it
|
||||
// doesn't exist locally
|
||||
func getManifest(ctx context.Context, dockerCli command.Cli, listRef, namedRef reference.Named, insecure bool) (types.ImageManifest, error) {
|
||||
data, err := dockerCli.ManifestStore().Get(listRef, namedRef)
|
||||
func getManifest(ctx context.Context, dockerCLI command.Cli, listRef, namedRef reference.Named, insecure bool) (types.ImageManifest, error) {
|
||||
data, err := newManifestStore(dockerCLI).Get(listRef, namedRef)
|
||||
switch {
|
||||
case store.IsNotFound(err):
|
||||
return dockerCli.RegistryClient(insecure).GetManifest(ctx, namedRef)
|
||||
return newRegistryClient(dockerCLI, insecure).GetManifest(ctx, namedRef)
|
||||
case err != nil:
|
||||
return types.ImageManifest{}, err
|
||||
case len(data.Raw) == 0:
|
||||
return dockerCli.RegistryClient(insecure).GetManifest(ctx, namedRef)
|
||||
return newRegistryClient(dockerCLI, insecure).GetManifest(ctx, namedRef)
|
||||
default:
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package network
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
@ -68,7 +69,7 @@ func runRemove(ctx context.Context, dockerCLI command.Cli, networks []string, op
|
||||
}
|
||||
|
||||
if status != 0 {
|
||||
return cli.StatusError{StatusCode: status}
|
||||
return cli.StatusError{StatusCode: status, Status: "exit status " + strconv.Itoa(status)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
37
cli/command/node/completion.go
Normal file
37
cli/command/node/completion.go
Normal file
@ -0,0 +1,37 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// completeNodeNames offers completion for swarm node (host)names and optional IDs.
|
||||
// By default, only names are returned.
|
||||
// Set DOCKER_COMPLETION_SHOW_NODE_IDS=yes to also complete IDs.
|
||||
//
|
||||
// TODO(thaJeztah): add support for filters.
|
||||
func completeNodeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
// https://github.com/docker/cli/blob/f9ced58158d5e0b358052432244b483774a1983d/contrib/completion/bash/docker#L41-L43
|
||||
showIDs := os.Getenv("DOCKER_COMPLETION_SHOW_NODE_IDS") == "yes"
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().NodeList(cmd.Context(), types.NodeListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(list)+1)
|
||||
for _, node := range list {
|
||||
if showIDs {
|
||||
names = append(names, node.Description.Hostname, node.ID)
|
||||
} else {
|
||||
names = append(names, node.Description.Hostname)
|
||||
}
|
||||
}
|
||||
// Nodes allow "self" as magic word for the current node.
|
||||
names = append(names, "self")
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,7 @@ func newDemoteCommand(dockerCli command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDemote(cmd.Context(), dockerCli, args)
|
||||
},
|
||||
ValidArgsFunction: completeNodeNames(dockerCli),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
@ -147,11 +146,11 @@ func (c *nodeContext) Hostname() string {
|
||||
}
|
||||
|
||||
func (c *nodeContext) Status() string {
|
||||
return command.PrettyPrint(string(c.n.Status.State))
|
||||
return formatter.PrettyPrint(string(c.n.Status.State))
|
||||
}
|
||||
|
||||
func (c *nodeContext) Availability() string {
|
||||
return command.PrettyPrint(string(c.n.Spec.Availability))
|
||||
return formatter.PrettyPrint(string(c.n.Spec.Availability))
|
||||
}
|
||||
|
||||
func (c *nodeContext) ManagerStatus() string {
|
||||
@ -163,7 +162,7 @@ func (c *nodeContext) ManagerStatus() string {
|
||||
reachability = string(c.n.ManagerStatus.Reachability)
|
||||
}
|
||||
}
|
||||
return command.PrettyPrint(reachability)
|
||||
return formatter.PrettyPrint(reachability)
|
||||
}
|
||||
|
||||
func (c *nodeContext) TLSStatus() string {
|
||||
@ -226,11 +225,11 @@ func (ctx *nodeInspectContext) Hostname() string {
|
||||
}
|
||||
|
||||
func (ctx *nodeInspectContext) CreatedAt() string {
|
||||
return command.PrettyPrint(ctx.Node.CreatedAt)
|
||||
return formatter.PrettyPrint(ctx.Node.CreatedAt)
|
||||
}
|
||||
|
||||
func (ctx *nodeInspectContext) StatusState() string {
|
||||
return command.PrettyPrint(ctx.Node.Status.State)
|
||||
return formatter.PrettyPrint(ctx.Node.Status.State)
|
||||
}
|
||||
|
||||
func (ctx *nodeInspectContext) HasStatusMessage() bool {
|
||||
@ -238,11 +237,11 @@ func (ctx *nodeInspectContext) HasStatusMessage() bool {
|
||||
}
|
||||
|
||||
func (ctx *nodeInspectContext) StatusMessage() string {
|
||||
return command.PrettyPrint(ctx.Node.Status.Message)
|
||||
return formatter.PrettyPrint(ctx.Node.Status.Message)
|
||||
}
|
||||
|
||||
func (ctx *nodeInspectContext) SpecAvailability() string {
|
||||
return command.PrettyPrint(ctx.Node.Spec.Availability)
|
||||
return formatter.PrettyPrint(ctx.Node.Spec.Availability)
|
||||
}
|
||||
|
||||
func (ctx *nodeInspectContext) HasStatusAddr() bool {
|
||||
@ -262,7 +261,7 @@ func (ctx *nodeInspectContext) ManagerStatusAddr() string {
|
||||
}
|
||||
|
||||
func (ctx *nodeInspectContext) ManagerStatusReachability() string {
|
||||
return command.PrettyPrint(ctx.Node.ManagerStatus.Reachability)
|
||||
return formatter.PrettyPrint(ctx.Node.ManagerStatus.Reachability)
|
||||
}
|
||||
|
||||
func (ctx *nodeInspectContext) IsManagerStatusLeader() bool {
|
||||
|
||||
@ -32,6 +32,7 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts.nodeIds = args
|
||||
return runInspect(cmd.Context(), dockerCli, opts)
|
||||
},
|
||||
ValidArgsFunction: completeNodeNames(dockerCli),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type listOptions struct {
|
||||
@ -40,6 +41,12 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
|
||||
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
|
||||
flags.VisitAll(func(flag *pflag.Flag) {
|
||||
// Set a default completion function if none was set. We don't look
|
||||
// up if it does already have one set, because Cobra does this for
|
||||
// us, and returns an error (which we ignore for this reason).
|
||||
_ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete)
|
||||
})
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ func newPromoteCommand(dockerCli command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runPromote(cmd.Context(), dockerCli, args)
|
||||
},
|
||||
ValidArgsFunction: completeNodeNames(dockerCli),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type psOptions struct {
|
||||
@ -41,7 +42,7 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
return runPs(cmd.Context(), dockerCli, options)
|
||||
},
|
||||
ValidArgsFunction: completion.NoComplete,
|
||||
ValidArgsFunction: completeNodeNames(dockerCli),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&options.noTrunc, "no-trunc", false, "Do not truncate output")
|
||||
@ -50,6 +51,12 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.StringVar(&options.format, "format", "", "Pretty-print tasks using a Go template")
|
||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display task IDs")
|
||||
|
||||
flags.VisitAll(func(flag *pflag.Flag) {
|
||||
// Set a default completion function if none was set. We don't look
|
||||
// up if it does already have one set, because Cobra does this for
|
||||
// us, and returns an error (which we ignore for this reason).
|
||||
_ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete)
|
||||
})
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRemove(cmd.Context(), dockerCli, args, opts)
|
||||
},
|
||||
ValidArgsFunction: completeNodeNames(dockerCli),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.force, "force", "f", false, "Force remove a node from the swarm")
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/pkg/errors"
|
||||
@ -25,6 +26,7 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runUpdate(cmd.Context(), dockerCli, cmd.Flags(), args[0])
|
||||
},
|
||||
ValidArgsFunction: completeNodeNames(dockerCli),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -33,6 +35,15 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.Var(&options.annotations.labels, flagLabelAdd, `Add or update a node label ("key=value")`)
|
||||
labelKeys := opts.NewListOpts(nil)
|
||||
flags.Var(&labelKeys, flagLabelRemove, "Remove a node label if exists")
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagRole, completion.FromList("worker", "manager"))
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagAvailability, completion.FromList("active", "pause", "drain"))
|
||||
flags.VisitAll(func(flag *pflag.Flag) {
|
||||
// Set a default completion function if none was set. We don't look
|
||||
// up if it does already have one set, because Cobra does this for
|
||||
// us, and returns an error (which we ignore for this reason).
|
||||
_ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete)
|
||||
})
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@ -64,10 +64,7 @@ func buildPullConfig(ctx context.Context, dockerCli command.Cli, opts pluginOpti
|
||||
return types.PluginInstallOptions{}, err
|
||||
}
|
||||
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return types.PluginInstallOptions{}, err
|
||||
}
|
||||
repoInfo, _ := registry.ParseRepositoryInfo(ref)
|
||||
|
||||
remote := ref.String()
|
||||
|
||||
|
||||
@ -6,8 +6,8 @@ import (
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
@ -49,10 +49,7 @@ func runPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error
|
||||
|
||||
named = reference.TagNameOnly(named)
|
||||
|
||||
repoInfo, err := registry.ParseRepositoryInfo(named)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repoInfo, _ := registry.ParseRepositoryInfo(named)
|
||||
authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index)
|
||||
encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig)
|
||||
if err != nil {
|
||||
@ -66,7 +63,7 @@ func runPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error
|
||||
defer responseBody.Close()
|
||||
|
||||
if !opts.untrusted {
|
||||
return image.PushTrustedReference(ctx, dockerCli, repoInfo, named, authConfig, responseBody)
|
||||
return trust.PushTrustedReference(ctx, dockerCli, repoInfo, named, authConfig, responseBody, command.UserAgent())
|
||||
}
|
||||
|
||||
return jsonstream.Display(ctx, responseBody, dockerCli.Out())
|
||||
|
||||
@ -15,7 +15,6 @@ import (
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -28,16 +27,22 @@ const (
|
||||
"for organizations using SSO. Learn more at https://docs.docker.com/go/access-tokens/"
|
||||
)
|
||||
|
||||
// authConfigKey is the key used to store credentials for Docker Hub. It is
|
||||
// a copy of [registry.IndexServer].
|
||||
//
|
||||
// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker/registry#IndexServer
|
||||
const authConfigKey = "https:/index.docker.io/v1/"
|
||||
|
||||
// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
|
||||
// for the given command.
|
||||
func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) registrytypes.RequestAuthConfig {
|
||||
configKey := getAuthConfigKey(index.Name)
|
||||
isDefaultRegistry := configKey == authConfigKey || index.Official
|
||||
return func(ctx context.Context) (string, error) {
|
||||
_, _ = fmt.Fprintf(cli.Out(), "\nLogin prior to %s:\n", cmdName)
|
||||
indexServer := registry.GetAuthConfigKey(index)
|
||||
isDefaultRegistry := indexServer == registry.IndexServer
|
||||
authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, indexServer, isDefaultRegistry)
|
||||
authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, configKey, isDefaultRegistry)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", indexServer, err)
|
||||
_, _ = fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", authConfigKey, err)
|
||||
}
|
||||
|
||||
select {
|
||||
@ -46,7 +51,7 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
|
||||
default:
|
||||
}
|
||||
|
||||
authConfig, err = PromptUserForCredentials(ctx, cli, "", "", authConfig.Username, indexServer)
|
||||
authConfig, err = PromptUserForCredentials(ctx, cli, "", "", authConfig.Username, authConfigKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -63,7 +68,7 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
|
||||
func ResolveAuthConfig(cfg *configfile.ConfigFile, index *registrytypes.IndexInfo) registrytypes.AuthConfig {
|
||||
configKey := index.Name
|
||||
if index.Official {
|
||||
configKey = registry.IndexServer
|
||||
configKey = authConfigKey
|
||||
}
|
||||
|
||||
a, _ := cfg.GetAuthConfig(configKey)
|
||||
@ -132,7 +137,7 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
|
||||
|
||||
argUser = strings.TrimSpace(argUser)
|
||||
if argUser == "" {
|
||||
if serverAddress == registry.IndexServer {
|
||||
if serverAddress == authConfigKey {
|
||||
// When signing in to the default (Docker Hub) registry, we display
|
||||
// hints for creating an account, and (if hints are enabled), using
|
||||
// a token instead of a password.
|
||||
@ -225,9 +230,25 @@ func resolveAuthConfigFromImage(cfg *configfile.ConfigFile, image string) (regis
|
||||
if err != nil {
|
||||
return registrytypes.AuthConfig{}, err
|
||||
}
|
||||
repoInfo, err := registry.ParseRepositoryInfo(registryRef)
|
||||
configKey := getAuthConfigKey(reference.Domain(registryRef))
|
||||
a, err := cfg.GetAuthConfig(configKey)
|
||||
if err != nil {
|
||||
return registrytypes.AuthConfig{}, err
|
||||
}
|
||||
return ResolveAuthConfig(cfg, repoInfo.Index), nil
|
||||
return registrytypes.AuthConfig(a), nil
|
||||
}
|
||||
|
||||
// getAuthConfigKey special-cases using the full index address of the official
|
||||
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
|
||||
//
|
||||
// It is similar to [registry.GetAuthConfigKey], but does not require on
|
||||
// [registrytypes.IndexInfo] as intermediate.
|
||||
//
|
||||
// [registry.GetAuthConfigKey]: https://pkg.go.dev/github.com/docker/docker/registry#GetAuthConfigKey
|
||||
// [registrytypes.IndexInfo]:https://pkg.go.dev/github.com/docker/docker/api/types/registry#IndexInfo
|
||||
func getAuthConfigKey(domainName string) string {
|
||||
if domainName == "docker.io" || domainName == "index.docker.io" {
|
||||
return authConfigKey
|
||||
}
|
||||
return domainName
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ func NewSecretCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
// completeNames offers completion for swarm secrets
|
||||
func completeNames(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||
func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().SecretList(cmd.Context(), types.SecretListOptions{})
|
||||
if err != nil {
|
||||
|
||||
@ -52,14 +52,19 @@ func newSecretCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func runSecretCreate(ctx context.Context, dockerCli command.Cli, options createOptions) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
if options.driver != "" && options.file != "" {
|
||||
return errors.Errorf("When using secret driver secret data must be empty")
|
||||
var secretData []byte
|
||||
if options.driver != "" {
|
||||
if options.file != "" {
|
||||
return errors.Errorf("When using secret driver secret data must be empty")
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
secretData, err = readSecretData(dockerCli.In(), options.file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
secretData, err := readSecretData(dockerCli.In(), options.file)
|
||||
if err != nil {
|
||||
return errors.Errorf("Error reading content from %q: %v", options.file, err)
|
||||
}
|
||||
spec := swarm.SecretSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: options.name,
|
||||
@ -82,26 +87,54 @@ func runSecretCreate(ctx context.Context, dockerCli command.Cli, options createO
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(dockerCli.Out(), r.ID)
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), r.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func readSecretData(in io.ReadCloser, file string) ([]byte, error) {
|
||||
// Read secret value from external driver
|
||||
if file == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if file != "-" {
|
||||
var err error
|
||||
in, err = sequential.Open(file)
|
||||
// maxSecretSize is the maximum byte length of the [swarm.SecretSpec.Data] field,
|
||||
// as defined by [MaxSecretSize] in SwarmKit.
|
||||
//
|
||||
// [MaxSecretSize]: https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/api/validation#MaxSecretSize
|
||||
const maxSecretSize = 500 * 1024 // 500KB
|
||||
|
||||
// readSecretData reads the secret from either stdin or the given fileName.
|
||||
//
|
||||
// It reads up to twice the maximum size of the secret ([maxSecretSize]),
|
||||
// just in case swarm's limit changes; this is only a safeguard to prevent
|
||||
// reading arbitrary files into memory.
|
||||
func readSecretData(in io.Reader, fileName string) ([]byte, error) {
|
||||
switch fileName {
|
||||
case "-":
|
||||
data, err := io.ReadAll(io.LimitReader(in, 2*maxSecretSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("error reading from STDIN: %w", err)
|
||||
}
|
||||
defer in.Close()
|
||||
if len(data) == 0 {
|
||||
return nil, errors.New("error reading from STDIN: data is empty")
|
||||
}
|
||||
return data, nil
|
||||
case "":
|
||||
return nil, errors.New("secret file is required")
|
||||
default:
|
||||
// Open file with [FILE_FLAG_SEQUENTIAL_SCAN] on Windows, which
|
||||
// prevents Windows from aggressively caching it. We expect this
|
||||
// file to be only read once. Given that this is expected to be
|
||||
// a small file, this may not be a significant optimization, so
|
||||
// we could choose to omit this, and use a regular [os.Open].
|
||||
//
|
||||
// [FILE_FLAG_SEQUENTIAL_SCAN]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
|
||||
f, err := sequential.Open(fileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
|
||||
}
|
||||
defer f.Close()
|
||||
data, err := io.ReadAll(io.LimitReader(f, 2*maxSecretSize))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("error reading from %s: data is empty", fileName)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
data, err := io.ReadAll(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ func TestSecretCreateErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSecretCreateWithName(t *testing.T) {
|
||||
name := "foo"
|
||||
const name = "secret-with-name"
|
||||
data, err := os.ReadFile(filepath.Join("testdata", secretDataFile))
|
||||
assert.NilError(t, err)
|
||||
|
||||
@ -89,7 +89,7 @@ func TestSecretCreateWithDriver(t *testing.T) {
|
||||
expectedDriver := &swarm.Driver{
|
||||
Name: "secret-driver",
|
||||
}
|
||||
name := "foo"
|
||||
const name = "secret-with-driver"
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
secretCreateFunc: func(_ context.Context, spec swarm.SecretSpec) (types.SecretCreateResponse, error) {
|
||||
@ -118,7 +118,7 @@ func TestSecretCreateWithTemplatingDriver(t *testing.T) {
|
||||
expectedDriver := &swarm.Driver{
|
||||
Name: "template-driver",
|
||||
}
|
||||
const name = "foo"
|
||||
const name = "secret-with-template-driver"
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
secretCreateFunc: func(_ context.Context, spec swarm.SecretSpec) (types.SecretCreateResponse, error) {
|
||||
@ -137,7 +137,7 @@ func TestSecretCreateWithTemplatingDriver(t *testing.T) {
|
||||
})
|
||||
|
||||
cmd := newSecretCreateCommand(cli)
|
||||
cmd.SetArgs([]string{name})
|
||||
cmd.SetArgs([]string{name, filepath.Join("testdata", secretDataFile)})
|
||||
assert.Check(t, cmd.Flags().Set("template-driver", expectedDriver.Name))
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal("ID-"+name, strings.TrimSpace(cli.OutBuffer().String())))
|
||||
@ -148,7 +148,7 @@ func TestSecretCreateWithLabels(t *testing.T) {
|
||||
"lbl1": "Label-foo",
|
||||
"lbl2": "Label-bar",
|
||||
}
|
||||
const name = "foo"
|
||||
const name = "secret-with-labels"
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
secretCreateFunc: func(_ context.Context, spec swarm.SecretSpec) (types.SecretCreateResponse, error) {
|
||||
|
||||
@ -5,7 +5,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
@ -171,9 +170,9 @@ func (ctx *secretInspectContext) Driver() string {
|
||||
}
|
||||
|
||||
func (ctx *secretInspectContext) CreatedAt() string {
|
||||
return command.PrettyPrint(ctx.Secret.CreatedAt)
|
||||
return formatter.PrettyPrint(ctx.Secret.CreatedAt)
|
||||
}
|
||||
|
||||
func (ctx *secretInspectContext) UpdatedAt() string {
|
||||
return command.PrettyPrint(ctx.Secret.UpdatedAt)
|
||||
return formatter.PrettyPrint(ctx.Secret.UpdatedAt)
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
@ -34,16 +36,25 @@ func NewServiceCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// CompletionFn offers completion for swarm services
|
||||
func CompletionFn(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||
// CompletionFn offers completion for swarm service names and optional IDs.
|
||||
// By default, only names are returned.
|
||||
// Set DOCKER_COMPLETION_SHOW_SERVICE_IDS=yes to also complete IDs.
|
||||
func CompletionFn(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
// https://github.com/docker/cli/blob/f9ced58158d5e0b358052432244b483774a1983d/contrib/completion/bash/docker#L41-L43
|
||||
showIDs := os.Getenv("DOCKER_COMPLETION_SHOW_SERVICE_IDS") == "yes"
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().ServiceList(cmd.Context(), types.ServiceListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
|
||||
names := make([]string, 0, len(list))
|
||||
for _, service := range list {
|
||||
names = append(names, service.ID)
|
||||
if showIDs {
|
||||
names = append(names, service.Spec.Name, service.ID)
|
||||
} else {
|
||||
names = append(names, service.Spec.Name)
|
||||
}
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
opts := newServiceOptions()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -28,7 +28,7 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
if len(args) > 1 {
|
||||
opts.args = args[1:]
|
||||
}
|
||||
return runCreate(cmd.Context(), dockerCli, cmd.Flags(), opts)
|
||||
return runCreate(cmd.Context(), dockerCLI, cmd.Flags(), opts)
|
||||
},
|
||||
ValidArgsFunction: completion.NoComplete,
|
||||
}
|
||||
@ -75,6 +75,28 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"})
|
||||
|
||||
flags.SetInterspersed(false)
|
||||
|
||||
// TODO(thaJeztah): add completion for capabilities, stop-signal (currently non-exported in container package)
|
||||
// _ = cmd.RegisterFlagCompletionFunc(flagCapAdd, completeLinuxCapabilityNames)
|
||||
// _ = cmd.RegisterFlagCompletionFunc(flagCapDrop, completeLinuxCapabilityNames)
|
||||
// _ = cmd.RegisterFlagCompletionFunc(flagStopSignal, completeSignals)
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagMode, completion.FromList("replicated", "global", "replicated-job", "global-job"))
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagEnv, completion.EnvVarNames) // TODO(thaJeztah): flagEnvRemove (needs to read current env-vars on the service)
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagEnvFile, completion.FileNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagNetwork, completion.NetworkNames(dockerCLI))
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagRestartCondition, completion.FromList("none", "on-failure", "any"))
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagRollbackOrder, completion.FromList("start-first", "stop-first"))
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagRollbackFailureAction, completion.FromList("pause", "continue"))
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagUpdateOrder, completion.FromList("start-first", "stop-first"))
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagUpdateFailureAction, completion.FromList("pause", "continue", "rollback"))
|
||||
|
||||
flags.VisitAll(func(flag *pflag.Flag) {
|
||||
// Set a default completion function if none was set. We don't look
|
||||
// up if it does already have one set, because Cobra does this for
|
||||
// us, and returns an error (which we ignore for this reason).
|
||||
_ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete)
|
||||
})
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -87,10 +109,6 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
|
||||
return err
|
||||
}
|
||||
|
||||
if err = validateAPIVersion(service, dockerCLI.Client().ClientVersion()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
specifiedSecrets := opts.secrets.Value()
|
||||
if len(specifiedSecrets) > 0 {
|
||||
// parse and validate secrets
|
||||
|
||||
@ -4,12 +4,11 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/opts/swarmopts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
|
||||
cliopts "github.com/docker/cli/opts"
|
||||
)
|
||||
|
||||
// fakeConfigAPIClientList is used to let us pass a closure as a
|
||||
@ -43,8 +42,8 @@ func (fakeConfigAPIClientList) ConfigUpdate(_ context.Context, _ string, _ swarm
|
||||
func TestSetConfigsWithCredSpecAndConfigs(t *testing.T) {
|
||||
// we can't directly access the internal fields of the ConfigOpt struct, so
|
||||
// we need to let it do the parsing
|
||||
configOpt := &cliopts.ConfigOpt{}
|
||||
configOpt.Set("bar")
|
||||
configOpt := &swarmopts.ConfigOpt{}
|
||||
assert.Check(t, configOpt.Set("bar"))
|
||||
opts := &serviceOptions{
|
||||
credentialSpec: credentialSpecOpt{
|
||||
value: &swarm.CredentialSpec{
|
||||
@ -187,8 +186,8 @@ func TestSetConfigsOnlyCredSpec(t *testing.T) {
|
||||
// TestSetConfigsOnlyConfigs verifies setConfigs when only configs (and not a
|
||||
// CredentialSpec) is needed.
|
||||
func TestSetConfigsOnlyConfigs(t *testing.T) {
|
||||
configOpt := &cliopts.ConfigOpt{}
|
||||
configOpt.Set("bar")
|
||||
configOpt := &swarmopts.ConfigOpt{}
|
||||
assert.Check(t, configOpt.Set("bar"))
|
||||
opts := &serviceOptions{
|
||||
configs: *configOpt,
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/docker/api/types"
|
||||
@ -16,6 +17,7 @@ import (
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type inspectOptions struct {
|
||||
@ -47,6 +49,13 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp)
|
||||
flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format")
|
||||
|
||||
flags.VisitAll(func(flag *pflag.Flag) {
|
||||
// Set a default completion function if none was set. We don't look
|
||||
// up if it does already have one set, because Cobra does this for
|
||||
// us, and returns an error (which we ignore for this reason).
|
||||
_ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete)
|
||||
})
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type listOptions struct {
|
||||
@ -41,6 +42,12 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
|
||||
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
|
||||
flags.VisitAll(func(flag *pflag.Flag) {
|
||||
// Set a default completion function if none was set. We don't look
|
||||
// up if it does already have one set, because Cobra does this for
|
||||
// us, and returns an error (which we ignore for this reason).
|
||||
_ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete)
|
||||
})
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@ -11,8 +11,9 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/idresolver"
|
||||
"github.com/docker/cli/service/logs"
|
||||
"github.com/docker/cli/cli/internal/logdetails"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
@ -22,6 +23,7 @@ import (
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type logsOptions struct {
|
||||
@ -69,6 +71,13 @@ func newLogsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs")
|
||||
flags.SetAnnotation("details", "version", []string{"1.30"})
|
||||
flags.StringVarP(&opts.tail, "tail", "n", "all", "Number of lines to show from the end of the logs")
|
||||
|
||||
flags.VisitAll(func(flag *pflag.Flag) {
|
||||
// Set a default completion function if none was set. We don't look
|
||||
// up if it does already have one set, because Cobra does this for
|
||||
// us, and returns an error (which we ignore for this reason).
|
||||
_ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete)
|
||||
})
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -258,7 +267,7 @@ func (lw *logWriter) Write(buf []byte) (int, error) {
|
||||
return 0, errors.Errorf("invalid context in log message: %v", string(buf))
|
||||
}
|
||||
// parse the details out
|
||||
details, err := logs.ParseLogDetails(string(parts[detailsIndex]))
|
||||
details, err := logdetails.Parse(string(parts[detailsIndex]))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@ -11,8 +11,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/cli/opts/swarmopts"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
@ -395,7 +395,7 @@ func convertNetworks(networks opts.NetworkOpt) []swarm.NetworkAttachmentConfig {
|
||||
|
||||
type endpointOptions struct {
|
||||
mode string
|
||||
publishPorts opts.PortOpt
|
||||
publishPorts swarmopts.PortOpt
|
||||
}
|
||||
|
||||
func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
|
||||
@ -553,8 +553,8 @@ type serviceOptions struct {
|
||||
logDriver logDriverOptions
|
||||
|
||||
healthcheck healthCheckOptions
|
||||
secrets opts.SecretOpt
|
||||
configs opts.ConfigOpt
|
||||
secrets swarmopts.SecretOpt
|
||||
configs swarmopts.ConfigOpt
|
||||
|
||||
isolation string
|
||||
}
|
||||
@ -1047,12 +1047,3 @@ const (
|
||||
flagUlimitRemove = "ulimit-rm"
|
||||
flagOomScoreAdj = "oom-score-adj"
|
||||
)
|
||||
|
||||
func validateAPIVersion(c swarm.ServiceSpec, serverAPIVersion string) error {
|
||||
for _, m := range c.TaskTemplate.ContainerSpec.Mounts {
|
||||
if err := command.ValidateMountWithAPIVersion(m, serverAPIVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/idresolver"
|
||||
"github.com/docker/cli/cli/command/node"
|
||||
"github.com/docker/cli/cli/command/task"
|
||||
@ -15,6 +16,7 @@ import (
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type psOptions struct {
|
||||
@ -48,6 +50,12 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.StringVar(&options.format, "format", "", "Pretty-print tasks using a Go template")
|
||||
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
|
||||
flags.VisitAll(func(flag *pflag.Flag) {
|
||||
// Set a default completion function if none was set. We don't look
|
||||
// up if it does already have one set, because Cobra does this for
|
||||
// us, and returns an error (which we ignore for this reason).
|
||||
_ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete)
|
||||
})
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@ -6,9 +6,11 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newRollbackCommand(dockerCli command.Cli) *cobra.Command {
|
||||
@ -31,6 +33,12 @@ func newRollbackCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.BoolVarP(&options.quiet, flagQuiet, "q", false, "Suppress progress output")
|
||||
addDetachFlag(flags, &options.detach)
|
||||
|
||||
flags.VisitAll(func(flag *pflag.Flag) {
|
||||
// Set a default completion function if none was set. We don't look
|
||||
// up if it does already have one set, because Cobra does this for
|
||||
// us, and returns an error (which we ignore for this reason).
|
||||
_ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete)
|
||||
})
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@ -51,11 +51,7 @@ func resolveServiceImageDigestContentTrust(dockerCli command.Cli, service *swarm
|
||||
}
|
||||
|
||||
func trustedResolveDigest(cli command.Cli, ref reference.NamedTagged) (reference.Canonical, error) {
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoInfo, _ := registry.ParseRepositoryInfo(ref)
|
||||
authConfig := command.ResolveAuthConfig(cli.ConfigFile(), repoInfo.Index)
|
||||
|
||||
notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull")
|
||||
|
||||
@ -9,7 +9,9 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/cli/opts/swarmopts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
mounttypes "github.com/docker/docker/api/types/mount"
|
||||
@ -23,7 +25,7 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func newUpdateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
options := newServiceOptions()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -31,10 +33,10 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Short: "Update a service",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runUpdate(cmd.Context(), dockerCli, cmd.Flags(), options, args[0])
|
||||
return runUpdate(cmd.Context(), dockerCLI, cmd.Flags(), options, args[0])
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return CompletionFn(dockerCli)(cmd, args, toComplete)
|
||||
return CompletionFn(dockerCLI)(cmd, args, toComplete)
|
||||
},
|
||||
}
|
||||
|
||||
@ -54,7 +56,7 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key")
|
||||
flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path")
|
||||
// flags.Var(newListOptsVar().WithValidator(validatePublishRemove), flagPublishRemove, "Remove a published port by its target port")
|
||||
flags.Var(&opts.PortOpt{}, flagPublishRemove, "Remove a published port by its target port")
|
||||
flags.Var(&swarmopts.PortOpt{}, flagPublishRemove, "Remove a published port by its target port")
|
||||
flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint")
|
||||
flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server")
|
||||
flags.SetAnnotation(flagDNSRemove, "version", []string{"1.25"})
|
||||
@ -117,6 +119,30 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.Var(newListOptsVarWithValidator(ValidateSingleGenericResource), flagGenericResourcesAdd, "Add a Generic resource")
|
||||
flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"})
|
||||
|
||||
// TODO(thaJeztah): add completion for capabilities, stop-signal (currently non-exported in container package)
|
||||
// _ = cmd.RegisterFlagCompletionFunc(flagCapAdd, completeLinuxCapabilityNames)
|
||||
// _ = cmd.RegisterFlagCompletionFunc(flagCapDrop, completeLinuxCapabilityNames)
|
||||
// _ = cmd.RegisterFlagCompletionFunc(flagStopSignal, completeSignals)
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagEnvAdd, completion.EnvVarNames)
|
||||
// TODO(thaJeztah): flagEnvRemove (needs to read current env-vars on the service)
|
||||
_ = cmd.RegisterFlagCompletionFunc("image", completion.ImageNames(dockerCLI, -1))
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagNetworkAdd, completion.NetworkNames(dockerCLI))
|
||||
// TODO(thaJeztha): flagNetworkRemove (needs to read current list of networks from the service)
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagRestartCondition, completion.FromList("none", "on-failure", "any"))
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagRollbackOrder, completion.FromList("start-first", "stop-first"))
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagRollbackFailureAction, completion.FromList("pause", "continue"))
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagUpdateOrder, completion.FromList("start-first", "stop-first"))
|
||||
_ = cmd.RegisterFlagCompletionFunc(flagUpdateFailureAction, completion.FromList("pause", "continue", "rollback"))
|
||||
|
||||
completion.ImageNames(dockerCLI, -1)
|
||||
flags.VisitAll(func(flag *pflag.Flag) {
|
||||
// Set a default completion function if none was set. We don't look
|
||||
// up if it does already have one set, because Cobra does this for
|
||||
// us, and returns an error (which we ignore for this reason).
|
||||
_ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete)
|
||||
})
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -779,7 +805,7 @@ func getUpdatedSecrets(ctx context.Context, apiClient client.SecretAPIClient, fl
|
||||
}
|
||||
|
||||
if flags.Changed(flagSecretAdd) {
|
||||
values := flags.Lookup(flagSecretAdd).Value.(*opts.SecretOpt).Value()
|
||||
values := flags.Lookup(flagSecretAdd).Value.(*swarmopts.SecretOpt).Value()
|
||||
|
||||
addSecrets, err := ParseSecrets(ctx, apiClient, values)
|
||||
if err != nil {
|
||||
@ -827,7 +853,7 @@ func getUpdatedConfigs(ctx context.Context, apiClient client.ConfigAPIClient, fl
|
||||
resolveConfigs := []*swarm.ConfigReference{}
|
||||
|
||||
if flags.Changed(flagConfigAdd) {
|
||||
resolveConfigs = append(resolveConfigs, flags.Lookup(flagConfigAdd).Value.(*opts.ConfigOpt).Value()...)
|
||||
resolveConfigs = append(resolveConfigs, flags.Lookup(flagConfigAdd).Value.(*swarmopts.ConfigOpt).Value()...)
|
||||
}
|
||||
|
||||
// if credSpecConfigNameis non-empty at this point, it means its a new
|
||||
@ -1066,7 +1092,7 @@ func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
|
||||
newPorts := []swarm.PortConfig{}
|
||||
|
||||
// Clean current ports
|
||||
toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.PortOpt).Value()
|
||||
toRemove := flags.Lookup(flagPublishRemove).Value.(*swarmopts.PortOpt).Value()
|
||||
portLoop:
|
||||
for _, port := range portSet {
|
||||
for _, pConfig := range toRemove {
|
||||
@ -1082,7 +1108,7 @@ portLoop:
|
||||
|
||||
// Check to see if there are any conflict in flags.
|
||||
if flags.Changed(flagPublishAdd) {
|
||||
ports := flags.Lookup(flagPublishAdd).Value.(*opts.PortOpt).Value()
|
||||
ports := flags.Lookup(flagPublishAdd).Value.(*swarmopts.PortOpt).Value()
|
||||
|
||||
for _, port := range ports {
|
||||
if _, ok := portSet[portConfigToString(&port)]; ok {
|
||||
|
||||
@ -46,7 +46,7 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
// completeNames offers completion for swarm stacks
|
||||
func completeNames(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||
func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := swarm.GetStacks(cmd.Context(), dockerCLI.Client())
|
||||
if err != nil {
|
||||
|
||||
@ -85,7 +85,7 @@ var (
|
||||
)
|
||||
|
||||
// completeEventFilters provides completion for the filters that can be used with `--filter`.
|
||||
func completeEventFilters(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||
func completeEventFilters(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
key, _, ok := strings.Cut(toComplete, "=")
|
||||
if !ok {
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/internal/test"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
@ -201,7 +202,7 @@ var samplePluginsInfo = []pluginmanager.Plugin{
|
||||
{
|
||||
Name: "goodplugin",
|
||||
Path: "/path/to/docker-goodplugin",
|
||||
Metadata: pluginmanager.Metadata{
|
||||
Metadata: metadata.Metadata{
|
||||
SchemaVersion: "0.1.0",
|
||||
ShortDescription: "unit test is good",
|
||||
Vendor: "ACME Corp",
|
||||
@ -211,7 +212,7 @@ var samplePluginsInfo = []pluginmanager.Plugin{
|
||||
{
|
||||
Name: "unversionedplugin",
|
||||
Path: "/path/to/docker-unversionedplugin",
|
||||
Metadata: pluginmanager.Metadata{
|
||||
Metadata: metadata.Metadata{
|
||||
SchemaVersion: "0.1.0",
|
||||
ShortDescription: "this plugin has no version",
|
||||
Vendor: "ACME Corp",
|
||||
|
||||
@ -16,6 +16,7 @@ import (
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/cli/version"
|
||||
"github.com/docker/cli/templates"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@ -89,20 +90,20 @@ type clientVersion struct {
|
||||
// information.
|
||||
func newClientVersion(contextName string, dockerCli command.Cli) clientVersion {
|
||||
v := clientVersion{
|
||||
Version: version.Version,
|
||||
GoVersion: runtime.Version(),
|
||||
GitCommit: version.GitCommit,
|
||||
BuildTime: reformatDate(version.BuildTime),
|
||||
Os: runtime.GOOS,
|
||||
Arch: arch(),
|
||||
Context: contextName,
|
||||
Version: version.Version,
|
||||
DefaultAPIVersion: api.DefaultVersion,
|
||||
GoVersion: runtime.Version(),
|
||||
GitCommit: version.GitCommit,
|
||||
BuildTime: reformatDate(version.BuildTime),
|
||||
Os: runtime.GOOS,
|
||||
Arch: arch(),
|
||||
Context: contextName,
|
||||
}
|
||||
if version.PlatformName != "" {
|
||||
v.Platform = &platformInfo{Name: version.PlatformName}
|
||||
}
|
||||
if dockerCli != nil {
|
||||
v.APIVersion = dockerCli.CurrentVersion()
|
||||
v.DefaultAPIVersion = dockerCli.DefaultVersion()
|
||||
}
|
||||
return v
|
||||
}
|
||||
@ -196,8 +197,8 @@ func runVersion(ctx context.Context, dockerCli command.Cli, opts *versionOptions
|
||||
func prettyPrintVersion(dockerCli command.Cli, vd versionInfo, tmpl *template.Template) error {
|
||||
t := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 1, ' ', 0)
|
||||
err := tmpl.Execute(t, vd)
|
||||
t.Write([]byte("\n"))
|
||||
t.Flush()
|
||||
_, _ = t.Write([]byte("\n"))
|
||||
_ = t.Flush()
|
||||
return err
|
||||
}
|
||||
|
||||
@ -210,8 +211,10 @@ func newVersionTemplate(templateFormat string) (*template.Template, error) {
|
||||
}
|
||||
tmpl := templates.New("version").Funcs(template.FuncMap{"getDetailsOrder": getDetailsOrder})
|
||||
tmpl, err := tmpl.Parse(templateFormat)
|
||||
|
||||
return tmpl, errors.Wrap(err, "template parsing error")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "template parsing error")
|
||||
}
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
func getDetailsOrder(v types.ComponentVersion) []string {
|
||||
|
||||
@ -6,7 +6,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
@ -110,12 +109,12 @@ func (c *taskContext) Node() string {
|
||||
}
|
||||
|
||||
func (c *taskContext) DesiredState() string {
|
||||
return command.PrettyPrint(c.task.DesiredState)
|
||||
return formatter.PrettyPrint(c.task.DesiredState)
|
||||
}
|
||||
|
||||
func (c *taskContext) CurrentState() string {
|
||||
return fmt.Sprintf("%s %s ago",
|
||||
command.PrettyPrint(c.task.Status.State),
|
||||
formatter.PrettyPrint(c.task.Status.State),
|
||||
strings.ToLower(units.HumanDuration(time.Since(c.task.Status.Timestamp))),
|
||||
)
|
||||
}
|
||||
|
||||
@ -4,10 +4,11 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/uuid"
|
||||
"github.com/google/uuid"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||
@ -142,7 +143,7 @@ func defaultResourceOptions() []resource.Option {
|
||||
// of the CLI is its own instance. Without this, downstream
|
||||
// OTEL processors may think the same process is restarting
|
||||
// continuously.
|
||||
semconv.ServiceInstanceID(uuid.Generate().String()),
|
||||
semconv.ServiceInstanceID(uuid.NewString()),
|
||||
),
|
||||
resource.WithFromEnv(),
|
||||
resource.WithTelemetrySDK(),
|
||||
@ -216,3 +217,49 @@ func (r *cliReader) ForceFlush(ctx context.Context) error {
|
||||
func deltaTemporality(_ sdkmetric.InstrumentKind) metricdata.Temporality {
|
||||
return metricdata.DeltaTemporality
|
||||
}
|
||||
|
||||
// resourceAttributesEnvVar is the name of the envvar that includes additional
|
||||
// resource attributes for OTEL as defined in the [OpenTelemetry specification].
|
||||
//
|
||||
// [OpenTelemetry specification]: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration
|
||||
const resourceAttributesEnvVar = "OTEL_RESOURCE_ATTRIBUTES"
|
||||
|
||||
func filterResourceAttributesEnvvar() {
|
||||
if v := os.Getenv(resourceAttributesEnvVar); v != "" {
|
||||
if filtered := filterResourceAttributes(v); filtered != "" {
|
||||
_ = os.Setenv(resourceAttributesEnvVar, filtered)
|
||||
} else {
|
||||
_ = os.Unsetenv(resourceAttributesEnvVar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dockerCLIAttributePrefix is the prefix for any docker cli OTEL attributes.
|
||||
// When updating, make sure to also update the copy in cli-plugins/manager.
|
||||
//
|
||||
// TODO(thaJeztah): move telemetry-related code to an (internal) package to reduce dependency on cli/command in cli-plugins, which has too many imports.
|
||||
const dockerCLIAttributePrefix = "docker.cli."
|
||||
|
||||
func filterResourceAttributes(s string) string {
|
||||
if trimmed := strings.TrimSpace(s); trimmed == "" {
|
||||
return trimmed
|
||||
}
|
||||
|
||||
pairs := strings.Split(s, ",")
|
||||
elems := make([]string, 0, len(pairs))
|
||||
for _, p := range pairs {
|
||||
k, _, found := strings.Cut(p, "=")
|
||||
if !found {
|
||||
// Do not interact with invalid otel resources.
|
||||
elems = append(elems, p)
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip attributes that have our docker.cli prefix.
|
||||
if strings.HasPrefix(k, dockerCLIAttributePrefix) {
|
||||
continue
|
||||
}
|
||||
elems = append(elems, p)
|
||||
}
|
||||
return strings.Join(elems, ",")
|
||||
}
|
||||
|
||||
@ -49,6 +49,20 @@ type trustKey struct {
|
||||
ID string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// notaryClientProvider is used in tests to provide a dummy notary client.
|
||||
type notaryClientProvider interface {
|
||||
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error)
|
||||
}
|
||||
|
||||
// newNotaryClient provides a Notary Repository to interact with signed metadata for an image.
|
||||
func newNotaryClient(cli command.Streams, imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) {
|
||||
if ncp, ok := cli.(notaryClientProvider); ok {
|
||||
// notaryClientProvider is used in tests to provide a dummy notary client.
|
||||
return ncp.NotaryClient(imgRefAndAuth, actions)
|
||||
}
|
||||
return trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
|
||||
}
|
||||
|
||||
// lookupTrustInfo returns processed signature and role information about a notary repository.
|
||||
// This information is to be pretty printed or serialized into a machine-readable format.
|
||||
func lookupTrustInfo(ctx context.Context, cli command.Cli, remote string) ([]trustTagRow, []client.RoleWithSignatures, []data.Role, error) {
|
||||
@ -57,7 +71,7 @@ func lookupTrustInfo(ctx context.Context, cli command.Cli, remote string) ([]tru
|
||||
return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, err
|
||||
}
|
||||
tag := imgRefAndAuth.Tag()
|
||||
notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPullOnly)
|
||||
notaryRepo, err := newNotaryClient(cli, imgRefAndAuth, trust.ActionsPullOnly)
|
||||
if err != nil {
|
||||
return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ func notaryRoleToSigner(tufRole data.RoleName) string {
|
||||
return strings.TrimPrefix(tufRole.String(), "targets/")
|
||||
}
|
||||
|
||||
// clearChangelist clears the notary staging changelist.
|
||||
// clearChangeList clears the notary staging changelist.
|
||||
func clearChangeList(notaryRepo client.Repository) error {
|
||||
cl, err := notaryRepo.GetChangelist()
|
||||
if err != nil {
|
||||
@ -47,3 +47,9 @@ func getOrGenerateRootKeyAndInitRepo(notaryRepo client.Repository) error {
|
||||
}
|
||||
return notaryRepo.Initialize([]string{rootKey.ID()}, data.CanonicalSnapshotRole)
|
||||
}
|
||||
|
||||
const testPass = "password"
|
||||
|
||||
func testPassRetriever(string, string, bool, int) (string, bool, error) {
|
||||
return testPass, false, nil
|
||||
}
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/theupdateframework/notary/client"
|
||||
"github.com/theupdateframework/notary/passphrase"
|
||||
"github.com/theupdateframework/notary/trustpinning"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestGetOrGenerateNotaryKeyAndInitRepo(t *testing.T) {
|
||||
notaryRepo, err := client.NewFileCachedRepository(t.TempDir(), "gun", "https://localhost", nil, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
|
||||
assert.NilError(t, err)
|
||||
|
||||
err = getOrGenerateRootKeyAndInitRepo(notaryRepo)
|
||||
assert.Error(t, err, "client is offline")
|
||||
}
|
||||
@ -11,7 +11,6 @@ import (
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/theupdateframework/notary"
|
||||
"github.com/theupdateframework/notary/passphrase"
|
||||
"github.com/theupdateframework/notary/trustmanager"
|
||||
tufutils "github.com/theupdateframework/notary/tuf/utils"
|
||||
"gotest.tools/v3/assert"
|
||||
@ -51,11 +50,9 @@ func TestGenerateKeySuccess(t *testing.T) {
|
||||
pubKeyCWD := t.TempDir()
|
||||
privKeyStorageDir := t.TempDir()
|
||||
|
||||
const testPass = "password"
|
||||
cannedPasswordRetriever := passphrase.ConstantRetriever(testPass)
|
||||
// generate a single key
|
||||
keyName := "alice"
|
||||
privKeyFileStore, err := trustmanager.NewKeyFileStore(privKeyStorageDir, cannedPasswordRetriever)
|
||||
privKeyFileStore, err := trustmanager.NewKeyFileStore(privKeyStorageDir, testPassRetriever)
|
||||
assert.NilError(t, err)
|
||||
|
||||
pubKeyPEM, err := generateKeyAndOutputPubPEM(keyName, privKeyFileStore)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user