diff options
-rw-r--r-- | cmd/podman/machine/machine_unix.go | 4 | ||||
-rw-r--r-- | docs/source/Tutorials.rst | 3 | ||||
-rw-r--r-- | docs/tutorials/mac_win_client.md | 4 | ||||
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | go.sum | 3 | ||||
-rwxr-xr-x | hack/parse-localbenchmarks | 104 | ||||
-rw-r--r-- | libpod/info.go | 11 | ||||
-rw-r--r-- | libpod/info_test.go | 8 | ||||
-rw-r--r-- | libpod/runtime.go | 2 | ||||
-rw-r--r-- | libpod/runtime_ctr.go | 6 | ||||
-rw-r--r-- | pkg/api/handlers/compat/images_build.go | 49 | ||||
-rw-r--r-- | pkg/machine/wsl/machine.go | 40 | ||||
-rw-r--r-- | pkg/specgen/generate/pause_image.go | 6 | ||||
-rw-r--r-- | test/apiv2/10-images.at | 4 | ||||
-rw-r--r-- | test/system/170-run-userns.bats | 13 | ||||
-rw-r--r-- | vendor/modules.txt | 2 |
16 files changed, 234 insertions, 27 deletions
diff --git a/cmd/podman/machine/machine_unix.go b/cmd/podman/machine/machine_unix.go index 213c24f8c..b56d081ec 100644 --- a/cmd/podman/machine/machine_unix.go +++ b/cmd/podman/machine/machine_unix.go @@ -1,5 +1,5 @@ -//go:build linux || ignore || aix || ignore || android || ignore || darwin || ignore || freebsd || ignore || hurd || ignore || illumos || ignore || ios || ignore || netbsd || ignore || openbsd || ignore || solaris -// +build linux ignore aix ignore android ignore darwin ignore freebsd ignore hurd ignore illumos ignore ios ignore netbsd ignore openbsd ignore solaris +//go:build linux || aix || android || darwin || dragonfly || freebsd || hurd || illumos || ios || netbsd || openbsd || solaris +// +build linux aix android darwin dragonfly freebsd hurd illumos ios netbsd openbsd solaris package machine diff --git a/docs/source/Tutorials.rst b/docs/source/Tutorials.rst index 34a029484..c2cbcb8a9 100644 --- a/docs/source/Tutorials.rst +++ b/docs/source/Tutorials.rst @@ -6,7 +6,8 @@ Here are a number of useful tutorials to get you up and running with Podman. If * `Basic Setup and Use of Podman <https://github.com/containers/podman/blob/main/docs/tutorials/podman_tutorial.md>`_: Learn how to setup Podman and perform some basic commands with the utility. * `Basic Setup and Use of Podman in a Rootless environment <https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md>`_: The steps required to setup rootless Podman are enumerated. -* `Podman Mac/Windows tutorial <https://github.com/containers/podman/blob/main/docs/tutorials/mac_win_client.md>`_: Special setup for running the Podman remote client on a Mac or Windows PC and connecting to Podman running on a Linux VM are documented. +* `Podman for Windows <https://github.com/containers/podman/blob/main/docs/tutorials/podman-for-windows.md>`_: A guide to installing and using Podman on Windows. +* `Podman Remote Clients on Mac/Windows <https://github.com/containers/podman/blob/main/docs/tutorials/mac_win_client.md>`_: Advanced setup for connecting to a remote Linux system using the Podman remote client on Mac and Windows. * `How to sign and distribute container images using Podman <https://github.com/containers/podman/blob/main/docs/tutorials/image_signing.md>`_: Learn how to setup and use image signing with Podman. * `Podman remote-client tutorial <https://github.com/containers/podman/blob/main/docs/tutorials/remote_client.md>`_: A brief how-to on using the Podman remote-client. * `How to use libpod for custom/derivative projects <https://github.com/containers/podman/blob/main/docs/tutorials/podman-derivative-api.md>`_: How the libpod API can be used within your own project. diff --git a/docs/tutorials/mac_win_client.md b/docs/tutorials/mac_win_client.md index 159296d5e..553a38394 100644 --- a/docs/tutorials/mac_win_client.md +++ b/docs/tutorials/mac_win_client.md @@ -1,5 +1,9 @@ # Podman Remote clients for macOS and Windows +*** +**_NOTE:_** For running Podman on Windows, refer to the [Podman for Windows](podman-for-windows.md) guide, which uses the recommended approach of a Podman-managed Linux backend. For Mac, see the [Podman installation instructions](https://podman.io/getting-started/installation). This guide covers the advanced usage of Podman with a custom Linux VM or a remote external Linux system. +*** + ## Introduction The core Podman runtime environment can only run on Linux operating systems. But other operating systems can use the “remote client” to manage their containers to a Linux backend. This remote client is nearly identical to the standard Podman program. Certain functions that do not make sense for remote clients have been removed. For example, the “--latest” switch for container commands has been removed. @@ -24,7 +24,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/digitalocean/go-qemu v0.0.0-20210326154740-ac9e0b687001 github.com/docker/distribution v2.8.1+incompatible - github.com/docker/docker v20.10.15+incompatible + github.com/docker/docker v20.10.16+incompatible github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11 github.com/docker/go-plugins-helpers v0.0.0-20211224144127-6eecb7beb651 github.com/docker/go-units v0.4.0 @@ -431,8 +431,9 @@ github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r github.com/docker/docker v20.10.3-0.20220208084023-a5c757555091+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.12+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.14+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v20.10.15+incompatible h1:dk9FewY/9Xwm4ay/HViEEHSQuM/kL4F+JaG6GQdgmGo= github.com/docker/docker v20.10.15+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.16+incompatible h1:2Db6ZR/+FUR3hqPMwnogOPHFn405crbpxvWzKovETOQ= +github.com/docker/docker v20.10.16+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o= github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= diff --git a/hack/parse-localbenchmarks b/hack/parse-localbenchmarks new file mode 100755 index 000000000..6e22cabbb --- /dev/null +++ b/hack/parse-localbenchmarks @@ -0,0 +1,104 @@ +#!/usr/bin/perl +# +# parse-localbenchmarks - convert localbenchmarks output to CSV +# +# This is a filter. It transforms data from one format to another. Usage: +# +# $ make localbenchmarks &> mylogfile +# $ hack/parse-localbenchmarks <mylogfile > benchmarks.csv +# +# To be more precise, this is a very stupid simpleminded filter. It is +# not a complete solution to the benchmarks problem. In particular, +# other tools are still needed to: +# +# * Actually _run_ the benchmarks in some standard production environment +# * Run this script on the results +# * Save results, with identifying tags (datetime, git hash, PR id, ...) +# * Compare two or more sets of CSVs +# +(our $ME = $0) =~ s|^.*/||; # script name + +use v5.14; +use utf8; + +# FIXME: add --help. Some day. Not urgent. +die "$ME: This is a filter, not an interactive tool\n" if -t *STDIN; + +my $n_samples; # Number of timing runs (FIXME: unused) +my %results; # Timing results +my @benchmarks; # Names of benchmarks +my ($type, $testname); # Current context + +# +# Pass 1: read in timings +# +while (my $line = <STDIN>) { + # Log will have lots of ginkgo output. The only thing we care about is + # the summary at the end, which will look something like: + # + # * [MEASUREMENT] + # Podman Benchmark Suite + # .... + # Ran 3 samples: + # [CPU] podman images: + # Fastest Time: 0.265s + # Slowest Time: 0.322s + # Average Time: 0.302s ± 0.018s + # [MEM] podman images: + # Smallest: 44076.0KB + # Largest: 44616.0KB + # Average: 44338.7KB ± 171.2KB + # [CPU] podman push: + # ....repeat [CPU] and [MEM] for each test + # -------------------------- + # SSSSSSSSSSSSSSSSSSSSS (and more ginkgo output we don't care about) + # + chomp $line; + next unless $line =~ /^.{1,3}\s+\[MEASUREMENT\]/ .. $line =~ /^-{20,}$/; + + # Trim leading & trailing whitespace + $line =~ s/(^\s+|\s+$)//g; + + # FIXME: we don't actually emit this. What would be a good way to do so? + if ($line =~ /^Ran\s+(\d+)\s+samples/) { + $n_samples = $1; + } + + # e.g., [CPU] podman foo: + elsif ($line =~ /^\[([A-Z]+)\]\s+(\S.*\S):$/) { + ($type, $testname) = ($1, $2); + } + + # e.g., 'Fastest Time: 0.265s' + elsif ($line =~ /^(\S.*?\S):\s+(.*)/) { + my $benchmark = "$type $1"; + $results{$testname}{$benchmark} = $2; + + # Keep an ordered list of benchmark names (as in, the order we + # encounter them) + push @benchmarks, $benchmark + unless grep { $_ eq $benchmark } @benchmarks; + } + + else { + warn "Cannot grok '$line'\n" if $ENV{DEBUG_PARSELOCALBENCHMARKS}; + } +} + +# +# Pass 2: write out CSV +# + +# Headings... +print "\"Test Name\""; +printf ", \"%s\"", $_ for @benchmarks; +print "\n"; + +# ...then data +for my $t (sort keys %results) { + printf "\"%s\"", $t; + for my $benchmark (@benchmarks) { + printf ", \"%s\"", $results{$t}{$benchmark} || ''; + } + print "\n"; +} diff --git a/libpod/info.go b/libpod/info.go index 321680a81..bc49a6cc9 100644 --- a/libpod/info.go +++ b/libpod/info.go @@ -406,26 +406,25 @@ func getCPUUtilization() (*define.CPUUsage, error) { } defer f.Close() scanner := bufio.NewScanner(f) - // Read firt line of /proc/stat + // Read first line of /proc/stat that has entries for system ("cpu" line) for scanner.Scan() { break } // column 1 is user, column 3 is system, column 4 is idle - stats := strings.Split(scanner.Text(), " ") + stats := strings.Fields(scanner.Text()) return statToPercent(stats) } func statToPercent(stats []string) (*define.CPUUsage, error) { - // There is always an extra space between cpu and the first metric - userTotal, err := strconv.ParseFloat(stats[2], 64) + userTotal, err := strconv.ParseFloat(stats[1], 64) if err != nil { return nil, errors.Wrapf(err, "unable to parse user value %q", stats[1]) } - systemTotal, err := strconv.ParseFloat(stats[4], 64) + systemTotal, err := strconv.ParseFloat(stats[3], 64) if err != nil { return nil, errors.Wrapf(err, "unable to parse system value %q", stats[3]) } - idleTotal, err := strconv.ParseFloat(stats[5], 64) + idleTotal, err := strconv.ParseFloat(stats[4], 64) if err != nil { return nil, errors.Wrapf(err, "unable to parse idle value %q", stats[4]) } diff --git a/libpod/info_test.go b/libpod/info_test.go index 909b573c0..b0e4bf8c0 100644 --- a/libpod/info_test.go +++ b/libpod/info_test.go @@ -20,7 +20,7 @@ func Test_statToPercent(t *testing.T) { }{ { name: "GoodParse", - args: args{in0: []string{"cpu", " ", "33628064", "27537", "9696996", "1314806705", "588142", "4775073", "2789228", "0", "598711", "0"}}, + args: args{in0: []string{"cpu", "33628064", "27537", "9696996", "1314806705", "588142", "4775073", "2789228", "0", "598711", "0"}}, want: &define.CPUUsage{ UserPercent: 2.48, SystemPercent: 0.71, @@ -30,19 +30,19 @@ func Test_statToPercent(t *testing.T) { }, { name: "BadUserValue", - args: args{in0: []string{"cpu", " ", "k", "27537", "9696996", "1314806705", "588142", "4775073", "2789228", "0", "598711", "0"}}, + args: args{in0: []string{"cpu", "k", "27537", "9696996", "1314806705", "588142", "4775073", "2789228", "0", "598711", "0"}}, want: nil, wantErr: assert.Error, }, { name: "BadSystemValue", - args: args{in0: []string{"cpu", " ", "33628064", "27537", "k", "1314806705", "588142", "4775073", "2789228", "0", "598711", "0"}}, + args: args{in0: []string{"cpu", "33628064", "27537", "k", "1314806705", "588142", "4775073", "2789228", "0", "598711", "0"}}, want: nil, wantErr: assert.Error, }, { name: "BadIdleValue", - args: args{in0: []string{"cpu", " ", "33628064", "27537", "9696996", "k", "588142", "4775073", "2789228", "0", "598711", "0"}}, + args: args{in0: []string{"cpu", "33628064", "27537", "9696996", "k", "588142", "4775073", "2789228", "0", "598711", "0"}}, want: nil, wantErr: assert.Error, }, diff --git a/libpod/runtime.go b/libpod/runtime.go index f4cd9cf00..58f20ef5b 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -349,7 +349,7 @@ func makeRuntime(runtime *Runtime) (retErr error) { // it will try to use existing XDG_RUNTIME_DIR // if current user has no write access to XDG_RUNTIME_DIR we will fail later if err := unix.Access(runtime.storageConfig.RunRoot, unix.W_OK); err != nil { - msg := "XDG_RUNTIME_DIR is pointing to a path which is not writable. Most likely podman will fail." + msg := fmt.Sprintf("RunRoot is pointing to a path (%s) which is not writable. Most likely podman will fail.", runtime.storageConfig.RunRoot) if errors.Is(err, os.ErrNotExist) { // if dir does not exists try to create it if err := os.MkdirAll(runtime.storageConfig.RunRoot, 0700); err != nil { diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index df7174ac6..7e8a21a8c 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -732,7 +732,11 @@ func (r *Runtime) removeContainer(ctx context.Context, c *Container, force, remo // after setting the state to ContainerStateRemoving will prevent that the container is // restarted if err := c.removeAllExecSessions(); err != nil { - return err + if cleanupErr == nil { + cleanupErr = err + } else { + logrus.Errorf("Remove exec sessions: %v", err) + } } // Stop the container's storage diff --git a/pkg/api/handlers/compat/images_build.go b/pkg/api/handlers/compat/images_build.go index f0d07f492..318688222 100644 --- a/pkg/api/handlers/compat/images_build.go +++ b/pkg/api/handlers/compat/images_build.go @@ -119,6 +119,7 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { Registry string `schema:"registry"` Rm bool `schema:"rm"` RusageLogFile string `schema:"rusagelogfile"` + Remote string `schema:"remote"` Seccomp string `schema:"seccomp"` Secrets string `schema:"secrets"` SecurityOpt string `schema:"securityopt"` @@ -169,14 +170,50 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { // convert addcaps formats containerFiles := []string{} - if _, found := r.URL.Query()["dockerfile"]; found { - var m = []string{} - if err := json.Unmarshal([]byte(query.Dockerfile), &m); err != nil { - // it's not json, assume just a string - m = []string{filepath.Join(contextDirectory, query.Dockerfile)} + // Tells if query paramemter `dockerfile` is set or not. + dockerFileSet := false + if utils.IsLibpodRequest(r) && query.Remote != "" { + // The context directory could be a URL. Try to handle that. + anchorDir, err := ioutil.TempDir(parse.GetTempDir(), "libpod_builder") + if err != nil { + utils.InternalServerError(w, err) + } + tempDir, subDir, err := buildahDefine.TempDirForURL(anchorDir, "buildah", query.Remote) + if err != nil { + utils.InternalServerError(w, err) + } + if tempDir != "" { + // We had to download it to a temporary directory. + // Delete it later. + defer func() { + if err = os.RemoveAll(tempDir); err != nil { + // We are deleting this on server so log on server end + // client does not have to worry about server cleanup. + logrus.Errorf("Cannot delete downloaded temp dir %q: %s", tempDir, err) + } + }() + contextDirectory = filepath.Join(tempDir, subDir) + } else { + // Nope, it was local. Use it as is. + absDir, err := filepath.Abs(query.Remote) + if err != nil { + utils.BadRequest(w, "remote", query.Remote, err) + } + contextDirectory = absDir } - containerFiles = m } else { + if _, found := r.URL.Query()["dockerfile"]; found { + var m = []string{} + if err := json.Unmarshal([]byte(query.Dockerfile), &m); err != nil { + // it's not json, assume just a string + m = []string{filepath.Join(contextDirectory, query.Dockerfile)} + } + containerFiles = m + dockerFileSet = true + } + } + + if !dockerFileSet { containerFiles = []string{filepath.Join(contextDirectory, "Dockerfile")} if utils.IsLibpodRequest(r) { containerFiles = []string{filepath.Join(contextDirectory, "Containerfile")} diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index 57fb36fc9..0b2874baf 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -36,6 +36,7 @@ var ( const ( ErrorSuccessRebootInitiated = 1641 ErrorSuccessRebootRequired = 3010 + currentMachineVersion = 2 ) const containersConf = `[containers] @@ -168,6 +169,8 @@ type MachineVM struct { Rootful bool // SSH identity, username, etc machine.SSHConfig + // machine version + Version int } type ExitCodeError struct { @@ -241,12 +244,29 @@ func readAndMigrate(configPath string, name string) (*MachineVM, error) { return vm, err } err = json.Unmarshal(b, vm) - if err == nil && vm.Created.IsZero() { - err = vm.migrate40(configPath) + if err == nil && vm.Version < currentMachineVersion { + err = vm.migrateMachine(configPath) } + return vm, err } +func (v *MachineVM) migrateMachine(configPath string) error { + if v.Created.IsZero() { + if err := v.migrate40(configPath); err != nil { + return err + } + } + + // Update older machines to use lingering + if err := enableUserLinger(v, toDist(v.Name)); err != nil { + return err + } + + v.Version = currentMachineVersion + return v.writeConfig() +} + func (v *MachineVM) migrate40(configPath string) error { v.ConfigPath = configPath fi, err := os.Stat(configPath) @@ -255,7 +275,7 @@ func (v *MachineVM) migrate40(configPath string) error { } v.Created = fi.ModTime() v.LastUp = getLegacyLastStart(v) - return v.writeConfig() + return nil } func getLegacyLastStart(vm *MachineVM) time.Time { @@ -284,6 +304,7 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { sshDir := filepath.Join(homeDir, ".ssh") v.IdentityPath = filepath.Join(sshDir, v.Name) v.Rootful = opts.Rootful + v.Version = currentMachineVersion if err := downloadDistro(v, opts); err != nil { return false, err @@ -486,6 +507,10 @@ func configureSystem(v *MachineVM, dist string) error { return errors.Wrap(err, "could not generate linger service for guest OS") } + if err := enableUserLinger(v, dist); err != nil { + return err + } + if err := pipeCmdPassThrough("wsl", withUser(lingerSetup, user), "-d", dist, "sh"); err != nil { return errors.Wrap(err, "could not configure systemd settomgs for guest OS") } @@ -501,6 +526,15 @@ func configureSystem(v *MachineVM, dist string) error { return nil } +func enableUserLinger(v *MachineVM, dist string) error { + lingerCmd := "mkdir -p /var/lib/systemd/linger; touch /var/lib/systemd/linger/" + v.RemoteUsername + if err := runCmdPassThrough("wsl", "-d", dist, "sh", "-c", lingerCmd); err != nil { + return errors.Wrap(err, "could not enable linger for remote user on guest OS") + } + + return nil +} + func installScripts(dist string) error { if err := pipeCmdPassThrough("wsl", enterns, "-d", dist, "sh", "-c", "cat > /usr/local/bin/enterns; chmod 755 /usr/local/bin/enterns"); err != nil { diff --git a/pkg/specgen/generate/pause_image.go b/pkg/specgen/generate/pause_image.go index 4aba230a3..ddf35f230 100644 --- a/pkg/specgen/generate/pause_image.go +++ b/pkg/specgen/generate/pause_image.go @@ -80,6 +80,12 @@ ENTRYPOINT ["/catatonit", "-P"]`, catatonitPath) Quiet: true, IgnoreFile: "/dev/null", // makes sure to not read a local .ignorefile (see #13529) IIDFile: "/dev/null", // prevents Buildah from writing the ID on stdout + IDMappingOptions: &buildahDefine.IDMappingOptions{ + // Use the host UID/GID mappings for the build to avoid issues when + // running with a custom mapping (BZ #2083997). + HostUIDMapping: true, + HostGIDMapping: true, + }, } if _, _, err := rt.Build(context.Background(), buildOptions, tmpF.Name()); err != nil { return "", err diff --git a/test/apiv2/10-images.at b/test/apiv2/10-images.at index 9526183e3..a994f8e11 100644 --- a/test/apiv2/10-images.at +++ b/test/apiv2/10-images.at @@ -190,6 +190,10 @@ t POST "libpod/build?dockerfile=containerfile" $CONTAINERFILE_TAR application/js t POST "build?dockerfile=containerfile" $CONTAINERFILE_TAR application/json 200 \ .stream~"STEP 1/1: FROM $IMAGE" +# Libpod: allow building from url: https://github.com/alpinelinux/docker-alpine.git and must ignore any provided tar +t POST "libpod/build?remote=https%3A%2F%2Fgithub.com%2Falpinelinux%2Fdocker-alpine.git" $CONTAINERFILE_TAR 200 \ + .stream~"STEP 1/5: FROM alpine:3.14" + # Build api response header must contain Content-type: application/json t POST "build?dockerfile=containerfile" $CONTAINERFILE_TAR application/json 200 response_headers=$(cat "$WORKDIR/curl.headers.out") diff --git a/test/system/170-run-userns.bats b/test/system/170-run-userns.bats index d754306b2..b80351902 100644 --- a/test/system/170-run-userns.bats +++ b/test/system/170-run-userns.bats @@ -36,6 +36,19 @@ function _require_crun() { is "$output" ".*457" "Check group leaked into container" } +@test "rootful pod with custom ID mapping" { + skip_if_rootless "does not work rootless - rootful feature" + skip_if_remote "remote --uidmap is broken (see #14233)" + random_pod_name=$(random_string 30) + run_podman pod create --uidmap 0:200000:5000 --name=$random_pod_name + run_podman pod start $random_pod_name + + # Remove the pod and the pause image + run_podman pod rm $random_pod_name + run_podman version --format "{{.Server.Version}}-{{.Server.Built}}" + run_podman rmi -f localhost/podman-pause:$output +} + @test "podman --remote --group-add keep-groups " { if is_remote; then run_podman 125 run --rm --group-add keep-groups $IMAGE id diff --git a/vendor/modules.txt b/vendor/modules.txt index 0576d8f20..c73f11330 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -328,7 +328,7 @@ github.com/docker/distribution/registry/client/auth/challenge github.com/docker/distribution/registry/client/transport github.com/docker/distribution/registry/storage/cache github.com/docker/distribution/registry/storage/cache/memory -# github.com/docker/docker v20.10.15+incompatible +# github.com/docker/docker v20.10.16+incompatible ## explicit github.com/docker/docker/api github.com/docker/docker/api/types |