Compare commits

..
Author SHA1 Message Date
Andrew Hsu 7178075fda Merge pull request #1205 from thaJeztah/18.06-bump-dockerd
[18.06] Bump engine to tip of 18.06 branch
2018-07-11 18:19:53 -07:00
Sebastiaan van Stijn 744938f0b9 Bump engine to tip of 18.06 branch
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-12 02:53:31 +02:00
Sebastiaan van Stijn 1d5e206fc2 Merge pull request #1199 from thaJeztah/18.06-backport-fix-swarm-ca-command
[18.06] Propagate the provided external CA certificate to the external CA object in swarm
2018-07-10 10:47:13 +02:00
Ying Li 31d6292458 Propagate the provided external CA certificate to the external CA object
in swarm.

Also, fix some CLI command confusions:
1. If the --external-ca flag is provided, require a --ca-cert flag as well, otherwise
   the external CA is set but the CA certificate is actually rotated to an internal
   cert
2. If a --ca-cert flag is provided, require a --ca-key or --external-ca flag be
   provided as well, otherwise either the server will say that the request is
   invalid, or if there was previously an external CA corresponding to the cert, it
   will succeed.  While that works, it's better to require the user to explicitly
   set all the parameters of the new desired root CA.

This also changes the `swarm update` function to set the external CA's CACert field,
which while not strictly necessary, makes the CA list more explicit.

Signed-off-by: Ying Li <ying.li@docker.com>
(cherry picked from commit 4243440e1f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-10 01:32:52 +02:00
Andrew Hsu ebd85b10e2 Merge pull request #1190 from tiborvass/18.06-vndr
[18.06] vendor: docker/docker to docker/engine 0d029b0
2018-07-05 18:29:29 -07:00
Tibor Vass 8f5f3adf49 vendor: docker/docker moby/buildkit containerd/containerd containerd/console containerd/continuity opencontainers/runc
Signed-off-by: Tibor Vass <tibor@docker.com>
2018-07-05 18:23:07 +00:00
Andrew Hsu 9fbab758a9 Merge pull request #1185 from tiborvass/18.06-cp
[18.06] build: --iidfile support with buildkit
2018-07-05 11:06:11 -07:00
Andrew Hsu b3d8fd5261 Merge pull request #1181 from thaJeztah/18.06-bump-kubernetes-1.8.14
[18.06] Bump kubernetes dependencies to 1.8.14
2018-07-05 08:47:14 -07:00
Sebastiaan van Stijn a7c8c474b9 Merge pull request #1174 from thaJeztah/18.06-backport-no-need-to-check-files
[18.06] Remove composefiles lenght check on k8s RunDeploy..
2018-07-05 16:35:54 +02:00
Silvin 5f42140bab Merge pull request #1184 from thaJeztah/18.06-backport-credentials
[18.06] backport credential-helper fixes
2018-07-05 15:23:36 +02:00
Tibor Vass ad80af43d0 Merge pull request #1187 from thaJeztah/18.06-buildkit-buildkit-envvar-zero
[18.06] build: use strconv.ParseBool to parse DOCKER_BUILDKIT to allow value "0"
2018-07-03 17:10:35 -07:00
Tibor Vass 359d5c8a76 build: use strconv.ParseBool to parse DOCKER_BUILDKIT to allow value "0"
Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit 721000e6c9)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-04 01:29:55 +02:00
Euan Kemp 08479b0776 config/credentials: don't run 'pass' to detect it
'CheckInitialized' in the credential-helper library actually invokes
`pass`, which isn't desirable (see #699).

This moves the check to be simpler, and then pass will only be invoked
when it's needed (such as for `docker login` or when pulling from a
private registry).

This logic could also reasonably live in the credential-helper library,
but it's simple enough it seems fine in either location.

Signed-off-by: Euan Kemp <euank@euank.com>
(cherry picked from commit 056015c3d8)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-04 00:51:28 +02:00
Vincent Demeester 00affb1dd5 Update docker-credential-helpers dependency
This is mainly for the `pass` helper ; `pass` shouldn't be called
every docker command anymore ;).

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
(cherry picked from commit d9741fc96b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-04 00:51:20 +02:00
Sebastiaan van Stijn 0627568d60 Merge pull request #1183 from andrewhsu/manifest
[18.06] Fix manifest lists to always use correct size
2018-07-04 00:46:50 +02:00
Tibor Vass aeceff447c build: --iidfile support with buildkit
Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit c7e85c09d2)
Signed-off-by: Tibor Vass <tibor@docker.com>
2018-07-03 22:44:22 +00:00
Derek McGowan 15695813a4 Fix manifest lists to always use correct size
Stores complete OCI descriptor instead of digest and platform
fields. This includes the size which was getting lost by not
storing the original manifest bytes.

