Compare commits
301 Commits
v18.06.1-c
...
v18.09.2
| Author | SHA1 | Date | |
|---|---|---|---|
| af2647d55b | |||
| c71aa11c0a | |||
| 336b2a5cac | |||
| c462e06fcd | |||
| 719508a935 | |||
| 2fa3aae9ed | |||
| 6c3a10aaed | |||
| 3ee6755815 | |||
| 16349f6e33 | |||
| 2aa77af30f | |||
| 456c1ce695 | |||
| bcadc9061c | |||
| e05745b4a5 | |||
| b6ecef353f | |||
| e380ddaddf | |||
| 12834eeff6 | |||
| bb46da9fba | |||
| 871d24d3fc | |||
| 61a9096b8d | |||
| 2ac475cf97 | |||
| 2a36695037 | |||
| dc74fc81f2 | |||
| 7e90635652 | |||
| 3f7989903a | |||
| 7059d069c3 | |||
| 4a4a1f3615 | |||
| 1274f23252 | |||
| 3af1848dda | |||
| 6d91f5d55d | |||
| d56948c12c | |||
| 9b3eea87ee | |||
| 31c092e155 | |||
| 046ffa4e87 | |||
| 51668a30f2 | |||
| 5e7f9d3c84 | |||
| 72ddefbada | |||
| 135aa72476 | |||
| 7c7fe26a6f | |||
| 1df47ffb4d | |||
| 2e7e529a18 | |||
| f8f230181e | |||
| 0ee4693953 | |||
| cb4cd04c64 | |||
| d2e771fed6 | |||
| b8911a3b33 | |||
| ebe071a9b3 | |||
| ecb972ab38 | |||
| 4c68a9666f | |||
| e245b72381 | |||
| 0ff9e5cd10 | |||
| 8e565d0399 | |||
| 8a424333f9 | |||
| fde819236b | |||
| aa6314c663 | |||
| 81ee98e861 | |||
| 8ae4453d46 | |||
| aeea559129 | |||
| 22336b332c | |||
| 2961611fda | |||
| 17adf05188 | |||
| 39f1110308 | |||
| 3dfacb55a4 | |||
| e942084530 | |||
| 50f529fa47 | |||
| b4bee9be75 | |||
| 8b0d34a5a1 | |||
| f93908213a | |||
| 4280972d65 | |||
| 984bc7411e | |||
| 92932647d3 | |||
| dee37936e5 | |||
| 3e1a0bdc23 | |||
| f2b2061cc3 | |||
| 4925fd9c34 | |||
| 5d3ab5bc0c | |||
| c12e23a4c1 | |||
| aca3f2d382 | |||
| a7488d1bcd | |||
| 5a97a93ae1 | |||
| 41910b6d68 | |||
| 1a087e87c9 | |||
| 0b11120060 | |||
| e57b20642d | |||
| b8702b8a9a | |||
| a31b20d7db | |||
| 5ba5678898 | |||
| 9de1318e36 | |||
| 19e1ab273e | |||
| ec1812188f | |||
| 6004d74b1f | |||
| e79e591ee9 | |||
| 0f22d7e295 | |||
| f250152bf4 | |||
| f9d666b057 | |||
| 342afe44fb | |||
| cfec8027ed | |||
| 78c42cf031 | |||
| dd2f13bed4 | |||
| 3b991ec615 | |||
| 34ea8bb5a5 | |||
| afb17ec70b | |||
| 62aed95bc1 | |||
| 649e4916bb | |||
| 3597d75281 | |||
| 5673816fec | |||
| a8c69c8287 | |||
| fc3dc8f058 | |||
| 2a46a3d46c | |||
| b2cf18ac2e | |||
| 44371c7c34 | |||
| 4e6798794d | |||
| d8aefad94a | |||
| 3c37d6a034 | |||
| 9d43f1ed48 | |||
| a818677813 | |||
| c204959687 | |||
| 76c09259db | |||
| 0efb62cab1 | |||
| 8789e93d6e | |||
| 0ee05a6353 | |||
| 68be7cb376 | |||
| de805da04c | |||
| b75350de7a | |||
| f96ddaedf7 | |||
| 0fb6bb35a4 | |||
| 264ee43c2a | |||
| 7f4c842e8a | |||
| e25e9d68be | |||
| 6877dedeee | |||
| 08cf36daa6 | |||
| a500c394df | |||
| 60c75fda67 | |||
| ca782599fb | |||
| 3c27ce21c9 | |||
| 7da71329bc | |||
| e97d004395 | |||
| 587a94c935 | |||
| 2461cd618d | |||
| acf43b62b5 | |||
| ef09ca8987 | |||
| 466e1b0741 | |||
| 7a73d112ff | |||
| 0c444c521f | |||
| fd2f1b3b66 | |||
| 11a312118f | |||
| 3f7c6c8200 | |||
| c11acddfb5 | |||
| 5706f9518a | |||
| f472a1a480 | |||
| b3d8c5deda | |||
| 8ae74b38d5 | |||
| 50f918801f | |||
| cb142fa49f | |||
| b4057f0293 | |||
| f597f2d026 | |||
| c4c4825591 | |||
| 964173997d | |||
| e92614a175 | |||
| 4d4392ba04 | |||
| 340e4ee8e5 | |||
| 3a3e720f91 | |||
| 560b0cd863 | |||
| 984d76b9dd | |||
| 6ef11c516d | |||
| f8f0d72cf9 | |||
| 5cc1f9006a | |||
| 04bb3c770f | |||
| a921313caf | |||
| faeb8bb571 | |||
| 1d04f7d66b | |||
| 9cd7c1361c | |||
| b5768bea9b | |||
| e8cd06c8eb | |||
| ad28cec012 | |||
| e587ec293b | |||
| 7b4e2f3145 | |||
| b9b3754ad3 | |||
| e902ae9f84 | |||
| 021bf39d76 | |||
| 4f388ffca3 | |||
| ff1a34d9a9 | |||
| 731b4f1fb4 | |||
| 4c87725c35 | |||
| 08f8ee1320 | |||
| 8ef01e869e | |||
| ff953751d3 | |||
| 4fbb009d39 | |||
| 6f61cf053a | |||
| 261ff66d61 | |||
| b59823c784 | |||
| da544e8938 | |||
| afb87e42f2 | |||
| 2093fd6e52 | |||
| 9022a00fbe | |||
| 21cce52b30 | |||
| 08f5f52cdc | |||
| 24b7effa30 | |||
| 74071f2347 | |||
| 760ca04709 | |||
| 7f853fee87 | |||
| 265dec037b | |||
| 40650cfbd5 | |||
| 1f1507b0a4 | |||
| 70d5cb0dd0 | |||
| 0246bc1b3b | |||
| b4e50635a2 | |||
| 19653e7fad | |||
| 6cd0e2fe70 | |||
| fffec04374 | |||
| ed335aba8c | |||
| 164e812b7a | |||
| 91b1ad9d2b | |||
| edfd623594 | |||
| 55edeb497a | |||
| a3464c0a20 | |||
| b3b2ace735 | |||
| d13e2df65b | |||
| effe36a155 | |||
| 8788a4804f | |||
| da00d1c49f | |||
| c8f0e211b9 | |||
| 601131634e | |||
| 13db1bc95f | |||
| c922ea2f45 | |||
| 543d6fb8da | |||
| 7fba38acad | |||
| 2c7822b036 | |||
| 48fbb12b7c | |||
| bdd58a4096 | |||
| 4912846de2 | |||
| 9e71207327 | |||
| 7b82276c88 | |||
| b395d2d6f5 | |||
| ce3d069936 | |||
| 13eb2aa125 | |||
| d57cc1782e | |||
| e2a56c47da | |||
| 9847e96765 | |||
| f3811e865e | |||
| d97f378009 | |||
| 97d312e02a | |||
| ee8cdb3850 | |||
| 0f7ae34ea9 | |||
| 55ff66d967 | |||
| c8b9c21ef9 | |||
| b91953f507 | |||
| 71d650ee17 | |||
| 2b221d8f1c | |||
| ceed42217d | |||
| 2634562119 | |||
| 8160759013 | |||
| 249c8652a2 | |||
| 26151d910a | |||
| 29612ccefe | |||
| f285fe67e9 | |||
| 5f6d5c7328 | |||
| bded5beb78 | |||
| 721000e6c9 | |||
| 7b255e653a | |||
| c7e85c09d2 | |||
| 69e1743e3d | |||
| 4243440e1f | |||
| f5393c904a | |||
| b59c41b2a7 | |||
| 95a9b4d5fe | |||
| 847e0c22d4 | |||
| a2b4d30cd0 | |||
| d0ddf91539 | |||
| 18091ea7e2 | |||
| f05ab2b1fb | |||
| 981c099b96 | |||
| 1c69e83034 | |||
| 3a8ef767f8 | |||
| 9e36ff4491 | |||
| 057bf6f4d1 | |||
| b91fd12996 | |||
| 96c026eb30 | |||
| 1e89745704 | |||
| 34ba66b0c5 | |||
| 056015c3d8 | |||
| c98c4080a3 | |||
| da59ccb601 | |||
| 9faf728089 | |||
| 7c7c299eee | |||
| 3991b2fae3 | |||
| fe7ec42566 | |||
| 8443982188 | |||
| 0e6d9dfe85 | |||
| 1fd2d66df8 | |||
| c26121df5c | |||
| ea65e9043c | |||
| f1fa1f3f15 | |||
| 293553944d | |||
| d9741fc96b | |||
| b21f9dde61 | |||
| bc9b42ea9b | |||
| f2e6ee6899 | |||
| a205aecb80 | |||
| a522a78231 | |||
| 0e83042e54 | |||
| 63e5c29e00 |
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@ -1,8 +1,8 @@
|
||||
# GitHub code owners
|
||||
# See https://github.com/blog/2392-introducing-code-owners
|
||||
|
||||
cli/command/stack/** @vdemeester
|
||||
cli/command/stack/** @vdemeester @silvin-lubecki
|
||||
cli/compose/** @vdemeester
|
||||
contrib/completion/bash/** @albers
|
||||
contrib/completion/zsh/** @sdurrheimer
|
||||
docs/** @mistyhacks @vdemeester @thaJeztah
|
||||
docs/** @vdemeester @thaJeztah
|
||||
|
||||
@ -41,7 +41,6 @@
|
||||
# TODO Describe the docs maintainers role.
|
||||
|
||||
people = [
|
||||
"misty",
|
||||
"thajeztah"
|
||||
]
|
||||
|
||||
@ -95,11 +94,6 @@
|
||||
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"
|
||||
|
||||
4
Makefile
4
Makefile
@ -46,10 +46,6 @@ 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'
|
||||
|
||||
@ -26,13 +26,11 @@ Test<Function Name><Test Case Name>
|
||||
where appropriate, but may not be appropriate in all cases.
|
||||
|
||||
Assertions should be made using
|
||||
[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).
|
||||
[gotest.tools/assert](https://godoc.org/gotest.tools/assert).
|
||||
|
||||
Fakes, and testing utilities can be found in
|
||||
[internal/test](https://godoc.org/github.com/docker/cli/internal/test) and
|
||||
[gotestyourself](https://godoc.org/github.com/gotestyourself/gotestyourself).
|
||||
[gotest.tools](https://godoc.org/gotest.tools).
|
||||
|
||||
## End-to-End Test Suite
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ clone_folder: c:\gopath\src\github.com\docker\cli
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
GOVERSION: 1.10.3
|
||||
GOVERSION: 1.10.8
|
||||
DEPVERSION: v0.4.1
|
||||
|
||||
install:
|
||||
|
||||
19
circle.yml
19
circle.yml
@ -4,10 +4,11 @@ jobs:
|
||||
|
||||
lint:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:17.06-git'}]
|
||||
docker: [{image: 'docker:18.03-git'}]
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 18.03.1-ce
|
||||
reusable: true
|
||||
exclusive: false
|
||||
- run:
|
||||
@ -22,11 +23,12 @@ jobs:
|
||||
|
||||
cross:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:17.06-git'}]
|
||||
docker: [{image: 'docker:18.03-git'}]
|
||||
parallelism: 3
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 18.03.1-ce
|
||||
reusable: true
|
||||
exclusive: false
|
||||
- run:
|
||||
@ -48,10 +50,11 @@ jobs:
|
||||
|
||||
test:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:17.06-git'}]
|
||||
docker: [{image: 'docker:18.03-git'}]
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 18.03.1-ce
|
||||
reusable: true
|
||||
exclusive: false
|
||||
- run:
|
||||
@ -76,10 +79,11 @@ jobs:
|
||||
|
||||
validate:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:17.06-git'}]
|
||||
docker: [{image: 'docker:18.03-git'}]
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 18.03.1-ce
|
||||
reusable: true
|
||||
exclusive: false
|
||||
- run:
|
||||
@ -93,10 +97,13 @@ jobs:
|
||||
make ci-validate
|
||||
shellcheck:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:17.06-git'}]
|
||||
docker: [{image: 'docker:18.03-git'}]
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- setup_remote_docker:
|
||||
version: 18.03.1-ce
|
||||
reusable: true
|
||||
exclusive: false
|
||||
- run:
|
||||
name: "Run shellcheck"
|
||||
command: |
|
||||
|
||||
22
cli/command/builder/cmd.go
Normal file
22
cli/command/builder/cmd.go
Normal file
@ -0,0 +1,22 @@
|
||||
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
|
||||
}
|
||||
96
cli/command/builder/prune.go
Normal file
96
cli/command/builder/prune.go
Normal file
@ -0,0 +1,96 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type pruneOptions struct {
|
||||
force bool
|
||||
all bool
|
||||
filter opts.FilterOpt
|
||||
keepStorage opts.MemBytes
|
||||
}
|
||||
|
||||
// NewPruneCommand returns a new cobra prune command for images
|
||||
func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options := pruneOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "prune",
|
||||
Short: "Remove build cache",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
spaceReclaimed, output, err := runPrune(dockerCli, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if output != "" {
|
||||
fmt.Fprintln(dockerCli.Out(), output)
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
|
||||
return nil
|
||||
},
|
||||
Annotations: map[string]string{"version": "1.39"},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&options.force, "force", "f", false, "Do not prompt for confirmation")
|
||||
flags.BoolVarP(&options.all, "all", "a", false, "Remove all unused images, not just dangling ones")
|
||||
flags.Var(&options.filter, "filter", "Provide filter values (e.g. 'unused-for=24h')")
|
||||
flags.Var(&options.keepStorage, "keep-storage", "Amount of disk space to keep for cache")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
const (
|
||||
normalWarning = `WARNING! This will remove all dangling build cache. Are you sure you want to continue?`
|
||||
allCacheWarning = `WARNING! This will remove all build cache. Are you sure you want to continue?`
|
||||
)
|
||||
|
||||
func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
|
||||
pruneFilters := options.filter.Value()
|
||||
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
|
||||
|
||||
warning := normalWarning
|
||||
if options.all {
|
||||
warning = allCacheWarning
|
||||
}
|
||||
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
|
||||
return 0, "", nil
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().BuildCachePrune(context.Background(), types.BuildCachePruneOptions{
|
||||
All: options.all,
|
||||
KeepStorage: options.keepStorage.Value(),
|
||||
Filters: pruneFilters,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
if len(report.CachesDeleted) > 0 {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Deleted build cache objects:\n")
|
||||
for _, id := range report.CachesDeleted {
|
||||
sb.WriteString(id)
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
output = sb.String()
|
||||
}
|
||||
|
||||
return report.SpaceReclaimed, output, nil
|
||||
}
|
||||
|
||||
// CachePrune executes a prune command for build cache
|
||||
func CachePrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
|
||||
return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter})
|
||||
}
|
||||
@ -8,17 +8,20 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"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"
|
||||
dopts "github.com/docker/cli/opts"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
@ -53,19 +56,21 @@ type Cli interface {
|
||||
ManifestStore() manifeststore.Store
|
||||
RegistryClient(bool) registryclient.RegistryClient
|
||||
ContentTrustEnabled() bool
|
||||
NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error)
|
||||
}
|
||||
|
||||
// DockerCli is an instance the docker command line client.
|
||||
// Instances of the client can be returned from NewDockerCli.
|
||||
type DockerCli struct {
|
||||
configFile *configfile.ConfigFile
|
||||
in *InStream
|
||||
out *OutStream
|
||||
err io.Writer
|
||||
client client.APIClient
|
||||
serverInfo ServerInfo
|
||||
clientInfo ClientInfo
|
||||
contentTrust bool
|
||||
configFile *configfile.ConfigFile
|
||||
in *InStream
|
||||
out *OutStream
|
||||
err io.Writer
|
||||
client client.APIClient
|
||||
serverInfo ServerInfo
|
||||
clientInfo ClientInfo
|
||||
contentTrust bool
|
||||
newContainerizeClient func(string) (clitypes.ContainerizedClient, error)
|
||||
}
|
||||
|
||||
// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
|
||||
@ -129,6 +134,20 @@ func (cli *DockerCli) ContentTrustEnabled() bool {
|
||||
return cli.contentTrust
|
||||
}
|
||||
|
||||
// BuildKitEnabled returns whether buildkit is enabled either through a daemon setting
|
||||
// or otherwise the client-side DOCKER_BUILDKIT environment variable
|
||||
func BuildKitEnabled(si ServerInfo) (bool, error) {
|
||||
buildkitEnabled := si.BuildkitVersion == types.BuilderBuildKit
|
||||
if buildkitEnv := os.Getenv("DOCKER_BUILDKIT"); buildkitEnv != "" {
|
||||
var err error
|
||||
buildkitEnabled, err = strconv.ParseBool(buildkitEnv)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
|
||||
}
|
||||
}
|
||||
return buildkitEnabled, nil
|
||||
}
|
||||
|
||||
// ManifestStore returns a store for local manifests
|
||||
func (cli *DockerCli) ManifestStore() manifeststore.Store {
|
||||
// TODO: support override default location from config file
|
||||
@ -205,6 +224,7 @@ func (cli *DockerCli) initializeFromClient() {
|
||||
cli.serverInfo = ServerInfo{
|
||||
HasExperimental: ping.Experimental,
|
||||
OSType: ping.OSType,
|
||||
BuildkitVersion: ping.BuilderVersion,
|
||||
}
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
}
|
||||
@ -228,11 +248,17 @@ 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) (clitypes.ContainerizedClient, error) {
|
||||
return cli.newContainerizeClient(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
|
||||
@ -242,8 +268,8 @@ type ClientInfo struct {
|
||||
}
|
||||
|
||||
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
|
||||
func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool) *DockerCli {
|
||||
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err, contentTrust: isTrusted}
|
||||
func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool, containerizedFn func(string) (clitypes.ContainerizedClient, error)) *DockerCli {
|
||||
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err, contentTrust: isTrusted, newContainerizeClient: containerizedFn}
|
||||
}
|
||||
|
||||
// NewAPIClientFromFlags creates a new APIClient from command line flags
|
||||
@ -252,24 +278,43 @@ func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.
|
||||
if err != nil {
|
||||
return &client.Client{}, err
|
||||
}
|
||||
var clientOpts []func(*client.Client) error
|
||||
helper, err := connhelper.GetConnectionHelper(host)
|
||||
if err != nil {
|
||||
return &client.Client{}, err
|
||||
}
|
||||
if helper == nil {
|
||||
clientOpts = append(clientOpts, withHTTPClient(opts.TLSOptions))
|
||||
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(
|
||||
withHTTPClient(opts.TLSOptions),
|
||||
client.WithHTTPHeaders(customHeaders),
|
||||
client.WithVersion(verStr),
|
||||
client.WithHost(host),
|
||||
)
|
||||
return client.NewClientWithOpts(clientOpts...)
|
||||
}
|
||||
|
||||
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
|
||||
|
||||
@ -43,6 +43,26 @@ func TestNewAPIClientFromFlags(t *testing.T) {
|
||||
assert.Check(t, is.Equal(api.DefaultVersion, apiclient.ClientVersion()))
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
|
||||
host := ":2375"
|
||||
opts := &flags.CommonOptions{Hosts: []string{host}}
|
||||
configFile := &configfile.ConfigFile{
|
||||
HTTPHeaders: map[string]string{
|
||||
"My-Header": "Custom-Value",
|
||||
},
|
||||
}
|
||||
apiclient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal("tcp://localhost"+host, apiclient.DaemonHost()))
|
||||
|
||||
expectedHeaders := map[string]string{
|
||||
"My-Header": "Custom-Value",
|
||||
"User-Agent": UserAgent(),
|
||||
}
|
||||
assert.Check(t, is.DeepEqual(expectedHeaders, apiclient.(*client.Client).CustomHTTPHeaders()))
|
||||
assert.Check(t, is.Equal(api.DefaultVersion, apiclient.ClientVersion()))
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
||||
customVersion := "v3.3.3"
|
||||
defer env.Patch(t, "DOCKER_API_VERSION", customVersion)()
|
||||
|
||||
@ -2,11 +2,14 @@ package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"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"
|
||||
@ -40,6 +43,9 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
|
||||
image.NewImageCommand(dockerCli),
|
||||
image.NewBuildCommand(dockerCli),
|
||||
|
||||
// builder
|
||||
builder.NewBuilderCommand(dockerCli),
|
||||
|
||||
// manifest
|
||||
manifest.NewManifestCommand(dockerCli),
|
||||
|
||||
@ -116,7 +122,10 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
|
||||
hide(image.NewSaveCommand(dockerCli)),
|
||||
hide(image.NewTagCommand(dockerCli)),
|
||||
)
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
// engine
|
||||
cmd.AddCommand(engine.NewEngineCommand(dockerCli))
|
||||
}
|
||||
}
|
||||
|
||||
func hide(cmd *cobra.Command) *cobra.Command {
|
||||
|
||||
@ -9,19 +9,10 @@ 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
|
||||
@ -67,7 +58,9 @@ func runConfigList(dockerCli command.Cli, options listOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(byConfigName(configs))
|
||||
sort.Slice(configs, func(i, j int) bool {
|
||||
return sortorder.NaturalLess(configs[i].Spec.Name, configs[j].Spec.Name)
|
||||
})
|
||||
|
||||
configCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
|
||||
@ -370,9 +370,24 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
|
||||
entrypoint = []string{""}
|
||||
}
|
||||
|
||||
ports, portBindings, err := nat.ParsePortSpecs(copts.publish.GetAll())
|
||||
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
|
||||
if err != nil {
|
||||
return nil, err
|
||||
publishOpts, err = parsePortOpts(publishOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ports, portBindings, err = nat.ParsePortSpecs(publishOpts)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Merge in exposed ports to the map of published ports
|
||||
@ -661,6 +676,23 @@ 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 {
|
||||
|
||||
@ -42,7 +42,6 @@ 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 {
|
||||
|
||||
@ -73,6 +73,6 @@ func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint6
|
||||
|
||||
// RunPrune calls the Container Prune API
|
||||
// This returns the amount of space reclaimed and a detailed output string
|
||||
func RunPrune(dockerCli command.Cli, filter opts.FilterOpt) (uint64, string, error) {
|
||||
func RunPrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
|
||||
return runPrune(dockerCli, pruneOptions{force: true, filter: filter})
|
||||
}
|
||||
|
||||
209
cli/command/engine/activate.go
Normal file
209
cli/command/engine/activate.go
Normal file
@ -0,0 +1,209 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/internal/licenseutils"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"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
|
||||
licenseLoginFunc func(ctx context.Context, authConfig *types.AuthConfig) (licenseutils.HubUser, error)
|
||||
}
|
||||
|
||||
// newActivateCommand creates a new `docker engine activate` command
|
||||
func newActivateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var options activateOptions
|
||||
options.licenseLoginFunc = licenseutils.Login
|
||||
|
||||
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", clitypes.RegistryPrefix, "Override the default location where engine images are pulled")
|
||||
flags.StringVar(&options.image, "engine-image", "", "Specify engine image")
|
||||
flags.StringVar(&options.format, "format", "", "Pretty-print licenses using a Go template")
|
||||
flags.BoolVar(&options.displayOnly, "display-only", false, "only display license information 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 {
|
||||
if !isRoot() {
|
||||
return errors.New("this command must be run as a privileged user")
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
summary, err := licenseutils.GetLicenseSummary(ctx, *license)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(cli.Out(), "License: %s\n", summary)
|
||||
if options.displayOnly {
|
||||
return nil
|
||||
}
|
||||
dclient := cli.Client()
|
||||
if err = licenseutils.ApplyLicense(ctx, dclient, license); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Short circuit if the user didn't specify a version and we're already running enterprise
|
||||
if options.version == "" {
|
||||
serverVersion, err := dclient.ServerVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(strings.ToLower(serverVersion.Platform.Name), "enterprise") {
|
||||
fmt.Fprintln(cli.Out(), "Successfully activated engine license on existing enterprise engine.")
|
||||
return nil
|
||||
}
|
||||
options.version = serverVersion.Version
|
||||
}
|
||||
|
||||
opts := clitypes.EngineInitOptions{
|
||||
RegistryPrefix: options.registryPrefix,
|
||||
EngineImage: options.image,
|
||||
EngineVersion: options.version,
|
||||
}
|
||||
|
||||
if err := client.ActivateEngine(ctx, opts, cli.Out(), authConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(cli.Out(), `Successfully activated engine.
|
||||
Restart docker with 'systemctl restart docker' to complete the activation.`)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLicenses(ctx context.Context, authConfig *types.AuthConfig, cli command.Cli, options activateOptions) (*model.IssuedLicense, error) {
|
||||
user, err := options.licenseLoginFunc(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)
|
||||
}
|
||||
146
cli/command/engine/activate_test.go
Normal file
146
cli/command/engine/activate_test.go
Normal file
@ -0,0 +1,146 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/internal/licenseutils"
|
||||
"github.com/docker/cli/internal/test"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/licensing"
|
||||
"github.com/docker/licensing/model"
|
||||
"gotest.tools/assert"
|
||||
"gotest.tools/fs"
|
||||
"gotest.tools/golden"
|
||||
)
|
||||
|
||||
const (
|
||||
// nolint: lll
|
||||
expiredLicense = `{"key_id":"irlYm3b9fdD8hMUXjazF39im7VQSSbAm9tfHK8cKUxJt","private_key":"aH5tTRDAVJpCRS2CRetTQVXIKgWUPfoCHODhDvNPvAbz","authorization":"ewogICAicGF5bG9hZCI6ICJleUpsZUhCcGNtRjBhVzl1SWpvaU1qQXhPQzB3TXkweE9GUXdOem93TURvd01Gb2lMQ0owYjJ0bGJpSTZJbkZtTVMxMlVtRmtialp5YjFaMldXdHJlVXN4VFdKMGNGUmpXR1ozVjA4MVRWZFFTM2cwUnpJd2NIYzlJaXdpYldGNFJXNW5hVzVsY3lJNk1Td2ljMk5oYm01cGJtZEZibUZpYkdWa0lqcDBjblZsTENKc2FXTmxibk5sVkhsd1pTSTZJazltWm14cGJtVWlMQ0owYVdWeUlqb2lVSEp2WkhWamRHbHZiaUo5IiwKICAgInNpZ25hdHVyZXMiOiBbCiAgICAgIHsKICAgICAgICAgImhlYWRlciI6IHsKICAgICAgICAgICAgImp3ayI6IHsKICAgICAgICAgICAgICAgImUiOiAiQVFBQiIsCiAgICAgICAgICAgICAgICJrZXlJRCI6ICJKN0xEOjY3VlI6TDVIWjpVN0JBOjJPNEc6NEFMMzpPRjJOOkpIR0I6RUZUSDo1Q1ZROk1GRU86QUVJVCIsCiAgICAgICAgICAgICAgICJraWQiOiAiSjdMRDo2N1ZSOkw1SFo6VTdCQToyTzRHOjRBTDM6T0YyTjpKSEdCOkVGVEg6NUNWUTpNRkVPOkFFSVQiLAogICAgICAgICAgICAgICAia3R5IjogIlJTQSIsCiAgICAgICAgICAgICAgICJuIjogInlkSXktbFU3bzdQY2VZLTQtcy1DUTVPRWdDeUY4Q3hJY1FJV3VLODRwSWlaY2lZNjczMHlDWW53TFNLVGx3LVU2VUNfUVJlV1Jpb01OTkU1RHM1VFlFWGJHRzZvbG0ycWRXYkJ3Y0NnLTJVVUhfT2NCOVd1UDZnUlBIcE1GTXN4RHpXd3ZheThKVXVIZ1lVTFVwbTFJdi1tcTdscDVuUV9SeHJUMEtaUkFRVFlMRU1FZkd3bTNoTU9fZ2VMUFMtaGdLUHRJSGxrZzZfV2NveFRHb0tQNzlkX3dhSFl4R05sN1doU25laUJTeGJwYlFBS2syMWxnNzk4WGI3dlp5RUFURE1yUlI5TWVFNkFkajVISnBZM0NveVJBUENtYUtHUkNLNHVvWlNvSXUwaEZWbEtVUHliYncwMDBHTy13YTJLTjhVd2dJSW0waTVJMXVXOUdrcTR6akJ5NXpoZ3F1VVhiRzliV1BBT1lycTVRYTgxRHhHY0JsSnlIWUFwLUREUEU5VEdnNHpZbVhqSm54WnFIRWR1R3FkZXZaOFhNSTB1a2ZrR0lJMTR3VU9pTUlJSXJYbEVjQmZfNDZJOGdRV0R6eHljWmVfSkdYLUxBdWF5WHJ5clVGZWhWTlVkWlVsOXdYTmFKQi1rYUNxejVRd2FSOTNzR3ctUVNmdEQwTnZMZTdDeU9ILUU2dmc2U3RfTmVUdmd2OFluaENpWElsWjhIT2ZJd05lN3RFRl9VY3o1T2JQeWttM3R5bHJOVWp0MFZ5QW10dGFjVkkyaUdpaGNVUHJtazRsVklaN1ZEX0xTVy1pN3lvU3VydHBzUFhjZTJwS0RJbzMwbEpHaE9fM0tVbWwyU1VaQ3F6SjF5RW1LcHlzSDVIRFc5Y3NJRkNBM2RlQWpmWlV2TjdVIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiYWxnIjogIlJTMjU2IgogICAgICAgICB9LAogICAgICAgICAic2lnbmF0dXJlIjogIm5saTZIdzRrbW5KcTBSUmRXaGVfbkhZS2VJLVpKenM1U0d5SUpDakh1dWtnVzhBYklpVzFZYWJJR2NqWUt0QTY4dWN6T1hyUXZreGxWQXJLSlgzMDJzN0RpbzcxTlNPRzJVcnhsSjlibDFpd0F3a3ZyTEQ2T0p5MGxGLVg4WnRabXhPVmNQZmwzcmJwZFQ0dnlnWTdNcU1QRXdmb0IxTmlWZDYyZ1cxU2NSREZZcWw3R0FVaFVKNkp4QU15VzVaOXl5YVE0NV8wd0RMUk5mRjA5YWNXeVowTjRxVS1hZjhrUTZUUWZUX05ERzNCR3pRb2V3cHlEajRiMFBHb0diOFhLdDlwekpFdEdxM3lQM25VMFFBbk90a2gwTnZac1l1UFcyUnhDT3lRNEYzVlR3UkF2eF9HSTZrMVRpYmlKNnByUWluUy16Sjh6RE8zUjBuakE3OFBwNXcxcVpaUE9BdmtzZFNSYzJDcVMtcWhpTmF5YUhOVHpVNnpyOXlOZHR2S0o1QjNST0FmNUtjYXNiWURjTnVpeXBUNk90LUtqQ2I1dmYtWVpnc2FRNzJBdFBhSU4yeUpNREZHbmEwM0hpSjMxcTJRUlp5eTZrd3RYaGtwcDhTdEdIcHYxSWRaV09SVWttb0g5SFBzSGk4SExRLTZlM0tEY2x1RUQyMTNpZnljaVhtN0YzdHdaTTNHeDd1UXR1SldHaUlTZ2Z0QW9lVjZfUmI2VThkMmZxNzZuWHYxak5nckRRcE5waEZFd2tCdGRtZHZ2THByZVVYX3BWangza1AxN3pWbXFKNmNOOWkwWUc4WHg2VmRzcUxsRXUxQ2Rhd3Q0eko1M3VHMFlKTjRnUDZwc25yUS1uM0U1aFdlMDJ3d3dBZ3F3bGlPdmd4V1RTeXJyLXY2eDI0IiwKICAgICAgICAgInByb3RlY3RlZCI6ICJleUptYjNKdFlYUk1aVzVuZEdnaU9qRTNNeXdpWm05eWJXRjBWR0ZwYkNJNkltWlJJaXdpZEdsdFpTSTZJakl3TVRjdE1EVXRNRFZVTWpFNk5UYzZNek5hSW4wIgogICAgICB9CiAgIF0KfQ=="}`
|
||||
)
|
||||
|
||||
func TestActivateNoContainerd(t *testing.T) {
|
||||
testCli.SetContainerizedEngineClient(
|
||||
func(string) (clitypes.ContainerizedClient, error) {
|
||||
return nil, fmt.Errorf("some error")
|
||||
},
|
||||
)
|
||||
isRoot = func() bool { return true }
|
||||
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) (clitypes.ContainerizedClient, error) {
|
||||
return &fakeContainerizedEngineClient{}, nil
|
||||
},
|
||||
)
|
||||
isRoot = func() bool { return true }
|
||||
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")
|
||||
}
|
||||
|
||||
func TestActivateExpiredLicenseDryRun(t *testing.T) {
|
||||
dir := fs.NewDir(t, "license", fs.WithFile("docker.lic", expiredLicense, fs.WithMode(0644)))
|
||||
defer dir.Remove()
|
||||
filename := dir.Join("docker.lic")
|
||||
isRoot = func() bool { return true }
|
||||
c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil, types.Info{}, nil})
|
||||
c.SetContainerizedEngineClient(
|
||||
func(string) (clitypes.ContainerizedClient, error) {
|
||||
return &fakeContainerizedEngineClient{}, nil
|
||||
},
|
||||
)
|
||||
cmd := newActivateCommand(c)
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
cmd.Flags().Set("license", filename)
|
||||
cmd.Flags().Set("display-only", "true")
|
||||
c.OutBuffer().Reset()
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, c.OutBuffer().String(), "expired-license-display-only.golden")
|
||||
}
|
||||
|
||||
type mockLicenseClient struct{}
|
||||
|
||||
func (c mockLicenseClient) LoginViaAuth(ctx context.Context, username, password string) (authToken string, err error) {
|
||||
return "", fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (c mockLicenseClient) GetHubUserOrgs(ctx context.Context, authToken string) (orgs []model.Org, err error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) GetHubUserByName(ctx context.Context, username string) (user *model.User, err error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) VerifyLicense(ctx context.Context, license model.IssuedLicense) (res *model.CheckResponse, err error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) GenerateNewTrialSubscription(ctx context.Context, authToken, dockerID string) (subscriptionID string, err error) {
|
||||
return "", fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) ListSubscriptions(ctx context.Context, authToken, dockerID string) (response []*model.Subscription, err error) {
|
||||
expires := time.Date(2010, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
return []*model.Subscription{
|
||||
{
|
||||
State: "active",
|
||||
Expires: &expires,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
func (c mockLicenseClient) ListSubscriptionsDetails(ctx context.Context, authToken, dockerID string) (response []*model.SubscriptionDetail, err error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) DownloadLicenseFromHub(ctx context.Context, authToken, subscriptionID string) (license *model.IssuedLicense, err error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) ParseLicense(license []byte) (parsedLicense *model.IssuedLicense, err error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) StoreLicense(ctx context.Context, dclnt licensing.WrappedDockerClient, licenses *model.IssuedLicense, localRootDir string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) LoadLocalLicense(ctx context.Context, dclnt licensing.WrappedDockerClient) (*model.Subscription, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) SummarizeLicense(res *model.CheckResponse, keyID string) *model.Subscription {
|
||||
return nil
|
||||
}
|
||||
func TestActivateDisplayOnlyHub(t *testing.T) {
|
||||
isRoot = func() bool { return true }
|
||||
c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil, types.Info{}, nil})
|
||||
c.SetContainerizedEngineClient(
|
||||
func(string) (clitypes.ContainerizedClient, error) {
|
||||
return &fakeContainerizedEngineClient{}, nil
|
||||
},
|
||||
)
|
||||
|
||||
hubUser := licenseutils.HubUser{
|
||||
Client: mockLicenseClient{},
|
||||
}
|
||||
options := activateOptions{
|
||||
licenseLoginFunc: func(ctx context.Context, authConfig *types.AuthConfig) (licenseutils.HubUser, error) {
|
||||
return hubUser, nil
|
||||
},
|
||||
displayOnly: true,
|
||||
}
|
||||
c.OutBuffer().Reset()
|
||||
err := runActivate(c, options)
|
||||
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, c.OutBuffer().String(), "expired-hub-license-display-only.golden")
|
||||
}
|
||||
13
cli/command/engine/activate_unix.go
Normal file
13
cli/command/engine/activate_unix.go
Normal file
@ -0,0 +1,13 @@
|
||||
// +build !windows
|
||||
|
||||
package engine
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var (
|
||||
isRoot = func() bool {
|
||||
return unix.Geteuid() == 0
|
||||
}
|
||||
)
|
||||
9
cli/command/engine/activate_windows.go
Normal file
9
cli/command/engine/activate_windows.go
Normal file
@ -0,0 +1,9 @@
|
||||
// +build windows
|
||||
|
||||
package engine
|
||||
|
||||
var (
|
||||
isRoot = func() bool {
|
||||
return true
|
||||
}
|
||||
)
|
||||
34
cli/command/engine/auth.go
Normal file
34
cli/command/engine/auth.go
Normal file
@ -0,0 +1,34 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"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 = clitypes.RegistryPrefix
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
125
cli/command/engine/check.go
Normal file
125
cli/command/engine/check.go
Normal file
@ -0,0 +1,125 @@
|
||||
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/versions"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type checkOptions struct {
|
||||
registryPrefix string
|
||||
preReleases bool
|
||||
engineImage string
|
||||
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", clitypes.RegistryPrefix, "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.StringVar(&options.engineImage, "engine-image", "", "Specify engine image (default uses the same image as currently running)")
|
||||
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 {
|
||||
if !isRoot() {
|
||||
return errors.New("this command must be run as a privileged user")
|
||||
}
|
||||
ctx := context.Background()
|
||||
client := dockerCli.Client()
|
||||
serverVersion, err := client.ServerVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
availVersions, err := versions.GetEngineVersions(ctx, dockerCli.RegistryClient(false), options.registryPrefix, options.engineImage, serverVersion.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
availUpdates := []clitypes.Update{
|
||||
{Type: "current", Version: serverVersion.Version},
|
||||
}
|
||||
if len(availVersions.Patches) > 0 {
|
||||
availUpdates = append(availUpdates,
|
||||
processVersions(
|
||||
serverVersion.Version,
|
||||
"patch",
|
||||
options.preReleases,
|
||||
availVersions.Patches)...)
|
||||
}
|
||||
if options.upgrades {
|
||||
availUpdates = append(availUpdates,
|
||||
processVersions(
|
||||
serverVersion.Version,
|
||||
"upgrade",
|
||||
options.preReleases,
|
||||
availVersions.Upgrades)...)
|
||||
}
|
||||
if options.downgrades {
|
||||
availUpdates = append(availUpdates,
|
||||
processVersions(
|
||||
serverVersion.Version,
|
||||
"downgrade",
|
||||
options.preReleases,
|
||||
availVersions.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,
|
||||
availVersions []clitypes.DockerVersion) []clitypes.Update {
|
||||
availUpdates := []clitypes.Update{}
|
||||
for _, ver := range availVersions {
|
||||
if !includePrerelease && ver.Prerelease() != "" {
|
||||
continue
|
||||
}
|
||||
if ver.Tag != currentVersion {
|
||||
availUpdates = append(availUpdates, clitypes.Update{
|
||||
Type: verType,
|
||||
Version: ver.Tag,
|
||||
Notes: fmt.Sprintf("%s?%s", clitypes.ReleaseNotePrefix, ver.Tag),
|
||||
})
|
||||
}
|
||||
}
|
||||
return availUpdates
|
||||
}
|
||||
114
cli/command/engine/check_test.go
Normal file
114
cli/command/engine/check_test.go
Normal file
@ -0,0 +1,114 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
manifesttypes "github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"gotest.tools/assert"
|
||||
"gotest.tools/golden"
|
||||
)
|
||||
|
||||
var (
|
||||
testCli = test.NewFakeCli(&client.Client{})
|
||||
)
|
||||
|
||||
type verClient struct {
|
||||
client.Client
|
||||
ver types.Version
|
||||
verErr error
|
||||
info types.Info
|
||||
infoErr error
|
||||
}
|
||||
|
||||
func (c *verClient) ServerVersion(ctx context.Context) (types.Version, error) {
|
||||
return c.ver, c.verErr
|
||||
}
|
||||
|
||||
func (c *verClient) Info(ctx context.Context) (types.Info, error) {
|
||||
return c.info, c.infoErr
|
||||
}
|
||||
|
||||
type testRegistryClient struct {
|
||||
tags []string
|
||||
}
|
||||
|
||||
func (c testRegistryClient) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
return manifesttypes.ImageManifest{}, nil
|
||||
}
|
||||
func (c testRegistryClient) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (c testRegistryClient) MountBlob(ctx context.Context, source reference.Canonical, target reference.Named) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c testRegistryClient) PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (c testRegistryClient) GetTags(ctx context.Context, ref reference.Named) ([]string, error) {
|
||||
return c.tags, nil
|
||||
}
|
||||
|
||||
func TestCheckForUpdatesNoCurrentVersion(t *testing.T) {
|
||||
isRoot = func() bool { return true }
|
||||
c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil, types.Info{}, nil})
|
||||
c.SetRegistryClient(testRegistryClient{})
|
||||
cmd := newCheckForUpdatesCommand(c)
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
err := cmd.Execute()
|
||||
assert.ErrorContains(t, err, "no such file or directory")
|
||||
}
|
||||
|
||||
func TestCheckForUpdatesGetEngineVersionsHappy(t *testing.T) {
|
||||
c := test.NewFakeCli(&verClient{client.Client{}, types.Version{Version: "1.1.0"}, nil, types.Info{ServerVersion: "1.1.0"}, nil})
|
||||
c.SetRegistryClient(testRegistryClient{[]string{
|
||||
"1.0.1", "1.0.2", "1.0.3-beta1",
|
||||
"1.1.1", "1.1.2", "1.1.3-beta1",
|
||||
"1.2.0", "2.0.0", "2.1.0-beta1",
|
||||
}})
|
||||
|
||||
isRoot = func() bool { return true }
|
||||
cmd := newCheckForUpdatesCommand(c)
|
||||
cmd.Flags().Set("pre-releases", "true")
|
||||
cmd.Flags().Set("downgrades", "true")
|
||||
cmd.Flags().Set("engine-image", "engine-community")
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, c.OutBuffer().String(), "check-all.golden")
|
||||
|
||||
c.OutBuffer().Reset()
|
||||
cmd.Flags().Set("pre-releases", "false")
|
||||
cmd.Flags().Set("downgrades", "true")
|
||||
err = cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
fmt.Println(c.OutBuffer().String())
|
||||
golden.Assert(t, c.OutBuffer().String(), "check-no-prerelease.golden")
|
||||
|
||||
c.OutBuffer().Reset()
|
||||
cmd.Flags().Set("pre-releases", "false")
|
||||
cmd.Flags().Set("downgrades", "false")
|
||||
err = cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
fmt.Println(c.OutBuffer().String())
|
||||
golden.Assert(t, c.OutBuffer().String(), "check-no-downgrades.golden")
|
||||
|
||||
c.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(c.OutBuffer().String())
|
||||
golden.Assert(t, c.OutBuffer().String(), "check-patches-only.golden")
|
||||
}
|
||||
101
cli/command/engine/client_test.go
Normal file
101
cli/command/engine/client_test.go
Normal file
@ -0,0 +1,101 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
)
|
||||
|
||||
type (
|
||||
fakeContainerizedEngineClient struct {
|
||||
closeFunc func() error
|
||||
activateEngineFunc func(ctx context.Context,
|
||||
opts clitypes.EngineInitOptions,
|
||||
out clitypes.OutStream,
|
||||
authConfig *types.AuthConfig) error
|
||||
initEngineFunc func(ctx context.Context,
|
||||
opts clitypes.EngineInitOptions,
|
||||
out clitypes.OutStream,
|
||||
authConfig *types.AuthConfig,
|
||||
healthfn func(context.Context) error) error
|
||||
doUpdateFunc func(ctx context.Context,
|
||||
opts clitypes.EngineInitOptions,
|
||||
out clitypes.OutStream,
|
||||
authConfig *types.AuthConfig) error
|
||||
getEngineVersionsFunc func(ctx context.Context,
|
||||
registryClient registryclient.RegistryClient,
|
||||
currentVersion,
|
||||
imageName string) (clitypes.AvailableVersions, error)
|
||||
|
||||
getEngineFunc func(ctx context.Context) (containerd.Container, error)
|
||||
removeEngineFunc func(ctx context.Context) error
|
||||
getCurrentEngineVersionFunc func(ctx context.Context) (clitypes.EngineInitOptions, error)
|
||||
}
|
||||
)
|
||||
|
||||
func (w *fakeContainerizedEngineClient) Close() error {
|
||||
if w.closeFunc != nil {
|
||||
return w.closeFunc()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *fakeContainerizedEngineClient) ActivateEngine(ctx context.Context,
|
||||
opts clitypes.EngineInitOptions,
|
||||
out clitypes.OutStream,
|
||||
authConfig *types.AuthConfig) error {
|
||||
if w.activateEngineFunc != nil {
|
||||
return w.activateEngineFunc(ctx, opts, out, authConfig)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (w *fakeContainerizedEngineClient) InitEngine(ctx context.Context,
|
||||
opts clitypes.EngineInitOptions,
|
||||
out clitypes.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 clitypes.EngineInitOptions,
|
||||
out clitypes.OutStream,
|
||||
authConfig *types.AuthConfig) error {
|
||||
if w.doUpdateFunc != nil {
|
||||
return w.doUpdateFunc(ctx, opts, out, authConfig)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (w *fakeContainerizedEngineClient) GetEngineVersions(ctx context.Context,
|
||||
registryClient registryclient.RegistryClient,
|
||||
currentVersion, imageName string) (clitypes.AvailableVersions, error) {
|
||||
|
||||
if w.getEngineVersionsFunc != nil {
|
||||
return w.getEngineVersionsFunc(ctx, registryClient, currentVersion, imageName)
|
||||
}
|
||||
return clitypes.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) error {
|
||||
if w.removeEngineFunc != nil {
|
||||
return w.removeEngineFunc(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (w *fakeContainerizedEngineClient) GetCurrentEngineVersion(ctx context.Context) (clitypes.EngineInitOptions, error) {
|
||||
if w.getCurrentEngineVersionFunc != nil {
|
||||
return w.getCurrentEngineVersionFunc(ctx)
|
||||
}
|
||||
return clitypes.EngineInitOptions{}, nil
|
||||
}
|
||||
23
cli/command/engine/cmd.go
Normal file
23
cli/command/engine/cmd.go
Normal file
@ -0,0 +1,23 @@
|
||||
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(
|
||||
newActivateCommand(dockerCli),
|
||||
newCheckForUpdatesCommand(dockerCli),
|
||||
newUpdateCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
14
cli/command/engine/cmd_test.go
Normal file
14
cli/command/engine/cmd_test.go
Normal file
@ -0,0 +1,14 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func TestNewEngineCommand(t *testing.T) {
|
||||
cmd := NewEngineCommand(testCli)
|
||||
|
||||
subcommands := cmd.Commands()
|
||||
assert.Assert(t, len(subcommands) == 3)
|
||||
}
|
||||
10
cli/command/engine/init.go
Normal file
10
cli/command/engine/init.go
Normal file
@ -0,0 +1,10 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
clitypes "github.com/docker/cli/types"
|
||||
)
|
||||
|
||||
type extendedEngineInitOptions struct {
|
||||
clitypes.EngineInitOptions
|
||||
sockPath string
|
||||
}
|
||||
11
cli/command/engine/testdata/check-all.golden
vendored
Normal file
11
cli/command/engine/testdata/check-all.golden
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
TYPE VERSION NOTES
|
||||
current 1.1.0
|
||||
patch 1.1.1 https://docker.com/engine/releasenotes?1.1.1
|
||||
patch 1.1.2 https://docker.com/engine/releasenotes?1.1.2
|
||||
patch 1.1.3-beta1 https://docker.com/engine/releasenotes?1.1.3-beta1
|
||||
upgrade 1.2.0 https://docker.com/engine/releasenotes?1.2.0
|
||||
upgrade 2.0.0 https://docker.com/engine/releasenotes?2.0.0
|
||||
upgrade 2.1.0-beta1 https://docker.com/engine/releasenotes?2.1.0-beta1
|
||||
downgrade 1.0.1 https://docker.com/engine/releasenotes?1.0.1
|
||||
downgrade 1.0.2 https://docker.com/engine/releasenotes?1.0.2
|
||||
downgrade 1.0.3-beta1 https://docker.com/engine/releasenotes?1.0.3-beta1
|
||||
6
cli/command/engine/testdata/check-no-downgrades.golden
vendored
Normal file
6
cli/command/engine/testdata/check-no-downgrades.golden
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
TYPE VERSION NOTES
|
||||
current 1.1.0
|
||||
patch 1.1.1 https://docker.com/engine/releasenotes?1.1.1
|
||||
patch 1.1.2 https://docker.com/engine/releasenotes?1.1.2
|
||||
upgrade 1.2.0 https://docker.com/engine/releasenotes?1.2.0
|
||||
upgrade 2.0.0 https://docker.com/engine/releasenotes?2.0.0
|
||||
8
cli/command/engine/testdata/check-no-prerelease.golden
vendored
Normal file
8
cli/command/engine/testdata/check-no-prerelease.golden
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
TYPE VERSION NOTES
|
||||
current 1.1.0
|
||||
patch 1.1.1 https://docker.com/engine/releasenotes?1.1.1
|
||||
patch 1.1.2 https://docker.com/engine/releasenotes?1.1.2
|
||||
upgrade 1.2.0 https://docker.com/engine/releasenotes?1.2.0
|
||||
upgrade 2.0.0 https://docker.com/engine/releasenotes?2.0.0
|
||||
downgrade 1.0.1 https://docker.com/engine/releasenotes?1.0.1
|
||||
downgrade 1.0.2 https://docker.com/engine/releasenotes?1.0.2
|
||||
4
cli/command/engine/testdata/check-patches-only.golden
vendored
Normal file
4
cli/command/engine/testdata/check-patches-only.golden
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
TYPE VERSION NOTES
|
||||
current 1.1.0
|
||||
patch 1.1.1 https://docker.com/engine/releasenotes?1.1.1
|
||||
patch 1.1.2 https://docker.com/engine/releasenotes?1.1.2
|
||||
3
cli/command/engine/testdata/expired-hub-license-display-only.golden
vendored
Normal file
3
cli/command/engine/testdata/expired-hub-license-display-only.golden
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
Looking for existing licenses for ...
|
||||
NUM OWNER PRODUCT ID EXPIRES PRICING COMPONENTS
|
||||
0 2010-01-01 00:00:00 +0000 UTC
|
||||
1
cli/command/engine/testdata/expired-license-display-only.golden
vendored
Normal file
1
cli/command/engine/testdata/expired-license-display-only.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
License: Quantity: 1 Nodes Expiration date: 2018-03-18 Expired! You will no longer receive updates. Please renew at https://docker.com/licensing
|
||||
55
cli/command/engine/update.go
Normal file
55
cli/command/engine/update.go
Normal file
@ -0,0 +1,55 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"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 (default uses the same image as currently running)")
|
||||
flags.StringVar(&options.RegistryPrefix, "registry-prefix", clitypes.RegistryPrefix, "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 {
|
||||
if !isRoot() {
|
||||
return errors.New("this command must be run as a privileged user")
|
||||
}
|
||||
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
|
||||
}
|
||||
if err := client.DoUpdate(ctx, options.EngineInitOptions, dockerCli.Out(), authConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Out(), `Successfully updated engine.
|
||||
Restart docker with 'systemctl restart docker' to complete the update.`)
|
||||
return nil
|
||||
}
|
||||
40
cli/command/engine/update_test.go
Normal file
40
cli/command/engine/update_test.go
Normal file
@ -0,0 +1,40 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func TestUpdateNoContainerd(t *testing.T) {
|
||||
testCli.SetContainerizedEngineClient(
|
||||
func(string) (clitypes.ContainerizedClient, 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) {
|
||||
c := test.NewFakeCli(&verClient{client.Client{}, types.Version{Version: "1.1.0"}, nil, types.Info{ServerVersion: "1.1.0"}, nil})
|
||||
c.SetContainerizedEngineClient(
|
||||
func(string) (clitypes.ContainerizedClient, error) {
|
||||
return &fakeContainerizedEngineClient{}, nil
|
||||
},
|
||||
)
|
||||
cmd := newUpdateCommand(c)
|
||||
cmd.Flags().Set("registry-prefix", clitypes.RegistryPrefix)
|
||||
cmd.Flags().Set("version", "someversion")
|
||||
cmd.Flags().Set("engine-image", "someimage")
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
179
cli/command/formatter/buildcache.go
Normal file
179
cli/command/formatter/buildcache.go
Normal file
@ -0,0 +1,179 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBuildCacheTableFormat = "table {{.ID}}\t{{.Type}}\t{{.Size}}\t{{.CreatedSince}}\t{{.LastUsedSince}}\t{{.UsageCount}}\t{{.Shared}}\t{{.Description}}"
|
||||
|
||||
cacheIDHeader = "CACHE ID"
|
||||
cacheTypeHeader = "CACHE TYPE"
|
||||
parentHeader = "PARENT"
|
||||
lastUsedSinceHeader = "LAST USED"
|
||||
usageCountHeader = "USAGE"
|
||||
inUseHeader = "IN USE"
|
||||
sharedHeader = "SHARED"
|
||||
)
|
||||
|
||||
// NewBuildCacheFormat returns a Format for rendering using a Context
|
||||
func NewBuildCacheFormat(source string, quiet bool) Format {
|
||||
switch source {
|
||||
case TableFormatKey:
|
||||
if quiet {
|
||||
return defaultQuietFormat
|
||||
}
|
||||
return Format(defaultBuildCacheTableFormat)
|
||||
case RawFormatKey:
|
||||
if quiet {
|
||||
return `build_cache_id: {{.ID}}`
|
||||
}
|
||||
format := `build_cache_id: {{.ID}}
|
||||
parent_id: {{.Parent}}
|
||||
build_cache_type: {{.CacheType}}
|
||||
description: {{.Description}}
|
||||
created_at: {{.CreatedAt}}
|
||||
created_since: {{.CreatedSince}}
|
||||
last_used_at: {{.LastUsedAt}}
|
||||
last_used_since: {{.LastUsedSince}}
|
||||
usage_count: {{.UsageCount}}
|
||||
in_use: {{.InUse}}
|
||||
shared: {{.Shared}}
|
||||
`
|
||||
return Format(format)
|
||||
}
|
||||
return Format(source)
|
||||
}
|
||||
|
||||
func buildCacheSort(buildCache []*types.BuildCache) {
|
||||
sort.Slice(buildCache, func(i, j int) bool {
|
||||
lui, luj := buildCache[i].LastUsedAt, buildCache[j].LastUsedAt
|
||||
switch {
|
||||
case lui == nil && luj == nil:
|
||||
return strings.Compare(buildCache[i].ID, buildCache[j].ID) < 0
|
||||
case lui == nil:
|
||||
return true
|
||||
case luj == nil:
|
||||
return false
|
||||
case lui.Equal(*luj):
|
||||
return strings.Compare(buildCache[i].ID, buildCache[j].ID) < 0
|
||||
default:
|
||||
return lui.Before(*luj)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BuildCacheWrite renders the context for a list of containers
|
||||
func BuildCacheWrite(ctx Context, buildCaches []*types.BuildCache) error {
|
||||
render := func(format func(subContext subContext) error) error {
|
||||
buildCacheSort(buildCaches)
|
||||
for _, bc := range buildCaches {
|
||||
err := format(&buildCacheContext{trunc: ctx.Trunc, v: bc})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ctx.Write(newBuildCacheContext(), render)
|
||||
}
|
||||
|
||||
type buildCacheHeaderContext map[string]string
|
||||
|
||||
type buildCacheContext struct {
|
||||
HeaderContext
|
||||
trunc bool
|
||||
v *types.BuildCache
|
||||
}
|
||||
|
||||
func newBuildCacheContext() *buildCacheContext {
|
||||
buildCacheCtx := buildCacheContext{}
|
||||
buildCacheCtx.header = buildCacheHeaderContext{
|
||||
"ID": cacheIDHeader,
|
||||
"Parent": parentHeader,
|
||||
"CacheType": cacheTypeHeader,
|
||||
"Size": sizeHeader,
|
||||
"CreatedSince": createdSinceHeader,
|
||||
"LastUsedSince": lastUsedSinceHeader,
|
||||
"UsageCount": usageCountHeader,
|
||||
"InUse": inUseHeader,
|
||||
"Shared": sharedHeader,
|
||||
"Description": descriptionHeader,
|
||||
}
|
||||
return &buildCacheCtx
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) MarshalJSON() ([]byte, error) {
|
||||
return marshalJSON(c)
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) ID() string {
|
||||
id := c.v.ID
|
||||
if c.trunc {
|
||||
id = stringid.TruncateID(c.v.ID)
|
||||
}
|
||||
if c.v.InUse {
|
||||
return id + "*"
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) Parent() string {
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(c.v.Parent)
|
||||
}
|
||||
return c.v.Parent
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) CacheType() string {
|
||||
return c.v.Type
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) Description() string {
|
||||
return c.v.Description
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) Size() string {
|
||||
return units.HumanSizeWithPrecision(float64(c.v.Size), 3)
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) CreatedAt() string {
|
||||
return c.v.CreatedAt.String()
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) CreatedSince() string {
|
||||
return units.HumanDuration(time.Now().UTC().Sub(c.v.CreatedAt)) + " ago"
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) LastUsedAt() string {
|
||||
if c.v.LastUsedAt == nil {
|
||||
return ""
|
||||
}
|
||||
return c.v.LastUsedAt.String()
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) LastUsedSince() string {
|
||||
if c.v.LastUsedAt == nil {
|
||||
return ""
|
||||
}
|
||||
return units.HumanDuration(time.Now().UTC().Sub(*c.v.LastUsedAt)) + " ago"
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) UsageCount() string {
|
||||
return fmt.Sprintf("%d", c.v.UsageCount)
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) InUse() string {
|
||||
return fmt.Sprintf("%t", c.v.InUse)
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) Shared() string {
|
||||
return fmt.Sprintf("%t", c.v.Shared)
|
||||
}
|
||||
@ -269,7 +269,10 @@ func DisplayablePorts(ports []types.Port) string {
|
||||
var result []string
|
||||
var hostMappings []string
|
||||
var groupMapKeys []string
|
||||
sort.Sort(byPortInfo(ports))
|
||||
sort.Slice(ports, func(i, j int) bool {
|
||||
return comparePorts(ports[i], ports[j])
|
||||
})
|
||||
|
||||
for _, port := range ports {
|
||||
current := port.PrivatePort
|
||||
portKey := port.Type
|
||||
@ -322,23 +325,18 @@ func formGroup(key string, start, last uint16) string {
|
||||
return fmt.Sprintf("%s/%s", group, groupType)
|
||||
}
|
||||
|
||||
// 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
|
||||
func comparePorts(i, j types.Port) bool {
|
||||
if i.PrivatePort != j.PrivatePort {
|
||||
return i.PrivatePort < j.PrivatePort
|
||||
}
|
||||
|
||||
if r[i].IP != r[j].IP {
|
||||
return r[i].IP < r[j].IP
|
||||
if i.IP != j.IP {
|
||||
return i.IP < j.IP
|
||||
}
|
||||
|
||||
if r[i].PublicPort != r[j].PublicPort {
|
||||
return r[i].PublicPort < r[j].PublicPort
|
||||
if i.PublicPort != j.PublicPort {
|
||||
return i.PublicPort < j.PublicPort
|
||||
}
|
||||
|
||||
return r[i].Type < r[j].Type
|
||||
return i.Type < j.Type
|
||||
}
|
||||
|
||||
@ -12,19 +12,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultDiskUsageImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.VirtualSize}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}"
|
||||
defaultDiskUsageContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Names}}"
|
||||
defaultDiskUsageVolumeTableFormat = "table {{.Name}}\t{{.Links}}\t{{.Size}}"
|
||||
defaultDiskUsageTableFormat = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}"
|
||||
defaultBuildCacheVerboseFormat = `
|
||||
ID: {{.ID}}
|
||||
Description: {{.Description}}
|
||||
Mutable: {{.Mutable}}
|
||||
Size: {{.Size}}
|
||||
CreatedAt: {{.CreatedAt}}
|
||||
LastUsedAt: {{.LastUsedAt}}
|
||||
UsageCount: {{.UsageCount}}
|
||||
`
|
||||
defaultDiskUsageImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.VirtualSize}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}"
|
||||
defaultDiskUsageContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}}\t{{.Status}}\t{{.Names}}"
|
||||
defaultDiskUsageVolumeTableFormat = "table {{.Name}}\t{{.Links}}\t{{.Size}}"
|
||||
defaultDiskUsageBuildCacheTableFormat = "table {{.ID}}\t{{.CacheType}}\t{{.Size}}\t{{.CreatedSince}}\t{{.LastUsedSince}}\t{{.UsageCount}}\t{{.Shared}}"
|
||||
defaultDiskUsageTableFormat = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}"
|
||||
|
||||
typeHeader = "TYPE"
|
||||
totalHeader = "TOTAL"
|
||||
@ -32,7 +24,7 @@ UsageCount: {{.UsageCount}}
|
||||
reclaimableHeader = "RECLAIMABLE"
|
||||
containersHeader = "CONTAINERS"
|
||||
sharedSizeHeader = "SHARED SIZE"
|
||||
uniqueSizeHeader = "UNIQUE SiZE"
|
||||
uniqueSizeHeader = "UNIQUE SIZE"
|
||||
)
|
||||
|
||||
// DiskUsageContext contains disk usage specific information required by the formatter, encapsulate a Context struct.
|
||||
@ -56,14 +48,26 @@ func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template,
|
||||
return ctx.parseFormat()
|
||||
}
|
||||
|
||||
//
|
||||
// NewDiskUsageFormat returns a format for rendering an DiskUsageContext
|
||||
func NewDiskUsageFormat(source string) Format {
|
||||
switch source {
|
||||
case TableFormatKey:
|
||||
format := defaultDiskUsageTableFormat
|
||||
return Format(format)
|
||||
case RawFormatKey:
|
||||
func NewDiskUsageFormat(source string, verbose bool) Format {
|
||||
switch {
|
||||
case verbose && source == RawFormatKey:
|
||||
format := `{{range .Images}}type: Image
|
||||
` + NewImageFormat(source, false, true) + `
|
||||
{{end -}}
|
||||
{{range .Containers}}type: Container
|
||||
` + NewContainerFormat(source, false, true) + `
|
||||
{{end -}}
|
||||
{{range .Volumes}}type: Volume
|
||||
` + NewVolumeFormat(source, false) + `
|
||||
{{end -}}
|
||||
{{range .BuildCache}}type: Build Cache
|
||||
` + NewBuildCacheFormat(source, false) + `
|
||||
{{end -}}`
|
||||
return format
|
||||
case !verbose && source == TableFormatKey:
|
||||
return Format(defaultDiskUsageTableFormat)
|
||||
case !verbose && source == RawFormatKey:
|
||||
format := `type: {{.Type}}
|
||||
total: {{.TotalCount}}
|
||||
active: {{.Active}}
|
||||
@ -71,8 +75,9 @@ size: {{.Size}}
|
||||
reclaimable: {{.Reclaimable}}
|
||||
`
|
||||
return Format(format)
|
||||
default:
|
||||
return Format(source)
|
||||
}
|
||||
return Format(source)
|
||||
}
|
||||
|
||||
func (ctx *DiskUsageContext) Write() (err error) {
|
||||
@ -129,14 +134,23 @@ func (ctx *DiskUsageContext) Write() (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func (ctx *DiskUsageContext) verboseWrite() error {
|
||||
// First images
|
||||
tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
type diskUsageContext struct {
|
||||
Images []*imageContext
|
||||
Containers []*containerContext
|
||||
Volumes []*volumeContext
|
||||
BuildCache []*buildCacheContext
|
||||
}
|
||||
|
||||
ctx.Output.Write([]byte("Images space usage:\n\n"))
|
||||
func (ctx *DiskUsageContext) verboseWrite() error {
|
||||
duc := &diskUsageContext{
|
||||
Images: make([]*imageContext, 0, len(ctx.Images)),
|
||||
Containers: make([]*containerContext, 0, len(ctx.Containers)),
|
||||
Volumes: make([]*volumeContext, 0, len(ctx.Volumes)),
|
||||
BuildCache: make([]*buildCacheContext, 0, len(ctx.BuildCache)),
|
||||
}
|
||||
trunc := ctx.Format.IsTable()
|
||||
|
||||
// First images
|
||||
for _, i := range ctx.Images {
|
||||
repo := "<none>"
|
||||
tag := "<none>"
|
||||
@ -152,55 +166,92 @@ func (ctx *DiskUsageContext) verboseWrite() error {
|
||||
}
|
||||
}
|
||||
|
||||
err := ctx.contextFormat(tmpl, &imageContext{
|
||||
duc.Images = append(duc.Images, &imageContext{
|
||||
repo: repo,
|
||||
tag: tag,
|
||||
trunc: true,
|
||||
trunc: trunc,
|
||||
i: *i,
|
||||
})
|
||||
if err != nil {
|
||||
}
|
||||
|
||||
// Now containers
|
||||
for _, c := range ctx.Containers {
|
||||
// Don't display the virtual size
|
||||
c.SizeRootFs = 0
|
||||
duc.Containers = append(duc.Containers, &containerContext{trunc: trunc, c: *c})
|
||||
}
|
||||
|
||||
// And volumes
|
||||
for _, v := range ctx.Volumes {
|
||||
duc.Volumes = append(duc.Volumes, &volumeContext{v: *v})
|
||||
}
|
||||
|
||||
// And build cache
|
||||
buildCacheSort(ctx.BuildCache)
|
||||
for _, v := range ctx.BuildCache {
|
||||
duc.BuildCache = append(duc.BuildCache, &buildCacheContext{v: v, trunc: trunc})
|
||||
}
|
||||
|
||||
if ctx.Format == TableFormatKey {
|
||||
return ctx.verboseWriteTable(duc)
|
||||
}
|
||||
|
||||
ctx.preFormat()
|
||||
tmpl, err := ctx.parseFormat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tmpl.Execute(ctx.Output, duc)
|
||||
}
|
||||
|
||||
func (ctx *DiskUsageContext) verboseWriteTable(duc *diskUsageContext) error {
|
||||
tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.Output.Write([]byte("Images space usage:\n\n"))
|
||||
for _, img := range duc.Images {
|
||||
if err := ctx.contextFormat(tmpl, img); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ctx.postFormat(tmpl, newImageContext())
|
||||
|
||||
// Now containers
|
||||
ctx.Output.Write([]byte("\nContainers space usage:\n\n"))
|
||||
tmpl, err = ctx.startSubsection(defaultDiskUsageContainerTableFormat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range ctx.Containers {
|
||||
// Don't display the virtual size
|
||||
c.SizeRootFs = 0
|
||||
err := ctx.contextFormat(tmpl, &containerContext{trunc: true, c: *c})
|
||||
if err != nil {
|
||||
ctx.Output.Write([]byte("\nContainers space usage:\n\n"))
|
||||
for _, c := range duc.Containers {
|
||||
if err := ctx.contextFormat(tmpl, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ctx.postFormat(tmpl, newContainerContext())
|
||||
|
||||
// And volumes
|
||||
ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n"))
|
||||
tmpl, err = ctx.startSubsection(defaultDiskUsageVolumeTableFormat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, v := range ctx.Volumes {
|
||||
if err := ctx.contextFormat(tmpl, &volumeContext{v: *v}); err != nil {
|
||||
ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n"))
|
||||
for _, v := range duc.Volumes {
|
||||
if err := ctx.contextFormat(tmpl, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ctx.postFormat(tmpl, newVolumeContext())
|
||||
|
||||
// And build cache
|
||||
fmt.Fprintf(ctx.Output, "\nBuild cache usage: %s\n\n", units.HumanSize(float64(ctx.BuilderSize)))
|
||||
|
||||
t := template.Must(template.New("buildcache").Parse(defaultBuildCacheVerboseFormat))
|
||||
|
||||
for _, v := range ctx.BuildCache {
|
||||
t.Execute(ctx.Output, *v)
|
||||
tmpl, err = ctx.startSubsection(defaultDiskUsageBuildCacheTableFormat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(ctx.Output, "\nBuild cache usage: %s\n\n", units.HumanSize(float64(ctx.BuilderSize)))
|
||||
for _, v := range duc.BuildCache {
|
||||
if err := ctx.contextFormat(tmpl, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ctx.postFormat(tmpl, newBuildCacheContext())
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -416,7 +467,7 @@ func (c *diskUsageBuilderContext) Size() string {
|
||||
func (c *diskUsageBuilderContext) Reclaimable() string {
|
||||
var inUseBytes int64
|
||||
for _, bc := range c.buildCache {
|
||||
if bc.InUse {
|
||||
if bc.InUse && !bc.Shared {
|
||||
inUseBytes += bc.Size
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ func TestDiskUsageContextFormatWrite(t *testing.T) {
|
||||
{
|
||||
DiskUsageContext{
|
||||
Context: Context{
|
||||
Format: NewDiskUsageFormat("table"),
|
||||
Format: NewDiskUsageFormat("table", false),
|
||||
},
|
||||
Verbose: false},
|
||||
`TYPE TOTAL ACTIVE SIZE RECLAIMABLE
|
||||
@ -29,14 +29,14 @@ Build Cache 0 0 0B
|
||||
`,
|
||||
},
|
||||
{
|
||||
DiskUsageContext{Verbose: true},
|
||||
DiskUsageContext{Verbose: true, Context: Context{Format: NewDiskUsageFormat("table", true)}},
|
||||
`Images space usage:
|
||||
|
||||
REPOSITORY TAG IMAGE ID CREATED ago SIZE SHARED SIZE UNIQUE SiZE CONTAINERS
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE SHARED SIZE UNIQUE SIZE CONTAINERS
|
||||
|
||||
Containers space usage:
|
||||
|
||||
CONTAINER ID IMAGE COMMAND LOCAL VOLUMES SIZE CREATED ago STATUS NAMES
|
||||
CONTAINER ID IMAGE COMMAND LOCAL VOLUMES SIZE CREATED STATUS NAMES
|
||||
|
||||
Local Volumes space usage:
|
||||
|
||||
@ -44,8 +44,17 @@ VOLUME NAME LINKS SIZE
|
||||
|
||||
Build cache usage: 0B
|
||||
|
||||
CACHE ID CACHE TYPE SIZE CREATED LAST USED USAGE SHARED
|
||||
`,
|
||||
},
|
||||
{
|
||||
DiskUsageContext{Verbose: true, Context: Context{Format: NewDiskUsageFormat("raw", true)}},
|
||||
``,
|
||||
},
|
||||
{
|
||||
DiskUsageContext{Verbose: true, Context: Context{Format: NewDiskUsageFormat("{{json .}}", true)}},
|
||||
`{"Images":[],"Containers":[],"Volumes":[],"BuildCache":[]}`,
|
||||
},
|
||||
// Errors
|
||||
{
|
||||
DiskUsageContext{
|
||||
@ -69,7 +78,7 @@ Build cache usage: 0B
|
||||
{
|
||||
DiskUsageContext{
|
||||
Context: Context{
|
||||
Format: NewDiskUsageFormat("table"),
|
||||
Format: NewDiskUsageFormat("table", false),
|
||||
},
|
||||
},
|
||||
`TYPE TOTAL ACTIVE SIZE RECLAIMABLE
|
||||
@ -82,7 +91,7 @@ Build Cache 0 0 0B
|
||||
{
|
||||
DiskUsageContext{
|
||||
Context: Context{
|
||||
Format: NewDiskUsageFormat("table {{.Type}}\t{{.Active}}"),
|
||||
Format: NewDiskUsageFormat("table {{.Type}}\t{{.Active}}", false),
|
||||
},
|
||||
},
|
||||
string(golden.Get(t, "disk-usage-context-write-custom.golden")),
|
||||
@ -91,7 +100,7 @@ Build Cache 0 0 0B
|
||||
{
|
||||
DiskUsageContext{
|
||||
Context: Context{
|
||||
Format: NewDiskUsageFormat("raw"),
|
||||
Format: NewDiskUsageFormat("raw", false),
|
||||
},
|
||||
},
|
||||
string(golden.Get(t, "disk-usage-raw-format.golden")),
|
||||
|
||||
154
cli/command/formatter/licenses.go
Normal file
154
cli/command/formatter/licenses.go
Normal file
@ -0,0 +1,154 @@
|
||||
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
cli/command/formatter/licenses_test.go
Normal file
256
cli/command/formatter/licenses_test.go
Normal file
@ -0,0 +1,256 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
@ -599,7 +599,13 @@ func (c *serviceContext) Ports() string {
|
||||
pr := portRange{}
|
||||
ports := []string{}
|
||||
|
||||
sort.Sort(byProtocolAndPublishedPort(c.service.Endpoint.Ports))
|
||||
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
|
||||
})
|
||||
|
||||
for _, p := range c.service.Endpoint.Ports {
|
||||
if p.PublishMode == swarm.PortConfigPublishModeIngress {
|
||||
@ -633,14 +639,3 @@ 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
|
||||
}
|
||||
|
||||
@ -133,18 +133,3 @@ 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]
|
||||
}
|
||||
|
||||
@ -222,7 +222,7 @@ eve foobarbazquxquux, key31, key32
|
||||
}
|
||||
|
||||
for _, testcase := range cases {
|
||||
signerInfo := SignerInfoList{
|
||||
signerInfo := []SignerInfo{
|
||||
{Name: "alice", Keys: []string{"key11", "key12"}},
|
||||
{Name: "bob", Keys: []string{"key21"}},
|
||||
{Name: "eve", Keys: []string{"key31", "key32", "foobarbazquxquux"}},
|
||||
|
||||
73
cli/command/formatter/updates.go
Normal file
73
cli/command/formatter/updates.go
Normal file
@ -0,0 +1,73 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
clitypes "github.com/docker/cli/types"
|
||||
)
|
||||
|
||||
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 []clitypes.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 clitypes.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
cli/command/formatter/updates_test.go
Normal file
143
cli/command/formatter/updates_test.go
Normal file
@ -0,0 +1,143 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"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 := []clitypes.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 := []clitypes.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 := []clitypes.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))
|
||||
}
|
||||
}
|
||||
@ -57,7 +57,7 @@ type buildOptions struct {
|
||||
isolation string
|
||||
quiet bool
|
||||
noCache bool
|
||||
console opts.NullableBool
|
||||
progress string
|
||||
rm bool
|
||||
forceRm bool
|
||||
pull bool
|
||||
@ -71,6 +71,8 @@ type buildOptions struct {
|
||||
stream bool
|
||||
platform string
|
||||
untrusted bool
|
||||
secrets []string
|
||||
ssh []string
|
||||
}
|
||||
|
||||
// dockerfileFromStdin returns true when the user specified that the Dockerfile
|
||||
@ -134,6 +136,8 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image")
|
||||
flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources")
|
||||
flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip")
|
||||
flags.SetAnnotation("compress", "no-buildkit", nil)
|
||||
|
||||
flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options")
|
||||
flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build")
|
||||
flags.SetAnnotation("network", "version", []string{"1.25"})
|
||||
@ -151,10 +155,18 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.BoolVar(&options.stream, "stream", false, "Stream attaches to server to negotiate build context")
|
||||
flags.SetAnnotation("stream", "experimental", nil)
|
||||
flags.SetAnnotation("stream", "version", []string{"1.31"})
|
||||
flags.SetAnnotation("stream", "no-buildkit", nil)
|
||||
|
||||
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"})
|
||||
flags.StringVar(&options.progress, "progress", "auto", "Set type of progress output (auto, plain, tty). Use plain to show container output")
|
||||
flags.SetAnnotation("progress", "buildkit", nil)
|
||||
|
||||
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.SetAnnotation("secret", "buildkit", nil)
|
||||
|
||||
flags.StringArrayVar(&options.ssh, "ssh", []string{}, "SSH agent socket or keys to expose to the build (only if BuildKit enabled) (format: default|<id>[=<socket>|<key>[,<key>]])")
|
||||
flags.SetAnnotation("ssh", "version", []string{"1.39"})
|
||||
flags.SetAnnotation("ssh", "buildkit", nil)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -176,14 +188,17 @@ func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error {
|
||||
|
||||
// nolint: gocyclo
|
||||
func runBuild(dockerCli command.Cli, options buildOptions) error {
|
||||
if os.Getenv("DOCKER_BUILDKIT") != "" {
|
||||
buildkitEnabled, err := command.BuildKitEnabled(dockerCli.ServerInfo())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if buildkitEnabled {
|
||||
return runBuildBuildKit(dockerCli, options)
|
||||
}
|
||||
|
||||
var (
|
||||
buildCtx io.ReadCloser
|
||||
dockerfileCtx io.ReadCloser
|
||||
err error
|
||||
contextDir string
|
||||
tempDir string
|
||||
relDockerfile string
|
||||
@ -263,15 +278,12 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
|
||||
}
|
||||
|
||||
// And canonicalize dockerfile name to a platform-independent one
|
||||
relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile)
|
||||
if err != nil {
|
||||
return errors.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err)
|
||||
}
|
||||
relDockerfile = archive.CanonicalTarNameForPath(relDockerfile)
|
||||
|
||||
excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, options.dockerfileFromStdin())
|
||||
buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
|
||||
ExcludePatterns: excludes,
|
||||
ChownOpts: &idtools.IDPair{UID: 0, GID: 0},
|
||||
ChownOpts: &idtools.Identity{UID: 0, GID: 0},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@ -338,7 +350,7 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
|
||||
buildCtx = dockerfileCtx
|
||||
}
|
||||
|
||||
s, err := trySession(dockerCli, contextDir)
|
||||
s, err := trySession(dockerCli, contextDir, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -1,29 +1,37 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"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/session/sshforward/sshprovider"
|
||||
"github.com/moby/buildkit/util/appcontext"
|
||||
"github.com/moby/buildkit/util/progress/progressui"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tonistiigi/fsutil"
|
||||
fsutiltypes "github.com/tonistiigi/fsutil/types"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
@ -35,7 +43,7 @@ var errDockerfileConflict = errors.New("ambiguous Dockerfile source: both stdin
|
||||
func runBuildBuildKit(dockerCli command.Cli, options buildOptions) error {
|
||||
ctx := appcontext.Context()
|
||||
|
||||
s, err := trySession(dockerCli, options.context)
|
||||
s, err := trySession(dockerCli, options.context, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -43,6 +51,13 @@ func runBuildBuildKit(dockerCli command.Cli, options buildOptions) error {
|
||||
return errors.Errorf("buildkit not supported by daemon")
|
||||
}
|
||||
|
||||
if options.imageIDFile != "" {
|
||||
// Avoid leaving a stale file if we eventually fail
|
||||
if err := os.Remove(options.imageIDFile); err != nil && !os.IsNotExist(err) {
|
||||
return errors.Wrap(err, "removing image ID file")
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
remote string
|
||||
body io.Reader
|
||||
@ -117,6 +132,20 @@ 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)
|
||||
}
|
||||
if len(options.ssh) > 0 {
|
||||
sshp, err := parseSSHSpecs(options.ssh)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not parse ssh: %v", options.ssh)
|
||||
}
|
||||
s.Allow(sshp)
|
||||
}
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
@ -159,6 +188,7 @@ func runBuildBuildKit(dockerCli command.Cli, options buildOptions) error {
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
//nolint: gocyclo
|
||||
func doBuild(ctx context.Context, eg *errgroup.Group, dockerCli command.Cli, options buildOptions, buildOptions types.ImageBuildOptions) (finalErr error) {
|
||||
response, err := dockerCli.Client().ImageBuild(context.Background(), nil, buildOptions)
|
||||
if err != nil {
|
||||
@ -180,17 +210,19 @@ func doBuild(ctx context.Context, eg *errgroup.Group, dockerCli command.Cli, opt
|
||||
t := newTracer()
|
||||
ssArr := []*client.SolveStatus{}
|
||||
|
||||
displayStatus := func(displayCh chan *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
|
||||
out := os.Stderr
|
||||
// TODO: Handle interactive output in non-interactive environment.
|
||||
consoleOpt := options.console.Value()
|
||||
if cons, err := console.ConsoleFromFile(out); err == nil && (consoleOpt == nil || *consoleOpt) {
|
||||
// TODO: Handle tty output in non-tty environment.
|
||||
if cons, err := console.ConsoleFromFile(out); err == nil && (options.progress == "auto" || options.progress == "tty") {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@ -210,15 +242,31 @@ func doBuild(ctx context.Context, eg *errgroup.Group, dockerCli command.Cli, opt
|
||||
}
|
||||
close(displayCh)
|
||||
}()
|
||||
displayStatus(displayCh)
|
||||
displayStatus(os.Stderr, displayCh)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
displayStatus(t.displayCh)
|
||||
displayStatus(os.Stdout, t.displayCh)
|
||||
}
|
||||
defer close(t.displayCh)
|
||||
err = jsonmessage.DisplayJSONMessagesStream(response.Body, os.Stdout, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), t.write)
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
imageID := ""
|
||||
writeAux := func(msg jsonmessage.JSONMessage) {
|
||||
if msg.ID == "moby.image.id" {
|
||||
var result types.BuildResult
|
||||
if err := json.Unmarshal(*msg.Aux, &result); err != nil {
|
||||
fmt.Fprintf(dockerCli.Err(), "failed to parse aux message: %v", err)
|
||||
}
|
||||
imageID = result.ID
|
||||
return
|
||||
}
|
||||
t.write(msg)
|
||||
}
|
||||
|
||||
err = jsonmessage.DisplayJSONMessagesStream(response.Body, buf, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), writeAux)
|
||||
if err != nil {
|
||||
if jerr, ok := err.(*jsonmessage.JSONError); ok {
|
||||
// If no error code is set, default to 1
|
||||
@ -228,10 +276,30 @@ func doBuild(ctx context.Context, eg *errgroup.Group, dockerCli command.Cli, opt
|
||||
return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code}
|
||||
}
|
||||
}
|
||||
|
||||
// Everything worked so if -q was provided the output from the daemon
|
||||
// should be just the image ID and we'll print that to stdout.
|
||||
//
|
||||
// TODO: we may want to use Aux messages with ID "moby.image.id" regardless of options.quiet (i.e. don't send HTTP param q=1)
|
||||
// instead of assuming that output is image ID if options.quiet.
|
||||
if options.quiet {
|
||||
imageID = buf.String()
|
||||
fmt.Fprint(dockerCli.Out(), imageID)
|
||||
}
|
||||
|
||||
if options.imageIDFile != "" {
|
||||
if imageID == "" {
|
||||
return errors.Errorf("cannot write %s because server did not provide an image ID", options.imageIDFile)
|
||||
}
|
||||
imageID = strings.TrimSpace(imageID)
|
||||
if err := ioutil.WriteFile(options.imageIDFile, []byte(imageID), 0666); err != nil {
|
||||
return errors.Wrap(err, "cannot write image ID file")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func resetUIDAndGID(s *fsutil.Stat) bool {
|
||||
func resetUIDAndGID(s *fsutiltypes.Stat) bool {
|
||||
s.Uid = 0
|
||||
s.Gid = 0
|
||||
return true
|
||||
@ -298,3 +366,76 @@ 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
|
||||
}
|
||||
|
||||
func parseSSHSpecs(sl []string) (session.Attachable, error) {
|
||||
configs := make([]sshprovider.AgentConfig, 0, len(sl))
|
||||
for _, v := range sl {
|
||||
c, err := parseSSH(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configs = append(configs, *c)
|
||||
}
|
||||
return sshprovider.NewSSHAgentProvider(configs)
|
||||
}
|
||||
|
||||
func parseSSH(value string) (*sshprovider.AgentConfig, error) {
|
||||
parts := strings.SplitN(value, "=", 2)
|
||||
cfg := sshprovider.AgentConfig{
|
||||
ID: parts[0],
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
cfg.Paths = strings.Split(parts[1], ",")
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
@ -27,13 +27,16 @@ import (
|
||||
|
||||
const clientSessionRemote = "client-session"
|
||||
|
||||
func isSessionSupported(dockerCli command.Cli) bool {
|
||||
func isSessionSupported(dockerCli command.Cli, forStream bool) bool {
|
||||
if !forStream && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.39") {
|
||||
return true
|
||||
}
|
||||
return dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31")
|
||||
}
|
||||
|
||||
func trySession(dockerCli command.Cli, contextDir string) (*session.Session, error) {
|
||||
func trySession(dockerCli command.Cli, contextDir string, forStream bool) (*session.Session, error) {
|
||||
var s *session.Session
|
||||
if isSessionSupported(dockerCli) {
|
||||
if isSessionSupported(dockerCli, forStream) {
|
||||
sharedKey, err := getBuildSharedKey(contextDir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get build shared key")
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@ -17,6 +18,7 @@ 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"
|
||||
@ -173,6 +175,66 @@ 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
|
||||
|
||||
@ -19,6 +19,7 @@ type importOptions struct {
|
||||
reference string
|
||||
changes dockeropts.ListOpts
|
||||
message string
|
||||
platform string
|
||||
}
|
||||
|
||||
// NewImportCommand creates a new `docker import` command
|
||||
@ -43,6 +44,7 @@ func NewImportCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options.changes = dockeropts.NewListOpts(nil)
|
||||
flags.VarP(&options.changes, "change", "c", "Apply Dockerfile instruction to the created image")
|
||||
flags.StringVarP(&options.message, "message", "m", "", "Set commit message for imported image")
|
||||
command.AddPlatformFlag(flags, &options.platform)
|
||||
|
||||
return cmd
|
||||
}
|
||||
@ -71,8 +73,9 @@ func runImport(dockerCli command.Cli, options importOptions) error {
|
||||
}
|
||||
|
||||
importOptions := types.ImageImportOptions{
|
||||
Message: options.message,
|
||||
Changes: options.changes.GetAll(),
|
||||
Message: options.message,
|
||||
Changes: options.changes.GetAll(),
|
||||
Platform: options.platform,
|
||||
}
|
||||
|
||||
clnt := dockerCli.Client()
|
||||
|
||||
@ -3,11 +3,14 @@ package image
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -54,8 +57,24 @@ Are you sure you want to continue?`
|
||||
Are you sure you want to continue?`
|
||||
)
|
||||
|
||||
// cloneFilter is a temporary workaround that uses existing public APIs from the filters package to clone a filter.
|
||||
// TODO(tiborvass): remove this once filters.Args.Clone() is added.
|
||||
func cloneFilter(args filters.Args) (newArgs filters.Args, err error) {
|
||||
if args.Len() == 0 {
|
||||
return filters.NewArgs(), nil
|
||||
}
|
||||
b, err := args.MarshalJSON()
|
||||
if err != nil {
|
||||
return newArgs, err
|
||||
}
|
||||
return filters.FromJSON(string(b))
|
||||
}
|
||||
|
||||
func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
|
||||
pruneFilters := options.filter.Value()
|
||||
pruneFilters, err := cloneFilter(options.filter.Value())
|
||||
if err != nil {
|
||||
return 0, "", errors.Wrap(err, "could not copy filter in image prune")
|
||||
}
|
||||
pruneFilters.Add("dangling", fmt.Sprintf("%v", !options.all))
|
||||
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
|
||||
|
||||
@ -73,14 +92,20 @@ func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint6
|
||||
}
|
||||
|
||||
if len(report.ImagesDeleted) > 0 {
|
||||
output = "Deleted Images:\n"
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Deleted Images:\n")
|
||||
for _, st := range report.ImagesDeleted {
|
||||
if st.Untagged != "" {
|
||||
output += fmt.Sprintln("untagged:", st.Untagged)
|
||||
sb.WriteString("untagged: ")
|
||||
sb.WriteString(st.Untagged)
|
||||
sb.WriteByte('\n')
|
||||
} else {
|
||||
output += fmt.Sprintln("deleted:", st.Deleted)
|
||||
sb.WriteString("deleted: ")
|
||||
sb.WriteString(st.Deleted)
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
output = sb.String()
|
||||
spaceReclaimed = report.SpaceReclaimed
|
||||
}
|
||||
|
||||
|
||||
@ -70,6 +70,14 @@ func TestNewPruneCommandSuccess(t *testing.T) {
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "label-filter",
|
||||
args: []string{"--force", "--filter", "label=foobar"},
|
||||
imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) {
|
||||
assert.Check(t, is.Equal("foobar", pruneFilter.Get("label")[0]))
|
||||
return types.ImagesPruneReport{}, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "force-untagged",
|
||||
args: []string{"--force"},
|
||||
|
||||
1
cli/command/image/testdata/prune-command-success.label-filter.golden
vendored
Normal file
1
cli/command/image/testdata/prune-command-success.label-filter.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
Total reclaimed space: 0B
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/store"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -64,20 +65,23 @@ func runManifestAnnotate(dockerCli command.Cli, opts annotateOptions) error {
|
||||
}
|
||||
|
||||
// Update the mf
|
||||
if imageManifest.Descriptor.Platform == nil {
|
||||
imageManifest.Descriptor.Platform = new(ocispec.Platform)
|
||||
}
|
||||
if opts.os != "" {
|
||||
imageManifest.Platform.OS = opts.os
|
||||
imageManifest.Descriptor.Platform.OS = opts.os
|
||||
}
|
||||
if opts.arch != "" {
|
||||
imageManifest.Platform.Architecture = opts.arch
|
||||
imageManifest.Descriptor.Platform.Architecture = opts.arch
|
||||
}
|
||||
for _, osFeature := range opts.osFeatures {
|
||||
imageManifest.Platform.OSFeatures = appendIfUnique(imageManifest.Platform.OSFeatures, osFeature)
|
||||
imageManifest.Descriptor.Platform.OSFeatures = appendIfUnique(imageManifest.Descriptor.Platform.OSFeatures, osFeature)
|
||||
}
|
||||
if opts.variant != "" {
|
||||
imageManifest.Platform.Variant = opts.variant
|
||||
imageManifest.Descriptor.Platform.Variant = opts.variant
|
||||
}
|
||||
|
||||
if !isValidOSArch(imageManifest.Platform.OS, imageManifest.Platform.Architecture) {
|
||||
if !isValidOSArch(imageManifest.Descriptor.Platform.OS, imageManifest.Descriptor.Platform.Architecture) {
|
||||
return errors.Errorf("manifest entry for image has unsupported os/arch combination: %s/%s", opts.os, opts.arch)
|
||||
}
|
||||
return manifestStore.Save(targetRef, imgRef, imageManifest)
|
||||
|
||||
@ -15,6 +15,7 @@ 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) {
|
||||
@ -45,4 +46,11 @@ 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{}
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -123,7 +124,7 @@ func printManifestList(dockerCli command.Cli, namedRef reference.Named, list []t
|
||||
for _, img := range list {
|
||||
mfd, err := buildManifestDescriptor(targetRepo, img)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error assembling ManifestDescriptor")
|
||||
return errors.Wrap(err, "failed to assemble ManifestDescriptor")
|
||||
}
|
||||
manifests = append(manifests, mfd)
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/distribution/reference"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/assert"
|
||||
is "gotest.tools/assert/cmp"
|
||||
@ -50,8 +51,22 @@ func fullImageManifest(t *testing.T, ref reference.Named) types.ImageManifest {
|
||||
},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
// TODO: include image data for verbose inspect
|
||||
return types.NewImageManifest(ref, digest.Digest("sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd"), types.Image{OS: "linux", Architecture: "amd64"}, man)
|
||||
mt, raw, err := man.Payload()
|
||||
assert.NilError(t, err)
|
||||
|
||||
desc := ocispec.Descriptor{
|
||||
Digest: digest.FromBytes(raw),
|
||||
Size: int64(len(raw)),
|
||||
MediaType: mt,
|
||||
Platform: &ocispec.Platform{
|
||||
Architecture: "amd64",
|
||||
OS: "linux",
|
||||
},
|
||||
}
|
||||
|
||||
return types.NewImageManifest(ref, desc, man)
|
||||
}
|
||||
|
||||
func TestInspectCommandLocalManifestNotFound(t *testing.T) {
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/distribution/reference"
|
||||
@ -141,7 +142,9 @@ func buildManifestList(manifests []types.ImageManifest, targetRef reference.Name
|
||||
|
||||
descriptors := []manifestlist.ManifestDescriptor{}
|
||||
for _, imageManifest := range manifests {
|
||||
if imageManifest.Platform.Architecture == "" || imageManifest.Platform.OS == "" {
|
||||
if imageManifest.Descriptor.Platform == nil ||
|
||||
imageManifest.Descriptor.Platform.Architecture == "" ||
|
||||
imageManifest.Descriptor.Platform.OS == "" {
|
||||
return nil, errors.Errorf(
|
||||
"manifest %s must have an OS and Architecture to be pushed to a registry", imageManifest.Ref)
|
||||
}
|
||||
@ -167,17 +170,18 @@ func buildManifestDescriptor(targetRepo *registry.RepositoryInfo, imageManifest
|
||||
return manifestlist.ManifestDescriptor{}, errors.Errorf("cannot use source images from a different registry than the target image: %s != %s", manifestRepoHostname, targetRepoHostname)
|
||||
}
|
||||
|
||||
mediaType, raw, err := imageManifest.Payload()
|
||||
if err != nil {
|
||||
return manifestlist.ManifestDescriptor{}, err
|
||||
manifest := manifestlist.ManifestDescriptor{
|
||||
Descriptor: distribution.Descriptor{
|
||||
Digest: imageManifest.Descriptor.Digest,
|
||||
Size: imageManifest.Descriptor.Size,
|
||||
MediaType: imageManifest.Descriptor.MediaType,
|
||||
},
|
||||
}
|
||||
|
||||
manifest := manifestlist.ManifestDescriptor{
|
||||
Platform: imageManifest.Platform,
|
||||
platform := types.PlatformSpecFromOCI(imageManifest.Descriptor.Platform)
|
||||
if platform != nil {
|
||||
manifest.Platform = *platform
|
||||
}
|
||||
manifest.Descriptor.Digest = imageManifest.Digest
|
||||
manifest.Size = int64(len(raw))
|
||||
manifest.MediaType = mediaType
|
||||
|
||||
if err = manifest.Descriptor.Digest.Validate(); err != nil {
|
||||
return manifestlist.ManifestDescriptor{}, errors.Wrapf(err,
|
||||
@ -195,7 +199,11 @@ func buildBlobRequestList(imageManifest types.ImageManifest, repoName reference.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blobReqs = append(blobReqs, manifestBlob{canonical: canonical, os: imageManifest.Platform.OS})
|
||||
var os string
|
||||
if imageManifest.Descriptor.Platform != nil {
|
||||
os = imageManifest.Descriptor.Platform.OS
|
||||
}
|
||||
blobReqs = append(blobReqs, manifestBlob{canonical: canonical, os: os})
|
||||
}
|
||||
return blobReqs, nil
|
||||
}
|
||||
@ -206,7 +214,7 @@ func buildPutManifestRequest(imageManifest types.ImageManifest, targetRef refere
|
||||
if err != nil {
|
||||
return mountRequest{}, err
|
||||
}
|
||||
mountRef, err := reference.WithDigest(refWithoutTag, imageManifest.Digest)
|
||||
mountRef, err := reference.WithDigest(refWithoutTag, imageManifest.Descriptor.Digest)
|
||||
if err != nil {
|
||||
return mountRequest{}, err
|
||||
}
|
||||
|
||||
@ -1,6 +1,18 @@
|
||||
{
|
||||
"Ref": "example.com/alpine:3.0",
|
||||
"Digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd",
|
||||
"Descriptor": {
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"digest": "sha256:1072e499f3f655a032e88542330cf75b02e7bdf673278f701d7ba61629ee3ebe",
|
||||
"size": 528,
|
||||
"platform": {
|
||||
"architecture": "arm",
|
||||
"os": "freebsd",
|
||||
"os.features": [
|
||||
"feature1"
|
||||
],
|
||||
"variant": "v7"
|
||||
}
|
||||
},
|
||||
"SchemaV2Manifest": {
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
@ -16,13 +28,5 @@
|
||||
"digest": "sha256:88286f41530e93dffd4b964e1db22ce4939fffa4a4c665dab8591fbab03d4926"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Platform": {
|
||||
"architecture": "arm",
|
||||
"os": "freebsd",
|
||||
"os.features": [
|
||||
"feature1"
|
||||
],
|
||||
"variant": "v7"
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"size": 428,
|
||||
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd",
|
||||
"size": 528,
|
||||
"digest": "sha256:1072e499f3f655a032e88542330cf75b02e7bdf673278f701d7ba61629ee3ebe",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
@ -13,8 +13,8 @@
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"size": 428,
|
||||
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd",
|
||||
"size": 528,
|
||||
"digest": "sha256:1072e499f3f655a032e88542330cf75b02e7bdf673278f701d7ba61629ee3ebe",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
|
||||
@ -18,6 +18,7 @@ type osArch struct {
|
||||
// list of valid os/arch values (see "Optional Environment Variables" section
|
||||
// of https://golang.org/doc/install/source
|
||||
// Added linux/s390x as we know System z support already exists
|
||||
// Keep in sync with _docker_manifest_annotate in contrib/completion/bash/docker
|
||||
var validOSArches = map[osArch]bool{
|
||||
{os: "darwin", arch: "386"}: true,
|
||||
{os: "darwin", arch: "amd64"}: true,
|
||||
|
||||
@ -10,14 +10,9 @@ 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
|
||||
@ -64,7 +59,9 @@ func runList(dockerCli command.Cli, options listOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(byNetworkName(networkResources))
|
||||
sort.Slice(networkResources, func(i, j int) bool {
|
||||
return sortorder.NaturalLess(networkResources[i].Name, networkResources[j].Name)
|
||||
})
|
||||
|
||||
networksCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
|
||||
@ -3,7 +3,6 @@ package network
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
@ -41,23 +40,55 @@ func TestNetworkListErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetworkListWithFlags(t *testing.T) {
|
||||
expectedOpts := types.NetworkListOptions{
|
||||
Filters: filters.NewArgs(filters.Arg("image.name", "ubuntu")),
|
||||
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
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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")
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,7 +70,7 @@ func runPrune(dockerCli command.Cli, options pruneOptions) (output string, err e
|
||||
|
||||
// RunPrune calls the Network Prune API
|
||||
// This returns the amount of space reclaimed and a detailed output string
|
||||
func RunPrune(dockerCli command.Cli, filter opts.FilterOpt) (uint64, string, error) {
|
||||
func RunPrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
|
||||
output, err := runPrune(dockerCli, pruneOptions{force: true, filter: filter})
|
||||
return 0, output, err
|
||||
}
|
||||
|
||||
3
cli/command/network/testdata/network-list-sort.golden
vendored
Normal file
3
cli/command/network/testdata/network-list-sort.golden
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
network-1-foo
|
||||
network-2-foo
|
||||
network-10-foo
|
||||
@ -1,2 +1,2 @@
|
||||
NETWORK ID NAME DRIVER SCOPE
|
||||
123454321 network_1 09.7.01 global
|
||||
123454321 network_1 09.7.01 global
|
||||
|
||||
@ -9,19 +9,10 @@ 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
|
||||
@ -80,6 +71,8 @@ func runList(dockerCli command.Cli, options listOptions) error {
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewNodeFormat(format, options.quiet),
|
||||
}
|
||||
sort.Sort(byHostname(nodes))
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
return sortorder.NaturalLess(nodes[i].Description.Hostname, nodes[j].Description.Hostname)
|
||||
})
|
||||
return formatter.NodeWrite(nodesCtx, nodes, info)
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
@ -14,6 +15,8 @@ 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 {
|
||||
@ -43,3 +46,18 @@ 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
|
||||
}
|
||||
|
||||
@ -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
cli/command/plugin/install_test.go
Normal file
141
cli/command/plugin/install_test.go
Normal file
@ -0,0 +1,141 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
@ -2,12 +2,14 @@ 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 {
|
||||
@ -46,6 +48,10 @@ 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
cli/command/plugin/list_test.go
Normal file
174
cli/command/plugin/list_test.go
Normal file
@ -0,0 +1,174 @@
|
||||
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
cli/command/plugin/testdata/plugin-list-sort.golden
vendored
Normal file
3
cli/command/plugin/testdata/plugin-list-sort.golden
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
plugin-1-foo
|
||||
plugin-2-foo
|
||||
plugin-10-foo
|
||||
1
cli/command/plugin/testdata/plugin-list-with-format.golden
vendored
Normal file
1
cli/command/plugin/testdata/plugin-list-with-format.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
name-foo
|
||||
1
cli/command/plugin/testdata/plugin-list-with-no-trunc-option.golden
vendored
Normal file
1
cli/command/plugin/testdata/plugin-list-with-no-trunc-option.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
xyg4z2hiSLO5yTnBJfg4OYia9gKA6Qjd
|
||||
1
cli/command/plugin/testdata/plugin-list-with-quiet-option.golden
vendored
Normal file
1
cli/command/plugin/testdata/plugin-list-with-quiet-option.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
id-foo
|
||||
2
cli/command/plugin/testdata/plugin-list-without-format.golden
vendored
Normal file
2
cli/command/plugin/testdata/plugin-list-without-format.golden
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
ID NAME DESCRIPTION ENABLED
|
||||
id-foo name-foo desc-bar true
|
||||
@ -11,6 +11,7 @@ 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"
|
||||
@ -26,9 +27,10 @@ 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 {
|
||||
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
|
||||
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 == "" {
|
||||
} else if info.IndexServerAddress == "" && debug.IsEnabled() {
|
||||
fmt.Fprintf(cli.Err(), "Warning: Empty registry endpoint from daemon. Using system default: %s\n", serverAddress)
|
||||
} else {
|
||||
serverAddress = info.IndexServerAddress
|
||||
|
||||
@ -125,6 +125,11 @@ 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
|
||||
}
|
||||
@ -167,3 +172,17 @@ 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
|
||||
}
|
||||
|
||||
@ -28,7 +28,6 @@ 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")
|
||||
|
||||
@ -9,7 +9,6 @@ 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"
|
||||
)
|
||||
@ -81,13 +80,14 @@ func runSearch(dockerCli command.Cli, options searchOptions) error {
|
||||
|
||||
clnt := dockerCli.Client()
|
||||
|
||||
unorderedResults, err := clnt.ImageSearch(ctx, options.term, searchOptions)
|
||||
results, err := clnt.ImageSearch(ctx, options.term, searchOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
results := searchResultsByStars(unorderedResults)
|
||||
sort.Sort(results)
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[j].StarCount < results[i].StarCount
|
||||
})
|
||||
searchCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewSearchFormat(options.format),
|
||||
@ -95,10 +95,3 @@ 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 }
|
||||
|
||||
@ -13,6 +13,7 @@ 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"
|
||||
@ -78,6 +79,8 @@ 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)
|
||||
|
||||
@ -9,19 +9,10 @@ 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
|
||||
@ -66,7 +57,9 @@ func runSecretList(dockerCli command.Cli, options listOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(bySecretName(secrets))
|
||||
sort.Slice(secrets, func(i, j int) bool {
|
||||
return sortorder.NaturalLess(secrets[i].Spec.Name, secrets[j].Spec.Name)
|
||||
})
|
||||
|
||||
secretCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
|
||||
@ -44,12 +44,6 @@ 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()
|
||||
@ -60,7 +54,9 @@ func runList(dockerCli command.Cli, options listOptions) error {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Sort(byName(services))
|
||||
sort.Slice(services, func(i, j int) bool {
|
||||
return sortorder.NaturalLess(services[i].Spec.Name, services[j].Spec.Name)
|
||||
})
|
||||
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
|
||||
|
||||
@ -598,7 +598,9 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
|
||||
}
|
||||
networks[i].Target = nwID
|
||||
}
|
||||
sort.Sort(byNetworkTarget(networks))
|
||||
sort.Slice(networks, func(i, j int) bool {
|
||||
return networks[i].Target < networks[j].Target
|
||||
})
|
||||
|
||||
resources, err := options.resources.ToResourceRequirements()
|
||||
if err != nil {
|
||||
|
||||
@ -140,7 +140,7 @@ loop:
|
||||
}
|
||||
|
||||
func updateNodeFilter(ctx context.Context, client client.APIClient, filter filters.Args) error {
|
||||
if filter.Include("node") {
|
||||
if filter.Contains("node") {
|
||||
nodeFilters := filter.Get("node")
|
||||
for _, nodeFilter := range nodeFilters {
|
||||
nodeReference, err := node.Reference(ctx, client, nodeFilter)
|
||||
|
||||
@ -302,6 +302,12 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags
|
||||
if task.Resources == nil {
|
||||
task.Resources = &swarm.ResourceRequirements{}
|
||||
}
|
||||
if task.Resources.Limits == nil {
|
||||
task.Resources.Limits = &swarm.Resources{}
|
||||
}
|
||||
if task.Resources.Reservations == nil {
|
||||
task.Resources.Reservations = &swarm.Resources{}
|
||||
}
|
||||
return task.Resources
|
||||
}
|
||||
|
||||
@ -753,20 +759,6 @@ 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{}
|
||||
|
||||
@ -796,7 +788,15 @@ func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) error {
|
||||
newMounts = append(newMounts, mount)
|
||||
}
|
||||
}
|
||||
sort.Sort(byMountSource(newMounts))
|
||||
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
|
||||
})
|
||||
*mounts = newMounts
|
||||
return nil
|
||||
}
|
||||
@ -886,16 +886,6 @@ 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
|
||||
@ -944,7 +934,11 @@ portLoop:
|
||||
}
|
||||
|
||||
// Sort the PortConfig to avoid unnecessary updates
|
||||
sort.Sort(byPortConfig(newPorts))
|
||||
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])
|
||||
})
|
||||
*portConfig = newPorts
|
||||
return nil
|
||||
}
|
||||
@ -1142,14 +1136,6 @@ 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
|
||||
@ -1198,7 +1184,9 @@ func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flag
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(byNetworkTarget(newNetworks))
|
||||
sort.Slice(newNetworks, func(i, j int) bool {
|
||||
return newNetworks[i].Target < newNetworks[j].Target
|
||||
})
|
||||
|
||||
spec.TaskTemplate.Networks = newNetworks
|
||||
return nil
|
||||
|
||||
@ -617,6 +617,38 @@ func TestUpdateIsolationValid(t *testing.T) {
|
||||
// and that values are not updated are not reset to their default value
|
||||
func TestUpdateLimitsReservations(t *testing.T) {
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{},
|
||||
},
|
||||
}
|
||||
|
||||
// test that updating works if the service did not previously
|
||||
// have limits set (https://github.com/moby/moby/issues/38363)
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
err := flags.Set(flagLimitCPU, "2")
|
||||
assert.NilError(t, err)
|
||||
err = flags.Set(flagLimitMemory, "200M")
|
||||
assert.NilError(t, err)
|
||||
err = updateService(context.Background(), nil, flags, &spec)
|
||||
assert.NilError(t, err)
|
||||
|
||||
spec = swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{},
|
||||
},
|
||||
}
|
||||
|
||||
// test that updating works if the service did not previously
|
||||
// have reservations set (https://github.com/moby/moby/issues/38363)
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
err = flags.Set(flagReserveCPU, "2")
|
||||
assert.NilError(t, err)
|
||||
err = flags.Set(flagReserveMemory, "200M")
|
||||
assert.NilError(t, err)
|
||||
err = updateService(context.Background(), nil, flags, &spec)
|
||||
assert.NilError(t, err)
|
||||
|
||||
spec = swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{},
|
||||
Resources: &swarm.ResourceRequirements{
|
||||
@ -632,8 +664,8 @@ func TestUpdateLimitsReservations(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
err := flags.Set(flagLimitCPU, "2")
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
err = flags.Set(flagLimitCPU, "2")
|
||||
assert.NilError(t, err)
|
||||
err = flags.Set(flagReserveCPU, "2")
|
||||
assert.NilError(t, err)
|
||||
|
||||
@ -8,6 +8,7 @@ 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"
|
||||
@ -27,7 +28,11 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Short: "Manage Docker stacks",
|
||||
Args: cli.NoArgs,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
orchestrator, err := getOrchestrator(dockerCli.ConfigFile(), cmd, dockerCli.Err())
|
||||
configFile := dockerCli.ConfigFile()
|
||||
if configFile == nil {
|
||||
configFile = cliconfig.LoadDefaultConfigFile(dockerCli.Err())
|
||||
}
|
||||
orchestrator, err := getOrchestrator(configFile, cmd, dockerCli.Err())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -42,9 +47,13 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command {
|
||||
},
|
||||
}
|
||||
defaultHelpFunc := cmd.HelpFunc()
|
||||
cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
||||
hideOrchestrationFlags(cmd, opts.orchestrator)
|
||||
defaultHelpFunc(cmd, args)
|
||||
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.AddCommand(
|
||||
newDeployCommand(dockerCli, &opts),
|
||||
|
||||
@ -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")
|
||||
flags.StringSliceVarP(&opts.Composefiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`)
|
||||
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 err
|
||||
return errors.Wrap(err, "unable to deploy to Kubernetes")
|
||||
}
|
||||
return kubernetes.RunDeploy(kli, opts, config)
|
||||
default:
|
||||
|
||||
@ -4,10 +4,12 @@ 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"
|
||||
@ -58,7 +60,10 @@ func WrapCli(dockerCli command.Cli, opts Options) (*KubeCli, error) {
|
||||
cli.kubeNamespace = opts.Namespace
|
||||
if opts.Namespace == "" {
|
||||
configNamespace, _, err := clientConfig.Namespace()
|
||||
if err != nil {
|
||||
switch {
|
||||
case os.IsNotExist(err), os.IsPermission(err):
|
||||
return nil, errors.Wrap(err, "unable to load configuration file")
|
||||
case err != nil:
|
||||
return nil, err
|
||||
}
|
||||
cli.kubeNamespace = configNamespace
|
||||
|
||||
@ -8,13 +8,72 @@ 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 {
|
||||
@ -30,44 +89,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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
cli/command/stack/kubernetes/convert_test.go
Normal file
18
cli/command/stack/kubernetes/convert_test.go
Normal file
@ -0,0 +1,18 @@
|
||||
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)
|
||||
}
|
||||
@ -8,16 +8,11 @@ import (
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// RunDeploy is the kubernetes implementation of docker stack deploy
|
||||
func RunDeploy(dockerCli *KubeCli, opts options.Deploy, cfg *composetypes.Config) error {
|
||||
cmdOut := dockerCli.Out()
|
||||
// Check arguments
|
||||
if len(opts.Composefiles) == 0 {
|
||||
return errors.Errorf("Please specify only one compose file (with --compose-file).")
|
||||
}
|
||||
|
||||
// Initialize clients
|
||||
composeClient, err := dockerCli.composeClient()
|
||||
@ -75,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
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,17 +2,10 @@ 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"
|
||||
@ -20,16 +13,17 @@ import (
|
||||
|
||||
// StackClient talks to a kubernetes compose component.
|
||||
type StackClient interface {
|
||||
CreateOrUpdate(s stack) error
|
||||
StackConverter
|
||||
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
|
||||
FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error)
|
||||
Get(name string) (Stack, error)
|
||||
List(opts metav1.ListOptions) ([]Stack, error)
|
||||
IsColliding(servicesClient corev1.ServiceInterface, s Stack) error
|
||||
}
|
||||
|
||||
// stackV1Beta1 implements stackClient interface and talks to compose component v1beta1.
|
||||
type stackV1Beta1 struct {
|
||||
stackV1Beta1Converter
|
||||
stacks composev1beta1.StackInterface
|
||||
}
|
||||
|
||||
@ -41,10 +35,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
|
||||
}
|
||||
@ -57,20 +51,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 {
|
||||
@ -82,9 +76,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
|
||||
}
|
||||
}
|
||||
@ -108,31 +102,9 @@ 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
|
||||
}
|
||||
|
||||
@ -144,10 +116,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
|
||||
}
|
||||
@ -160,20 +132,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])
|
||||
}
|
||||
@ -181,17 +153,6 @@ 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,18 +25,14 @@ 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
|
||||
networks: {}
|
||||
volumes: {}
|
||||
secrets: {}
|
||||
configs: {}
|
||||
`), s.composeFile)
|
||||
`), s.ComposeFile)
|
||||
}
|
||||
|
||||
func TestFromComposeUnsupportedVersion(t *testing.T) {
|
||||
|
||||
@ -51,121 +51,134 @@ func TestStackPsErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPSWithEmptyName(t *testing.T) {
|
||||
cmd := newPsCommand(test.NewFakeCli(&fakeClient{}), &orchestrator)
|
||||
cmd.SetArgs([]string{"' '"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
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")
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user