Attempt to support existing cached files, if not output
the filename with the incorrect content.

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
(cherry picked from commit 1fd2d66df8)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2018-07-03 22:23:13 +00:00
Vincent Demeester 2ea275157d Bump kubernetes dependencies to 1.8.14
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
(cherry picked from commit b59c41b2a7)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-03 23:09:21 +02:00
Harald Albers c6c52ae29a Merge pull request #1173 from thaJeztah/18.06-backport-bash-completion
[18.06] backport bash completion enhancements
2018-07-02 20:26:58 +02:00
Vincent Demeester 8600d82bd7 Remove composefiles lenght check on k8s RunDeploy..
The compose file(s) are already loaded at that point.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
(cherry picked from commit 847e0c22d4)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-02 15:58:52 +02:00
Harald Albers 74b30e7d58 Add bash completion for exec_die event
Signed-off-by: Harald Albers <github@albersweb.de>
(cherry picked from commit 8443982188)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-02 15:12:31 +02:00
Harald Albers 547b9a4aba Add bash completion for dockerd --default-address-pool
Signed-off-by: Harald Albers <github@albersweb.de>
(cherry picked from commit 0e6d9dfe85)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-02 15:12:25 +02:00
Silvin 9bea855d07 Merge pull request #1165 from thaJeztah/18.06-update_go_winio
[18.06] Updated the go-winio library to release 0.4.8 that has the fix for Windows containers
2018-07-02 14:38:18 +02:00
Tejaswini Duggaraju fe4d20bbb3 Updated the go-winio library to release 0.4.8 that has the fix for Windows containers
Signed-off-by: Tejaswini Duggaraju <naduggar@microsoft.com>
(cherry picked from commit c98c4080a3)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-06-30 00:09:18 +02:00
1715 changed files with 67302 additions and 216340 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
# GitHub code owners
# See https://github.com/blog/2392-introducing-code-owners
cli/command/stack/** @vdemeester @silvin-lubecki
cli/command/stack/** @vdemeester
cli/compose/** @vdemeester
contrib/completion/bash/** @albers
contrib/completion/zsh/** @sdurrheimer
docs/** @vdemeester @thaJeztah
docs/** @mistyhacks @vdemeester @thaJeztah
+6
View File
@@ -41,6 +41,7 @@
# TODO Describe the docs maintainers role.
people = [
"misty",
"thajeztah"
]
@@ -94,6 +95,11 @@
Email = "justin.cormack@docker.com"
GitHub = "justincormack"
[people.misty]
Name = "Misty Stanley-Jones"
Email = "misty@docker.com"
GitHub = "mistyhacks"
[people.programmerq]
Name = "Jeff Anderson"
Email = "jeff@docker.com"
+6 -2
View File
@@ -12,14 +12,14 @@ clean: ## remove build artifacts
.PHONY: test-unit
test-unit: ## run unit test
./scripts/test/unit $(shell go list ./... | grep -vE '/vendor/|/e2e/|/e2eengine/')
./scripts/test/unit $(shell go list ./... | grep -vE '/vendor/|/e2e/')
.PHONY: test
test: test-unit ## run tests
.PHONY: test-coverage
test-coverage: ## run test coverage
./scripts/test/unit-with-coverage $(shell go list ./... | grep -vE '/vendor/|/e2e/|/e2eengine/')
./scripts/test/unit-with-coverage $(shell go list ./... | grep -vE '/vendor/|/e2e/')
.PHONY: lint
lint: ## run all the lint tools
@@ -46,6 +46,10 @@ binary-osx: ## build executable for macOS
dynbinary: ## build dynamically linked binary
./scripts/build/dynbinary
.PHONY: watch
watch: ## monitor file changes and run go test
./scripts/test/watch
vendor: vendor.conf ## check that vendor matches vendor.conf
rm -rf vendor
bash -c 'vndr |& grep -v -i clone'
+4 -2
View File
@@ -26,11 +26,13 @@ Test<Function Name><Test Case Name>
where appropriate, but may not be appropriate in all cases.
Assertions should be made using
[gotest.tools/assert](https://godoc.org/gotest.tools/assert).
[testify/assert](https://godoc.org/github.com/stretchr/testify/assert) and test
requirements should be verified using
[testify/require](https://godoc.org/github.com/stretchr/testify/require).
Fakes, and testing utilities can be found in
[internal/test](https://godoc.org/github.com/docker/cli/internal/test) and
[gotest.tools](https://godoc.org/gotest.tools).
[gotestyourself](https://godoc.org/github.com/gotestyourself/gotestyourself).
## End-to-End Test Suite
+1 -1
View File
@@ -1 +1 @@
18.09.0-dev
18.06.0-dev
+6 -13
View File
@@ -4,11 +4,10 @@ jobs:
lint:
working_directory: /work
docker: [{image: 'docker:18.03-git'}]
docker: [{image: 'docker:17.06-git'}]
steps:
- checkout
- setup_remote_docker:
version: 18.03.1-ce
reusable: true
exclusive: false
- run:
@@ -23,12 +22,11 @@ jobs:
cross:
working_directory: /work
docker: [{image: 'docker:18.03-git'}]
docker: [{image: 'docker:17.06-git'}]
parallelism: 3
steps:
- checkout
- setup_remote_docker:
version: 18.03.1-ce
reusable: true
exclusive: false
- run:
@@ -50,11 +48,10 @@ jobs:
test:
working_directory: /work
docker: [{image: 'docker:18.03-git'}]
docker: [{image: 'docker:17.06-git'}]
steps:
- checkout
- setup_remote_docker:
version: 18.03.1-ce
reusable: true
exclusive: false
- run:
@@ -79,11 +76,10 @@ jobs:
validate:
working_directory: /work
docker: [{image: 'docker:18.03-git'}]
docker: [{image: 'docker:17.06-git'}]
steps:
- checkout
- setup_remote_docker:
version: 18.03.1-ce
reusable: true
exclusive: false
- run:
@@ -97,13 +93,10 @@ jobs:
make ci-validate
shellcheck:
working_directory: /work
docker: [{image: 'docker:18.03-git'}]
docker: [{image: 'docker:17.06-git'}]
steps:
- checkout
- setup_remote_docker:
version: 18.03.1-ce
reusable: true
exclusive: false
- setup_remote_docker
- run:
name: "Run shellcheck"
command: |
-22
View File
@@ -1,22 +0,0 @@
package builder
import (
"github.com/spf13/cobra"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
)
// NewBuilderCommand returns a cobra command for `builder` subcommands
func NewBuilderCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "builder",
Short: "Manage builds",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
}
cmd.AddCommand(
NewPruneCommand(dockerCli),
)
return cmd
}
-31
View File
@@ -1,31 +0,0 @@
package builder
import (
"context"
"fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
units "github.com/docker/go-units"
"github.com/spf13/cobra"
)
// NewPruneCommand returns a new cobra prune command for images
func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "prune",
Short: "Remove build cache",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
report, err := dockerCli.Client().BuildCachePrune(context.Background())
if err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(report.SpaceReclaimed)))
return nil
},
Annotations: map[string]string{"version": "1.39"},
}
return cmd
}
+10 -42
View File
@@ -14,12 +14,10 @@ import (
"github.com/docker/cli/cli/config"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/connhelper"
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/trust"
"github.com/docker/cli/internal/containerizedengine"
dopts "github.com/docker/cli/opts"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
@@ -55,7 +53,6 @@ type Cli interface {
ManifestStore() manifeststore.Store
RegistryClient(bool) registryclient.RegistryClient
ContentTrustEnabled() bool
NewContainerizedEngineClient(sockPath string) (containerizedengine.Client, error)
}
// DockerCli is an instance the docker command line client.
@@ -208,7 +205,6 @@ func (cli *DockerCli) initializeFromClient() {
cli.serverInfo = ServerInfo{
HasExperimental: ping.Experimental,
OSType: ping.OSType,
BuildkitVersion: ping.BuilderVersion,
}
cli.client.NegotiateAPIVersionPing(ping)
}
@@ -232,17 +228,11 @@ func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
}
// NewContainerizedEngineClient returns a containerized engine client
func (cli *DockerCli) NewContainerizedEngineClient(sockPath string) (containerizedengine.Client, error) {
return containerizedengine.NewClient(sockPath)
}
// ServerInfo stores details about the supported features and platform of the
// server
type ServerInfo struct {
HasExperimental bool
OSType string
BuildkitVersion types.BuilderVersion
}
// ClientInfo stores details about the supported features of the client
@@ -258,54 +248,31 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool) *DockerC
// NewAPIClientFromFlags creates a new APIClient from command line flags
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
unparsedHost, err := getUnparsedServerHost(opts.Hosts)
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
if err != nil {
return &client.Client{}, err
}
var clientOpts []func(*client.Client) error
helper, err := connhelper.GetConnectionHelper(unparsedHost)
if err != nil {
return &client.Client{}, err
}
if helper == nil {
clientOpts = append(clientOpts, withHTTPClient(opts.TLSOptions))
host, err := dopts.ParseHost(opts.TLSOptions != nil, unparsedHost)
if err != nil {
return &client.Client{}, err
}
clientOpts = append(clientOpts, client.WithHost(host))
} else {
clientOpts = append(clientOpts, func(c *client.Client) error {
httpClient := &http.Client{
// No tls
// No proxy
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
return client.WithHTTPClient(httpClient)(c)
})
clientOpts = append(clientOpts, client.WithHost(helper.Host))
clientOpts = append(clientOpts, client.WithDialContext(helper.Dialer))
}
customHeaders := configFile.HTTPHeaders
if customHeaders == nil {
customHeaders = map[string]string{}
}
customHeaders["User-Agent"] = UserAgent()
clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))
verStr := api.DefaultVersion
if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" {
verStr = tmpStr
}
clientOpts = append(clientOpts, client.WithVersion(verStr))
return client.NewClientWithOpts(clientOpts...)
return client.NewClientWithOpts(
withHTTPClient(opts.TLSOptions),
client.WithHTTPHeaders(customHeaders),
client.WithVersion(verStr),
client.WithHost(host),
)
}
func getUnparsedServerHost(hosts []string) (string, error) {
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
var host string
switch len(hosts) {
case 0:
@@ -315,7 +282,8 @@ func getUnparsedServerHost(hosts []string) (string, error) {
default:
return "", errors.New("Please specify only one -H")
}
return host, nil
return dopts.ParseHost(tlsOptions != nil, host)
}
func withHTTPClient(tlsOpts *tlsconfig.Options) func(*client.Client) error {
-8
View File
@@ -4,11 +4,9 @@ import (
"os"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/builder"
"github.com/docker/cli/cli/command/checkpoint"
"github.com/docker/cli/cli/command/config"
"github.com/docker/cli/cli/command/container"
"github.com/docker/cli/cli/command/engine"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/command/manifest"
"github.com/docker/cli/cli/command/network"
@@ -42,9 +40,6 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
image.NewImageCommand(dockerCli),
image.NewBuildCommand(dockerCli),
// builder
builder.NewBuilderCommand(dockerCli),
// manifest
manifest.NewManifestCommand(dockerCli),
@@ -85,9 +80,6 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
// volume
volume.NewVolumeCommand(dockerCli),
// engine
engine.NewEngineCommand(dockerCli),
// legacy commands may be hidden
hide(system.NewEventsCommand(dockerCli)),
hide(system.NewInfoCommand(dockerCli)),
+10 -3
View File
@@ -9,10 +9,19 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/spf13/cobra"
"vbom.ml/util/sortorder"
)
type byConfigName []swarm.Config
func (r byConfigName) Len() int { return len(r) }
func (r byConfigName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r byConfigName) Less(i, j int) bool {
return sortorder.NaturalLess(r[i].Spec.Name, r[j].Spec.Name)
}
type listOptions struct {
quiet bool
format string
@@ -58,9 +67,7 @@ func runConfigList(dockerCli command.Cli, options listOptions) error {
}
}
sort.Slice(configs, func(i, j int) bool {
return sortorder.NaturalLess(configs[i].Spec.Name, configs[j].Spec.Name)
})
sort.Sort(byConfigName(configs))
configCtx := formatter.Context{
Output: dockerCli.Out(),
+2 -34
View File
@@ -370,24 +370,9 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
entrypoint = []string{""}
}
publishOpts := copts.publish.GetAll()
var ports map[nat.Port]struct{}
var portBindings map[nat.Port][]nat.PortBinding
ports, portBindings, err = nat.ParsePortSpecs(publishOpts)
// If simple port parsing fails try to parse as long format
ports, portBindings, err := nat.ParsePortSpecs(copts.publish.GetAll())
if err != nil {
publishOpts, err = parsePortOpts(publishOpts)
if err != nil {
return nil, err
}
ports, portBindings, err = nat.ParsePortSpecs(publishOpts)
if err != nil {
return nil, err
}
return nil, err
}
// Merge in exposed ports to the map of published ports
@@ -676,23 +661,6 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
}, nil
}
func parsePortOpts(publishOpts []string) ([]string, error) {
optsList := []string{}
for _, publish := range publishOpts {
params := map[string]string{"protocol": "tcp"}
for _, param := range strings.Split(publish, ",") {
opt := strings.Split(param, "=")
if len(opt) < 2 {
return optsList, errors.Errorf("invalid publish opts format (should be name=value but got '%s')", param)
}
params[opt[0]] = opt[1]
}
optsList = append(optsList, fmt.Sprintf("%s:%s/%s", params["target"], params["published"], params["protocol"]))
}
return optsList, nil
}
func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) {
loggingOptsMap := opts.ConvertKVStringsToMap(loggingOpts)
if loggingDriver == "none" && len(loggingOpts) > 0 {
+1
View File
@@ -42,6 +42,7 @@ func TestValidateAttach(t *testing.T) {
}
}
// nolint: unparam
func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) {
flags, copts := setupRunFlags()
if err := flags.Parse(args); err != nil {
-181
View File
@@ -1,181 +0,0 @@
package engine
import (
"context"
"fmt"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/containerizedengine"
"github.com/docker/cli/internal/licenseutils"
"github.com/docker/docker/api/types"
"github.com/docker/licensing/model"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type activateOptions struct {
licenseFile string
version string
registryPrefix string
format string
image string
quiet bool
displayOnly bool
sockPath string
}
// newActivateCommand creates a new `docker engine activate` command
func newActivateCommand(dockerCli command.Cli) *cobra.Command {
var options activateOptions
cmd := &cobra.Command{
Use: "activate [OPTIONS]",
Short: "Activate Enterprise Edition",
Long: `Activate Enterprise Edition.
With this command you may apply an existing Docker enterprise license, or
interactively download one from Docker. In the interactive exchange, you can
sign up for a new trial, or download an existing license. If you are
currently running a Community Edition engine, the daemon will be updated to
the Enterprise Edition Docker engine with additional capabilities and long
term support.
For more information about different Docker Enterprise license types visit
https://www.docker.com/licenses
For non-interactive scriptable deployments, download your license from
https://hub.docker.com/ then specify the file with the '--license' flag.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runActivate(dockerCli, options)
},
}
flags := cmd.Flags()
flags.StringVar(&options.licenseFile, "license", "", "License File")
flags.StringVar(&options.version, "version", "", "Specify engine version (default is to use currently running version)")
flags.StringVar(&options.registryPrefix, "registry-prefix", "docker.io/docker", "Override the default location where engine images are pulled")
flags.StringVar(&options.image, "engine-image", containerizedengine.EnterpriseEngineImage, "Specify engine image")
flags.StringVar(&options.format, "format", "", "Pretty-print licenses using a Go template")
flags.BoolVar(&options.displayOnly, "display-only", false, "only display the available licenses and exit")
flags.BoolVar(&options.quiet, "quiet", false, "Only display available licenses by ID")
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
return cmd
}
func runActivate(cli command.Cli, options activateOptions) error {
ctx := context.Background()
client, err := cli.NewContainerizedEngineClient(options.sockPath)
if err != nil {
return errors.Wrap(err, "unable to access local containerd")
}
defer client.Close()
authConfig, err := getRegistryAuth(cli, options.registryPrefix)
if err != nil {
return err
}
var license *model.IssuedLicense
// Lookup on hub if no license provided via params
if options.licenseFile == "" {
if license, err = getLicenses(ctx, authConfig, cli, options); err != nil {
return err
}
if options.displayOnly {
return nil
}
} else {
if license, err = licenseutils.LoadLocalIssuedLicense(ctx, options.licenseFile); err != nil {
return err
}
}
if err = licenseutils.ApplyLicense(ctx, cli.Client(), license); err != nil {
return err
}
opts := containerizedengine.EngineInitOptions{
RegistryPrefix: options.registryPrefix,
EngineImage: options.image,
EngineVersion: options.version,
}
return client.ActivateEngine(ctx, opts, cli.Out(), authConfig,
func(ctx context.Context) error {
client := cli.Client()
_, err := client.Ping(ctx)
return err
})
}
func getLicenses(ctx context.Context, authConfig *types.AuthConfig, cli command.Cli, options activateOptions) (*model.IssuedLicense, error) {
user, err := licenseutils.Login(ctx, authConfig)
if err != nil {
return nil, err
}
fmt.Fprintf(cli.Out(), "Looking for existing licenses for %s...\n", user.User.Username)
subs, err := user.GetAvailableLicenses(ctx)
if err != nil {
return nil, err
}
if len(subs) == 0 {
return doTrialFlow(ctx, cli, user)
}
format := options.format
if len(format) == 0 {
format = formatter.TableFormatKey
}
updatesCtx := formatter.Context{
Output: cli.Out(),
Format: formatter.NewSubscriptionsFormat(format, options.quiet),
Trunc: false,
}
if err := formatter.SubscriptionsWrite(updatesCtx, subs); err != nil {
return nil, err
}
if options.displayOnly {
return nil, nil
}
fmt.Fprintf(cli.Out(), "Please pick a license by number: ")
var num int
if _, err := fmt.Fscan(cli.In(), &num); err != nil {
return nil, errors.Wrap(err, "failed to read user input")
}
if num < 0 || num >= len(subs) {
return nil, fmt.Errorf("invalid choice")
}
return user.GetIssuedLicense(ctx, subs[num].ID)
}
func doTrialFlow(ctx context.Context, cli command.Cli, user licenseutils.HubUser) (*model.IssuedLicense, error) {
if !command.PromptForConfirmation(cli.In(), cli.Out(),
"No existing licenses found, would you like to set up a new Enterprise Basic Trial license?") {
return nil, fmt.Errorf("you must have an existing enterprise license or generate a new trial to use the Enterprise Docker Engine")
}
targetID := user.User.ID
// If the user is a member of any organizations, allow trials generated against them
if len(user.Orgs) > 0 {
fmt.Fprintf(cli.Out(), "%d\t%s\n", 0, user.User.Username)
for i, org := range user.Orgs {
fmt.Fprintf(cli.Out(), "%d\t%s\n", i+1, org.Orgname)
}
fmt.Fprintf(cli.Out(), "Please choose an account to generate the trial in:")
var num int
if _, err := fmt.Fscan(cli.In(), &num); err != nil {
return nil, errors.Wrap(err, "failed to read user input")
}
if num < 0 || num > len(user.Orgs) {
return nil, fmt.Errorf("invalid choice")
}
if num > 0 {
targetID = user.Orgs[num-1].ID
}
}
return user.GenerateTrialLicense(ctx, targetID)
}
-37
View File
@@ -1,37 +0,0 @@
package engine
import (
"fmt"
"testing"
"github.com/docker/cli/internal/containerizedengine"
"gotest.tools/assert"
)
func TestActivateNoContainerd(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return nil, fmt.Errorf("some error")
},
)
cmd := newActivateCommand(testCli)
cmd.Flags().Set("license", "invalidpath")
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
assert.ErrorContains(t, err, "unable to access local containerd")
}
func TestActivateBadLicense(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return &fakeContainerizedEngineClient{}, nil
},
)
cmd := newActivateCommand(testCli)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
cmd.Flags().Set("license", "invalidpath")
err := cmd.Execute()
assert.Error(t, err, "open invalidpath: no such file or directory")
}
-33
View File
@@ -1,33 +0,0 @@
package engine
import (
"context"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/trust"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/pkg/errors"
)
func getRegistryAuth(cli command.Cli, registryPrefix string) (*types.AuthConfig, error) {
if registryPrefix == "" {
registryPrefix = "docker.io/docker"
}
distributionRef, err := reference.ParseNormalizedNamed(registryPrefix)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse image name: %s", registryPrefix)
}
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(context.Background(), nil, authResolver(cli), distributionRef.String())
if err != nil {
return nil, errors.Wrap(err, "failed to get imgRefAndAuth")
}
return imgRefAndAuth.AuthConfig(), nil
}
func authResolver(cli command.Cli) func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
return func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
return command.ResolveAuthConfig(ctx, cli, index)
}
}
-133
View File
@@ -1,133 +0,0 @@
package engine
import (
"context"
"fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/containerizedengine"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
const (
releaseNotePrefix = "https://docs.docker.com/releasenotes"
)
type checkOptions struct {
registryPrefix string
preReleases bool
downgrades bool
upgrades bool
format string
quiet bool
sockPath string
}
func newCheckForUpdatesCommand(dockerCli command.Cli) *cobra.Command {
var options checkOptions
cmd := &cobra.Command{
Use: "check [OPTIONS]",
Short: "Check for available engine updates",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runCheck(dockerCli, options)
},
}
flags := cmd.Flags()
flags.StringVar(&options.registryPrefix, "registry-prefix", "", "Override the existing location where engine images are pulled")
flags.BoolVar(&options.downgrades, "downgrades", false, "Report downgrades (default omits older versions)")
flags.BoolVar(&options.preReleases, "pre-releases", false, "Include pre-release versions")
flags.BoolVar(&options.upgrades, "upgrades", true, "Report available upgrades")
flags.StringVar(&options.format, "format", "", "Pretty-print updates using a Go template")
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display available versions")
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
return cmd
}
func runCheck(dockerCli command.Cli, options checkOptions) error {
ctx := context.Background()
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath)
if err != nil {
return errors.Wrap(err, "unable to access local containerd")
}
defer client.Close()
currentOpts, err := client.GetCurrentEngineVersion(ctx)
if err != nil {
return err
}
// override with user provided prefix if specified
if options.registryPrefix != "" {
currentOpts.RegistryPrefix = options.registryPrefix
}
imageName := currentOpts.RegistryPrefix + "/" + currentOpts.EngineImage
currentVersion := currentOpts.EngineVersion
versions, err := client.GetEngineVersions(ctx, dockerCli.RegistryClient(false), currentVersion, imageName)
if err != nil {
return err
}
availUpdates := []containerizedengine.Update{
{Type: "current", Version: currentVersion},
}
if len(versions.Patches) > 0 {
availUpdates = append(availUpdates,
processVersions(
currentVersion,
"patch",
options.preReleases,
versions.Patches)...)
}
if options.upgrades {
availUpdates = append(availUpdates,
processVersions(
currentVersion,
"upgrade",
options.preReleases,
versions.Upgrades)...)
}
if options.downgrades {
availUpdates = append(availUpdates,
processVersions(
currentVersion,
"downgrade",
options.preReleases,
versions.Downgrades)...)
}
format := options.format
if len(format) == 0 {
format = formatter.TableFormatKey
}
updatesCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewUpdatesFormat(format, options.quiet),
Trunc: false,
}
return formatter.UpdatesWrite(updatesCtx, availUpdates)
}
func processVersions(currentVersion, verType string,
includePrerelease bool,
versions []containerizedengine.DockerVersion) []containerizedengine.Update {
availUpdates := []containerizedengine.Update{}
for _, ver := range versions {
if !includePrerelease && ver.Prerelease() != "" {
continue
}
if ver.Tag != currentVersion {
availUpdates = append(availUpdates, containerizedengine.Update{
Type: verType,
Version: ver.Tag,
Notes: fmt.Sprintf("%s/%s", releaseNotePrefix, ver.Tag),
})
}
}
return availUpdates
}
-143
View File
@@ -1,143 +0,0 @@
package engine
import (
"context"
"fmt"
"testing"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/internal/containerizedengine"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/client"
ver "github.com/hashicorp/go-version"
"gotest.tools/assert"
"gotest.tools/golden"
)
var (
testCli = test.NewFakeCli(&client.Client{})
)
func TestCheckForUpdatesNoContainerd(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return nil, fmt.Errorf("some error")
},
)
cmd := newCheckForUpdatesCommand(testCli)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
assert.ErrorContains(t, err, "unable to access local containerd")
}
func TestCheckForUpdatesNoCurrentVersion(t *testing.T) {
retErr := fmt.Errorf("some failure")
getCurrentEngineVersionFunc := func(ctx context.Context) (containerizedengine.EngineInitOptions, error) {
return containerizedengine.EngineInitOptions{}, retErr
}
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return &fakeContainerizedEngineClient{
getCurrentEngineVersionFunc: getCurrentEngineVersionFunc,
}, nil
},
)
cmd := newCheckForUpdatesCommand(testCli)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
assert.Assert(t, err == retErr)
}
func TestCheckForUpdatesGetEngineVersionsFail(t *testing.T) {
retErr := fmt.Errorf("some failure")
getEngineVersionsFunc := func(ctx context.Context,
registryClient registryclient.RegistryClient,
currentVersion, imageName string) (containerizedengine.AvailableVersions, error) {
return containerizedengine.AvailableVersions{}, retErr
}
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return &fakeContainerizedEngineClient{
getEngineVersionsFunc: getEngineVersionsFunc,
}, nil
},
)
cmd := newCheckForUpdatesCommand(testCli)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
assert.Assert(t, err == retErr)
}
func TestCheckForUpdatesGetEngineVersionsHappy(t *testing.T) {
getCurrentEngineVersionFunc := func(ctx context.Context) (containerizedengine.EngineInitOptions, error) {
return containerizedengine.EngineInitOptions{
EngineImage: "current engine",
EngineVersion: "1.1.0",
}, nil
}
getEngineVersionsFunc := func(ctx context.Context,
registryClient registryclient.RegistryClient,
currentVersion, imageName string) (containerizedengine.AvailableVersions, error) {
return containerizedengine.AvailableVersions{
Downgrades: parseVersions(t, "1.0.1", "1.0.2", "1.0.3-beta1"),
Patches: parseVersions(t, "1.1.1", "1.1.2", "1.1.3-beta1"),
Upgrades: parseVersions(t, "1.2.0", "2.0.0", "2.1.0-beta1"),
}, nil
}
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return &fakeContainerizedEngineClient{
getEngineVersionsFunc: getEngineVersionsFunc,
getCurrentEngineVersionFunc: getCurrentEngineVersionFunc,
}, nil
},
)
cmd := newCheckForUpdatesCommand(testCli)
cmd.Flags().Set("pre-releases", "true")
cmd.Flags().Set("downgrades", "true")
err := cmd.Execute()
assert.NilError(t, err)
golden.Assert(t, testCli.OutBuffer().String(), "check-all.golden")
testCli.OutBuffer().Reset()
cmd.Flags().Set("pre-releases", "false")
cmd.Flags().Set("downgrades", "true")
err = cmd.Execute()
assert.NilError(t, err)
fmt.Println(testCli.OutBuffer().String())
golden.Assert(t, testCli.OutBuffer().String(), "check-no-prerelease.golden")
testCli.OutBuffer().Reset()
cmd.Flags().Set("pre-releases", "false")
cmd.Flags().Set("downgrades", "false")
err = cmd.Execute()
assert.NilError(t, err)
fmt.Println(testCli.OutBuffer().String())
golden.Assert(t, testCli.OutBuffer().String(), "check-no-downgrades.golden")
testCli.OutBuffer().Reset()
cmd.Flags().Set("pre-releases", "false")
cmd.Flags().Set("downgrades", "false")
cmd.Flags().Set("upgrades", "false")
err = cmd.Execute()
assert.NilError(t, err)
fmt.Println(testCli.OutBuffer().String())
golden.Assert(t, testCli.OutBuffer().String(), "check-patches-only.golden")
}
func makeVersion(t *testing.T, tag string) containerizedengine.DockerVersion {
v, err := ver.NewVersion(tag)
assert.NilError(t, err)
return containerizedengine.DockerVersion{Version: *v, Tag: tag}
}
func parseVersions(t *testing.T, tags ...string) []containerizedengine.DockerVersion {
ret := make([]containerizedengine.DockerVersion, len(tags))
for i, tag := range tags {
ret[i] = makeVersion(t, tag)
}
return ret
}
-105
View File
@@ -1,105 +0,0 @@
package engine
import (
"context"
"github.com/containerd/containerd"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/internal/containerizedengine"
"github.com/docker/docker/api/types"
)
type (
fakeContainerizedEngineClient struct {
closeFunc func() error
activateEngineFunc func(ctx context.Context,
opts containerizedengine.EngineInitOptions,
out containerizedengine.OutStream,
authConfig *types.AuthConfig,
healthfn func(context.Context) error) error
initEngineFunc func(ctx context.Context,
opts containerizedengine.EngineInitOptions,
out containerizedengine.OutStream,
authConfig *types.AuthConfig,
healthfn func(context.Context) error) error
doUpdateFunc func(ctx context.Context,
opts containerizedengine.EngineInitOptions,
out containerizedengine.OutStream,
authConfig *types.AuthConfig,
healthfn func(context.Context) error) error
getEngineVersionsFunc func(ctx context.Context,
registryClient registryclient.RegistryClient,
currentVersion,
imageName string) (containerizedengine.AvailableVersions, error)
getEngineFunc func(ctx context.Context) (containerd.Container, error)
removeEngineFunc func(ctx context.Context, engine containerd.Container) error
getCurrentEngineVersionFunc func(ctx context.Context) (containerizedengine.EngineInitOptions, error)
}
)
func (w *fakeContainerizedEngineClient) Close() error {
if w.closeFunc != nil {
return w.closeFunc()
}
return nil
}
func (w *fakeContainerizedEngineClient) ActivateEngine(ctx context.Context,
opts containerizedengine.EngineInitOptions,
out containerizedengine.OutStream,
authConfig *types.AuthConfig,
healthfn func(context.Context) error) error {
if w.activateEngineFunc != nil {
return w.activateEngineFunc(ctx, opts, out, authConfig, healthfn)
}
return nil
}
func (w *fakeContainerizedEngineClient) InitEngine(ctx context.Context,
opts containerizedengine.EngineInitOptions,
out containerizedengine.OutStream,
authConfig *types.AuthConfig,
healthfn func(context.Context) error) error {
if w.initEngineFunc != nil {
return w.initEngineFunc(ctx, opts, out, authConfig, healthfn)
}
return nil
}
func (w *fakeContainerizedEngineClient) DoUpdate(ctx context.Context,
opts containerizedengine.EngineInitOptions,
out containerizedengine.OutStream,
authConfig *types.AuthConfig,
healthfn func(context.Context) error) error {
if w.doUpdateFunc != nil {
return w.doUpdateFunc(ctx, opts, out, authConfig, healthfn)
}
return nil
}
func (w *fakeContainerizedEngineClient) GetEngineVersions(ctx context.Context,
registryClient registryclient.RegistryClient,
currentVersion, imageName string) (containerizedengine.AvailableVersions, error) {
if w.getEngineVersionsFunc != nil {
return w.getEngineVersionsFunc(ctx, registryClient, currentVersion, imageName)
}
return containerizedengine.AvailableVersions{}, nil
}
func (w *fakeContainerizedEngineClient) GetEngine(ctx context.Context) (containerd.Container, error) {
if w.getEngineFunc != nil {
return w.getEngineFunc(ctx)
}
return nil, nil
}
func (w *fakeContainerizedEngineClient) RemoveEngine(ctx context.Context, engine containerd.Container) error {
if w.removeEngineFunc != nil {
return w.removeEngineFunc(ctx, engine)
}
return nil
}
func (w *fakeContainerizedEngineClient) GetCurrentEngineVersion(ctx context.Context) (containerizedengine.EngineInitOptions, error) {
if w.getCurrentEngineVersionFunc != nil {
return w.getCurrentEngineVersionFunc(ctx)
}
return containerizedengine.EngineInitOptions{}, nil
}
-25
View File
@@ -1,25 +0,0 @@
package engine
import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
// NewEngineCommand returns a cobra command for `engine` subcommands
func NewEngineCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "engine COMMAND",
Short: "Manage the docker engine",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
}
cmd.AddCommand(
newInitCommand(dockerCli),
newActivateCommand(dockerCli),
newCheckForUpdatesCommand(dockerCli),
newUpdateCommand(dockerCli),
newRmCommand(dockerCli),
)
return cmd
}
-14
View File
@@ -1,14 +0,0 @@
package engine
import (
"testing"
"gotest.tools/assert"
)
func TestNewEngineCommand(t *testing.T) {
cmd := NewEngineCommand(testCli)
subcommands := cmd.Commands()
assert.Assert(t, len(subcommands) == 5)
}
-62
View File
@@ -1,62 +0,0 @@
package engine
import (
"context"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/containerizedengine"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type extendedEngineInitOptions struct {
containerizedengine.EngineInitOptions
sockPath string
}
func newInitCommand(dockerCli command.Cli) *cobra.Command {
var options extendedEngineInitOptions
cmd := &cobra.Command{
Use: "init [OPTIONS]",
Short: "Initialize a local engine",
Long: `This command will initialize a local engine running on containerd.
Configuration of the engine is managed through the daemon.json configuration
file on the host and may be pre-created before running the 'init' command.
`,
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runInit(dockerCli, options)
},
Annotations: map[string]string{"experimentalCLI": ""},
}
flags := cmd.Flags()
flags.StringVar(&options.EngineVersion, "version", cli.Version, "Specify engine version")
flags.StringVar(&options.EngineImage, "engine-image", containerizedengine.CommunityEngineImage, "Specify engine image")
flags.StringVar(&options.RegistryPrefix, "registry-prefix", "docker.io/docker", "Override the default location where engine images are pulled")
flags.StringVar(&options.ConfigFile, "config-file", "/etc/docker/daemon.json", "Specify the location of the daemon configuration file on the host")
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
return cmd
}
func runInit(dockerCli command.Cli, options extendedEngineInitOptions) error {
ctx := context.Background()
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath)
if err != nil {
return errors.Wrap(err, "unable to access local containerd")
}
defer client.Close()
authConfig, err := getRegistryAuth(dockerCli, options.RegistryPrefix)
if err != nil {
return err
}
return client.InitEngine(ctx, options.EngineInitOptions, dockerCli.Out(), authConfig,
func(ctx context.Context) error {
client := dockerCli.Client()
_, err := client.Ping(ctx)
return err
})
}
-33
View File
@@ -1,33 +0,0 @@
package engine
import (
"fmt"
"testing"
"github.com/docker/cli/internal/containerizedengine"
"gotest.tools/assert"
)
func TestInitNoContainerd(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return nil, fmt.Errorf("some error")
},
)
cmd := newInitCommand(testCli)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
assert.ErrorContains(t, err, "unable to access local containerd")
}
func TestInitHappy(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return &fakeContainerizedEngineClient{}, nil
},
)
cmd := newInitCommand(testCli)
err := cmd.Execute()
assert.NilError(t, err)
}
-54
View File
@@ -1,54 +0,0 @@
package engine
import (
"context"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
// TODO - consider adding a "purge" flag that also removes
// configuration files and the docker root dir.
type rmOptions struct {
sockPath string
}
func newRmCommand(dockerCli command.Cli) *cobra.Command {
var options rmOptions
cmd := &cobra.Command{
Use: "rm [OPTIONS]",
Short: "Remove the local engine",
Long: `This command will remove the local engine running on containerd.
No state files will be removed from the host filesystem.
`,
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runRm(dockerCli, options)
},
Annotations: map[string]string{"experimentalCLI": ""},
}
flags := cmd.Flags()
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
return cmd
}
func runRm(dockerCli command.Cli, options rmOptions) error {
ctx := context.Background()
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath)
if err != nil {
return errors.Wrap(err, "unable to access local containerd")
}
defer client.Close()
engine, err := client.GetEngine(ctx)
if err != nil {
return err
}
return client.RemoveEngine(ctx, engine)
}
-33
View File
@@ -1,33 +0,0 @@
package engine
import (
"fmt"
"testing"
"github.com/docker/cli/internal/containerizedengine"
"gotest.tools/assert"
)
func TestRmNoContainerd(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return nil, fmt.Errorf("some error")
},
)
cmd := newRmCommand(testCli)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
assert.ErrorContains(t, err, "unable to access local containerd")
}
func TestRmHappy(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return &fakeContainerizedEngineClient{}, nil
},
)
cmd := newRmCommand(testCli)
err := cmd.Execute()
assert.NilError(t, err)
}
-11
View File
@@ -1,11 +0,0 @@
TYPE VERSION NOTES
current 1.1.0
patch 1.1.1 https://docs.docker.com/releasenotes/1.1.1
patch 1.1.2 https://docs.docker.com/releasenotes/1.1.2
patch 1.1.3-beta1 https://docs.docker.com/releasenotes/1.1.3-beta1
upgrade 1.2.0 https://docs.docker.com/releasenotes/1.2.0
upgrade 2.0.0 https://docs.docker.com/releasenotes/2.0.0
upgrade 2.1.0-beta1 https://docs.docker.com/releasenotes/2.1.0-beta1
downgrade 1.0.1 https://docs.docker.com/releasenotes/1.0.1
downgrade 1.0.2 https://docs.docker.com/releasenotes/1.0.2
downgrade 1.0.3-beta1 https://docs.docker.com/releasenotes/1.0.3-beta1
@@ -1,6 +0,0 @@
TYPE VERSION NOTES
current 1.1.0
patch 1.1.1 https://docs.docker.com/releasenotes/1.1.1
patch 1.1.2 https://docs.docker.com/releasenotes/1.1.2
upgrade 1.2.0 https://docs.docker.com/releasenotes/1.2.0
upgrade 2.0.0 https://docs.docker.com/releasenotes/2.0.0
@@ -1,8 +0,0 @@
TYPE VERSION NOTES
current 1.1.0
patch 1.1.1 https://docs.docker.com/releasenotes/1.1.1
patch 1.1.2 https://docs.docker.com/releasenotes/1.1.2
upgrade 1.2.0 https://docs.docker.com/releasenotes/1.2.0
upgrade 2.0.0 https://docs.docker.com/releasenotes/2.0.0
downgrade 1.0.1 https://docs.docker.com/releasenotes/1.0.1
downgrade 1.0.2 https://docs.docker.com/releasenotes/1.0.2
-4
View File
@@ -1,4 +0,0 @@
TYPE VERSION NOTES
current 1.1.0
patch 1.1.1 https://docs.docker.com/releasenotes/1.1.1
patch 1.1.2 https://docs.docker.com/releasenotes/1.1.2
-68
View File
@@ -1,68 +0,0 @@
package engine
import (
"context"
"fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
var options extendedEngineInitOptions
cmd := &cobra.Command{
Use: "update [OPTIONS]",
Short: "Update a local engine",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runUpdate(dockerCli, options)
},
}
flags := cmd.Flags()
flags.StringVar(&options.EngineVersion, "version", "", "Specify engine version")
flags.StringVar(&options.EngineImage, "engine-image", "", "Specify engine image")
flags.StringVar(&options.RegistryPrefix, "registry-prefix", "", "Override the current location where engine images are pulled")
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
return cmd
}
func runUpdate(dockerCli command.Cli, options extendedEngineInitOptions) error {
ctx := context.Background()
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath)
if err != nil {
return errors.Wrap(err, "unable to access local containerd")
}
defer client.Close()
if options.EngineImage == "" || options.RegistryPrefix == "" {
currentOpts, err := client.GetCurrentEngineVersion(ctx)
if err != nil {
return err
}
if options.EngineImage == "" {
options.EngineImage = currentOpts.EngineImage
}
if options.RegistryPrefix == "" {
options.RegistryPrefix = currentOpts.RegistryPrefix
}
}
authConfig, err := getRegistryAuth(dockerCli, options.RegistryPrefix)
if err != nil {
return err
}
if err := client.DoUpdate(ctx, options.EngineInitOptions, dockerCli.Out(), authConfig,
func(ctx context.Context) error {
client := dockerCli.Client()
_, err := client.Ping(ctx)
return err
}); err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), "Success! The docker engine is now running.")
return nil
}
-35
View File
@@ -1,35 +0,0 @@
package engine
import (
"fmt"
"testing"
"github.com/docker/cli/internal/containerizedengine"
"gotest.tools/assert"
)
func TestUpdateNoContainerd(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return nil, fmt.Errorf("some error")
},
)
cmd := newUpdateCommand(testCli)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
assert.ErrorContains(t, err, "unable to access local containerd")
}
func TestUpdateHappy(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return &fakeContainerizedEngineClient{}, nil
},
)
cmd := newUpdateCommand(testCli)
cmd.Flags().Set("registry-prefix", "docker.io/docker")
cmd.Flags().Set("version", "someversion")
err := cmd.Execute()
assert.NilError(t, err)
}
+14 -12
View File
@@ -269,10 +269,7 @@ func DisplayablePorts(ports []types.Port) string {
var result []string
var hostMappings []string
var groupMapKeys []string
sort.Slice(ports, func(i, j int) bool {
return comparePorts(ports[i], ports[j])
})
sort.Sort(byPortInfo(ports))
for _, port := range ports {
current := port.PrivatePort
portKey := port.Type
@@ -325,18 +322,23 @@ func formGroup(key string, start, last uint16) string {
return fmt.Sprintf("%s/%s", group, groupType)
}
func comparePorts(i, j types.Port) bool {
if i.PrivatePort != j.PrivatePort {
return i.PrivatePort < j.PrivatePort
// byPortInfo is a temporary type used to sort types.Port by its fields
type byPortInfo []types.Port
func (r byPortInfo) Len() int { return len(r) }
func (r byPortInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r byPortInfo) Less(i, j int) bool {
if r[i].PrivatePort != r[j].PrivatePort {
return r[i].PrivatePort < r[j].PrivatePort
}
if i.IP != j.IP {
return i.IP < j.IP
if r[i].IP != r[j].IP {
return r[i].IP < r[j].IP
}
if i.PublicPort != j.PublicPort {
return i.PublicPort < j.PublicPort
if r[i].PublicPort != r[j].PublicPort {
return r[i].PublicPort < r[j].PublicPort
}
return i.Type < j.Type
return r[i].Type < r[j].Type
}
-154
View File
@@ -1,154 +0,0 @@
package formatter
import (
"time"
"github.com/docker/cli/internal/licenseutils"
"github.com/docker/licensing/model"
)
const (
defaultSubscriptionsTableFormat = "table {{.Num}}\t{{.Owner}}\t{{.ProductID}}\t{{.Expires}}\t{{.ComponentsString}}"
defaultSubscriptionsQuietFormat = "{{.Num}}:{{.Summary}}"
numHeader = "NUM"
ownerHeader = "OWNER"
licenseNameHeader = "NAME"
idHeader = "ID"
dockerIDHeader = "DOCKER ID"
productIDHeader = "PRODUCT ID"
productRatePlanHeader = "PRODUCT RATE PLAN"
productRatePlanIDHeader = "PRODUCT RATE PLAN ID"
startHeader = "START"
expiresHeader = "EXPIRES"
stateHeader = "STATE"
eusaHeader = "EUSA"
pricingComponentsHeader = "PRICING COMPONENTS"
)
// NewSubscriptionsFormat returns a Format for rendering using a license Context
func NewSubscriptionsFormat(source string, quiet bool) Format {
switch source {
case TableFormatKey:
if quiet {
return defaultSubscriptionsQuietFormat
}
return defaultSubscriptionsTableFormat
case RawFormatKey:
if quiet {
return `license: {{.ID}}`
}
return `license: {{.ID}}\nname: {{.Name}}\nowner: {{.Owner}}\ncomponents: {{.ComponentsString}}\n`
}
return Format(source)
}
// SubscriptionsWrite writes the context
func SubscriptionsWrite(ctx Context, subs []licenseutils.LicenseDisplay) error {
render := func(format func(subContext subContext) error) error {
for _, sub := range subs {
licenseCtx := &licenseContext{trunc: ctx.Trunc, l: sub}
if err := format(licenseCtx); err != nil {
return err
}
}
return nil
}
licenseCtx := licenseContext{}
licenseCtx.header = map[string]string{
"Num": numHeader,
"Owner": ownerHeader,
"Name": licenseNameHeader,
"ID": idHeader,
"DockerID": dockerIDHeader,
"ProductID": productIDHeader,
"ProductRatePlan": productRatePlanHeader,
"ProductRatePlanID": productRatePlanIDHeader,
"Start": startHeader,
"Expires": expiresHeader,
"State": stateHeader,
"Eusa": eusaHeader,
"ComponentsString": pricingComponentsHeader,
}
return ctx.Write(&licenseCtx, render)
}
type licenseContext struct {
HeaderContext
trunc bool
l licenseutils.LicenseDisplay
}
func (c *licenseContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *licenseContext) Num() int {
return c.l.Num
}
func (c *licenseContext) Owner() string {
return c.l.Owner
}
func (c *licenseContext) ComponentsString() string {
return c.l.ComponentsString
}
func (c *licenseContext) Summary() string {
return c.l.String()
}
func (c *licenseContext) Name() string {
return c.l.Name
}
func (c *licenseContext) ID() string {
return c.l.ID
}
func (c *licenseContext) DockerID() string {
return c.l.DockerID
}
func (c *licenseContext) ProductID() string {
return c.l.ProductID
}
func (c *licenseContext) ProductRatePlan() string {
return c.l.ProductRatePlan
}
func (c *licenseContext) ProductRatePlanID() string {
return c.l.ProductRatePlanID
}
func (c *licenseContext) Start() *time.Time {
return c.l.Start
}
func (c *licenseContext) Expires() *time.Time {
return c.l.Expires
}
func (c *licenseContext) State() string {
return c.l.State
}
func (c *licenseContext) Eusa() *model.EusaState {
return c.l.Eusa
}
func (c *licenseContext) PricingComponents() []model.SubscriptionPricingComponent {
// Dereference the pricing component pointers in the pricing components
// so it can be rendered properly with the template formatter
var ret []model.SubscriptionPricingComponent
for _, spc := range c.l.PricingComponents {
if spc == nil {
continue
}
ret = append(ret, *spc)
}
return ret
}
-256
View File
@@ -1,256 +0,0 @@
package formatter
import (
"bytes"
"encoding/json"
"strings"
"testing"
"time"
"github.com/docker/cli/internal/licenseutils"
"github.com/docker/licensing/model"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestSubscriptionContextWrite(t *testing.T) {
cases := []struct {
context Context
expected string
}{
// Errors
{
Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
// Table format
{
Context{Format: NewSubscriptionsFormat("table", false)},
`NUM OWNER PRODUCT ID EXPIRES PRICING COMPONENTS
1 owner1 productid1 2020-01-01 10:00:00 +0000 UTC compstring
2 owner2 productid2 2020-01-01 10:00:00 +0000 UTC compstring
`,
},
{
Context{Format: NewSubscriptionsFormat("table", true)},
`1:License Name: name1 Quantity: 10 nodes Expiration date: 2020-01-01
2:License Name: name2 Quantity: 20 nodes Expiration date: 2020-01-01
`,
},
{
Context{Format: NewSubscriptionsFormat("table {{.Owner}}", false)},
`OWNER
owner1
owner2
`,
},
{
Context{Format: NewSubscriptionsFormat("table {{.Owner}}", true)},
`OWNER
owner1
owner2
`,
},
// Raw Format
{
Context{Format: NewSubscriptionsFormat("raw", false)},
`license: id1
name: name1
owner: owner1
components: compstring
license: id2
name: name2
owner: owner2
components: compstring
`,
},
{
Context{Format: NewSubscriptionsFormat("raw", true)},
`license: id1
license: id2
`,
},
// Custom Format
{
Context{Format: NewSubscriptionsFormat("{{.Owner}}", false)},
`owner1
owner2
`,
},
}
expiration, _ := time.Parse(time.RFC822, "01 Jan 20 10:00 UTC")
for _, testcase := range cases {
subscriptions := []licenseutils.LicenseDisplay{
{
Num: 1,
Owner: "owner1",
Subscription: model.Subscription{
ID: "id1",
Name: "name1",
ProductID: "productid1",
Expires: &expiration,
PricingComponents: model.PricingComponents{
&model.SubscriptionPricingComponent{
Name: "nodes",
Value: 10,
},
},
},
ComponentsString: "compstring",
},
{
Num: 2,
Owner: "owner2",
Subscription: model.Subscription{
ID: "id2",
Name: "name2",
ProductID: "productid2",
Expires: &expiration,
PricingComponents: model.PricingComponents{
&model.SubscriptionPricingComponent{
Name: "nodes",
Value: 20,
},
},
},
ComponentsString: "compstring",
},
}
out := &bytes.Buffer{}
testcase.context.Output = out
err := SubscriptionsWrite(testcase.context, subscriptions)
if err != nil {
assert.Error(t, err, testcase.expected)
} else {
assert.Check(t, is.Equal(testcase.expected, out.String()))
}
}
}
func TestSubscriptionContextWriteJSON(t *testing.T) {
expiration, _ := time.Parse(time.RFC822, "01 Jan 20 10:00 UTC")
subscriptions := []licenseutils.LicenseDisplay{
{
Num: 1,
Owner: "owner1",
Subscription: model.Subscription{
ID: "id1",
Name: "name1",
ProductID: "productid1",
Expires: &expiration,
PricingComponents: model.PricingComponents{
&model.SubscriptionPricingComponent{
Name: "nodes",
Value: 10,
},
},
},
ComponentsString: "compstring",
},
{
Num: 2,
Owner: "owner2",
Subscription: model.Subscription{
ID: "id2",
Name: "name2",
ProductID: "productid2",
Expires: &expiration,
PricingComponents: model.PricingComponents{
&model.SubscriptionPricingComponent{
Name: "nodes",
Value: 20,
},
},
},
ComponentsString: "compstring",
},
}
expectedJSONs := []map[string]interface{}{
{
"Owner": "owner1",
"ComponentsString": "compstring",
"Expires": "2020-01-01T10:00:00Z",
"DockerID": "",
"Eusa": nil,
"ID": "id1",
"Start": nil,
"Name": "name1",
"Num": float64(1),
"PricingComponents": []interface{}{
map[string]interface{}{
"name": "nodes",
"value": float64(10),
},
},
"ProductID": "productid1",
"ProductRatePlan": "",
"ProductRatePlanID": "",
"State": "",
"Summary": "License Name: name1\tQuantity: 10 nodes\tExpiration date: 2020-01-01",
},
{
"Owner": "owner2",
"ComponentsString": "compstring",
"Expires": "2020-01-01T10:00:00Z",
"DockerID": "",
"Eusa": nil,
"ID": "id2",
"Start": nil,
"Name": "name2",
"Num": float64(2),
"PricingComponents": []interface{}{
map[string]interface{}{
"name": "nodes",
"value": float64(20),
},
},
"ProductID": "productid2",
"ProductRatePlan": "",
"ProductRatePlanID": "",
"State": "",
"Summary": "License Name: name2\tQuantity: 20 nodes\tExpiration date: 2020-01-01",
},
}
out := &bytes.Buffer{}
err := SubscriptionsWrite(Context{Format: "{{json .}}", Output: out}, subscriptions)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
var m map[string]interface{}
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Check(t, is.DeepEqual(expectedJSONs[i], m))
}
}
func TestSubscriptionContextWriteJSONField(t *testing.T) {
subscriptions := []licenseutils.LicenseDisplay{
{Num: 1, Owner: "owner1"},
{Num: 2, Owner: "owner2"},
}
out := &bytes.Buffer{}
err := SubscriptionsWrite(Context{Format: "{{json .Owner}}", Output: out}, subscriptions)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
var s string
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Check(t, is.Equal(subscriptions[i].Owner, s))
}
}
+12 -7
View File
@@ -599,13 +599,7 @@ func (c *serviceContext) Ports() string {
pr := portRange{}
ports := []string{}
servicePorts := c.service.Endpoint.Ports
sort.Slice(servicePorts, func(i, j int) bool {
if servicePorts[i].Protocol == servicePorts[j].Protocol {
return servicePorts[i].PublishedPort < servicePorts[j].PublishedPort
}
return servicePorts[i].Protocol < servicePorts[j].Protocol
})
sort.Sort(byProtocolAndPublishedPort(c.service.Endpoint.Ports))
for _, p := range c.service.Endpoint.Ports {
if p.PublishMode == swarm.PortConfigPublishModeIngress {
@@ -639,3 +633,14 @@ func (c *serviceContext) Ports() string {
}
return strings.Join(ports, ", ")
}
type byProtocolAndPublishedPort []swarm.PortConfig
func (a byProtocolAndPublishedPort) Len() int { return len(a) }
func (a byProtocolAndPublishedPort) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byProtocolAndPublishedPort) Less(i, j int) bool {
if a[i].Protocol == a[j].Protocol {
return a[i].PublishedPort < a[j].PublishedPort
}
return a[i].Protocol < a[j].Protocol
}
+15
View File
@@ -133,3 +133,18 @@ func (c *signerInfoContext) Keys() string {
func (c *signerInfoContext) Signer() string {
return c.s.Name
}
// SignerInfoList helps sort []SignerInfo by signer names
type SignerInfoList []SignerInfo
func (signerInfoComp SignerInfoList) Len() int {
return len(signerInfoComp)
}
func (signerInfoComp SignerInfoList) Less(i, j int) bool {
return signerInfoComp[i].Name < signerInfoComp[j].Name
}
func (signerInfoComp SignerInfoList) Swap(i, j int) {
signerInfoComp[i], signerInfoComp[j] = signerInfoComp[j], signerInfoComp[i]
}
+1 -1
View File
@@ -222,7 +222,7 @@ eve foobarbazquxquux, key31, key32
}
for _, testcase := range cases {
signerInfo := []SignerInfo{
signerInfo := SignerInfoList{
{Name: "alice", Keys: []string{"key11", "key12"}},
{Name: "bob", Keys: []string{"key21"}},
{Name: "eve", Keys: []string{"key31", "key32", "foobarbazquxquux"}},
-73
View File
@@ -1,73 +0,0 @@
package formatter
import (
"github.com/docker/cli/internal/containerizedengine"
)
const (
defaultUpdatesTableFormat = "table {{.Type}}\t{{.Version}}\t{{.Notes}}"
defaultUpdatesQuietFormat = "{{.Version}}"
updatesTypeHeader = "TYPE"
versionHeader = "VERSION"
notesHeader = "NOTES"
)
// NewUpdatesFormat returns a Format for rendering using a updates context
func NewUpdatesFormat(source string, quiet bool) Format {
switch source {
case TableFormatKey:
if quiet {
return defaultUpdatesQuietFormat
}
return defaultUpdatesTableFormat
case RawFormatKey:
if quiet {
return `update_version: {{.Version}}`
}
return `update_version: {{.Version}}\ntype: {{.Type}}\nnotes: {{.Notes}}\n`
}
return Format(source)
}
// UpdatesWrite writes the context
func UpdatesWrite(ctx Context, availableUpdates []containerizedengine.Update) error {
render := func(format func(subContext subContext) error) error {
for _, update := range availableUpdates {
updatesCtx := &updateContext{trunc: ctx.Trunc, u: update}
if err := format(updatesCtx); err != nil {
return err
}
}
return nil
}
updatesCtx := updateContext{}
updatesCtx.header = map[string]string{
"Type": updatesTypeHeader,
"Version": versionHeader,
"Notes": notesHeader,
}
return ctx.Write(&updatesCtx, render)
}
type updateContext struct {
HeaderContext
trunc bool
u containerizedengine.Update
}
func (c *updateContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *updateContext) Type() string {
return c.u.Type
}
func (c *updateContext) Version() string {
return c.u.Version
}
func (c *updateContext) Notes() string {
return c.u.Notes
}
-143
View File
@@ -1,143 +0,0 @@
package formatter
import (
"bytes"
"encoding/json"
"strings"
"testing"
"github.com/docker/cli/internal/containerizedengine"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestUpdateContextWrite(t *testing.T) {
cases := []struct {
context Context
expected string
}{
// Errors
{
Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
// Table format
{
Context{Format: NewUpdatesFormat("table", false)},
`TYPE VERSION NOTES
updateType1 version1 description 1
updateType2 version2 description 2
`,
},
{
Context{Format: NewUpdatesFormat("table", true)},
`version1
version2
`,
},
{
Context{Format: NewUpdatesFormat("table {{.Version}}", false)},
`VERSION
version1
version2
`,
},
{
Context{Format: NewUpdatesFormat("table {{.Version}}", true)},
`VERSION
version1
version2
`,
},
// Raw Format
{
Context{Format: NewUpdatesFormat("raw", false)},
`update_version: version1
type: updateType1
notes: description 1
update_version: version2
type: updateType2
notes: description 2
`,
},
{
Context{Format: NewUpdatesFormat("raw", true)},
`update_version: version1
update_version: version2
`,
},
// Custom Format
{
Context{Format: NewUpdatesFormat("{{.Version}}", false)},
`version1
version2
`,
},
}
for _, testcase := range cases {
updates := []containerizedengine.Update{
{Type: "updateType1", Version: "version1", Notes: "description 1"},
{Type: "updateType2", Version: "version2", Notes: "description 2"},
}
out := &bytes.Buffer{}
testcase.context.Output = out
err := UpdatesWrite(testcase.context, updates)
if err != nil {
assert.Error(t, err, testcase.expected)
} else {
assert.Check(t, is.Equal(testcase.expected, out.String()))
}
}
}
func TestUpdateContextWriteJSON(t *testing.T) {
updates := []containerizedengine.Update{
{Type: "updateType1", Version: "version1", Notes: "note1"},
{Type: "updateType2", Version: "version2", Notes: "note2"},
}
expectedJSONs := []map[string]interface{}{
{"Version": "version1", "Notes": "note1", "Type": "updateType1"},
{"Version": "version2", "Notes": "note2", "Type": "updateType2"},
}
out := &bytes.Buffer{}
err := UpdatesWrite(Context{Format: "{{json .}}", Output: out}, updates)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
var m map[string]interface{}
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Check(t, is.DeepEqual(expectedJSONs[i], m))
}
}
func TestUpdateContextWriteJSONField(t *testing.T) {
updates := []containerizedengine.Update{
{Type: "updateType1", Version: "version1"},
{Type: "updateType2", Version: "version2"},
}
out := &bytes.Buffer{}
err := UpdatesWrite(Context{Format: "{{json .Type}}", Output: out}, updates)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
var s string
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Check(t, is.Equal(updates[i].Type, s))
}
}
+9 -10
View File
@@ -58,7 +58,7 @@ type buildOptions struct {
isolation string
quiet bool
noCache bool
progress string
console opts.NullableBool
rm bool
forceRm bool
pull bool
@@ -72,7 +72,6 @@ type buildOptions struct {
stream bool
platform string
untrusted bool
secrets []string
}
// dockerfileFromStdin returns true when the user specified that the Dockerfile
@@ -154,10 +153,9 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
flags.SetAnnotation("stream", "experimental", nil)
flags.SetAnnotation("stream", "version", []string{"1.31"})
flags.StringVar(&options.progress, "progress", "auto", "Set type of progress output (only if BuildKit enabled) (auto, plain, tty). Use plain to show container output")
flags.StringArrayVar(&options.secrets, "secret", []string{}, "Secret file to expose to the build (only if BuildKit enabled): id=mysecret,src=/local/secret")
flags.SetAnnotation("secret", "version", []string{"1.39"})
flags.Var(&options.console, "console", "Show console output (with buildkit only) (true, false, auto)")
flags.SetAnnotation("console", "experimental", nil)
flags.SetAnnotation("console", "version", []string{"1.38"})
return cmd
}
@@ -187,8 +185,6 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
if enableBuildkit {
return runBuildBuildKit(dockerCli, options)
}
} else if dockerCli.ServerInfo().BuildkitVersion == types.BuilderBuildKit {
return runBuildBuildKit(dockerCli, options)
}
var (
@@ -274,12 +270,15 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
}
// And canonicalize dockerfile name to a platform-independent one
relDockerfile = archive.CanonicalTarNameForPath(relDockerfile)
relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile)
if err != nil {
return errors.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err)
}
excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, options.dockerfileFromStdin())
buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
ExcludePatterns: excludes,
ChownOpts: &idtools.Identity{UID: 0, GID: 0},
ChownOpts: &idtools.IDPair{UID: 0, GID: 0},
})
if err != nil {
return err
+4 -68
View File
@@ -3,7 +3,6 @@ package image
import (
"bytes"
"context"
"encoding/csv"
"encoding/json"
"fmt"
"io"
@@ -16,17 +15,14 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image/build"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/pkg/urlutil"
controlapi "github.com/moby/buildkit/api/services/control"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/auth/authprovider"
"github.com/moby/buildkit/session/filesync"
"github.com/moby/buildkit/session/secrets/secretsprovider"
"github.com/moby/buildkit/util/appcontext"
"github.com/moby/buildkit/util/progress/progressui"
"github.com/pkg/errors"
@@ -131,13 +127,6 @@ func runBuildBuildKit(dockerCli command.Cli, options buildOptions) error {
}
s.Allow(authprovider.NewDockerAuthProvider())
if len(options.secrets) > 0 {
sp, err := parseSecretSpecs(options.secrets)
if err != nil {
return errors.Wrapf(err, "could not parse secrets: %v", options.secrets)
}
s.Allow(sp)
}
eg, ctx := errgroup.WithContext(ctx)
@@ -202,19 +191,16 @@ func doBuild(ctx context.Context, eg *errgroup.Group, dockerCli command.Cli, opt
t := newTracer()
ssArr := []*client.SolveStatus{}
if err := opts.ValidateProgressOutput(options.progress); err != nil {
return err
}
displayStatus := func(out *os.File, displayCh chan *client.SolveStatus) {
var c console.Console
// TODO: Handle tty output in non-tty environment.
if cons, err := console.ConsoleFromFile(out); err == nil && (options.progress == "auto" || options.progress == "tty") {
// TODO: Handle interactive output in non-interactive environment.
consoleOpt := options.console.Value()
if cons, err := console.ConsoleFromFile(out); err == nil && (consoleOpt == nil || *consoleOpt) {
c = cons
}
// not using shared context to not disrupt display but let is finish reporting errors
eg.Go(func() error {
return progressui.DisplaySolveStatus(context.TODO(), "", c, out, displayCh)
return progressui.DisplaySolveStatus(context.TODO(), c, out, displayCh)
})
}
@@ -358,53 +344,3 @@ func (t *tracer) write(msg jsonmessage.JSONMessage) {
t.displayCh <- &s
}
func parseSecretSpecs(sl []string) (session.Attachable, error) {
fs := make([]secretsprovider.FileSource, 0, len(sl))
for _, v := range sl {
s, err := parseSecret(v)
if err != nil {
return nil, err
}
fs = append(fs, *s)
}
store, err := secretsprovider.NewFileStore(fs)
if err != nil {
return nil, err
}
return secretsprovider.NewSecretProvider(store), nil
}
func parseSecret(value string) (*secretsprovider.FileSource, error) {
csvReader := csv.NewReader(strings.NewReader(value))
fields, err := csvReader.Read()
if err != nil {
return nil, errors.Wrap(err, "failed to parse csv secret")
}
fs := secretsprovider.FileSource{}
for _, field := range fields {
parts := strings.SplitN(field, "=", 2)
key := strings.ToLower(parts[0])
if len(parts) != 2 {
return nil, errors.Errorf("invalid field '%s' must be a key=value pair", field)
}
value := parts[1]
switch key {
case "type":
if value != "file" {
return nil, errors.Errorf("unsupported secret type %q", value)
}
case "id":
fs.ID = value
case "source", "src":
fs.FilePath = value
default:
return nil, errors.Errorf("unexpected key '%s' in '%s'", key, field)
}
}
return &fs, nil
}
-3
View File
@@ -28,9 +28,6 @@ import (
const clientSessionRemote = "client-session"
func isSessionSupported(dockerCli command.Cli) bool {
if versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.39") {
return true
}
return dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31")
}
-62
View File
@@ -5,7 +5,6 @@ import (
"bytes"
"compress/gzip"
"context"
"fmt"
"io"
"io/ioutil"
"os"
@@ -18,7 +17,6 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
"github.com/google/go-cmp/cmp"
"github.com/moby/buildkit/session/secrets/secretsprovider"
"gotest.tools/assert"
"gotest.tools/fs"
"gotest.tools/skip"
@@ -175,66 +173,6 @@ RUN echo hello world
assert.DeepEqual(t, fakeBuild.filenames(t), []string{"Dockerfile"})
}
func TestParseSecret(t *testing.T) {
type testcase struct {
value string
errExpected bool
errMatch string
filesource *secretsprovider.FileSource
}
var testcases = []testcase{
{
value: "",
errExpected: true,
}, {
value: "foobar",
errExpected: true,
errMatch: "must be a key=value pair",
}, {
value: "foo,bar",
errExpected: true,
errMatch: "must be a key=value pair",
}, {
value: "foo=bar",
errExpected: true,
errMatch: "unexpected key",
}, {
value: "src=somefile",
filesource: &secretsprovider.FileSource{FilePath: "somefile"},
}, {
value: "source=somefile",
filesource: &secretsprovider.FileSource{FilePath: "somefile"},
}, {
value: "id=mysecret",
filesource: &secretsprovider.FileSource{ID: "mysecret"},
}, {
value: "id=mysecret,src=somefile",
filesource: &secretsprovider.FileSource{ID: "mysecret", FilePath: "somefile"},
}, {
value: "id=mysecret,source=somefile,type=file",
filesource: &secretsprovider.FileSource{ID: "mysecret", FilePath: "somefile"},
}, {
value: "id=mysecret,src=somefile,src=othersecretfile",
filesource: &secretsprovider.FileSource{ID: "mysecret", FilePath: "othersecretfile"},
}, {
value: "type=invalid",
errExpected: true,
errMatch: "unsupported secret type",
},
}
for _, tc := range testcases {
t.Run(tc.value, func(t *testing.T) {
secret, err := parseSecret(tc.value)
assert.Equal(t, err != nil, tc.errExpected, fmt.Sprintf("err=%v errExpected=%t", err, tc.errExpected))
if tc.errMatch != "" {
assert.ErrorContains(t, err, tc.errMatch)
}
assert.DeepEqual(t, secret, tc.filesource)
})
}
}
type fakeBuild struct {
context *tar.Reader
options types.ImageBuildOptions
-8
View File
@@ -15,7 +15,6 @@ type fakeRegistryClient struct {
getManifestListFunc func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error)
mountBlobFunc func(ctx context.Context, source reference.Canonical, target reference.Named) error
putManifestFunc func(ctx context.Context, source reference.Named, mf distribution.Manifest) (digest.Digest, error)
getTagsFunc func(ctx context.Context, ref reference.Named) ([]string, error)
}
func (c *fakeRegistryClient) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
@@ -46,11 +45,4 @@ func (c *fakeRegistryClient) PutManifest(ctx context.Context, ref reference.Name
return digest.Digest(""), nil
}
func (c *fakeRegistryClient) GetTags(ctx context.Context, ref reference.Named) ([]string, error) {
if c.getTagsFunc != nil {
return c.getTagsFunc(ctx, ref)
}
return nil, nil
}
var _ client.RegistryClient = &fakeRegistryClient{}
+7 -4
View File
@@ -10,9 +10,14 @@ import (
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/spf13/cobra"
"vbom.ml/util/sortorder"
)
type byNetworkName []types.NetworkResource
func (r byNetworkName) Len() int { return len(r) }
func (r byNetworkName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r byNetworkName) Less(i, j int) bool { return r[i].Name < r[j].Name }
type listOptions struct {
quiet bool
noTrunc bool
@@ -59,9 +64,7 @@ func runList(dockerCli command.Cli, options listOptions) error {
}
}
sort.Slice(networkResources, func(i, j int) bool {
return sortorder.NaturalLess(networkResources[i].Name, networkResources[j].Name)
})
sort.Sort(byNetworkName(networkResources))
networksCtx := formatter.Context{
Output: dockerCli.Out(),
+18 -49
View File
@@ -3,6 +3,7 @@ package network
import (
"context"
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
@@ -40,55 +41,23 @@ func TestNetworkListErrors(t *testing.T) {
}
}
func TestNetworkList(t *testing.T) {
testCases := []struct {
doc string
networkListFunc func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error)
flags map[string]string
golden string
}{
{
doc: "network list with flags",
flags: map[string]string{
"filter": "image.name=ubuntu",
},
golden: "network-list.golden",
networkListFunc: func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
expectedOpts := types.NetworkListOptions{
Filters: filters.NewArgs(filters.Arg("image.name", "ubuntu")),
}
assert.Check(t, is.DeepEqual(expectedOpts, options, cmp.AllowUnexported(filters.Args{})))
return []types.NetworkResource{*NetworkResource(NetworkResourceID("123454321"),
NetworkResourceName("network_1"),
NetworkResourceDriver("09.7.01"),
NetworkResourceScope("global"))}, nil
},
},
{
doc: "network list sort order",
flags: map[string]string{
"format": "{{ .Name }}",
},
golden: "network-list-sort.golden",
networkListFunc: func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
return []types.NetworkResource{
*NetworkResource(NetworkResourceName("network-2-foo")),
*NetworkResource(NetworkResourceName("network-1-foo")),
*NetworkResource(NetworkResourceName("network-10-foo"))}, nil
},
},
func TestNetworkListWithFlags(t *testing.T) {
expectedOpts := types.NetworkListOptions{
Filters: filters.NewArgs(filters.Arg("image.name", "ubuntu")),
}
for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{networkListFunc: tc.networkListFunc})
cmd := newListCommand(cli)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
}
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), tc.golden)
})
}
cli := test.NewFakeCli(&fakeClient{
networkListFunc: func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
assert.Check(t, is.DeepEqual(expectedOpts, options, cmp.AllowUnexported(filters.Args{})))
return []types.NetworkResource{*NetworkResource(NetworkResourceID("123454321"),
NetworkResourceName("network_1"),
NetworkResourceDriver("09.7.01"),
NetworkResourceScope("global"))}, nil
},
})
cmd := newListCommand(cli)
cmd.Flags().Set("filter", "image.name=ubuntu")
assert.NilError(t, cmd.Execute())
golden.Assert(t, strings.TrimSpace(cli.OutBuffer().String()), "network-list.golden")
}
-3
View File
@@ -1,3 +0,0 @@
network-1-foo
network-2-foo
network-10-foo
+1 -1
View File
@@ -1,2 +1,2 @@
NETWORK ID NAME DRIVER SCOPE
123454321 network_1 09.7.01 global
123454321 network_1 09.7.01 global
+10 -3
View File
@@ -9,10 +9,19 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/spf13/cobra"
"vbom.ml/util/sortorder"
)
type byHostname []swarm.Node
func (n byHostname) Len() int { return len(n) }
func (n byHostname) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
func (n byHostname) Less(i, j int) bool {
return sortorder.NaturalLess(n[i].Description.Hostname, n[j].Description.Hostname)
}
type listOptions struct {
quiet bool
format string
@@ -71,8 +80,6 @@ func runList(dockerCli command.Cli, options listOptions) error {
Output: dockerCli.Out(),
Format: formatter.NewNodeFormat(format, options.quiet),
}
sort.Slice(nodes, func(i, j int) bool {
return sortorder.NaturalLess(nodes[i].Description.Hostname, nodes[j].Description.Hostname)
})
sort.Sort(byHostname(nodes))
return formatter.NodeWrite(nodesCtx, nodes, info)
}
-18
View File
@@ -5,7 +5,6 @@ import (
"io"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
)
@@ -15,8 +14,6 @@ type fakeClient struct {
pluginDisableFunc func(name string, disableOptions types.PluginDisableOptions) error
pluginEnableFunc func(name string, options types.PluginEnableOptions) error
pluginRemoveFunc func(name string, options types.PluginRemoveOptions) error
pluginInstallFunc func(name string, options types.PluginInstallOptions) (io.ReadCloser, error)
pluginListFunc func(filter filters.Args) (types.PluginsListResponse, error)
}
func (c *fakeClient) PluginCreate(ctx context.Context, createContext io.Reader, createOptions types.PluginCreateOptions) error {
@@ -46,18 +43,3 @@ func (c *fakeClient) PluginRemove(context context.Context, name string, removeOp
}
return nil
}
func (c *fakeClient) PluginInstall(context context.Context, name string, installOptions types.PluginInstallOptions) (io.ReadCloser, error) {
if c.pluginInstallFunc != nil {
return c.pluginInstallFunc(name, installOptions)
}
return nil, nil
}
func (c *fakeClient) PluginList(context context.Context, filter filters.Args) (types.PluginsListResponse, error) {
if c.pluginListFunc != nil {
return c.pluginListFunc(filter)
}
return types.PluginsListResponse{}, nil
}
+1 -1
View File
@@ -151,7 +151,7 @@ func runInstall(dockerCli command.Cli, opts pluginOptions) error {
responseBody, err := dockerCli.Client().PluginInstall(ctx, localName, options)
if err != nil {
if strings.Contains(err.Error(), "(image) when fetching") {
return errors.New(err.Error() + " - Use \"docker image pull\"")
return errors.New(err.Error() + " - Use `docker image pull`")
}
return err
}
-141
View File
@@ -1,141 +0,0 @@
package plugin
import (
"fmt"
"io"
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary"
"github.com/docker/docker/api/types"
"gotest.tools/assert"
)
func TestInstallErrors(t *testing.T) {
testCases := []struct {
description string
args []string
expectedError string
installFunc func(name string, options types.PluginInstallOptions) (io.ReadCloser, error)
}{
{
description: "insufficient number of arguments",
args: []string{},
expectedError: "requires at least 1 argument",
},
{
description: "invalid alias",
args: []string{"foo", "--alias", "UPPERCASE_ALIAS"},
expectedError: "invalid",
},
{
description: "invalid plugin name",
args: []string{"UPPERCASE_REPONAME"},
expectedError: "invalid",
},
{
description: "installation error",
args: []string{"foo"},
expectedError: "Error installing plugin",
installFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
return nil, fmt.Errorf("Error installing plugin")
},
},
{
description: "installation error due to missing image",
args: []string{"foo"},
expectedError: "docker image pull",
installFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
return nil, fmt.Errorf("(image) when fetching")
},
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc})
cmd := newInstallCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestInstallContentTrustErrors(t *testing.T) {
testCases := []struct {
description string
args []string
expectedError string
notaryFunc test.NotaryClientFuncType
}{
{
description: "install plugin, offline notary server",
args: []string{"plugin:tag"},
expectedError: "client is offline",
notaryFunc: notary.GetOfflineNotaryRepository,
},
{
description: "install plugin, uninitialized notary server",
args: []string{"plugin:tag"},
expectedError: "remote trust data does not exist",
notaryFunc: notary.GetUninitializedNotaryRepository,
},
{
description: "install plugin, empty notary server",
args: []string{"plugin:tag"},
expectedError: "No valid trust data for tag",
notaryFunc: notary.GetEmptyTargetsNotaryRepository,
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
pluginInstallFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
return nil, fmt.Errorf("should not try to install plugin")
},
}, test.EnableContentTrust)
cli.SetNotaryClient(tc.notaryFunc)
cmd := newInstallCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestInstall(t *testing.T) {
testCases := []struct {
description string
args []string
expectedOutput string
installFunc func(name string, options types.PluginInstallOptions) (io.ReadCloser, error)
}{
{
description: "install with no additional flags",
args: []string{"foo"},
expectedOutput: "Installed plugin foo\n",
installFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader("")), nil
},
},
{
description: "install with disable flag",
args: []string{"--disable", "foo"},
expectedOutput: "Installed plugin foo\n",
installFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
assert.Check(t, options.Disabled)
return ioutil.NopCloser(strings.NewReader("")), nil
},
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc})
cmd := newInstallCommand(cli)
cmd.SetArgs(tc.args)
assert.NilError(t, cmd.Execute())
assert.Check(t, strings.Contains(cli.OutBuffer().String(), tc.expectedOutput))
}
}
-6
View File
@@ -2,14 +2,12 @@ package plugin
import (
"context"
"sort"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/opts"
"github.com/spf13/cobra"
"vbom.ml/util/sortorder"
)
type listOptions struct {
@@ -48,10 +46,6 @@ func runList(dockerCli command.Cli, options listOptions) error {
return err
}
sort.Slice(plugins, func(i, j int) bool {
return sortorder.NaturalLess(plugins[i].Name, plugins[j].Name)
})
format := options.format
if len(format) == 0 {
if len(dockerCli.ConfigFile().PluginsFormat) > 0 && !options.quiet {
-174
View File
@@ -1,174 +0,0 @@
package plugin
import (
"fmt"
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/golden"
)
func TestListErrors(t *testing.T) {
testCases := []struct {
description string
args []string
flags map[string]string
expectedError string
listFunc func(filter filters.Args) (types.PluginsListResponse, error)
}{
{
description: "too many arguments",
args: []string{"foo"},
expectedError: "accepts no arguments",
},
{
description: "error listing plugins",
args: []string{},
expectedError: "error listing plugins",
listFunc: func(filter filters.Args) (types.PluginsListResponse, error) {
return types.PluginsListResponse{}, fmt.Errorf("error listing plugins")
},
},
{
description: "invalid format",
args: []string{},
flags: map[string]string{
"format": "{{invalid format}}",
},
expectedError: "Template parsing error",
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc})
cmd := newListCommand(cli)
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
}
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestList(t *testing.T) {
singlePluginListFunc := func(filter filters.Args) (types.PluginsListResponse, error) {
return types.PluginsListResponse{
{
ID: "id-foo",
Name: "name-foo",
Enabled: true,
Config: types.PluginConfig{
Description: "desc-bar",
},
},
}, nil
}
testCases := []struct {
description string
args []string
flags map[string]string
golden string
listFunc func(filter filters.Args) (types.PluginsListResponse, error)
}{
{
description: "list with no additional flags",
args: []string{},
golden: "plugin-list-without-format.golden",
listFunc: singlePluginListFunc,
},
{
description: "list with filters",
args: []string{},
flags: map[string]string{
"filter": "foo=bar",
},
golden: "plugin-list-without-format.golden",
listFunc: func(filter filters.Args) (types.PluginsListResponse, error) {
assert.Check(t, is.Equal("bar", filter.Get("foo")[0]))
return singlePluginListFunc(filter)
},
},
{
description: "list with quiet option",
args: []string{},
flags: map[string]string{
"quiet": "true",
},
golden: "plugin-list-with-quiet-option.golden",
listFunc: singlePluginListFunc,
},
{
description: "list with no-trunc option",
args: []string{},
flags: map[string]string{
"no-trunc": "true",
"format": "{{ .ID }}",
},
golden: "plugin-list-with-no-trunc-option.golden",
listFunc: func(filter filters.Args) (types.PluginsListResponse, error) {
return types.PluginsListResponse{
{
ID: "xyg4z2hiSLO5yTnBJfg4OYia9gKA6Qjd",
Name: "name-foo",
Enabled: true,
Config: types.PluginConfig{
Description: "desc-bar",
},
},
}, nil
},
},
{
description: "list with format",
args: []string{},
flags: map[string]string{
"format": "{{ .Name }}",
},
golden: "plugin-list-with-format.golden",
listFunc: singlePluginListFunc,
},
{
description: "list output is sorted based on plugin name",
args: []string{},
flags: map[string]string{
"format": "{{ .Name }}",
},
golden: "plugin-list-sort.golden",
listFunc: func(filter filters.Args) (types.PluginsListResponse, error) {
return types.PluginsListResponse{
{
ID: "id-1",
Name: "plugin-1-foo",
},
{
ID: "id-2",
Name: "plugin-10-foo",
},
{
ID: "id-3",
Name: "plugin-2-foo",
},
}, nil
},
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc})
cmd := newListCommand(cli)
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
}
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), tc.golden)
}
}
-3
View File
@@ -1,3 +0,0 @@
plugin-1-foo
plugin-2-foo
plugin-10-foo
@@ -1 +0,0 @@
name-foo
@@ -1 +0,0 @@
xyg4z2hiSLO5yTnBJfg4OYia9gKA6Qjd
@@ -1 +0,0 @@
id-foo
@@ -1,2 +0,0 @@
ID NAME DESCRIPTION ENABLED
id-foo name-foo desc-bar true
+2 -4
View File
@@ -11,7 +11,6 @@ import (
"runtime"
"strings"
"github.com/docker/cli/cli/debug"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
@@ -27,10 +26,9 @@ func ElectAuthServer(ctx context.Context, cli Cli) string {
// example a Linux client might be interacting with a Windows daemon, hence
// the default registry URL might be Windows specific.
serverAddress := registry.IndexServer
if info, err := cli.Client().Info(ctx); err != nil && debug.IsEnabled() {
// Only report the warning if we're in debug mode to prevent nagging during engine initialization workflows
if info, err := cli.Client().Info(ctx); err != nil {
fmt.Fprintf(cli.Err(), "Warning: failed to get default registry endpoint from daemon (%v). Using system default: %s\n", err, serverAddress)
} else if info.IndexServerAddress == "" && debug.IsEnabled() {
} else if info.IndexServerAddress == "" {
fmt.Fprintf(cli.Err(), "Warning: Empty registry endpoint from daemon. Using system default: %s\n", serverAddress)
} else {
serverAddress = info.IndexServerAddress
-19
View File
@@ -125,11 +125,6 @@ func runLogin(dockerCli command.Cli, opts loginOptions) error { //nolint: gocycl
}
response, err = clnt.RegistryLogin(ctx, *authConfig)
if err != nil && client.IsErrConnectionFailed(err) {
// If the server isn't responding (yet) attempt to login purely client side
response, err = loginClientSide(ctx, *authConfig)
}
// If we (still) have an error, give up
if err != nil {
return err
}
@@ -172,17 +167,3 @@ func loginWithCredStoreCreds(ctx context.Context, dockerCli command.Cli, authCon
}
return response, err
}
func loginClientSide(ctx context.Context, auth types.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
svc, err := registry.NewService(registry.ServiceOptions{})
if err != nil {
return registrytypes.AuthenticateOKBody{}, err
}
status, token, err := svc.Auth(ctx, &auth, command.UserAgent())
return registrytypes.AuthenticateOKBody{
Status: status,
IdentityToken: token,
}, err
}
+1
View File
@@ -28,6 +28,7 @@ type fakeClient struct {
client.Client
}
// nolint: unparam
func (c fakeClient) RegistryLogin(ctx context.Context, auth types.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
if auth.Password == expiredPassword {
return registrytypes.AuthenticateOKBody{}, fmt.Errorf("Invalid Username or Password")
+11 -4
View File
@@ -9,6 +9,7 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/registry"
"github.com/spf13/cobra"
)
@@ -80,14 +81,13 @@ func runSearch(dockerCli command.Cli, options searchOptions) error {
clnt := dockerCli.Client()
results, err := clnt.ImageSearch(ctx, options.term, searchOptions)
unorderedResults, err := clnt.ImageSearch(ctx, options.term, searchOptions)
if err != nil {
return err
}
sort.Slice(results, func(i, j int) bool {
return results[j].StarCount < results[i].StarCount
})
results := searchResultsByStars(unorderedResults)
sort.Sort(results)
searchCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewSearchFormat(options.format),
@@ -95,3 +95,10 @@ func runSearch(dockerCli command.Cli, options searchOptions) error {
}
return formatter.SearchWrite(searchCtx, results, options.automated, int(options.stars))
}
// searchResultsByStars sorts search results in descending order by number of stars.
type searchResultsByStars []registrytypes.SearchResult
func (r searchResultsByStars) Len() int { return len(r) }
func (r searchResultsByStars) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r searchResultsByStars) Less(i, j int) bool { return r[j].StarCount < r[i].StarCount }
-3
View File
@@ -13,7 +13,6 @@ import (
// Prevents a circular import with "github.com/docker/cli/internal/test"
. "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/debug"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
@@ -79,8 +78,6 @@ func TestElectAuthServer(t *testing.T) {
},
},
}
// Enable debug to see warnings we're checking for
debug.Enable()
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{infoFunc: tc.infoFunc})
server := ElectAuthServer(context.Background(), cli)
+10 -3
View File
@@ -9,10 +9,19 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/spf13/cobra"
"vbom.ml/util/sortorder"
)
type bySecretName []swarm.Secret
func (r bySecretName) Len() int { return len(r) }
func (r bySecretName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r bySecretName) Less(i, j int) bool {
return sortorder.NaturalLess(r[i].Spec.Name, r[j].Spec.Name)
}
type listOptions struct {
quiet bool
format string
@@ -57,9 +66,7 @@ func runSecretList(dockerCli command.Cli, options listOptions) error {
}
}
sort.Slice(secrets, func(i, j int) bool {
return sortorder.NaturalLess(secrets[i].Spec.Name, secrets[j].Spec.Name)
})
sort.Sort(bySecretName(secrets))
secretCtx := formatter.Context{
Output: dockerCli.Out(),
+7 -3
View File
@@ -44,6 +44,12 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
type byName []swarm.Service
func (n byName) Len() int { return len(n) }
func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
func (n byName) Less(i, j int) bool { return sortorder.NaturalLess(n[i].Spec.Name, n[j].Spec.Name) }
func runList(dockerCli command.Cli, options listOptions) error {
ctx := context.Background()
client := dockerCli.Client()
@@ -54,9 +60,7 @@ func runList(dockerCli command.Cli, options listOptions) error {
return err
}
sort.Slice(services, func(i, j int) bool {
return sortorder.NaturalLess(services[i].Spec.Name, services[j].Spec.Name)
})
sort.Sort(byName(services))
info := map[string]formatter.ServiceListInfo{}
if len(services) > 0 && !options.quiet {
// only non-empty services and not quiet, should we call TaskList and NodeList api
+1 -3
View File
@@ -598,9 +598,7 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
}
networks[i].Target = nwID
}
sort.Slice(networks, func(i, j int) bool {
return networks[i].Target < networks[j].Target
})
sort.Sort(byNetworkTarget(networks))
resources, err := options.resources.ToResourceRequirements()
if err != nil {
+1 -1
View File
@@ -140,7 +140,7 @@ loop:
}
func updateNodeFilter(ctx context.Context, client client.APIClient, filter filters.Args) error {
if filter.Contains("node") {
if filter.Include("node") {
nodeFilters := filter.Get("node")
for _, nodeFilter := range nodeFilters {
nodeReference, err := node.Reference(ctx, client, nodeFilter)
+35 -17
View File
@@ -753,6 +753,20 @@ func removeItems(
return newSeq
}
type byMountSource []mounttypes.Mount
func (m byMountSource) Len() int { return len(m) }
func (m byMountSource) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (m byMountSource) Less(i, j int) bool {
a, b := m[i], m[j]
if a.Source == b.Source {
return a.Target < b.Target
}
return a.Source < b.Source
}
func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) error {
mountsByTarget := map[string]mounttypes.Mount{}
@@ -782,15 +796,7 @@ func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) error {
newMounts = append(newMounts, mount)
}
}
sort.Slice(newMounts, func(i, j int) bool {
a, b := newMounts[i], newMounts[j]
if a.Source == b.Source {
return a.Target < b.Target
}
return a.Source < b.Source
})
sort.Sort(byMountSource(newMounts))
*mounts = newMounts
return nil
}
@@ -880,6 +886,16 @@ func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error {
return nil
}
type byPortConfig []swarm.PortConfig
func (r byPortConfig) Len() int { return len(r) }
func (r byPortConfig) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r byPortConfig) Less(i, j int) bool {
// We convert PortConfig into `port/protocol`, e.g., `80/tcp`
// In updatePorts we already filter out with map so there is duplicate entries
return portConfigToString(&r[i]) < portConfigToString(&r[j])
}
func portConfigToString(portConfig *swarm.PortConfig) string {
protocol := portConfig.Protocol
mode := portConfig.PublishMode
@@ -928,11 +944,7 @@ portLoop:
}
// Sort the PortConfig to avoid unnecessary updates
sort.Slice(newPorts, func(i, j int) bool {
// We convert PortConfig into `port/protocol`, e.g., `80/tcp`
// In updatePorts we already filter out with map so there is duplicate entries
return portConfigToString(&newPorts[i]) < portConfigToString(&newPorts[j])
})
sort.Sort(byPortConfig(newPorts))
*portConfig = newPorts
return nil
}
@@ -1130,6 +1142,14 @@ func updateHealthcheck(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec)
return nil
}
type byNetworkTarget []swarm.NetworkAttachmentConfig
func (m byNetworkTarget) Len() int { return len(m) }
func (m byNetworkTarget) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (m byNetworkTarget) Less(i, j int) bool {
return m[i].Target < m[j].Target
}
func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
// spec.TaskTemplate.Networks takes precedence over the deprecated
// spec.Networks field. If spec.Network is in use, we'll migrate those
@@ -1178,9 +1198,7 @@ func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flag
}
}
sort.Slice(newNetworks, func(i, j int) bool {
return newNetworks[i].Target < newNetworks[j].Target
})
sort.Sort(byNetworkTarget(newNetworks))
spec.TaskTemplate.Networks = newNetworks
return nil
+4 -13
View File
@@ -8,7 +8,6 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -28,11 +27,7 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command {
Short: "Manage Docker stacks",
Args: cli.NoArgs,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
configFile := dockerCli.ConfigFile()
if configFile == nil {
configFile = cliconfig.LoadDefaultConfigFile(dockerCli.Err())
}
orchestrator, err := getOrchestrator(configFile, cmd, dockerCli.Err())
orchestrator, err := getOrchestrator(dockerCli.ConfigFile(), cmd, dockerCli.Err())
if err != nil {
return err
}
@@ -47,13 +42,9 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command {
},
}
defaultHelpFunc := cmd.HelpFunc()
cmd.SetHelpFunc(func(c *cobra.Command, args []string) {
if err := cmd.PersistentPreRunE(c, args); err != nil {
fmt.Fprintln(dockerCli.Err(), err)
return
}
hideOrchestrationFlags(c, opts.orchestrator)
defaultHelpFunc(c, args)
cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
hideOrchestrationFlags(cmd, opts.orchestrator)
defaultHelpFunc(cmd, args)
})
cmd.AddCommand(
newDeployCommand(dockerCli, &opts),
+2 -2
View File
@@ -58,7 +58,7 @@ func newDeployCommand(dockerCli command.Cli, common *commonOptions) *cobra.Comma
flags.StringVar(&opts.Bundlefile, "bundle-file", "", "Path to a Distributed Application Bundle file")
flags.SetAnnotation("bundle-file", "experimental", nil)
flags.SetAnnotation("bundle-file", "swarm", nil)
flags.StringSliceVarP(&opts.Composefiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`)
flags.StringSliceVarP(&opts.Composefiles, "compose-file", "c", []string{}, "Path to a Compose file")
flags.SetAnnotation("compose-file", "version", []string{"1.25"})
flags.BoolVar(&opts.SendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
flags.SetAnnotation("with-registry-auth", "swarm", nil)
@@ -81,7 +81,7 @@ func RunDeploy(dockerCli command.Cli, flags *pflag.FlagSet, config *composetypes
case commonOrchestrator.HasKubernetes():
kli, err := kubernetes.WrapCli(dockerCli, kubernetes.NewOptions(flags, commonOrchestrator))
if err != nil {
return errors.Wrap(err, "unable to deploy to Kubernetes")
return err
}
return kubernetes.RunDeploy(kli, opts, config)
default:
+1 -6
View File
@@ -4,12 +4,10 @@ import (
"fmt"
"net"
"net/url"
"os"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/kubernetes"
cliv1beta1 "github.com/docker/cli/kubernetes/client/clientset/typed/compose/v1beta1"
"github.com/pkg/errors"
flag "github.com/spf13/pflag"
kubeclient "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
@@ -60,10 +58,7 @@ func WrapCli(dockerCli command.Cli, opts Options) (*KubeCli, error) {
cli.kubeNamespace = opts.Namespace
if opts.Namespace == "" {
configNamespace, _, err := clientConfig.Namespace()
switch {
case os.IsNotExist(err), os.IsPermission(err):
return nil, errors.Wrap(err, "unable to load configuration file")
case err != nil:
if err != nil {
return nil, err
}
cli.kubeNamespace = configNamespace
+18 -77
View File
@@ -8,72 +8,13 @@ import (
"strings"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/schema"
composeTypes "github.com/docker/cli/cli/compose/types"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/cli/kubernetes/compose/v1beta1"
"github.com/docker/cli/kubernetes/compose/v1beta2"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// NewStackConverter returns a converter from types.Config (compose) to the specified
// stack version or error out if the version is not supported or existent.
func NewStackConverter(version string) (StackConverter, error) {
switch version {
case "v1beta1":
return stackV1Beta1Converter{}, nil
case "v1beta2":
return stackV1Beta2Converter{}, nil
default:
return nil, errors.Errorf("stack version %s unsupported", version)
}
}
// StackConverter converts a compose types.Config to a Stack
type StackConverter interface {
FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error)
}
type stackV1Beta1Converter struct{}
func (s stackV1Beta1Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
cfg.Version = v1beta1.MaxComposeVersion
st, err := fromCompose(stderr, name, cfg)
if err != nil {
return Stack{}, err
}
res, err := yaml.Marshal(cfg)
if err != nil {
return Stack{}, err
}
// reload the result to check that it produced a valid 3.5 compose file
resparsedConfig, err := loader.ParseYAML(res)
if err != nil {
return Stack{}, err
}
if err = schema.Validate(resparsedConfig, v1beta1.MaxComposeVersion); err != nil {
return Stack{}, errors.Wrapf(err, "the compose yaml file is invalid with v%s", v1beta1.MaxComposeVersion)
}
st.ComposeFile = string(res)
return st, nil
}
type stackV1Beta2Converter struct{}
func (s stackV1Beta2Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
return fromCompose(stderr, name, cfg)
}
func fromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
return Stack{
Name: name,
Spec: fromComposeConfig(stderr, cfg),
}, nil
}
func loadStackData(composefile string) (*composetypes.Config, error) {
parsed, err := loader.ParseYAML([]byte(composefile))
if err != nil {
@@ -89,44 +30,44 @@ func loadStackData(composefile string) (*composetypes.Config, error) {
}
// Conversions from internal stack to different stack compose component versions.
func stackFromV1beta1(in *v1beta1.Stack) (Stack, error) {
func stackFromV1beta1(in *v1beta1.Stack) (stack, error) {
cfg, err := loadStackData(in.Spec.ComposeFile)
if err != nil {
return Stack{}, err
return stack{}, err
}
return Stack{
Name: in.ObjectMeta.Name,
Namespace: in.ObjectMeta.Namespace,
ComposeFile: in.Spec.ComposeFile,
Spec: fromComposeConfig(ioutil.Discard, cfg),
return stack{
name: in.ObjectMeta.Name,
namespace: in.ObjectMeta.Namespace,
composeFile: in.Spec.ComposeFile,
spec: fromComposeConfig(ioutil.Discard, cfg),
}, nil
}
func stackToV1beta1(s Stack) *v1beta1.Stack {
func stackToV1beta1(s stack) *v1beta1.Stack {
return &v1beta1.Stack{
ObjectMeta: metav1.ObjectMeta{
Name: s.Name,
Name: s.name,
},
Spec: v1beta1.StackSpec{
ComposeFile: s.ComposeFile,
ComposeFile: s.composeFile,
},
}
}
func stackFromV1beta2(in *v1beta2.Stack) Stack {
return Stack{
Name: in.ObjectMeta.Name,
Namespace: in.ObjectMeta.Namespace,
Spec: in.Spec,
func stackFromV1beta2(in *v1beta2.Stack) stack {
return stack{
name: in.ObjectMeta.Name,
namespace: in.ObjectMeta.Namespace,
spec: in.Spec,
}
}
func stackToV1beta2(s Stack) *v1beta2.Stack {
func stackToV1beta2(s stack) *v1beta2.Stack {
return &v1beta2.Stack{
ObjectMeta: metav1.ObjectMeta{
Name: s.Name,
Name: s.name,
},
Spec: s.Spec,
Spec: s.spec,
}
}
@@ -1,18 +0,0 @@
package kubernetes
import (
"testing"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestNewStackConverter(t *testing.T) {
_, err := NewStackConverter("v1alpha1")
assert.Check(t, is.ErrorContains(err, "stack version v1alpha1 unsupported"))
_, err = NewStackConverter("v1beta1")
assert.NilError(t, err)
_, err = NewStackConverter("v1beta2")
assert.NilError(t, err)
}
+2 -2
View File
@@ -70,13 +70,13 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy, cfg *composetypes.Config
}
}()
err = watcher.Watch(stack.Name, stack.getServices(), statusUpdates)
err = watcher.Watch(stack.name, stack.getServices(), statusUpdates)
close(statusUpdates)
<-displayDone
if err != nil {
return err
}
fmt.Fprintf(cmdOut, "\nStack %s is stable and running\n\n", stack.Name)
fmt.Fprintf(cmdOut, "\nStack %s is stable and running\n\n", stack.name)
return nil
}
+2 -2
View File
@@ -48,10 +48,10 @@ func getStacks(kubeCli *KubeCli, opts options.List) ([]*formatter.Stack, error)
var formattedStacks []*formatter.Stack
for _, stack := range stacks {
formattedStacks = append(formattedStacks, &formatter.Stack{
Name: stack.Name,
Name: stack.name,
Services: len(stack.getServices()),
Orchestrator: "Kubernetes",
Namespace: stack.Namespace,
Namespace: stack.namespace,
})
}
return formattedStacks, nil
+15 -15
View File
@@ -12,18 +12,18 @@ import (
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
// Stack is the main type used by stack commands so they remain independent from kubernetes compose component version.
type Stack struct {
Name string
Namespace string
ComposeFile string
Spec *v1beta2.StackSpec
// stack is the main type used by stack commands so they remain independent from kubernetes compose component version.
type stack struct {
name string
namespace string
composeFile string
spec *v1beta2.StackSpec
}
// getServices returns all the stack service names, sorted lexicographically
func (s *Stack) getServices() []string {
services := make([]string, len(s.Spec.Services))
for i, service := range s.Spec.Services {
func (s *stack) getServices() []string {
services := make([]string, len(s.spec.Services))
for i, service := range s.spec.Services {
services[i] = service.Name
}
sort.Strings(services)
@@ -31,8 +31,8 @@ func (s *Stack) getServices() []string {
}
// createFileBasedConfigMaps creates a Kubernetes ConfigMap for each Compose global file-based config.
func (s *Stack) createFileBasedConfigMaps(configMaps corev1.ConfigMapInterface) error {
for name, config := range s.Spec.Configs {
func (s *stack) createFileBasedConfigMaps(configMaps corev1.ConfigMapInterface) error {
for name, config := range s.spec.Configs {
if config.File == "" {
continue
}
@@ -43,7 +43,7 @@ func (s *Stack) createFileBasedConfigMaps(configMaps corev1.ConfigMapInterface)
return err
}
if _, err := configMaps.Create(toConfigMap(s.Name, name, fileName, content)); err != nil {
if _, err := configMaps.Create(toConfigMap(s.name, name, fileName, content)); err != nil {
return err
}
}
@@ -71,8 +71,8 @@ func toConfigMap(stackName, name, key string, content []byte) *apiv1.ConfigMap {
}
// createFileBasedSecrets creates a Kubernetes Secret for each Compose global file-based secret.
func (s *Stack) createFileBasedSecrets(secrets corev1.SecretInterface) error {
for name, secret := range s.Spec.Secrets {
func (s *stack) createFileBasedSecrets(secrets corev1.SecretInterface) error {
for name, secret := range s.spec.Secrets {
if secret.File == "" {
continue
}
@@ -83,7 +83,7 @@ func (s *Stack) createFileBasedSecrets(secrets corev1.SecretInterface) error {
return err
}
if _, err := secrets.Create(toSecret(s.Name, name, fileName, content)); err != nil {
if _, err := secrets.Create(toSecret(s.name, name, fileName, content)); err != nil {
return err
}
}
+63 -24
View File
@@ -2,10 +2,17 @@ package kubernetes
import (
"fmt"
"io"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/schema"
composetypes "github.com/docker/cli/cli/compose/types"
composev1beta1 "github.com/docker/cli/kubernetes/client/clientset/typed/compose/v1beta1"
composev1beta2 "github.com/docker/cli/kubernetes/client/clientset/typed/compose/v1beta2"
v1beta1types "github.com/docker/cli/kubernetes/compose/v1beta1"
"github.com/docker/cli/kubernetes/labels"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"
@@ -13,17 +20,16 @@ import (
// StackClient talks to a kubernetes compose component.
type StackClient interface {
StackConverter
CreateOrUpdate(s Stack) error
CreateOrUpdate(s stack) error
Delete(name string) error
Get(name string) (Stack, error)
List(opts metav1.ListOptions) ([]Stack, error)
IsColliding(servicesClient corev1.ServiceInterface, s Stack) error
Get(name string) (stack, error)
List(opts metav1.ListOptions) ([]stack, error)
IsColliding(servicesClient corev1.ServiceInterface, s stack) error
FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error)
}
// stackV1Beta1 implements stackClient interface and talks to compose component v1beta1.
type stackV1Beta1 struct {
stackV1Beta1Converter
stacks composev1beta1.StackInterface
}
@@ -35,10 +41,10 @@ func newStackV1Beta1(config *rest.Config, namespace string) (*stackV1Beta1, erro
return &stackV1Beta1{stacks: client.Stacks(namespace)}, nil
}
func (s *stackV1Beta1) CreateOrUpdate(internalStack Stack) error {
func (s *stackV1Beta1) CreateOrUpdate(internalStack stack) error {
// If it already exists, update the stack
if stackBeta1, err := s.stacks.Get(internalStack.Name, metav1.GetOptions{}); err == nil {
stackBeta1.Spec.ComposeFile = internalStack.ComposeFile
if stackBeta1, err := s.stacks.Get(internalStack.name, metav1.GetOptions{}); err == nil {
stackBeta1.Spec.ComposeFile = internalStack.composeFile
_, err := s.stacks.Update(stackBeta1)
return err
}
@@ -51,20 +57,20 @@ func (s *stackV1Beta1) Delete(name string) error {
return s.stacks.Delete(name, &metav1.DeleteOptions{})
}
func (s *stackV1Beta1) Get(name string) (Stack, error) {
func (s *stackV1Beta1) Get(name string) (stack, error) {
stackBeta1, err := s.stacks.Get(name, metav1.GetOptions{})
if err != nil {
return Stack{}, err
return stack{}, err
}
return stackFromV1beta1(stackBeta1)
}
func (s *stackV1Beta1) List(opts metav1.ListOptions) ([]Stack, error) {
func (s *stackV1Beta1) List(opts metav1.ListOptions) ([]stack, error) {
list, err := s.stacks.List(opts)
if err != nil {
return nil, err
}
stacks := make([]Stack, len(list.Items))
stacks := make([]stack, len(list.Items))
for i := range list.Items {
stack, err := stackFromV1beta1(&list.Items[i])
if err != nil {
@@ -76,9 +82,9 @@ func (s *stackV1Beta1) List(opts metav1.ListOptions) ([]Stack, error) {
}
// IsColliding verifies that services defined in the stack collides with already deployed services
func (s *stackV1Beta1) IsColliding(servicesClient corev1.ServiceInterface, st Stack) error {
func (s *stackV1Beta1) IsColliding(servicesClient corev1.ServiceInterface, st stack) error {
for _, srv := range st.getServices() {
if err := verify(servicesClient, st.Name, srv); err != nil {
if err := verify(servicesClient, st.name, srv); err != nil {
return err
}
}
@@ -102,9 +108,31 @@ func verify(services corev1.ServiceInterface, stackName string, service string)
return nil
}
func (s *stackV1Beta1) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) {
cfg.Version = v1beta1types.MaxComposeVersion
st, err := fromCompose(stderr, name, cfg)
if err != nil {
return stack{}, err
}
res, err := yaml.Marshal(cfg)
if err != nil {
return stack{}, err
}
// reload the result to check that it produced a valid 3.5 compose file
resparsedConfig, err := loader.ParseYAML(res)
if err != nil {
return stack{}, err
}
if err = schema.Validate(resparsedConfig, v1beta1types.MaxComposeVersion); err != nil {
return stack{}, errors.Wrapf(err, "the compose yaml file is invalid with v%s", v1beta1types.MaxComposeVersion)
}
st.composeFile = string(res)
return st, nil
}
// stackV1Beta2 implements stackClient interface and talks to compose component v1beta2.
type stackV1Beta2 struct {
stackV1Beta2Converter
stacks composev1beta2.StackInterface
}
@@ -116,10 +144,10 @@ func newStackV1Beta2(config *rest.Config, namespace string) (*stackV1Beta2, erro
return &stackV1Beta2{stacks: client.Stacks(namespace)}, nil
}
func (s *stackV1Beta2) CreateOrUpdate(internalStack Stack) error {
func (s *stackV1Beta2) CreateOrUpdate(internalStack stack) error {
// If it already exists, update the stack
if stackBeta2, err := s.stacks.Get(internalStack.Name, metav1.GetOptions{}); err == nil {
stackBeta2.Spec = internalStack.Spec
if stackBeta2, err := s.stacks.Get(internalStack.name, metav1.GetOptions{}); err == nil {
stackBeta2.Spec = internalStack.spec
_, err := s.stacks.Update(stackBeta2)
return err
}
@@ -132,20 +160,20 @@ func (s *stackV1Beta2) Delete(name string) error {
return s.stacks.Delete(name, &metav1.DeleteOptions{})
}
func (s *stackV1Beta2) Get(name string) (Stack, error) {
func (s *stackV1Beta2) Get(name string) (stack, error) {
stackBeta2, err := s.stacks.Get(name, metav1.GetOptions{})
if err != nil {
return Stack{}, err
return stack{}, err
}
return stackFromV1beta2(stackBeta2), nil
}
func (s *stackV1Beta2) List(opts metav1.ListOptions) ([]Stack, error) {
func (s *stackV1Beta2) List(opts metav1.ListOptions) ([]stack, error) {
list, err := s.stacks.List(opts)
if err != nil {
return nil, err
}
stacks := make([]Stack, len(list.Items))
stacks := make([]stack, len(list.Items))
for i := range list.Items {
stacks[i] = stackFromV1beta2(&list.Items[i])
}
@@ -153,6 +181,17 @@ func (s *stackV1Beta2) List(opts metav1.ListOptions) ([]Stack, error) {
}
// IsColliding is handle server side with the compose api v1beta2, so nothing to do here
func (s *stackV1Beta2) IsColliding(servicesClient corev1.ServiceInterface, st Stack) error {
func (s *stackV1Beta2) IsColliding(servicesClient corev1.ServiceInterface, st stack) error {
return nil
}
func (s *stackV1Beta2) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) {
return fromCompose(stderr, name, cfg)
}
func fromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) {
return stack{
name: name,
spec: fromComposeConfig(stderr, cfg),
}, nil
}
@@ -25,14 +25,18 @@ func TestFromCompose(t *testing.T) {
},
})
assert.NilError(t, err)
assert.Equal(t, "foo", s.Name)
assert.Equal(t, "foo", s.name)
assert.Equal(t, string(`version: "3.5"
services:
bar:
image: bar
foo:
image: foo
`), s.ComposeFile)
networks: {}
volumes: {}
secrets: {}
configs: {}
`), s.composeFile)
}
func TestFromComposeUnsupportedVersion(t *testing.T) {
+116 -129
View File
@@ -51,134 +51,121 @@ func TestStackPsErrors(t *testing.T) {
}
}
func TestStackPs(t *testing.T) {
testCases := []struct {
doc string
taskListFunc func(types.TaskListOptions) ([]swarm.Task, error)
nodeInspectWithRaw func(string) (swarm.Node, []byte, error)
config configfile.ConfigFile
args []string
flags map[string]string
expectedErr string
golden string
}{
{
doc: "WithEmptyName",
args: []string{"' '"},
expectedErr: `invalid stack name: "' '"`,
},
{
doc: "WithEmptyStack",
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{}, nil
},
args: []string{"foo"},
expectedErr: "nothing found in stack: foo",
},
{
doc: "WithQuietOption",
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(TaskID("id-foo"))}, nil
},
args: []string{"foo"},
flags: map[string]string{
"quiet": "true",
},
golden: "stack-ps-with-quiet-option.golden",
},
{
doc: "WithNoTruncOption",
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(TaskID("xn4cypcov06f2w8gsbaf2lst3"))}, nil
},
args: []string{"foo"},
flags: map[string]string{
"no-trunc": "true",
"format": "{{ .ID }}",
},
golden: "stack-ps-with-no-trunc-option.golden",
},
{
doc: "WithNoResolveOption",
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(
TaskNodeID("id-node-foo"),
)}, nil
},
nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) {
return *Node(NodeName("node-name-bar")), nil, nil
},
args: []string{"foo"},
flags: map[string]string{
"no-resolve": "true",
"format": "{{ .Node }}",
},
golden: "stack-ps-with-no-resolve-option.golden",
},
{
doc: "WithFormat",
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(TaskServiceID("service-id-foo"))}, nil
},
args: []string{"foo"},
flags: map[string]string{
"format": "{{ .Name }}",
},
golden: "stack-ps-with-format.golden",
},
{
doc: "WithConfigFormat",
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(TaskServiceID("service-id-foo"))}, nil
},
config: configfile.ConfigFile{
TasksFormat: "{{ .Name }}",
},
args: []string{"foo"},
golden: "stack-ps-with-config-format.golden",
},
{
doc: "WithoutFormat",
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(
TaskID("id-foo"),
TaskServiceID("service-id-foo"),
TaskNodeID("id-node"),
WithTaskSpec(TaskImage("myimage:mytag")),
TaskDesiredState(swarm.TaskStateReady),
WithStatus(TaskState(swarm.TaskStateFailed), Timestamp(time.Now().Add(-2*time.Hour))),
)}, nil
},
nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) {
return *Node(NodeName("node-name-bar")), nil, nil
},
args: []string{"foo"},
golden: "stack-ps-without-format.golden",
},
}
func TestRunPSWithEmptyName(t *testing.T) {
cmd := newPsCommand(test.NewFakeCli(&fakeClient{}), &orchestrator)
cmd.SetArgs([]string{"' '"})
cmd.SetOutput(ioutil.Discard)
for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
taskListFunc: tc.taskListFunc,
nodeInspectWithRaw: tc.nodeInspectWithRaw,
})
cli.SetConfigFile(&tc.config)
cmd := newPsCommand(cli, &orchestrator)
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
}
cmd.SetOutput(ioutil.Discard)
if tc.expectedErr != "" {
assert.Error(t, cmd.Execute(), tc.expectedErr)
assert.Check(t, is.Equal("", cli.OutBuffer().String()))
return
}
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), tc.golden)
})
}
assert.ErrorContains(t, cmd.Execute(), `invalid stack name: "' '"`)
}
func TestStackPsEmptyStack(t *testing.T) {
fakeCli := test.NewFakeCli(&fakeClient{
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{}, nil
},
})
cmd := newPsCommand(fakeCli, &orchestrator)
cmd.SetArgs([]string{"foo"})
cmd.SetOutput(ioutil.Discard)
assert.Error(t, cmd.Execute(), "nothing found in stack: foo")
assert.Check(t, is.Equal("", fakeCli.OutBuffer().String()))
}
func TestStackPsWithQuietOption(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(TaskID("id-foo"))}, nil
},
})
cmd := newPsCommand(cli, &orchestrator)
cmd.SetArgs([]string{"foo"})
cmd.Flags().Set("quiet", "true")
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "stack-ps-with-quiet-option.golden")
}
func TestStackPsWithNoTruncOption(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(TaskID("xn4cypcov06f2w8gsbaf2lst3"))}, nil
},
})
cmd := newPsCommand(cli, &orchestrator)
cmd.SetArgs([]string{"foo"})
cmd.Flags().Set("no-trunc", "true")
cmd.Flags().Set("format", "{{ .ID }}")
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "stack-ps-with-no-trunc-option.golden")
}
func TestStackPsWithNoResolveOption(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(
TaskNodeID("id-node-foo"),
)}, nil
},
nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) {
return *Node(NodeName("node-name-bar")), nil, nil
},
})
cmd := newPsCommand(cli, &orchestrator)
cmd.SetArgs([]string{"foo"})
cmd.Flags().Set("no-resolve", "true")
cmd.Flags().Set("format", "{{ .Node }}")
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "stack-ps-with-no-resolve-option.golden")
}
func TestStackPsWithFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(TaskServiceID("service-id-foo"))}, nil
},
})
cmd := newPsCommand(cli, &orchestrator)
cmd.SetArgs([]string{"foo"})
cmd.Flags().Set("format", "{{ .Name }}")
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "stack-ps-with-format.golden")
}
func TestStackPsWithConfigFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(TaskServiceID("service-id-foo"))}, nil
},
})
cli.SetConfigFile(&configfile.ConfigFile{
TasksFormat: "{{ .Name }}",
})
cmd := newPsCommand(cli, &orchestrator)
cmd.SetArgs([]string{"foo"})
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "stack-ps-with-config-format.golden")
}
func TestStackPsWithoutFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(
TaskID("id-foo"),
TaskServiceID("service-id-foo"),
TaskNodeID("id-node"),
WithTaskSpec(TaskImage("myimage:mytag")),
TaskDesiredState(swarm.TaskStateReady),
WithStatus(TaskState(swarm.TaskStateFailed), Timestamp(time.Now().Add(-2*time.Hour))),
)}, nil
},
nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) {
return *Node(NodeName("node-name-bar")), nil, nil
},
})
cmd := newPsCommand(cli, &orchestrator)
cmd.SetArgs([]string{"foo"})
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "stack-ps-without-format.golden")
}
+4 -19
View File
@@ -3,7 +3,6 @@ package swarm
import (
"context"
"fmt"
"net"
"strings"
"github.com/docker/cli/cli"
@@ -18,12 +17,10 @@ type initOptions struct {
swarmOptions
listenAddr NodeAddrOption
// Not a NodeAddrOption because it has no default port.
advertiseAddr string
dataPathAddr string
forceNewCluster bool
availability string
defaultAddrPools []net.IPNet
DefaultAddrPoolMaskLength uint32
advertiseAddr string
dataPathAddr string
forceNewCluster bool
availability string
}
func newInitCommand(dockerCli command.Cli) *cobra.Command {
@@ -44,36 +41,24 @@ func newInitCommand(dockerCli command.Cli) *cobra.Command {
flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: <ip|interface>[:port])")
flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: <ip|interface>[:port])")
flags.StringVar(&opts.dataPathAddr, flagDataPathAddr, "", "Address or interface to use for data path traffic (format: <ip|interface>)")
flags.SetAnnotation(flagDataPathAddr, "version", []string{"1.31"})
flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state")
flags.BoolVar(&opts.autolock, flagAutolock, false, "Enable manager autolocking (requiring an unlock key to start a stopped manager)")
flags.StringVar(&opts.availability, flagAvailability, "active", `Availability of the node ("active"|"pause"|"drain")`)
flags.IPNetSliceVar(&opts.defaultAddrPools, flagDefaultAddrPool, []net.IPNet{}, "default address pool in CIDR format")
flags.SetAnnotation(flagDefaultAddrPool, "version", []string{"1.39"})
flags.Uint32Var(&opts.DefaultAddrPoolMaskLength, flagDefaultAddrPoolMaskLength, 24, "default address pool subnet mask length")
flags.SetAnnotation(flagDefaultAddrPoolMaskLength, "version", []string{"1.39"})
addSwarmFlags(flags, &opts.swarmOptions)
return cmd
}
func runInit(dockerCli command.Cli, flags *pflag.FlagSet, opts initOptions) error {
var defaultAddrPool []string
client := dockerCli.Client()
ctx := context.Background()
for _, p := range opts.defaultAddrPools {
defaultAddrPool = append(defaultAddrPool, p.String())
}
req := swarm.InitRequest{
ListenAddr: opts.listenAddr.String(),
AdvertiseAddr: opts.advertiseAddr,
DataPathAddr: opts.dataPathAddr,
DefaultAddrPool: defaultAddrPool,
ForceNewCluster: opts.forceNewCluster,
Spec: opts.swarmOptions.ToSpec(flags),
AutoLockManagers: opts.swarmOptions.autolock,
SubnetSize: opts.DefaultAddrPoolMaskLength,
}
if flags.Changed(flagAvailability) {
availability := swarm.NodeAvailability(strings.ToLower(opts.availability))
-1
View File
@@ -42,7 +42,6 @@ func newJoinCommand(dockerCli command.Cli) *cobra.Command {
flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: <ip|interface>[:port])")
flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: <ip|interface>[:port])")
flags.StringVar(&opts.dataPathAddr, flagDataPathAddr, "", "Address or interface to use for data path traffic (format: <ip|interface>)")
flags.SetAnnotation(flagDataPathAddr, "version", []string{"1.31"})
flags.StringVar(&opts.token, flagToken, "", "Token for entry into the swarm")
flags.StringVar(&opts.availability, flagAvailability, "active", `Availability of the node ("active"|"pause"|"drain")`)
return cmd
+16 -18
View File
@@ -17,24 +17,22 @@ import (
const (
defaultListenAddr = "0.0.0.0:2377"
flagCertExpiry = "cert-expiry"
flagDispatcherHeartbeat = "dispatcher-heartbeat"
flagListenAddr = "listen-addr"
flagAdvertiseAddr = "advertise-addr"
flagDataPathAddr = "data-path-addr"
flagDefaultAddrPool = "default-addr-pool"
flagDefaultAddrPoolMaskLength = "default-addr-pool-mask-length"
flagQuiet = "quiet"
flagRotate = "rotate"
flagToken = "token"
flagTaskHistoryLimit = "task-history-limit"
flagExternalCA = "external-ca"
flagMaxSnapshots = "max-snapshots"
flagSnapshotInterval = "snapshot-interval"
flagAutolock = "autolock"
flagAvailability = "availability"
flagCACert = "ca-cert"
flagCAKey = "ca-key"
flagCertExpiry = "cert-expiry"
flagDispatcherHeartbeat = "dispatcher-heartbeat"
flagListenAddr = "listen-addr"
flagAdvertiseAddr = "advertise-addr"
flagDataPathAddr = "data-path-addr"
flagQuiet = "quiet"
flagRotate = "rotate"
flagToken = "token"
flagTaskHistoryLimit = "task-history-limit"
flagExternalCA = "external-ca"
flagMaxSnapshots = "max-snapshots"
flagSnapshotInterval = "snapshot-interval"
flagAutolock = "autolock"
flagAvailability = "availability"
flagCACert = "ca-cert"
flagCAKey = "ca-key"
)
type swarmOptions struct {
-1
View File
@@ -19,7 +19,6 @@ func NewSystemCommand(dockerCli command.Cli) *cobra.Command {
NewInfoCommand(dockerCli),
newDiskUsageCommand(dockerCli),
newPruneCommand(dockerCli),
newDialStdioCommand(dockerCli),
)
return cmd
-107
View File
@@ -1,107 +0,0 @@
package system
import (
"context"
"io"
"os"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// newDialStdioCommand creates a new cobra.Command for `docker system dial-stdio`
func newDialStdioCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "dial-stdio",
Short: "Proxy the stdio stream to the daemon connection. Should not be invoked manually.",
Args: cli.NoArgs,
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runDialStdio(dockerCli)
},
}
return cmd
}
func runDialStdio(dockerCli command.Cli) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
dialer := dockerCli.Client().Dialer()
conn, err := dialer(ctx)
if err != nil {
return errors.Wrap(err, "failed to open the raw stream connection")
}
connHalfCloser, ok := conn.(halfCloser)
if !ok {
return errors.New("the raw stream connection does not implement halfCloser")
}
stdin2conn := make(chan error)
conn2stdout := make(chan error)
go func() {
stdin2conn <- copier(connHalfCloser, &halfReadCloserWrapper{os.Stdin}, "stdin to stream")
}()
go func() {
conn2stdout <- copier(&halfWriteCloserWrapper{os.Stdout}, connHalfCloser, "stream to stdout")
}()
select {
case err = <-stdin2conn:
if err != nil {
return err
}
// wait for stdout
err = <-conn2stdout
case err = <-conn2stdout:
// return immediately without waiting for stdin to be closed.
// (stdin is never closed when tty)
}
return err
}
func copier(to halfWriteCloser, from halfReadCloser, debugDescription string) error {
defer func() {
if err := from.CloseRead(); err != nil {
logrus.Errorf("error while CloseRead (%s): %v", debugDescription, err)
}
if err := to.CloseWrite(); err != nil {
logrus.Errorf("error while CloseWrite (%s): %v", debugDescription, err)
}
}()
if _, err := io.Copy(to, from); err != nil {
return errors.Wrapf(err, "error while Copy (%s)", debugDescription)
}
return nil
}
type halfReadCloser interface {
io.Reader
CloseRead() error
}
type halfWriteCloser interface {
io.Writer
CloseWrite() error
}
type halfCloser interface {
halfReadCloser
halfWriteCloser
}
type halfReadCloserWrapper struct {
io.ReadCloser
}
func (x *halfReadCloserWrapper) CloseRead() error {
return x.Close()
}
type halfWriteCloserWrapper struct {
io.WriteCloser
}
func (x *halfWriteCloserWrapper) CloseWrite() error {
return x.Close()
}
+40 -74
View File
@@ -206,11 +206,48 @@ func prettyPrintInfo(dockerCli command.Cli, info types.Info) error {
fmt.Fprintln(dockerCli.Out(), "Live Restore Enabled:", info.LiveRestoreEnabled)
fmt.Fprint(dockerCli.Out(), "\n")
printWarnings(dockerCli, info)
// Only output these warnings if the server does not support these features
if info.OSType != "windows" {
printStorageDriverWarnings(dockerCli, info)
if !info.MemoryLimit {
fmt.Fprintln(dockerCli.Err(), "WARNING: No memory limit support")
}
if !info.SwapLimit {
fmt.Fprintln(dockerCli.Err(), "WARNING: No swap limit support")
}
if !info.KernelMemory {
fmt.Fprintln(dockerCli.Err(), "WARNING: No kernel memory limit support")
}
if !info.OomKillDisable {
fmt.Fprintln(dockerCli.Err(), "WARNING: No oom kill disable support")
}
if !info.CPUCfsQuota {
fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs quota support")
}
if !info.CPUCfsPeriod {
fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs period support")
}
if !info.CPUShares {
fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu shares support")
}
if !info.CPUSet {
fmt.Fprintln(dockerCli.Err(), "WARNING: No cpuset support")
}
if !info.IPv4Forwarding {
fmt.Fprintln(dockerCli.Err(), "WARNING: IPv4 forwarding is disabled")
}
if !info.BridgeNfIptables {
fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-iptables is disabled")
}
if !info.BridgeNfIP6tables {
fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-ip6tables is disabled")
}
}
return nil
}
// nolint: gocyclo
func printSwarmInfo(dockerCli command.Cli, info types.Info) {
if info.Swarm.LocalNodeState == swarm.LocalNodeStateInactive || info.Swarm.LocalNodeState == swarm.LocalNodeStateLocked {
return
@@ -224,16 +261,7 @@ func printSwarmInfo(dockerCli command.Cli, info types.Info) {
fmt.Fprintln(dockerCli.Out(), " ClusterID:", info.Swarm.Cluster.ID)
fmt.Fprintln(dockerCli.Out(), " Managers:", info.Swarm.Managers)
fmt.Fprintln(dockerCli.Out(), " Nodes:", info.Swarm.Nodes)
var strAddrPool strings.Builder
if info.Swarm.Cluster.DefaultAddrPool != nil {
for _, p := range info.Swarm.Cluster.DefaultAddrPool {
strAddrPool.WriteString(p + " ")
}
fmt.Fprintln(dockerCli.Out(), " Default Address Pool:", strAddrPool.String())
fmt.Fprintln(dockerCli.Out(), " SubnetSize:", info.Swarm.Cluster.SubnetSize)
}
fmt.Fprintln(dockerCli.Out(), " Orchestration:")
taskHistoryRetentionLimit := int64(0)
if info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit != nil {
taskHistoryRetentionLimit = *info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit
@@ -277,73 +305,11 @@ func printSwarmInfo(dockerCli command.Cli, info types.Info) {
}
}
func printWarnings(dockerCli command.Cli, info types.Info) {
if len(info.Warnings) > 0 {
fmt.Fprintln(dockerCli.Err(), strings.Join(info.Warnings, "\n"))
return
}
// daemon didn't return warnings. Fallback to old behavior
printStorageDriverWarnings(dockerCli, info)
printWarningsLegacy(dockerCli, info)
}
// printWarningsLegacy generates warnings based on information returned by the daemon.
// DEPRECATED: warnings are now generated by the daemon, and returned in
// info.Warnings. This function is used to provide backward compatibility with
// daemons that do not provide these warnings. No new warnings should be added
// here.
func printWarningsLegacy(dockerCli command.Cli, info types.Info) {
if info.OSType == "windows" {
return
}
if !info.MemoryLimit {
fmt.Fprintln(dockerCli.Err(), "WARNING: No memory limit support")
}
if !info.SwapLimit {
fmt.Fprintln(dockerCli.Err(), "WARNING: No swap limit support")
}
if !info.KernelMemory {
fmt.Fprintln(dockerCli.Err(), "WARNING: No kernel memory limit support")
}
if !info.OomKillDisable {
fmt.Fprintln(dockerCli.Err(), "WARNING: No oom kill disable support")
}
if !info.CPUCfsQuota {
fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs quota support")
}
if !info.CPUCfsPeriod {
fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs period support")
}
if !info.CPUShares {
fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu shares support")
}
if !info.CPUSet {
fmt.Fprintln(dockerCli.Err(), "WARNING: No cpuset support")
}
if !info.IPv4Forwarding {
fmt.Fprintln(dockerCli.Err(), "WARNING: IPv4 forwarding is disabled")
}
if !info.BridgeNfIptables {
fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-iptables is disabled")
}
if !info.BridgeNfIP6tables {
fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-ip6tables is disabled")
}
}
// printStorageDriverWarnings generates warnings based on storage-driver information
// returned by the daemon.
// DEPRECATED: warnings are now generated by the daemon, and returned in
// info.Warnings. This function is used to provide backward compatibility with
// daemons that do not provide these warnings. No new warnings should be added
// here.
func printStorageDriverWarnings(dockerCli command.Cli, info types.Info) {
if info.OSType == "windows" {
return
}
if info.DriverStatus == nil {
return
}
for _, pair := range info.DriverStatus {
if pair[0] == "Data loop file" {
fmt.Fprintf(dockerCli.Err(), "WARNING: %s: usage of loopback devices is "+
+8 -35
View File
@@ -207,59 +207,32 @@ func TestPrettyPrintInfo(t *testing.T) {
infoWithWarningsLinux.BridgeNfIptables = false
infoWithWarningsLinux.BridgeNfIP6tables = false
sampleInfoDaemonWarnings := sampleInfoNoSwarm
sampleInfoDaemonWarnings.Warnings = []string{
"WARNING: No memory limit support",
"WARNING: No swap limit support",
"WARNING: No kernel memory limit support",
"WARNING: No oom kill disable support",
"WARNING: No cpu cfs quota support",
"WARNING: No cpu cfs period support",
"WARNING: No cpu shares support",
"WARNING: No cpuset support",
"WARNING: IPv4 forwarding is disabled",
"WARNING: bridge-nf-call-iptables is disabled",
"WARNING: bridge-nf-call-ip6tables is disabled",
}
for _, tc := range []struct {
doc string
dockerInfo types.Info
expectedGolden string
warningsGolden string
}{
{
doc: "info without swarm",
dockerInfo: sampleInfoNoSwarm,
expectedGolden: "docker-info-no-swarm",
},
{
doc: "info with swarm",
dockerInfo: infoWithSwarm,
expectedGolden: "docker-info-with-swarm",
},
{
doc: "info with legacy warnings",
dockerInfo: infoWithWarningsLinux,
expectedGolden: "docker-info-no-swarm",
warningsGolden: "docker-info-warnings",
},
{
doc: "info with daemon warnings",
dockerInfo: sampleInfoDaemonWarnings,
expectedGolden: "docker-info-no-swarm",
warningsGolden: "docker-info-warnings",
},
} {
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
assert.NilError(t, prettyPrintInfo(cli, tc.dockerInfo))
golden.Assert(t, cli.OutBuffer().String(), tc.expectedGolden+".golden")
if tc.warningsGolden != "" {
golden.Assert(t, cli.ErrBuffer().String(), tc.warningsGolden+".golden")
} else {
assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
}
})
cli := test.NewFakeCli(&fakeClient{})
assert.NilError(t, prettyPrintInfo(cli, tc.dockerInfo))
golden.Assert(t, cli.OutBuffer().String(), tc.expectedGolden+".golden")
if tc.warningsGolden != "" {
golden.Assert(t, cli.ErrBuffer().String(), tc.warningsGolden+".golden")
} else {
assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
}
}
}
+1 -1
View File
@@ -68,7 +68,7 @@ func runBuildCachePrune(dockerCli command.Cli, _ opts.FilterOpt) (uint64, string
func runPrune(dockerCli command.Cli, options pruneOptions) error {
// TODO version this once "until" filter is supported for volumes
if options.pruneVolumes && options.filter.Value().Contains("until") {
if options.pruneVolumes && options.filter.Value().Include("until") {
return fmt.Errorf(`ERROR: The "until" filter is not supported with "--volumes"`)
}
if versions.LessThan(dockerCli.Client().ClientVersion(), "1.31") {
+7 -43
View File
@@ -1,44 +1,8 @@
Client:
Version: 18.99.5-ce
API version: 1.38
Go version: go1.10.2
Git commit: deadbeef
Built: Wed May 30 22:21:05 2018
OS/Arch: linux/amd64
Experimental: true
Server: Docker Enterprise Edition (EE) 2.0
Engine:
Version: 17.06.2-ee-15
API version: 1.30 (minimum version 1.12)
Go version: go1.8.7
Git commit: 64ddfa6
Built: Mon Jul 9 23:38:38 2018
OS/Arch: linux/amd64
Experimental: false
Universal Control Plane:
Version: 17.06.2-ee-15
ApiVersion: 1.30
Arch: amd64
BuildTime: Mon Jul 2 21:24:07 UTC 2018
GitCommit: 4513922
GoVersion: go1.9.4
MinApiVersion: 1.20
Os: linux
Version: 3.0.3-tp2
Kubernetes:
Version: 1.8+
buildDate: 2018-04-26T16:51:21Z
compiler: gc
gitCommit: 8d637aedf46b9c21dde723e29c645b9f27106fa5
gitTreeState: clean
gitVersion: v1.8.11-docker-8d637ae
goVersion: go1.8.3
major: 1
minor: 8+
platform: linux/amd64
Calico:
Version: v3.0.8
cni: v2.0.6
kube-controllers: v2.0.5
node: v3.0.8
Version: 18.99.5-ce
API version: 1.38
Go version: go1.10.2
Git commit: deadbeef
Built: Wed May 30 22:21:05 2018
OS/Arch: linux/amd64
Experimental: true
+1 -1
View File
@@ -204,7 +204,7 @@ func runVersion(dockerCli command.Cli, opts *versionOptions) error {
}
func prettyPrintVersion(dockerCli command.Cli, vd versionInfo, tmpl *template.Template) error {
t := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 1, ' ', 0)
t := tabwriter.NewWriter(dockerCli.Out(), 15, 1, 1, ' ', 0)
err := tmpl.Execute(t, vd)
t.Write([]byte("\n"))
t.Flush()
-59
View File
@@ -43,67 +43,8 @@ func TestVersionAlign(t *testing.T) {
BuildTime: "Wed May 30 22:21:05 2018",
Experimental: true,
},
Server: &types.Version{},
}
vi.Server.Platform.Name = "Docker Enterprise Edition (EE) 2.0"
vi.Server.Components = append(vi.Server.Components, types.ComponentVersion{
Name: "Engine",
Version: "17.06.2-ee-15",
Details: map[string]string{
"ApiVersion": "1.30",
"MinAPIVersion": "1.12",
"GitCommit": "64ddfa6",
"GoVersion": "go1.8.7",
"Os": "linux",
"Arch": "amd64",
"BuildTime": "Mon Jul 9 23:38:38 2018",
"Experimental": "false",
},
})
vi.Server.Components = append(vi.Server.Components, types.ComponentVersion{
Name: "Universal Control Plane",
Version: "17.06.2-ee-15",
Details: map[string]string{
"Version": "3.0.3-tp2",
"ApiVersion": "1.30",
"Arch": "amd64",
"BuildTime": "Mon Jul 2 21:24:07 UTC 2018",
"GitCommit": "4513922",
"GoVersion": "go1.9.4",
"MinApiVersion": "1.20",
"Os": "linux",
},
})
vi.Server.Components = append(vi.Server.Components, types.ComponentVersion{
Name: "Kubernetes",
Version: "1.8+",
Details: map[string]string{
"buildDate": "2018-04-26T16:51:21Z",
"compiler": "gc",
"gitCommit": "8d637aedf46b9c21dde723e29c645b9f27106fa5",
"gitTreeState": "clean",
"gitVersion": "v1.8.11-docker-8d637ae",
"goVersion": "go1.8.3",
"major": "1",
"minor": "8+",
"platform": "linux/amd64",
},
})
vi.Server.Components = append(vi.Server.Components, types.ComponentVersion{
Name: "Calico",
Version: "v3.0.8",
Details: map[string]string{
"cni": "v2.0.6",
"kube-controllers": "v2.0.5",
"node": "v3.0.8",
},
})
cli := test.NewFakeCli(&fakeClient{})
tmpl, err := newVersionTemplate("")
assert.NilError(t, err)
+27 -16
View File
@@ -14,7 +14,6 @@ import (
"github.com/theupdateframework/notary"
"github.com/theupdateframework/notary/client"
"github.com/theupdateframework/notary/tuf/data"
"vbom.ml/util/sortorder"
)
// trustTagKey represents a unique signed tag and hex-encoded hash pair
@@ -29,12 +28,26 @@ type trustTagRow struct {
Signers []string
}
type trustTagRowList []trustTagRow
func (tagComparator trustTagRowList) Len() int {
return len(tagComparator)
}
func (tagComparator trustTagRowList) Less(i, j int) bool {
return tagComparator[i].SignedTag < tagComparator[j].SignedTag
}
func (tagComparator trustTagRowList) Swap(i, j int) {
tagComparator[i], tagComparator[j] = tagComparator[j], tagComparator[i]
}
// trustRepo represents consumable information about a trusted repository
type trustRepo struct {
Name string
SignedTags []trustTagRow
Signers []trustSigner
AdministrativeKeys []trustSigner
Name string
SignedTags trustTagRowList
Signers []trustSigner
AdminstrativeKeys []trustSigner
}
// trustSigner represents a trusted signer in a trusted repository
@@ -51,20 +64,20 @@ type trustKey struct {
// 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(cli command.Cli, remote string) ([]trustTagRow, []client.RoleWithSignatures, []data.Role, error) {
func lookupTrustInfo(cli command.Cli, remote string) (trustTagRowList, []client.RoleWithSignatures, []data.Role, error) {
ctx := context.Background()
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, nil, image.AuthResolver(cli), remote)
if err != nil {
return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, err
return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, err
}
tag := imgRefAndAuth.Tag()
notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPullOnly)
if err != nil {
return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
}
if err = clearChangeList(notaryRepo); err != nil {
return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, err
return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, err
}
defer clearChangeList(notaryRepo)
@@ -74,7 +87,7 @@ func lookupTrustInfo(cli command.Cli, remote string) ([]trustTagRow, []client.Ro
logrus.Debug(trust.NotaryError(remote, err))
// print an empty table if we don't have signed targets, but have an initialized notary repo
if _, ok := err.(client.ErrNoSuchTarget); !ok {
return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("No signatures or cannot access %s", remote)
return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("No signatures or cannot access %s", remote)
}
}
signatureRows := matchReleasedSignatures(allSignedTargets)
@@ -82,7 +95,7 @@ func lookupTrustInfo(cli command.Cli, remote string) ([]trustTagRow, []client.Ro
// get the administrative roles
adminRolesWithSigs, err := notaryRepo.ListRoles()
if err != nil {
return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("No signers for %s", remote)
return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("No signers for %s", remote)
}
// get delegation roles with the canonical key IDs
@@ -125,8 +138,8 @@ func getDelegationRoleToKeyMap(rawDelegationRoles []data.Role) map[string][]stri
// aggregate all signers for a "released" hash+tagname pair. To be "released," the tag must have been
// signed into the "targets" or "targets/releases" role. Output is sorted by tag name
func matchReleasedSignatures(allTargets []client.TargetSignedStruct) []trustTagRow {
signatureRows := []trustTagRow{}
func matchReleasedSignatures(allTargets []client.TargetSignedStruct) trustTagRowList {
signatureRows := trustTagRowList{}
// do a first pass to get filter on tags signed into "targets" or "targets/releases"
releasedTargetRows := map[trustTagKey][]string{}
for _, tgt := range allTargets {
@@ -149,8 +162,6 @@ func matchReleasedSignatures(allTargets []client.TargetSignedStruct) []trustTagR
for targetKey, signers := range releasedTargetRows {
signatureRows = append(signatureRows, trustTagRow{targetKey, signers})
}
sort.Slice(signatureRows, func(i, j int) bool {
return sortorder.NaturalLess(signatureRows[i].SignedTag, signatureRows[j].SignedTag)
})
sort.Sort(signatureRows)
return signatureRows
}
-33
View File
@@ -1,33 +0,0 @@
package trust
import (
"testing"
"github.com/docker/cli/cli/trust"
"github.com/theupdateframework/notary/client"
"github.com/theupdateframework/notary/tuf/data"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestMatchReleasedSignaturesSortOrder(t *testing.T) {
var releasesRole = data.DelegationRole{BaseRole: data.BaseRole{Name: trust.ReleasesRole}}
targets := []client.TargetSignedStruct{
{Target: client.Target{Name: "target10-foo"}, Role: releasesRole},
{Target: client.Target{Name: "target1-foo"}, Role: releasesRole},
{Target: client.Target{Name: "target2-foo"}, Role: releasesRole},
}
rows := matchReleasedSignatures(targets)
var targetNames []string
for _, r := range rows {
targetNames = append(targetNames, r.SignedTag)
}
expected := []string{
"target1-foo",
"target2-foo",
"target10-foo",
}
assert.Check(t, is.DeepEqual(expected, targetNames))
}
+4 -4
View File
@@ -107,9 +107,9 @@ func getRepoTrustInfo(cli command.Cli, remote string) ([]byte, error) {
sort.Slice(adminList, func(i, j int) bool { return adminList[i].Name > adminList[j].Name })
return json.Marshal(trustRepo{
Name: remote,
SignedTags: signatureRows,
Signers: signerList,
AdministrativeKeys: adminList,
Name: remote,
SignedTags: signatureRows,
Signers: signerList,
AdminstrativeKeys: adminList,
})
}
+3 -6
View File
@@ -8,7 +8,6 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/theupdateframework/notary/client"
"vbom.ml/util/sortorder"
)
func prettyPrintTrustInfo(cli command.Cli, remote string) error {
@@ -52,7 +51,7 @@ func printSortedAdminKeys(out io.Writer, adminRoles []client.RoleWithSignatures)
}
// pretty print with ordered rows
func printSignatures(out io.Writer, signatureRows []trustTagRow) error {
func printSignatures(out io.Writer, signatureRows trustTagRowList) error {
trustTagCtx := formatter.Context{
Output: out,
Format: formatter.NewTrustTagFormat(),
@@ -79,15 +78,13 @@ func printSignerInfo(out io.Writer, roleToKeyIDs map[string][]string) error {
Format: formatter.NewSignerInfoFormat(),
Trunc: true,
}
formattedSignerInfo := []formatter.SignerInfo{}
formattedSignerInfo := formatter.SignerInfoList{}
for name, keyIDs := range roleToKeyIDs {
formattedSignerInfo = append(formattedSignerInfo, formatter.SignerInfo{
Name: name,
Keys: keyIDs,
})
}
sort.Slice(formattedSignerInfo, func(i, j int) bool {
return sortorder.NaturalLess(formattedSignerInfo[i].Name, formattedSignerInfo[j].Name)
})
sort.Sort(formattedSignerInfo)
return formatter.SignerInfoWrite(signerInfoCtx, formattedSignerInfo)
}
-18
View File
@@ -1,7 +1,6 @@
package trust
import (
"bytes"
"encoding/hex"
"io/ioutil"
"testing"
@@ -441,20 +440,3 @@ func TestFormatAdminRole(t *testing.T) {
targetsRoleWithSigs := client.RoleWithSignatures{Role: targetsRole, Signatures: nil}
assert.Check(t, is.Equal("Repository Key:\tabc, key11, key99\n", formatAdminRole(targetsRoleWithSigs)))
}
func TestPrintSignerInfoSortOrder(t *testing.T) {
roleToKeyIDs := map[string][]string{
"signer2-foo": {"B"},
"signer10-foo": {"C"},
"signer1-foo": {"A"},
}
expected := `SIGNER KEYS
signer1-foo A
signer2-foo B
signer10-foo C
`
buf := new(bytes.Buffer)
assert.NilError(t, printSignerInfo(buf, roleToKeyIDs))
assert.Check(t, is.Equal(expected, buf.String()))
}
+83 -104
View File
@@ -4,10 +4,8 @@ import (
"io/ioutil"
"testing"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary"
"github.com/theupdateframework/notary/client"
"gotest.tools/assert"
"gotest.tools/golden"
)
@@ -43,110 +41,91 @@ func TestTrustInspectCommandErrors(t *testing.T) {
}
}
func TestTrustInspectCommandRepositoryErrors(t *testing.T) {
testCases := []struct {
doc string
args []string
notaryRepository func(trust.ImageRefAndAuth, []string) (client.Repository, error)
err string
golden string
}{
{
doc: "OfflineErrors",
args: []string{"nonexistent-reg-name.io/image"},
notaryRepository: notary.GetOfflineNotaryRepository,
err: "No signatures or cannot access nonexistent-reg-name.io/image",
},
{
doc: "OfflineErrorsWithImageTag",
args: []string{"nonexistent-reg-name.io/image:tag"},
notaryRepository: notary.GetOfflineNotaryRepository,
err: "No signatures or cannot access nonexistent-reg-name.io/image:tag",
},
{
doc: "UninitializedErrors",
args: []string{"reg/unsigned-img"},
notaryRepository: notary.GetUninitializedNotaryRepository,
err: "No signatures or cannot access reg/unsigned-img",
golden: "trust-inspect-uninitialized.golden",
},
{
doc: "UninitializedErrorsWithImageTag",
args: []string{"reg/unsigned-img:tag"},
notaryRepository: notary.GetUninitializedNotaryRepository,
err: "No signatures or cannot access reg/unsigned-img:tag",
golden: "trust-inspect-uninitialized.golden",
},
}
func TestTrustInspectCommandOfflineErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetOfflineNotaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"nonexistent-reg-name.io/image"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image")
for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(tc.notaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.err)
if tc.golden != "" {
golden.Assert(t, cli.OutBuffer().String(), tc.golden)
}
})
}
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetOfflineNotaryRepository)
cmd = newInspectCommand(cli)
cmd.SetArgs([]string{"nonexistent-reg-name.io/image:tag"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image")
}
func TestTrustInspectCommand(t *testing.T) {
testCases := []struct {
doc string
args []string
notaryRepository func(trust.ImageRefAndAuth, []string) (client.Repository, error)
golden string
}{
{
doc: "EmptyNotaryRepo",
args: []string{"reg/img:unsigned-tag"},
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
golden: "trust-inspect-empty-repo.golden",
},
{
doc: "FullRepoWithoutSigners",
args: []string{"signed-repo"},
notaryRepository: notary.GetLoadedWithNoSignersNotaryRepository,
golden: "trust-inspect-full-repo-no-signers.golden",
},
{
doc: "OneTagWithoutSigners",
args: []string{"signed-repo:green"},
notaryRepository: notary.GetLoadedWithNoSignersNotaryRepository,
golden: "trust-inspect-one-tag-no-signers.golden",
},
{
doc: "FullRepoWithSigners",
args: []string{"signed-repo"},
notaryRepository: notary.GetLoadedNotaryRepository,
golden: "trust-inspect-full-repo-with-signers.golden",
},
{
doc: "MultipleFullReposWithSigners",
args: []string{"signed-repo", "signed-repo"},
notaryRepository: notary.GetLoadedNotaryRepository,
golden: "trust-inspect-multiple-repos-with-signers.golden",
},
{
doc: "UnsignedTagInSignedRepo",
args: []string{"signed-repo:unsigned"},
notaryRepository: notary.GetLoadedNotaryRepository,
golden: "trust-inspect-unsigned-tag-with-signers.golden",
},
}
func TestTrustInspectCommandUninitializedErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetUninitializedNotaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"reg/unsigned-img"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img")
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-uninitialized.golden")
for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(tc.notaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs(tc.args)
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), tc.golden)
})
}
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetUninitializedNotaryRepository)
cmd = newInspectCommand(cli)
cmd.SetArgs([]string{"reg/unsigned-img:tag"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img:tag")
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-uninitialized.golden")
}
func TestTrustInspectCommandEmptyNotaryRepo(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetEmptyTargetsNotaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"reg/img:unsigned-tag"})
cmd.SetOutput(ioutil.Discard)
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-empty-repo.golden")
}
func TestTrustInspectCommandFullRepoWithoutSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetLoadedWithNoSignersNotaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"signed-repo"})
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-full-repo-no-signers.golden")
}
func TestTrustInspectCommandOneTagWithoutSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetLoadedWithNoSignersNotaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"signed-repo:green"})
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-one-tag-no-signers.golden")
}
func TestTrustInspectCommandFullRepoWithSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetLoadedNotaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"signed-repo"})
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-full-repo-with-signers.golden")
}
func TestTrustInspectCommandMultipleFullReposWithSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetLoadedNotaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"signed-repo", "signed-repo"})
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-multiple-repos-with-signers.golden")
}
func TestTrustInspectCommandUnsignedTagInSignedRepo(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetLoadedNotaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"signed-repo:unsigned"})
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-unsigned-tag-with-signers.golden")
}
+77 -85
View File
@@ -5,7 +5,6 @@ import (
"os"
"testing"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary"
"github.com/theupdateframework/notary/client"
@@ -55,92 +54,85 @@ func TestTrustRevokeCommandErrors(t *testing.T) {
}
}
func TestTrustRevokeCommand(t *testing.T) {
testCases := []struct {
doc string
notaryRepository func(trust.ImageRefAndAuth, []string) (client.Repository, error)
args []string
expectedErr string
expectedMessage string
}{
{
doc: "OfflineErrors_Confirm",
notaryRepository: notary.GetOfflineNotaryRepository,
args: []string{"reg-name.io/image"},
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.",
},
{
doc: "OfflineErrors_Offline",
notaryRepository: notary.GetOfflineNotaryRepository,
args: []string{"reg-name.io/image", "-y"},
expectedErr: "could not remove signature for reg-name.io/image: client is offline",
},
{
doc: "OfflineErrors_WithTag_Offline",
notaryRepository: notary.GetOfflineNotaryRepository,
args: []string{"reg-name.io/image:tag"},
expectedErr: "could not remove signature for reg-name.io/image:tag: client is offline",
},
{
doc: "UninitializedErrors_Confirm",
notaryRepository: notary.GetUninitializedNotaryRepository,
args: []string{"reg-name.io/image"},
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.",
},
{
doc: "UninitializedErrors_NoTrustData",
notaryRepository: notary.GetUninitializedNotaryRepository,
args: []string{"reg-name.io/image", "-y"},
expectedErr: "could not remove signature for reg-name.io/image: does not have trust data for",
},
{
doc: "UninitializedErrors_WithTag_NoTrustData",
notaryRepository: notary.GetUninitializedNotaryRepository,
args: []string{"reg-name.io/image:tag"},
expectedErr: "could not remove signature for reg-name.io/image:tag: does not have trust data for",
},
{
doc: "EmptyNotaryRepo_Confirm",
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
args: []string{"reg-name.io/image"},
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.",
},
{
doc: "EmptyNotaryRepo_NoSignedTags",
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
args: []string{"reg-name.io/image", "-y"},
expectedErr: "could not remove signature for reg-name.io/image: no signed tags to remove",
},
{
doc: "EmptyNotaryRepo_NoValidTrustData",
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
args: []string{"reg-name.io/image:tag"},
expectedErr: "could not remove signature for reg-name.io/image:tag: No valid trust data for tag",
},
{
doc: "AllSigConfirmation",
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
args: []string{"alpine"},
expectedMessage: "Please confirm you would like to delete all signature data for alpine? [y/N] \nAborting action.",
},
}
func TestTrustRevokeCommandOfflineErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetOfflineNotaryRepository)
cmd := newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image"})
cmd.SetOutput(ioutil.Discard)
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Contains(cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action."))
for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(tc.notaryRepository)
cmd := newRevokeCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
if tc.expectedErr != "" {
assert.ErrorContains(t, cmd.Execute(), tc.expectedErr)
return
}
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Contains(cli.OutBuffer().String(), tc.expectedMessage))
})
}
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetOfflineNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image", "-y"})
cmd.SetOutput(ioutil.Discard)
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetOfflineNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image:tag"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image:tag: client is offline")
}
func TestTrustRevokeCommandUninitializedErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetUninitializedNotaryRepository)
cmd := newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image"})
cmd.SetOutput(ioutil.Discard)
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Contains(cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action."))
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetUninitializedNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image", "-y"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image: does not have trust data for")
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetUninitializedNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image:tag"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image:tag: does not have trust data for")
}
func TestTrustRevokeCommandEmptyNotaryRepo(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetEmptyTargetsNotaryRepository)
cmd := newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image"})
cmd.SetOutput(ioutil.Discard)
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Contains(cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action."))
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetEmptyTargetsNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image", "-y"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image: no signed tags to remove")
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetEmptyTargetsNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image:tag"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image:tag: No valid trust data for tag")
}
func TestNewRevokeTrustAllSigConfirmation(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetEmptyTargetsNotaryRepository)
cmd := newRevokeCommand(cli)
cmd.SetArgs([]string{"alpine"})
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Contains(cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for alpine? [y/N] \nAborting action."))
}
func TestGetSignableRolesForTargetAndRemoveError(t *testing.T) {
-1
View File
@@ -78,7 +78,6 @@ func isLastSignerForReleases(roleWithSig data.Role, allRoles []client.RoleWithSi
// removeSingleSigner attempts to remove a single signer and returns whether signer removal happened.
// The signer not being removed doesn't necessarily raise an error e.g. user choosing "No" when prompted for confirmation.
// nolint: unparam
func removeSingleSigner(cli command.Cli, repoName, signerName string, forceYes bool) (bool, error) {
ctx := context.Background()
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, nil, image.AuthResolver(cli), repoName)
+1 -1
View File
@@ -3,7 +3,7 @@
"Name": "reg/img:unsigned-tag",
"SignedTags": [],
"Signers": [],
"AdministrativeKeys": [
"AdminstrativeKeys": [
{
"Name": "Root",
"Keys": [

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