Compare commits

..

160 Commits

Author SHA1 Message Date
29fcd5dfae Merge pull request #81 from andrewhsu/cl
cherry-pick update rpm changelog
2017-06-15 02:46:13 -07:00
9600fb4e78 update rpm changelog
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
(cherry picked from commit 659d6015b6bf8ec3c24cf5377f3eda88beed0a10)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-15 09:44:20 +00:00
0f80f28c1a Merge pull request #80 from andrewhsu/rc4
bump VERSION files to 17.06.0-ce-rc4
2017-06-15 01:18:13 -07:00
29e7f7b8c9 bump VERSION files to 17.06.0-ce-rc4
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-15 08:17:14 +00:00
4c2cf22362 Merge pull request #79 from andrewhsu/systemd
cherry-pick Add systemd scripts
2017-06-15 01:13:42 -07:00
afcb08cfb0 Use internal systemd scripts for DEB packaging
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 477d0b1f735f7e243425cd2c5f48e8f216556c2f)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-15 08:10:46 +00:00
46a9d6b041 Use internal systemd scripts for RPM packaging
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit cbfe576e680b19c5f4c992e52bee59f2bc540026)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-15 08:10:33 +00:00
b8c7116a23 Add initial systemd scripts
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 7b31f87ffd8115f8226f68b788f5b36db53981f6)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-15 08:10:17 +00:00
b089783b38 Merge pull request #76 from andrewhsu/pool
cherry-pick container/stream/attach: use pools.Copy
2017-06-14 11:35:08 -07:00
605ffe91cf Merge pull request #75 from aaronlehmann/vendor-swarmkit-bf9b892
Vendor swarmkit bf9b892
2017-06-13 23:23:05 -07:00
eb837fd7bc pkg/pools: add buffer32KPool & use it for copy
Signed-off-by: Cristian Staretu <cristian.staretu@gmail.com>
(cherry picked from commit ba40f4593f79a653f1e3a8b9597b7900fb68a564)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-14 00:20:13 +00:00
475c218fd7 container/stream/attach: use pools.Copy
The use of pools.Copy avoids io.Copy's internal buffer allocation.
This commit replaces io.Copy with pools.Copy to avoid the allocation of
buffers in io.Copy.

Signed-off-by: Cristian Staretu <cristian.staretu@gmail.com>
(cherry picked from commit 014095e6a07748d0e1ce2f759f5c4f4b3783e765)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-14 00:20:13 +00:00
f2eb8b825b Vendor swarmkit bf9b892
Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
2017-06-13 11:37:41 -07:00
7953dbc649 Merge pull request #72 from andrewhsu/rc3
bump VERSION files to 17.06.0-ce-rc3
2017-06-13 00:27:55 -07:00
5c94f3eeda bump VERSION files to 17.06.0-ce-rc3
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-13 07:27:03 +00:00
d96b9b7edf Merge pull request #68 from mlaventure/17.06-rc3-changelog
Update changelog for 17.06.0-ce-rc3
2017-06-13 00:24:51 -07:00
be1ea9771c added changelog entry for swarmkit
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-13 07:21:48 +00:00
5d8e13a06c Merge pull request #71 from andrewhsu/dep-v1
cherry-pick: Disable legacy (v1) registries by default
2017-06-12 23:21:57 -07:00
d611e35db6 Merge pull request #69 from andrewhsu/sk
revendor github.com/docker/swarmkit to ef3c57a
2017-06-12 22:03:36 -07:00
85aaa4f261 Merge pull request #66 from mavenugo/17.06-ln
Vendoring libnetwork 4f5310be349d9299f6cab6d5822312f00cfa965c
2017-06-12 21:01:20 -07:00
56ecfabb9f Merge pull request #70 from tiborvass/revert-tty-changes
Revert tty changes
2017-06-12 20:53:00 -07:00
97a704b2a7 Merge pull request #52 from seemethere/cherry_pick_33335
Check signal is unset before using user stopsignal
2017-06-12 19:24:57 -07:00
a16a6004a8 revendor github.com/docker/swarmkit to 6083c76
To get the changes:
* https://github.com/docker/swarmkit/pull/2234
* https://github.com/docker/swarmkit/pull/2237
* https://github.com/docker/swarmkit/pull/2238

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-13 00:04:15 +00:00
17c2a50117 Update docs, completion scripts for disable-legacy-registry
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 2b8f0eef7338f37104464154ba65aef7db3b9703)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-12 23:28:29 +00:00
5e671f7b53 Disable legacy (v1) registries by default
Deprecation of interacting with v1 registries was
started in docker 1.8.3, which added a `--disable-legacy-registry`
flag.

This option was anounced to be the default starting
with docker 17.06, and v1 registries completely
removed in docker 17.12.

This patch updates the default, and disables
interaction with v1 registres by default.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 128280013f2ad90520c97b47a787be0db883e870)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-12 23:28:14 +00:00
986443bad2 [cli] vendor: Revert ONCLR and OPOST changes
This reverts to a version of runc without the ONCLR cleared to not cause
a regression with different clients using --tty.

This also reverts the OPOST changes to the term package to support the
initial change.

Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
(cherry picked from commit a5e83836a49547b2add871bb52cbd8bfedb57114)
Signed-off-by: Tibor Vass <tibor@docker.com>
2017-06-12 23:27:57 +00:00
eaac30972d [engine] Revert ONCLR and OPOST changes
This reverts to a version of runc without the ONCLR cleared to not cause
a regression with different clients using --tty.

This also reverts the OPOST changes to the term package to support the
initial change.

Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
(cherry picked from commit a5e83836a49547b2add871bb52cbd8bfedb57114)
Signed-off-by: Tibor Vass <tibor@docker.com>
2017-06-12 23:27:48 +00:00
6f00f5603d Merge pull request #59 from seemethere/cherry_pick_33473
Update deprecated.md for removal of --email flag
2017-06-12 15:43:01 -07:00
ad098a7a03 Merge pull request #64 from seemethere/cherry_pick_143
Remove deprecated -e/--email flag from docker login
2017-06-12 15:42:02 -07:00
9c1f95529e Update changelog for 17.06.0-ce-rc3
Signed-off-by: Kenfe-Mickael Laventure <mickael.laventure@gmail.com>
2017-06-12 11:41:44 -07:00
8c2c3e21a3 Remove "-e" / "--email" from integration tests
This option is no longer supported in docker 17.06,
so should not be used.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-06-12 10:18:49 -07:00
da2d17a18f Merge pull request #65 from seemethere/cherry_pick_33609
Merge pull request #29418 from aboch/p66
2017-06-12 09:27:42 -07:00
8ef3d608d9 Vendoring libnetwork 4f5310be349d9299f6cab6d5822312f00cfa965c
This is a cherry-pick of https://github.com/moby/moby/pull/33634
that brings in https://github.com/docker/libnetwork/pull/1796.

Signed-off-by: Madhu Venugopal <madhu@docker.com>
2017-06-12 09:04:36 -07:00
514d89f021 Merge pull request #55 from andrewhsu/revendor-libnetwork-netlink
revendor libnetwork and netlink in engine component
2017-06-11 10:32:17 -07:00
0189f9d259 Merge pull request #57 from seemethere/cherry_pick_33413
Do not reuse a http.Request after a failure in callWithRetry
2017-06-10 22:10:48 -07:00
ca7df6fe42 Merge pull request #61 from seemethere/cherry_pick_33583
Logging driver should receive same file in start/stop request
2017-06-10 22:09:47 -07:00
47c1b00158 Merge pull request #60 from seemethere/cherry_pick_144
Allow --detach and --quiet flags when using --rollback
2017-06-10 15:48:54 -07:00
f7e9f2cefa Merge pull request #58 from seemethere/cherry_pick_33363
Fix cancelling builder on chunked requests
2017-06-10 15:48:02 -07:00
960ddece48 Merge pull request #62 from seemethere/cherry_pick_33598
daemon: Remove daemon datastructure dump functionality
2017-06-10 14:41:21 -07:00
5a5e9e32bd Merge pull request #63 from seemethere/cherry_pick_33606
Backport 17.05.0-ce changelog to master
2017-06-10 13:41:31 -07:00
eff2539693 Merge pull request #44 from nishanttotla/cpick-cli-121
[cherry-pick] Enable client side digest pinning for stack deploy
2017-06-10 13:29:54 -07:00
e92c400a01 Merge pull request #32 from nishanttotla/cpick-moby-33279-17.06
[cherry-pick] vendor: Ensure service images get default tag and print familiar strings
2017-06-10 13:27:49 -07:00
5d4843baa3 Merge pull request #56 from tiborvass/cli-go1.8
Update golang 1.8.3
2017-06-09 18:28:53 -07:00
cfb8aafe5e Merge pull request #42 from mlaventure/17.06-rc2-changelog
Remove unneeded line in 17.06 CHANGELOG
2017-06-09 18:23:18 -07:00
4e1370233e Merge pull request #13 from mlaventure/chp-moby-33279
Ensure service images get default tag and print familiar strings
2017-06-09 17:58:16 -07:00
091732e350 Merge pull request #29418 from aboch/p66
[1.13.x] Fix buildIpamResources()
(cherry picked from commit 4d2be03b68dd54d8f2307e8b4178ad8ed8d5d343)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 27498a3c60018ea2880a18beec65710fd27971a7)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 17:38:11 -07:00
1af533909d Remove deprecated -e/--email flag from docker login
The `docker login -e` / `docker login --email` option was deprecated in
Docker 1.11 (https://github.com/moby/moby/releases/tag/v1.11.0) through
aee260d4eb3aa0fc86ee5038010b7bbc24512ae5 (April 2016), and when used has
been outputing a deprecation warning since;

    Flag --email has been deprecated, will be removed in 17.06.

Originally this option was scheduled to be removed in docker 1.13, but
extended to docker 17.06 due to a change in our deprecation policy.

Given that only docker 1.10 and older use this flag (which is EOL, including
for CS versions, as of February 2017), will now be removed.

With this patch, `docker login` will now produce an Error if the flag
is used.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 8b383d2351)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 17:37:14 -07:00
a014274b80 bump to GA
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
(cherry picked from commit 89658bed64c2a8fe05a978e5b87dbec409d57a0f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit d94f281d78e85f4c7de6b8347ab95d0afd5b8a8d)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 17:35:56 -07:00
7b65e51031 bump to rc2
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
(cherry picked from commit c57fdb2a14cfba584686ddad909e3006284d10aa)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit b4d36e47c3501541dc66b6336e286b8c11cbcec7)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 17:35:34 -07:00
ab5798a6f7 daemon: Remove daemon datastructure dump functionality
When sending SIGUSR1 to the daemon, it can crash because of a concurrent
map access panic, showing a stack trace involving dumpDaemon. It appears
it's not possible to recover from a concurrent map access panic. Since
it's important that SIGUSR1 not be a destructive operation, sadly the
best course of action I can think of is to remove this functionality.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
(cherry picked from commit a4c68ee8574c9b8a3309ebebee0d90108042ba61)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 17:34:45 -07:00
899ed1b641 Logging driver should receive same file in start/stop request
Signed-off-by: Peter Bücker <peter.buecker@gmail.com>
(cherry picked from commit e908e1a357b435d7fab497d51cdd3e58458a0590)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 17:33:46 -07:00
a9a1a9c7de Merge pull request #43 from cyli/re-vendor-swarmkit
[17.06] Re-vendor swarmkit
2017-06-09 17:32:55 -07:00
5ed0b2e10c Allow --detach and --quiet flags when using --rollback
Commit 78c204ef79 added
(f9bd8ec8b268581f93095c5a80679f0a8ff498bf in the moby repo)
a validation to prevent `--rollback` from being used
in combination with other flags that update the
service spec.

This validation was not taking into account that
some flags only affect the CLI behavior, and
are okay to be used when rolling back.

This patch updates the validation, and adds
`--quiet` and `--detach` to the list of allowed
flags.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit f10f29df8d)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 17:31:40 -07:00
7950a39242 Update deprecated.md for removal of --email flag
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 43239f62bedc4721d27744d21c122622988bb3ae)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 17:30:43 -07:00
5bd868da90 Fix cancelling builder on chunked requests
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
(cherry picked from commit 23628bd7efb0aae565cea5ee040e373b0c93fd8d)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 17:29:44 -07:00
e142124317 Do not reuse a http.Request after a failure in callWithRetry
Closes: #33412

Signed-off-by: Felix Abecassis <fabecassis@nvidia.com>
(cherry picked from commit 62871ef2fa52b0a2e426c30f36d35a9ba1e92fac)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 17:28:34 -07:00
0fa9fe7713 Fixing e2e test
Because of cherry-pick from commit
5efcec77170dd6c4087d4f1fb3695c32d94edfb4 into
components/cli/vendor/github.com/docker/docker

Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
2017-06-10 00:27:43 +00:00
69fc734572 Comment out vendoring for docker/docker in components/cli
Because of cherry-pick from commit 5efcec77170dd6c4087d4f1fb3695c32d94edfb4
into components/cli/vendor/github.com/docker/docker

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-10 00:27:43 +00:00
5e405a94e8 Ensure service images get default tag and print familiar strings
Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
(cherry picked from commit 5efcec77170dd6c4087d4f1fb3695c32d94edfb4)

Conflicts resolved:
components/cli/vendor/github.com/docker/docker/client/service_create.go

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-10 00:27:05 +00:00
897b692e1c Merge pull request #53 from seemethere/cherry_pick_33419
libcontainerd: fix reaper goroutine position
2017-06-09 17:21:17 -07:00
d8fc4f1ad2 Merge pull request #54 from andrewhsu/opost
revert Set OPOST on bsd
2017-06-09 17:17:37 -07:00
af4ae65a0d Merge pull request #51 from seemethere/cherry_pick_33307
Event tests need to wait for events
2017-06-09 16:59:23 -07:00
c8fa12c15c Merge pull request #49 from seemethere/cherry_pick_33464
Do not log the CA config CA signing key in debug mode.
2017-06-09 15:36:06 -07:00
36f4ffb042 Merge pull request #50 from seemethere/cherry_pick_33524
Fix Cache with ONBUILD
2017-06-09 15:34:01 -07:00
0cbae9b2ee Merge pull request #48 from seemethere/cherry_pick_33330
Don't create source directory while the daemon is being shutdown, fix…
2017-06-09 15:33:19 -07:00
dcd1f685c8 Merge pull request #47 from seemethere/cherry_pick_33249
Add container environment variables correctly to the health check
2017-06-09 15:32:27 -07:00
05648d36b5 Merge pull request #46 from seemethere/cherry_pick_cli_162
Handle case of configs on old daemon
2017-06-09 15:31:43 -07:00
e1ebcf33f6 Merge pull request #45 from seemethere/cherry_pick_33578
Service alias should not be copied to task alias
2017-06-09 15:31:00 -07:00
4ea6f802a5 Update golang 1.8.3
Signed-off-by: Stefan Scherer <scherer_stefan@icloud.com>
(cherry picked from commit b38c49411b)
Signed-off-by: Tibor Vass <tibor@docker.com>
2017-06-09 22:26:59 +00:00
be7a008421 revendoring netlink in engine component
bumping vendor of vishvananda/netlink to bd6d5de5ccef2d66b0a26177928d0d8895d7f969

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-09 21:37:15 +00:00
fcd821892e revendoring libnetwork in engine component
bumping vendor of docker/libnetwork to 411846172fb457a6459ea94eb536f91717ee0a04

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-09 21:27:44 +00:00
76b0c4884b Merge pull request #14 from mlaventure/update-containerd
Update containerd to cfb82a876ecc11b5ca0977d1733adbe58599088a
2017-06-09 14:22:20 -07:00
1f928815e5 Revert "Set OPOST on bsd"
This reverts commit fff42c853a.

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-09 20:55:50 +00:00
ad8c94e585 Revert "[vendor] Set OPOST on bsd"
This reverts commit 972de9a657.

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-09 20:55:49 +00:00
058e676c7f Revert "Comment out vendoring for docker/docker in components/cli"
This reverts commit 8928dccaa6.

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-09 20:55:41 +00:00
11637f7d81 Re-vendor swarmkit to include the following fixes:
- https://github.com/docker/swarmkit/pull/2218
- https://github.com/docker/swarmkit/pull/2215
- https://github.com/docker/swarmkit/pull/2233

Signed-off-by: Ying <ying.li@docker.com>
2017-06-09 13:25:15 -07:00
5965f0216f libcontainerd: fix reaper goroutine position
It has observed defunct containerd processes accumulating over
time while dockerd was permanently failing to restart containerd.
Due to a bug in the runContainerdDaemon() function, dockerd does not clean up
its child process if containerd already exits very soon after the (re)start.

The reproducer and analysis below comes from docker 1.12.x but bug
still applies on latest master.

- from libcontainerd/remote_linux.go:

  329 func (r *remote) runContainerdDaemon() error {
   :
   :      // start the containerd child process
   :
  403     if err := cmd.Start(); err != nil {
  404             return err
  405     }
   :
   :      // If containerd exits very soon after (re)start, it is
possible
   :      // that containerd is already in defunct state at the time
when
   :      // dockerd gets here. The setOOMScore() function tries to
write
   :      // to /proc/PID_OF_CONTAINERD/oom_score_adj. However, this
fails
   :      // with errno EINVAL because containerd is defunct. Please see
   :      // snippets of kernel source code and further explanation
below.
   :
  407     if err := setOOMScore(cmd.Process.Pid, r.oomScore); err != nil
{
  408             utils.KillProcess(cmd.Process.Pid)
   :
   :              // Due to the error from write() we return here. As
the
   :              // goroutine that would clean up the child has not
been
   :              // started yet, containerd remains in the defunct
state
   :              // and never gets reaped.
   :
  409             return err
  410     }
   :
  417     go func() {
  418             cmd.Wait()
  419             close(r.daemonWaitCh)
  420     }() // Reap our child when needed
   :
  423 }

This is the kernel function that gets invoked when dockerd tries to
write
to /proc/PID_OF_CONTAINERD/oom_score_adj.

- from fs/proc/base.c:

 1197 static ssize_t oom_score_adj_write(struct file *file, ...
 1198                                         size_t count, loff_t
*ppos)
 1199 {
   :
 1223         task = get_proc_task(file_inode(file));
   :
   :          // The defunct containerd process does not have a virtual
   :          // address space anymore, i.e. task->mm is NULL. Thus the
   :          // following code returns errno EINVAL to dockerd.
   :
 1230         if (!task->mm) {
 1231                 err = -EINVAL;
 1232                 goto err_task_lock;
 1233         }
   :
 1253 err_task_lock:
   :
 1257         return err < 0 ? err : count;
 1258 }

The purpose of the following program is to demonstrate the behavior of
the oom_score_adj_write() function in connection with a defunct process.

$ cat defunct_test.c

\#include <unistd.h>

main()
{
    pid_t pid = fork();

    if (pid == 0)
        // child
        _exit(0);

    // parent
    pause();
}

$ make defunct_test
cc     defunct_test.c   -o defunct_test

$ ./defunct_test &
[1] 3142

$ ps -f | grep defunct_test | grep -v grep
root      3142  2956  0 13:04 pts/0    00:00:00 ./defunct_test
root      3143  3142  0 13:04 pts/0    00:00:00 [defunct_test] <defunct>

$ echo "ps 3143" | crash -s
  PID    PPID  CPU       TASK        ST  %MEM     VSZ    RSS  COMM
  3143   3142   2  ffff880035def300  ZO   0.0       0      0
defunct_test

$ echo "px ((struct task_struct *)0xffff880035def300)->mm" | crash -s
$1 = (struct mm_struct *) 0x0
                          ^^^ task->mm is NULL

$ cat /proc/3143/oom_score_adj
0

$ echo 0 > /proc/3143/oom_score_adj
-bash: echo: write error: Invalid argument"

---

This patch fixes the above issue by making sure we start the reaper
goroutine as soon as possible.

Signed-off-by: Antonio Murdaca <runcom@redhat.com>

(cherry picked from commit 27087eacbf96e6ef9d48a6d3dc89c7c1cff155b4)

Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>

Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 13:23:20 -07:00
3910d5b571 Check signal is unset before using user stopsignal
This fixes an issue where if a stop signal is set, and a user sends
SIGKILL, `container.ExitOnNext()` is not set, thus causing the container
to restart.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
(cherry picked from commit 114652ab86609e5c0cbfad84f642942b466a0596)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 13:21:29 -07:00
03d8258b7d Event tests need to wait for events
Signed-off-by: Dong Chen <dongluo.chen@docker.com>
(cherry picked from commit 59b2d0473af5155bb31adcc17bc36857ebd13a15)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 13:19:58 -07:00
1c2ce3d977 Fix Cache with ONBUILD
Signed-off-by: Daniel Nephin <dnephin@docker.com>
(cherry picked from commit f1ade82d82e6436971c6b7d08eb1da57ed9ba756)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 13:17:47 -07:00
eb4ef82087 Do not log the CA config CA signing key in debug mode.
Signed-off-by: Ying Li <ying.li@docker.com>
(cherry picked from commit d60f18204978d438d1eb336512576d47991c8ac1)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 13:16:19 -07:00
d09575fb8f Don't create source directory while the daemon is being shutdown, fix #30348
If a container mount the socket the daemon is listening on into
container while the daemon is being shutdown, the socket will
not exist on the host, then daemon will assume it's a directory
and create it on the host, this will cause the daemon can't start
next time.

fix issue https://github.com/moby/moby/issues/30348

To reproduce this issue, you can add following code

```
--- a/daemon/oci_linux.go
+++ b/daemon/oci_linux.go
@@ -8,6 +8,7 @@ import (
        "sort"
        "strconv"
        "strings"
+       "time"

        "github.com/Sirupsen/logrus"
        "github.com/docker/docker/container"
@@ -666,7 +667,8 @@ func (daemon *Daemon) createSpec(c *container.Container) (*libcontainerd.Spec, e
        if err := daemon.setupIpcDirs(c); err != nil {
                return nil, err
        }
-
+       fmt.Printf("===please stop the daemon===\n")
+       time.Sleep(time.Second * 2)
        ms, err := daemon.setupMounts(c)
        if err != nil {
                return nil, err

```

step1 run a container which has `--restart always` and `-v /var/run/docker.sock:/sock`
```
$ docker run -ti --restart always -v /var/run/docker.sock:/sock busybox
/ #

```
step2 exit the the container
```
/ # exit
```
and kill the daemon when you see
```
===please stop the daemon===
```
in the daemon log

The daemon can't restart again and fail with `can't create unix socket /var/run/docker.sock: is a directory`.

Signed-off-by: Lei Jitang <leijitang@huawei.com>

(cherry picked from commit 7318eba5b2f8bb4b867ca943c3229260ca98a3bc)

Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>

Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 13:13:40 -07:00
13934b618c Add container environment variables correctly to the health check
The health check process doesn't have all the environment
varialbes in the container or has them set incorrectly.

This patch should fix that problem.

Signed-off-by: Boaz Shuster <ripcurld.github@gmail.com>
(cherry picked from commit 5836d86ac4d617e837d94010aa60384648ab59ea)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 13:09:40 -07:00
e9b6305e1d Handle case of configs on old daemon
If configs are declared for a service and pointing on an old
daemon, error out properly (instead of "page not found").

If there is no configs declared, don't call convertServiceConfigObjs
to avoid having an error.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
(cherry picked from commit cf5550c426)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 13:06:34 -07:00
94f4d72c55 Service alias should not be copied to task alias
If a service alias is copied to task, then the DNS resolution on the
service name will resolve to service VIP and all of Task-IPs and that
will break the concept of vip based load-balancing resulting in all the
dns-rr caching issues.

This is a regression introduced in #33130

Signed-off-by: Madhu Venugopal <madhu@docker.com>
(cherry picked from commit 38c15531501578b96d34be5ce7f33a0be6be078f)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-06-09 12:55:46 -07:00
765e46f7cc Update containerd to cfb82a876ecc11b5ca0977d1733adbe58599088a
Signed-off-by: Kenfe-Mickael Laventure <mickael.laventure@gmail.com>
2017-06-09 09:33:50 -04:00
f15c4fd160 Fixing stack deploy tests to not contact registry
Because of cherry-pick from commit
f790e839fc

Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
2017-06-08 15:20:09 -07:00
66d168dd55 Change --no-resolve-image flag to --resolve-image string flag
Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
(cherry picked from commit f790e839fc)
Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
2017-06-08 14:25:50 -07:00
59d6ed0a4d Enable client side digest pinning for stack deploy
Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
(cherry picked from commit 9f1bea2657)
Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
2017-06-08 14:24:57 -07:00
402dd4a9ea Merge pull request #38 from andrewhsu/rc2
bump VERSION files to 17.06.0-ce-rc2
2017-06-06 16:05:18 -07:00
14ff8286f7 Remove unneeded line in 17.06 CHANGELOG
Also update release date

Signed-off-by: Kenfe-Mickael Laventure <mickael.laventure@gmail.com>
2017-06-06 15:58:28 -07:00
eb1565bba1 Merge pull request #40 from mlaventure/17.06-rc2-changelog
Update 17.06 CHANGELOG with rc2 changes
2017-06-06 15:19:15 -07:00
de513d091d Merge pull request #41 from andrewhsu/update-pack
update with latest changes from docker-ce-packaging
2017-06-06 15:16:36 -07:00
1d4b51cccb Update 17.06 CHANGELOG with rc2 changes
Signed-off-by: Kenfe-Mickael Laventure <mickael.laventure@gmail.com>
2017-06-06 14:50:01 -07:00
b5411da4de Make the hash_files target non interactive
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 097516fc76a2c9acf798dc523326904520ff1fc7)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-06 21:26:55 +00:00
7239874782 Add file hashing for static files
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit a1b7f6f3407546ff41ed2e0d16d7e1b2a8ac0ef4)

Conflicts:
components/packaging/static/Makefile
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-06 21:26:38 +00:00
18355074d7 Move the hashing of files to its own target
So we can use it at will

Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 533a843393bd7c3674074ec9af73c8e666fc7484)

Conflicts:
components/packaging/static/Makefile
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-06 21:24:53 +00:00
1f94b6c7ec set $GOPATH/bin for s390x
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
(cherry picked from commit f6d30c3631abee04aa3586cba36960901f705fe2)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-06 21:21:03 +00:00
737e92d753 no need for tty
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
(cherry picked from commit ade95a2b6f8a16aa7e6c387dc9c9dd48bba9a672)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-06 21:20:47 +00:00
97b6afddfe bump VERSION files to 17.06.0-ce-rc2
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-06 16:54:24 +00:00
00327e2c1c Merge pull request #37 from andrewhsu/gen-man
generate man pages
2017-06-06 02:16:57 -07:00
0dbee88694 generate man pages
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
(cherry picked from commit e14fca86f8230e10d79a7bfae2873a98a16bb44d)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-06 09:01:36 +00:00
ce44edfbf8 Merge pull request #19 from aluzzardi/revendor-swarmkit
[17.06] Re-vendor SwarmKit to 4b872cfac8ffc0cc7fff434902cc05dbc7612da5
2017-06-06 00:37:05 -07:00
0efdcc403e Re-vendor SwarmKit to 4b872cfac8ffc0cc7fff434902cc05dbc7612da5
Includes:
- docker/swarmkit#2203
- docker/swarmkit#2210
- docker/swarmkit#2212

Signed-off-by: Andrea Luzzardi <aluzzardi@gmail.com>
Signed-off-by: Tibor Vass <tibor@docker.com>
2017-06-06 05:55:35 +00:00
8cf591e57d Merge pull request #21 from mavenugo/17.06
Vendoring libnetwork b54433f6459b84eae77892d0d359c1bafb133529
2017-06-05 22:25:27 -07:00
5cfdad44b3 Merge pull request #36 from tiborvass/fix-manpages-script
[cli] fix manpages script
2017-06-05 22:19:24 -07:00
5bb86daf51 [cli] fix manpages script
Signed-off-by: Tibor Vass <tibor@docker.com>
2017-06-06 05:05:14 +00:00
9b8ffe709d Merge pull request #25 from mlaventure/chp-moby-33502
Set OPOST on bsd
2017-06-05 21:56:30 -07:00
8928dccaa6 Comment out vendoring for docker/docker in components/cli
Because of cherry-pick from commit 17ec46a24316f59c808c112e3ca46d7c442a785a
into components/cli/vendor/github.com/docker/docker

Signed-off-by: Tibor Vass <tibor@docker.com>
2017-06-06 03:45:20 +00:00
972de9a657 [vendor] Set OPOST on bsd
Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
(cherry picked from commit 17ec46a24316f59c808c112e3ca46d7c442a785a)

Manually update pkg/term vendoring to match changes in moby

Signed-off-by: Tibor Vass <tibor@docker.com>
2017-06-06 03:42:16 +00:00
9dad38d70a Merge pull request #26 from mlaventure/chp-moby-33500
Fix ONBUILD COPY
2017-06-05 20:24:21 -07:00
699fcc1433 Vendoring libnetwork
This is a 17.06 equivalent cherry-pick of
https://github.com/moby/moby/pull/33463

Signed-off-by: Madhu Venugopal <madhu@docker.com>
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-06 03:18:59 +00:00
40d95168e1 Merge pull request #16 from mavenugo/cli-1706-stack
Host and Bridge network support in docker stack deploy
2017-06-05 19:22:40 -07:00
d236b522c5 Merge pull request #33 from andrewhsu/pkg-update
update with latest from docker-ce-packaging
2017-06-05 18:40:24 -07:00
111c39aff1 Change ARCH to be reflective
Signed-off-by: Eli Uriegas <seemethere101@gmail.com>
(cherry picked from commit 930db40ec8424555612ce70781e94d5cb172aa4c)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-06 00:02:26 +00:00
c1c839c28b Rename deb dockerfiles to $(uname -m) equivalents
Signed-off-by: Eli Uriegas <seemethere101@gmail.com>
(cherry picked from commit 3f7e423438733fb5f593f666d12714ae2f63fcc9)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-06 00:02:26 +00:00
f650eb38a6 Update GO_VERSION to 1.8.3 (#8)
Signed-off-by: Eli Uriegas <seemethere101@gmail.com>(cherry picked from commit 121e1c478d94a9be5713e944dde113f8637d6e41)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-06 00:02:26 +00:00
aa3a4c2846 Add armhf dockerfiles for deb building (#4)
Add armhf dockerfiles for deb building

Signed-off-by: Eli Uriegas <seemethere101@gmail.com>
(cherry picked from commit f0c8cea1b79b049743cd1503f7ac4a34c265f476)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-06 00:02:26 +00:00
6a52ceebf4 Add DISTRO/SUITE env. variables to the RPM dockerfiles
Signed-off-by: Roberto Gandolfo Hashioka <roberto_hashioka@hotmail.com>
(cherry picked from commit c4a9e41495fac5e4aa72151b80200aa453f65e08)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-06-06 00:02:26 +00:00
c6dd1afaa6 Merge pull request #24 from tiborvass/man-yaml-scripts
Add scripts and targets for manpages and yamldocs
2017-06-05 16:06:46 -07:00
f148100b07 Add scripts and targets for manpages and yamldocs
Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit ff615dbc4d)
2017-06-05 22:51:19 +00:00
8ce2c283f7 Merge pull request #23 from tiborvass/import-man-docs-completion
Import man docs completion
2017-06-05 15:40:42 -07:00
b2dacf38db Merge pull request #8 from mlaventure/chp-moby-33341
Do not clear swarm directory at the begining of swarm init and swarm …
2017-06-05 09:57:27 -07:00
6c636490a3 Merge pull request #22 from mlaventure/chp-cli-136
Fix stack compose bind-mount volumes for Windows
2017-06-05 09:54:06 -07:00
c1f7e41e4e Merge pull request #12 from mlaventure/chp-cli-135
Include stack service configs in service specs
2017-06-05 09:46:58 -07:00
a148bf60af Merge pull request #11 from mlaventure/chp-cli-134
Remove stack configs on stack removal
2017-06-05 09:46:13 -07:00
4910da9ddb Merge pull request #10 from mlaventure/chp-cli-126
Handle a Docker daemon without registry info
2017-06-05 09:45:31 -07:00
9e9c4b52b9 Fix ONBUILD COPY
the source was missing from the second dispatch

Signed-off-by: Daniel Nephin <dnephin@docker.com>
(cherry picked from commit 3f2604157790408acf5ad05c74cebe105f2b6979)
2017-06-05 08:01:05 -07:00
fff42c853a Set OPOST on bsd
Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
(cherry picked from commit 17ec46a24316f59c808c112e3ca46d7c442a785a)
2017-06-05 07:45:38 -07:00
2bdf7010a1 Merge pull request #7 from mlaventure/chp-moby-33308
remove RuntimeData from cluster and types
2017-06-05 06:10:46 -07:00
46f679cc1d Merge pull request #5 from mlaventure/chp-moby-33311
Adding support for DNS search on RS1
2017-06-05 06:09:52 -07:00
3a237eae5f Merge pull request #4 from mlaventure/chp-moby-33376
Ensure that a device mapper task is referenced until task is complete
2017-06-05 06:09:13 -07:00
8330e07f4a Merge pull request #3 from mlaventure/chp-moby-33365
Update go-winio to v0.4.2
2017-06-05 06:05:37 -07:00
9a732dbff3 Merge pull request #2 from mlaventure/chp-moby-33422
Don't unmount entire plugin manager tree on remove
2017-06-05 05:37:07 -07:00
08cb13b09b Fix stack compose bind-mount volumes for Windows
For stack compose files, use filepath.IsAbs instead of path.IsAbs, for
bind-mounted service volumes, because filepath.IsAbs handles Windows
paths, while path.IsAbs does not.

Signed-off-by: John Stephens <johnstep@docker.com>
(cherry picked from commit 9043d39dea)
2017-06-02 12:01:27 -07:00
09185931e1 Merge pull request #9 from mlaventure/chp-moby-33387
Bump go to go1.8.3
2017-06-02 10:38:59 -07:00
8d44c9a703 Merge pull request #1 from mlaventure/chp-moby-33409
prevent image prune panic
2017-06-02 10:38:26 -07:00
0334cfffff Merge pull request #20 from mlaventure/chp-moby-33474
Use actual cli version for TestConfigHTTPHeader
2017-06-02 10:33:44 -07:00
a9548340cb Use actual cli version for TestConfigHTTPHeader
Signed-off-by: Kenfe-Mickael Laventure <mickael.laventure@gmail.com>
(cherry picked from commit 0b90edc22fa95e04b4d3208a72bb276be699b260)
2017-06-01 17:13:23 -07:00
91d25d6fe0 Only set default aliases when the network is user defined.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
(cherry picked from commit d5b505ee8c)
2017-05-31 19:07:11 -07:00
6c534bb5a0 Add tests for verifyExternalNetwork
Signed-off-by: Daniel Nephin <dnephin@docker.com>
(cherry picked from commit 341703d21e)
2017-05-31 19:07:08 -07:00
1c46b8f4d3 With the introduction of node-local network support, docker services can
be attached to special networks such as host and bridge. This fix brings
in the required changes to make sure the stack file accepts these
networks as well.

Signed-off-by: Madhu Venugopal <madhu@docker.com>
(cherry picked from commit 123f0bfd98)
2017-05-31 19:07:05 -07:00
f156aa746e Adding unit tests for pin by digest (client)
Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
(cherry picked from commit 75c7536d2e2e328b644bf69153de879d1d197988)
2017-05-31 11:40:48 -07:00
5c5cc1fccd Ensure service images get default tag and print familiar strings
Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
(cherry picked from commit 5efcec77170dd6c4087d4f1fb3695c32d94edfb4)
2017-05-31 11:40:48 -07:00
cd6b682d4f Include stack service configs in service specs
Signed-off-by: John Stephens <johnstep@docker.com>
(cherry picked from commit b7cac96f69)
2017-05-31 11:37:32 -07:00
19e7c1efe9 Remove stack configs on stack removal
Signed-off-by: John Stephens <johnstep@docker.com>
(cherry picked from commit f05cd11ee2)
2017-05-31 11:35:59 -07:00
32807d0a87 Handle a Docker daemon without registry info
The current implementation of the ElectAuthServer doesn't handle well when the
default Registry server is not included in the response from the daemon Info
endpoint.

That leads to the storage and usage of the credentials for the default registry
(`https://index.docker.io/v1/`) under an empty string on the client config file.

Sample config file after a login via a Docker Daemon without Registry
information:
```json
{
	"auths": {
		"": {
			"auth": "***"
		}
	}
}
```

That can lead to duplication of the password for the default registry and
authentication failures against the default registry if a pull/push is performed
without first authenticating via the misbehaving daemon.

Also, changes the output of the warning message from stdout to sdterr as
per dnephin suggestion.

Signed-off-by: Marcus Martins <marcus@docker.com>
(cherry picked from commit 862649707e)
2017-05-31 11:34:01 -07:00
e7776047c2 Bump go to go1.8.3
Note that go1.8.2 contains a security fix (CVE-2017-8932).

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
(cherry picked from commit 0c7c900e9e66335a6bd486be008af43ae83a5a37)
2017-05-31 11:29:38 -07:00
30e966bc97 Do not clear swarm directory at the begining of swarm init and swarm join now.
However, do clear the directory if init or join fails, because we don't
want to leave it in a half-finished state.

Signed-off-by: Ying Li <ying.li@docker.com>
(cherry picked from commit bf3e9293a66c77a2fddf4e691222898846b4af9f)
2017-05-31 11:25:56 -07:00
bb1fe809d2 remove RuntimeData from cluster and types
Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>
(cherry picked from commit 8eeba751983d6499a07fcd21c26f381eb1d17d19)
2017-05-31 11:08:24 -07:00
e724dcd16b Adding support for DNS search on RS1
Signed-off-by: Sandeep Bansal <sabansal@microsoft.com>
(cherry picked from commit b8e8dcd6e031d005a2d46d95f84da35b436cd870)
2017-05-31 10:55:38 -07:00
96c9cd5c39 Ensure that a device mapper task is referenced until task is complete
DeviceMapper tasks in go use SetFinalizer to clean up C construct
counterparts in the C LVM library.  While thats well and good, it relies
heavily on the exact interpretation of when the golang garbage collector
determines that an object is unreachable is subject to reclaimation.
While common sense would assert that for stack variables (which these DM
tasks always are), are unreachable when the stack frame in which they
are declared returns, thats not the case.  According to this:

https://golang.org/pkg/runtime/#SetFinalizer

The garbage collector decides that, if a function calls into a
systemcall (which task.run() always will in LVM), and there are no
subsequent references to the task variable within that stack frame, then
it can be reclaimed.  Those conditions are met in several devmapper.go
routines, and if the garbage collector runs in the middle of a
deviceMapper operation, then the task can be destroyed while the
operation is in progress, leading to crashes, failed operations and
other unpredictable behavior.

The fix is to use the KeepAlive interface:

https://golang.org/pkg/runtime/#KeepAlive

The KeepAlive method is effectively an empy reference that fools the
garbage collector into thinking that a variable is still reachable.  By
adding a call to KeepAlive in the task.run() method, we can ensure that
the garbage collector won't reclaim a task object until its execution
within the deviceMapper C library is complete.

Signed-off-by: Neil Horman <nhorman@tuxdriver.com>
(cherry picked from commit d764d8b16624e4924b3949273089f851efa0f717)
2017-05-31 10:42:53 -07:00
4360062d33 Update go-winio to v0.4.2
Signed-off-by: Darren Stahl <darst@microsoft.com>
(cherry picked from commit 3f13107223a9753d709c196c46f1c846041fc0c3)
2017-05-31 10:39:35 -07:00
5acb14a901 Don't unmount entire plugin manager tree on remove
This was mistakenly unmounting everything under `plugins/*` instead of
just `plugins/<id>/*` anytime a plugin is removed.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
(cherry picked from commit db5f31732a9868c1e9e4f9a49be70b794ff82d4f)
2017-05-31 10:37:33 -07:00
43484d9284 prevent image prune panic
Signed-off-by: Alfred Landrum <alfred.landrum@docker.com>
(cherry picked from commit 32da2a4234c5c68ff466dc1afc91ba98dbbe199a)
2017-05-31 09:58:11 -07:00
7f8486a39a pass in git commit override when building deb
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
(cherry picked from commit 3c70b56937c415c6bf3516aeaa5231c6d7d3145d)
2017-05-25 06:00:03 +00:00
5f615dd295 override ver and git commit when building all deb pkgs
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
(cherry picked from commit 4397990e00863254d78c44fbe904a70272b5b340)
2017-05-25 05:59:47 +00:00
f3810787c8 pass in optional GITCOMMIT override
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
(cherry picked from commit 446af3a9b7)
2017-05-24 23:03:09 +00:00
2bcfe6ffc2 bump VERSION files to 17.06.0-ce-rc1
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-05-24 02:24:43 +00:00
0b4548c769 allow version number to be set in builds
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
(cherry picked from commit 3dfe334a7a)
2017-05-24 02:21:53 +00:00
3606 changed files with 177686 additions and 226760 deletions

1
.gitignore vendored
View File

@ -1 +0,0 @@
.helpers/

File diff suppressed because it is too large Load Diff

View File

@ -2,125 +2,5 @@
Want to contribute on Docker CE? Awesome!
This page contains information about reporting issues as well as some tips and
guidelines useful to experienced open source contributors. Finally, make sure
you read our [community guidelines](#docker-community-guidelines) before you
start participating.
## Topics
* [Reporting Security Issues](#reporting-security-issues)
* [Reporting Issues](#reporting-other-issues)
* [Submitting Pull Requests](#submitting-pull-requests)
* [Community Guidelines](#docker-community-guidelines)
## Reporting security issues
The Docker maintainers take security seriously. If you discover a security
issue, please bring it to their attention right away!
Please **DO NOT** file a public issue, instead send your report privately to
[security@docker.com](mailto:security@docker.com).
Security reports are greatly appreciated and we will publicly thank you for it.
We also like to send gifts&mdash;if you're into Docker schwag, make sure to let
us know. We currently do not offer a paid security bounty program, but are not
ruling it out in the future.
## Reporting other issues
There are separate issue-tracking repos for the end user Docker CE
products specialized for a platform. Find your issue or file a new issue
for the platform you are using:
* https://github.com/docker/for-linux
* https://github.com/docker/for-mac
* https://github.com/docker/for-win
* https://github.com/docker/for-aws
* https://github.com/docker/for-azure
When reporting issues, always include:
* The output of `docker version`.
* The output of `docker info`.
If presented with a template when creating an issue, please follow its directions.
Also include the steps required to reproduce the problem if possible and
applicable. This information will help us review and fix your issue faster.
When sending lengthy log-files, consider posting them as a gist (https://gist.github.com).
Don't forget to remove sensitive data from your logfiles before posting (you can
replace those parts with "REDACTED").
## Submitting pull requests
Please see the corresponding `CONTRIBUTING.md` file of each component for more information:
* Changes to the `engine` should be directed upstream to https://github.com/moby/moby
* Changes to the `cli` should be directed upstream to https://github.com/docker/cli
* Changes to the `packaging` should be directed upstream to https://github.com/docker/docker-ce-packaging
## Docker community guidelines
We want to keep the Docker community awesome, growing and collaborative. We need
your help to keep it that way. To help with this we've come up with some general
guidelines for the community as a whole:
* Be nice: Be courteous, respectful and polite to fellow community members.
Regional, racial, gender, or other abuse will not be tolerated. We like
nice people way better than mean ones!
* Encourage diversity and participation: Make everyone in our community feel
welcome, regardless of their background and the extent of their
contributions, and do everything possible to encourage participation in
our community.
* Keep it legal: Basically, don't get us in trouble. Share only content that
you own, do not share private or sensitive information, and don't break
the law.
* Stay on topic: Make sure that you are posting to the correct channel and
avoid off-topic discussions. Remember when you update an issue or respond
to an email you are potentially sending to a large number of people. Please
consider this before you update. Also remember that nobody likes spam.
* Don't send email to the maintainers: There's no need to send email to the
maintainers to ask them to investigate an issue or to take a look at a
pull request. Instead of sending an email, GitHub mentions should be
used to ping maintainers to review a pull request, a proposal or an
issue.
### Guideline violations — 3 strikes method
The point of this section is not to find opportunities to punish people, but we
do need a fair way to deal with people who are making our community suck.
1. First occurrence: We'll give you a friendly, but public reminder that the
behavior is inappropriate according to our guidelines.
2. Second occurrence: We will send you a private message with a warning that
any additional violations will result in removal from the community.
3. Third occurrence: Depending on the violation, we may need to delete or ban
your account.
**Notes:**
* Obvious spammers are banned on first occurrence. If we don't do this, we'll
have spam all over the place.
* Violations are forgiven after 6 months of good behavior, and we won't hold a
grudge.
* People who commit minor infractions will get some education, rather than
hammering them in the 3 strikes process.
* The rules apply equally to everyone in the community, no matter how much
you've contributed.
* Extreme violations of a threatening, abusive, destructive or illegal nature
will be addressed immediately and are not subject to 3 strikes or forgiveness.
* Contact abuse@docker.com to report abuse or appeal violations. In the case of
appeals, we know that mistakes happen, and we'll work with you to come up with a
fair solution if there has been a misunderstanding.

View File

@ -1,56 +1,27 @@
CLI_DIR:=$(CURDIR)/components/cli
ENGINE_DIR:=$(CURDIR)/components/engine
PACKAGING_DIR:=$(CURDIR)/components/packaging
MOBY_COMPONENTS_SHA=f79265f1412af0a68aadd11e1d2f374446f3681b
MOBY_COMPONENTS_URL=https://raw.githubusercontent.com/shykes/moby-extras/$(MOBY_COMPONENTS_SHA)/cmd/moby-components
MOBY_COMPONENTS=.helpers/moby-components-$(MOBY_COMPONENTS_SHA)
VERSION=$(shell cat VERSION)
.PHONY: help
help: ## show make targets
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
.PHONY: test-integration-cli
test-integration-cli: $(CLI_DIR)/build/docker ## test integration of cli and engine
$(MAKE) -C $(ENGINE_DIR) DOCKER_CLI_PATH=$< test-integration-cli
$(CLI_DIR)/build/docker:
$(MAKE) -C $(CLI_DIR) -f docker.Makefile build
.PHONY: deb
deb: ## build deb packages
$(MAKE) VERSION=$(VERSION) CLI_DIR=$(CLI_DIR) ENGINE_DIR=$(ENGINE_DIR) -C $(PACKAGING_DIR) deb
.PHONY: rpm
rpm: ## build rpm packages
$(MAKE) VERSION=$(VERSION) CLI_DIR=$(CLI_DIR) ENGINE_DIR=$(ENGINE_DIR) -C $(PACKAGING_DIR) rpm
.PHONY: static
static: ## build static packages
$(MAKE) VERSION=$(VERSION) CLI_DIR=$(CLI_DIR) ENGINE_DIR=$(ENGINE_DIR) -C $(PACKAGING_DIR) static
.PHONY: clean
clean: ## clean the build artifacts
-$(MAKE) -C $(CLI_DIR) clean
-$(MAKE) -C $(ENGINE_DIR) clean
-$(MAKE) -C $(PACKAGING_DIR) clean
$(MOBY_COMPONENTS):
mkdir -p .helpers
curl -fsSL $(MOBY_COMPONENTS_URL) > $(MOBY_COMPONENTS)
chmod +x $(MOBY_COMPONENTS)
.PHONY: update-components
update-components: update-components-cli update-components-engine update-components-packaging ## udpate components using moby extra tool
.PHONY: update-components-cli
update-components-cli: $(MOBY_COMPONENTS)
$(MOBY_COMPONENTS) update cli
.PHONY: update-components-engine
update-components-engine: $(MOBY_COMPONENTS)
$(MOBY_COMPONENTS) update engine
.PHONY: update-components-packaging
update-components-packaging: $(MOBY_COMPONENTS)
$(MOBY_COMPONENTS) update packaging

View File

@ -1,24 +1,12 @@
# Docker CE
This repository hosts open source components of Docker CE products. The
`master` branch serves to unify the upstream components on a regular
basis. Long-lived release branches host the code that goes into a product
version for the lifetime of the product.
This repository hosts open source components of Docker Community Edition
(CE) products. The `master` branch serves to unify the upstream components
on a regular basis. Long-lived release branches host the code that goes
into a product version for the lifetime of the product.
This repository is solely maintained by Docker, Inc.
## Issues
There are separate issue-tracking repos for the end user Docker CE
products specialized for a platform. Find your issue or file a new issue
for the platform you are using:
* https://github.com/docker/for-linux
* https://github.com/docker/for-mac
* https://github.com/docker/for-win
* https://github.com/docker/for-aws
* https://github.com/docker/for-azure
## Unifying upstream sources
The `master` branch is a combination of components adapted from

View File

@ -1 +1 @@
17.10.0-ce-rc2
17.06.0-ce-rc4

View File

@ -1,9 +0,0 @@
# Github code owners
# See https://github.com/blog/2392-introducing-code-owners
cli/command/stack/** @dnephin @vdemeester
cli/compose/** @dnephin @vdemeester
contrib/completion/bash/** @albers
contrib/completion/zsh/** @sdurrheimer
docs/** @mstanleyjones @vdemeester @thaJeztah
scripts/** @dnephin

View File

@ -6,5 +6,3 @@ cli/winresources/rsrc_amd64.syso
/man/man5/
/man/man8/
/docs/yaml/gen/
coverage.txt
profile.out

View File

@ -1,365 +0,0 @@
# Contributing to Docker
Want to hack on Docker? Awesome! We have a contributor's guide that explains
[setting up a Docker development environment and the contribution
process](https://docs.docker.com/opensource/project/who-written-for/).
This page contains information about reporting issues as well as some tips and
guidelines useful to experienced open source contributors. Finally, make sure
you read our [community guidelines](#docker-community-guidelines) before you
start participating.
## Topics
* [Reporting Security Issues](#reporting-security-issues)
* [Design and Cleanup Proposals](#design-and-cleanup-proposals)
* [Reporting Issues](#reporting-other-issues)
* [Quick Contribution Tips and Guidelines](#quick-contribution-tips-and-guidelines)
* [Community Guidelines](#docker-community-guidelines)
## Reporting security issues
The Docker maintainers take security seriously. If you discover a security
issue, please bring it to their attention right away!
Please **DO NOT** file a public issue, instead send your report privately to
[security@docker.com](mailto:security@docker.com).
Security reports are greatly appreciated and we will publicly thank you for it.
We also like to send gifts&mdash;if you're into Docker schwag, make sure to let
us know. We currently do not offer a paid security bounty program, but are not
ruling it out in the future.
## Reporting other issues
A great way to contribute to the project is to send a detailed report when you
encounter an issue. We always appreciate a well-written, thorough bug report,
and will thank you for it!
Check that [our issue database](https://github.com/docker/cli/issues)
doesn't already include that problem or suggestion before submitting an issue.
If you find a match, you can use the "subscribe" button to get notified on
updates. Do *not* leave random "+1" or "I have this too" comments, as they
only clutter the discussion, and don't help resolving it. However, if you
have ways to reproduce the issue or have additional information that may help
resolving the issue, please leave a comment.
When reporting issues, always include:
* The output of `docker version`.
* The output of `docker info`.
Also include the steps required to reproduce the problem if possible and
applicable. This information will help us review and fix your issue faster.
When sending lengthy log-files, consider posting them as a gist (https://gist.github.com).
Don't forget to remove sensitive data from your logfiles before posting (you can
replace those parts with "REDACTED").
## Quick contribution tips and guidelines
This section gives the experienced contributor some tips and guidelines.
### Pull requests are always welcome
Not sure if that typo is worth a pull request? Found a bug and know how to fix
it? Do it! We will appreciate it. Any significant improvement should be
documented as [a GitHub issue](https://github.com/docker/cli/issues) before
anybody starts working on it.
We are always thrilled to receive pull requests. We do our best to process them
quickly. If your pull request is not accepted on the first try,
don't get discouraged! Our contributor's guide explains [the review process we
use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contribution/).
### Talking to other Docker users and contributors
<table class="tg">
<col width="45%">
<col width="65%">
<tr>
<td>Forums</td>
<td>
A public forum for users to discuss questions and explore current design patterns and
best practices about Docker and related projects in the Docker Ecosystem. To participate,
just log in with your Docker Hub account on <a href="https://forums.docker.com" target="_blank">https://forums.docker.com</a>.
</td>
</tr>
<tr>
<td>Community Slack</td>
<td>
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://community.docker.com/registrations/groups/4316" target="_blank">with this link</a>.
</td>
</tr>
<tr>
<td>Twitter</td>
<td>
You can follow <a href="https://twitter.com/docker/" target="_blank">Docker's Twitter feed</a>
to get updates on our products. You can also tweet us questions or just
share blogs or stories.
</td>
</tr>
<tr>
<td>Stack Overflow</td>
<td>
Stack Overflow has over 17000 Docker questions listed. We regularly
monitor <a href="https://stackoverflow.com/search?tab=newest&q=docker" target="_blank">Docker questions</a>
and so do many other knowledgeable Docker users.
</td>
</tr>
</table>
### Conventions
Fork the repository and make changes on your fork in a feature branch:
- If it's a bug fix branch, name it XXXX-something where XXXX is the number of
the issue.
- If it's a feature branch, create an enhancement issue to announce
your intentions, and name it XXXX-something where XXXX is the number of the
issue.
Submit unit tests for your changes. Go has a great test framework built in; use
it! Take a look at existing tests for inspiration. [Run the full test
suite](README.md) on your branch before
submitting a pull request.
Update the documentation when creating or modifying features. Test your
documentation changes for clarity, concision, and correctness, as well as a
clean documentation build. See our contributors guide for [our style
guide](https://docs.docker.com/opensource/doc-style) and instructions on [building
the documentation](https://docs.docker.com/opensource/project/test-and-docs/#build-and-test-the-documentation).
Write clean code. Universally formatted code promotes ease of writing, reading,
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
committing your changes. Most editors have plug-ins that do this automatically.
Pull request descriptions should be as clear as possible and include a reference
to all the issues that they address.
Commit messages must start with a capitalized and short summary (max. 50 chars)
written in the imperative, followed by an optional, more detailed explanatory
text which is separated from the summary by an empty line.
Code review comments may be added to your pull request. Discuss, then make the
suggested modifications and push additional commits to your feature branch. Post
a comment after pushing. New commits show up in the pull request automatically,
but the reviewers are notified only when you comment.
Pull requests must be cleanly rebased on top of master without multiple branches
mixed into the PR.
**Git tip**: If your PR no longer merges cleanly, use `rebase master` in your
feature branch to update your pull request rather than `merge master`.
Before you make a pull request, squash your commits into logical units of work
using `git rebase -i` and `git push -f`. A logical unit of work is a consistent
set of patches that should be reviewed together: for example, upgrading the
version of a vendored dependency and taking advantage of its now available new
feature constitute two separate units of work. Implementing a new function and
calling it in another file constitute a single logical unit of work. The very
high majority of submissions should have a single commit, so if in doubt: squash
down to one.
After every commit, make sure the test suite passes. Include documentation
changes in the same pull request so that a revert would remove all traces of
the feature or fix.
Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in the pull request
description that close an issue. Including references automatically closes the issue
on a merge.
Please do not add yourself to the `AUTHORS` file, as it is regenerated regularly
from the Git history.
Please see the [Coding Style](#coding-style) for further guidelines.
### Merge approval
Docker maintainers use LGTM (Looks Good To Me) in comments on the code review to
indicate acceptance.
A change requires LGTMs from an absolute majority of the maintainers of each
component affected. For example, if a change affects `docs/` and `registry/`, it
needs an absolute majority from the maintainers of `docs/` AND, separately, an
absolute majority of the maintainers of `registry/`.
For more details, see the [MAINTAINERS](MAINTAINERS) page.
### Sign your work
The sign-off is a simple line at the end of the explanation for the patch. Your
signature certifies that you wrote the patch or otherwise have the right to pass
it on as an open-source patch. The rules are pretty simple: if you can certify
the below (from [developercertificate.org](http://developercertificate.org/)):
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
Then you just add a line to every git commit message:
Signed-off-by: Joe Smith <joe.smith@email.com>
Use your real name (sorry, no pseudonyms or anonymous contributions.)
If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`.
### How can I become a maintainer?
The procedures for adding new maintainers are explained in the
global [MAINTAINERS](https://github.com/docker/opensource/blob/master/MAINTAINERS)
file in the [https://github.com/docker/opensource/](https://github.com/docker/opensource/)
repository.
Don't forget: being a maintainer is a time investment. Make sure you
will have time to make yourself available. You don't have to be a
maintainer to make a difference on the project!
## Docker community guidelines
We want to keep the Docker community awesome, growing and collaborative. We need
your help to keep it that way. To help with this we've come up with some general
guidelines for the community as a whole:
* Be nice: Be courteous, respectful and polite to fellow community members:
no regional, racial, gender, or other abuse will be tolerated. We like
nice people way better than mean ones!
* Encourage diversity and participation: Make everyone in our community feel
welcome, regardless of their background and the extent of their
contributions, and do everything possible to encourage participation in
our community.
* Keep it legal: Basically, don't get us in trouble. Share only content that
you own, do not share private or sensitive information, and don't break
the law.
* Stay on topic: Make sure that you are posting to the correct channel and
avoid off-topic discussions. Remember when you update an issue or respond
to an email you are potentially sending to a large number of people. Please
consider this before you update. Also remember that nobody likes spam.
* Don't send email to the maintainers: There's no need to send email to the
maintainers to ask them to investigate an issue or to take a look at a
pull request. Instead of sending an email, GitHub mentions should be
used to ping maintainers to review a pull request, a proposal or an
issue.
### Guideline violations — 3 strikes method
The point of this section is not to find opportunities to punish people, but we
do need a fair way to deal with people who are making our community suck.
1. First occurrence: We'll give you a friendly, but public reminder that the
behavior is inappropriate according to our guidelines.
2. Second occurrence: We will send you a private message with a warning that
any additional violations will result in removal from the community.
3. Third occurrence: Depending on the violation, we may need to delete or ban
your account.
**Notes:**
* Obvious spammers are banned on first occurrence. If we don't do this, we'll
have spam all over the place.
* Violations are forgiven after 6 months of good behavior, and we won't hold a
grudge.
* People who commit minor infractions will get some education, rather than
hammering them in the 3 strikes process.
* The rules apply equally to everyone in the community, no matter how much
you've contributed.
* Extreme violations of a threatening, abusive, destructive or illegal nature
will be addressed immediately and are not subject to 3 strikes or forgiveness.
* Contact abuse@docker.com to report abuse or appeal violations. In the case of
appeals, we know that mistakes happen, and we'll work with you to come up with a
fair solution if there has been a misunderstanding.
## Coding Style
Unless explicitly stated, we follow all coding guidelines from the Go
community. While some of these standards may seem arbitrary, they somehow seem
to result in a solid, consistent codebase.
It is possible that the code base does not currently comply with these
guidelines. We are not looking for a massive PR that fixes this, since that
goes against the spirit of the guidelines. All new contributions should make a
best effort to clean up and make the code base better than they left it.
Obviously, apply your best judgement. Remember, the goal here is to make the
code base easier for humans to navigate and understand. Always keep that in
mind when nudging others to comply.
The rules:
1. All code should be formatted with `gofmt -s`.
2. All code should pass the default levels of
[`golint`](https://github.com/golang/lint).
3. All code should follow the guidelines covered in [Effective
Go](http://golang.org/doc/effective_go.html) and [Go Code Review
Comments](https://github.com/golang/go/wiki/CodeReviewComments).
4. Comment the code. Tell us the why, the history and the context.
5. Document _all_ declarations and methods, even private ones. Declare
expectations, caveats and anything else that may be important. If a type
gets exported, having the comments already there will ensure it's ready.
6. Variable name length should be proportional to its context and no longer.
`noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`.
In practice, short methods will have short variable names and globals will
have longer names.
7. No underscores in package names. If you need a compound name, step back,
and re-examine why you need a compound name. If you still think you need a
compound name, lose the underscore.
8. No utils or helpers packages. If a function is not general enough to
warrant its own package, it has not been written generally enough to be a
part of a util package. Just leave it unexported and well-documented.
9. All tests should run with `go test` and outside tooling should not be
required. No, we don't need another unit testing framework. Assertion
packages are acceptable if they provide _real_ incremental value.
10. Even though we call these "rules" above, they are actually just
guidelines. Since you've read all the rules, you now know that.
If you are having trouble getting into the mood of idiomatic Go, we recommend
reading through [Effective Go](https://golang.org/doc/effective_go.html). The
[Go Blog](https://blog.golang.org) is also a great resource. Drinking the
kool-aid is a lot easier than going thirsty.

View File

@ -1,12 +0,0 @@
wrappedNode(label: 'linux && x86_64', cleanWorkspace: true) {
timeout(time: 60, unit: 'MINUTES') {
stage "Git Checkout"
checkout scm
stage "Run end-to-end test suite"
sh "docker version"
sh "E2E_UNIQUE_ID=clie2e${BUILD_NUMBER} \
IMAGE_TAG=clie2e${BUILD_NUMBER} \
make -f docker.Makefile test-e2e"
}
}

View File

@ -27,7 +27,6 @@
people = [
"aaronlehmann",
"albers",
"aluzzardi",
"anusha",
"cpuguy83",
@ -85,11 +84,6 @@
Email = "aaron.lehmann@docker.com"
GitHub = "aaronlehmann"
[people.albers]
Name = "Harald Albers"
Email = "github@albersweb.de"
GitHub = "albers"
[people.aluzzardi]
Name = "Andrea Luzzardi"
Email = "al@docker.com"

View File

@ -1,85 +1,59 @@
#
# github.com/docker/cli
#
all: binary
_:=$(shell ./scripts/warn-outside-container $(MAKECMDGOALS))
# remove build artifacts
.PHONY: clean
clean: ## remove build artifacts
rm -rf ./build/* cli/winresources/rsrc_* ./man/man[1-9] docs/yaml/gen
.PHONY: test-unit
test-unit: ## run unit test
./scripts/test/unit $(shell go list ./... | grep -vE '/vendor/|/e2e/')
clean:
@rm -rf ./build/* cli/winresources/rsrc_* ./man/man[1-9] docs/yaml/gen
# run go test
# the "-tags daemon" part is temporary
.PHONY: test
test: test-unit ## run tests
.PHONY: test-coverage
test-coverage: ## run test coverage
./scripts/test/unit-with-coverage $(shell go list ./... | grep -vE '/vendor/|/e2e/')
test:
@go test -tags daemon -v $(shell go list ./... | grep -v '/vendor/')
.PHONY: lint
lint: ## run all the lint tools
gometalinter --config gometalinter.json ./...
lint:
@gometalinter --config gometalinter.json ./...
.PHONY: binary
binary: ## build executable for Linux
@echo "WARNING: binary creates a Linux executable. Use cross for macOS or Windows."
./scripts/build/binary
binary:
@./scripts/build/binary
# build the CLI for multiple architectures
.PHONY: cross
cross: ## build executable for macOS and Windows
./scripts/build/cross
.PHONY: binary-windows
binary-windows: ## build executable for Windows
./scripts/build/windows
.PHONY: binary-osx
binary-osx: ## build executable for macOS
./scripts/build/osx
cross:
@./scripts/build/cross
.PHONY: dynbinary
dynbinary: ## build dynamically linked binary
./scripts/build/dynbinary
dynbinary:
@./scripts/build/dynbinary
.PHONY: watch
watch: ## monitor file changes and run go test
./scripts/test/watch
watch:
@./scripts/test/watch
vendor: vendor.conf ## check that vendor matches vendor.conf
vndr 2> /dev/null
scripts/validate/check-git-diff vendor
# download dependencies (vendor/) listed in vendor.conf
.PHONY: vendor
vendor: vendor.conf
@vndr 2> /dev/null
@scripts/validate/check-git-diff vendor
## generate man pages from go source and markdown
.PHONY: manpages
manpages: ## generate man pages from go source and markdown
scripts/docs/generate-man.sh
manpages:
@scripts/docs/generate-man.sh
## generate documentation YAML files consumed by docs repo
.PHONY: yamldocs
yamldocs: ## generate documentation YAML files consumed by docs repo
scripts/docs/generate-yaml.sh
.PHONY: shellcheck
shellcheck: ## run shellcheck validation
scripts/validate/shellcheck
.PHONY: help
help: ## print this help
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
yamldocs:
@scripts/docs/generate-yaml.sh
cli/compose/schema/bindata.go: cli/compose/schema/data/*.json
go generate github.com/docker/cli/cli/compose/schema
compose-jsonschema: cli/compose/schema/bindata.go
scripts/validate/check-git-diff cli/compose/schema/bindata.go
.PHONY: ci-validate
ci-validate:
time make -B vendor
time make -B compose-jsonschema
time make manpages
time make yamldocs
@scripts/validate/check-git-diff cli/compose/schema/bindata.go

View File

@ -1,4 +1,4 @@
[![build status](https://circleci.com/gh/docker/cli.svg?style=shield)](https://circleci.com/gh/docker/cli/tree/master) [![Build Status](https://jenkins.dockerproject.org/job/docker/job/cli/job/master/badge/icon)](https://jenkins.dockerproject.org/job/docker/job/cli/job/master/)
[![build status](https://circleci.com/gh/docker/cli.svg?style=shield)](https://circleci.com/gh/docker/cli/tree/master)
docker/cli
==========
@ -29,12 +29,6 @@ Run all linting:
$ make -f docker.Makefile lint
```
List all the available targets:
```
$ make help
```
### In-container development environment
Start an interactive development environment:

View File

@ -1,87 +0,0 @@
# Testing
The following guidelines summarize the testing policy for docker/cli.
## Unit Test Suite
All code changes should have unit test coverage.
Error cases should be tested with unit tests.
Bug fixes should be covered by new unit tests or additional assertions in
existing unit tests.
### Details
The unit test suite follows the standard Go testing convention. Tests are
located in the package directory in `_test.go` files.
Unit tests should be named using the convention:
```
Test<Function Name><Test Case Name>
```
[Table tests](https://github.com/golang/go/wiki/TableDrivenTests) should be used
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).
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).
## End-to-End Test Suite
The end-to-end test suite tests a cli binary against a real API backend.
### Guidelines
Each feature (subcommand) should have a single end-to-end test for
the success case. The test should include all (or most) flags/options supported
by that feature.
In some rare cases a couple additional end-to-end tests may be written for a
sufficiently complex and critical feature (ex: `container run`, `service
create`, `service update`, and `docker build` may have ~3-5 cases each).
In some rare cases a sufficiently critical error paths may have a single
end-to-end test case.
In all other cases the behaviour should be covered by unit tests.
If a code change adds a new flag, that flag should be added to the existing
"success case" end-to-end test.
If a code change fixes a bug, that bug fix should be covered either by adding
assertions to the existing end-to-end test, or with one or more unit test.
### Details
The end-to-end test suite is located in
[./e2e](https://github.com/docker/cli/tree/master/e2e). Each directory in `e2e`
corresponds to a directory in `cli/command` and contains the tests for that
subcommand. Files in each directory should be named `<command>_test.go` where
command is the basename of the command (ex: the test for `docker stack deploy`
is found in `e2e/stack/deploy_test.go`).
Tests should be named using the convention:
```
Test<Command Basename>[<Test Case Name>]
```
where the test case name is only required when there are multiple test cases for
a single command.
End-to-end test should run the `docker` binary using
[gotestyourself/icmd](https://godoc.org/github.com/gotestyourself/gotestyourself/icmd)
and make assertions about the exit code, stdout, stderr, and local file system.
Any Docker image or registry operations should use `registry:5000/<image name>`
to communicate with the local instance of the Docker registry. To load
additional fixture images to the registry see
[scripts/test/e2e/run](https://github.com/docker/cli/blob/master/scripts/test/e2e/run).

View File

@ -1 +1 @@
17.10.0-ce-rc2
17.06.0-ce-rc4

View File

@ -1,116 +1,49 @@
version: 2
jobs:
lint:
build:
working_directory: /work
docker: [{image: 'docker:17.06-git'}]
docker:
- image: docker:17.05
parallelism: 4
steps:
- checkout
- setup_remote_docker:
reusable: true
exclusive: false
- run:
command: docker version
- run:
name: "Lint"
command: |
dockerfile=dockerfiles/Dockerfile.lint
echo "COPY . ." >> $dockerfile
docker build -f $dockerfile --tag cli-linter:$CIRCLE_BUILD_NUM .
docker run --rm cli-linter:$CIRCLE_BUILD_NUM
cross:
working_directory: /work
docker: [{image: 'docker:17.06-git'}]
parallelism: 3
steps:
- checkout
- setup_remote_docker:
reusable: true
exclusive: false
- run:
name: "Cross"
command: |
dockerfile=dockerfiles/Dockerfile.cross
echo "COPY . ." >> $dockerfile
docker build -f $dockerfile --tag cli-builder:$CIRCLE_BUILD_NUM .
name=cross-$CIRCLE_BUILD_NUM-$CIRCLE_NODE_INDEX
docker run \
-e CROSS_GROUP=$CIRCLE_NODE_INDEX \
--name $name cli-builder:$CIRCLE_BUILD_NUM \
make cross
docker cp \
$name:/go/src/github.com/docker/cli/build \
/work/build
- store_artifacts:
path: /work/build
test:
working_directory: /work
docker: [{image: 'docker:17.06-git'}]
steps:
- checkout
- setup_remote_docker:
reusable: true
exclusive: false
- run:
name: "Unit Test with Coverage"
command: |
dockerfile=dockerfiles/Dockerfile.dev
echo "COPY . ." >> $dockerfile
docker build -f $dockerfile --tag cli-builder:$CIRCLE_BUILD_NUM .
docker run --name \
test-$CIRCLE_BUILD_NUM cli-builder:$CIRCLE_BUILD_NUM \
make test-coverage
- run:
name: "Upload to Codecov"
command: |
docker cp \
test-$CIRCLE_BUILD_NUM:/go/src/github.com/docker/cli/coverage.txt \
coverage.txt
apk add -U bash curl
curl -s https://codecov.io/bash | bash || \
echo 'Codecov failed to upload'
validate:
working_directory: /work
docker: [{image: 'docker:17.06-git'}]
steps:
- checkout
- setup_remote_docker:
reusable: true
exclusive: false
- run:
name: "Validate Vendor, Docs, and Code Generation"
command: |
dockerfile=dockerfiles/Dockerfile.dev
echo "COPY . ." >> $dockerfile
rm -f .dockerignore # include .git
docker build -f $dockerfile --tag cli-builder-with-git:$CIRCLE_BUILD_NUM .
docker run --rm cli-builder-with-git:$CIRCLE_BUILD_NUM \
make ci-validate
shellcheck:
working_directory: /work
docker: [{image: 'docker:17.06-git'}]
steps:
name: "Install Git and SSH"
command: apk add -U git openssh
- checkout
- setup_remote_docker
- run:
name: "Run shellcheck"
name: "Lint"
command: |
dockerfile=dockerfiles/Dockerfile.shellcheck
if [ "$CIRCLE_NODE_INDEX" != "0" ]; then exit; fi
dockerfile=dockerfiles/Dockerfile.lint
echo "COPY . ." >> $dockerfile
docker build -f $dockerfile --tag cli-validator:$CIRCLE_BUILD_NUM .
docker run --rm cli-validator:$CIRCLE_BUILD_NUM \
make shellcheck
workflows:
version: 2
ci:
jobs:
- lint
- cross
- test
- validate
- shellcheck
docker build -f $dockerfile --tag cli-linter .
docker run cli-linter
- run:
name: "Cross"
command: |
if [ "$CIRCLE_NODE_INDEX" != "1" ]; then exit; fi
dockerfile=dockerfiles/Dockerfile.cross
echo "COPY . ." >> $dockerfile
docker build -f $dockerfile --tag cli-builder .
docker run --name cross cli-builder make cross
docker cp cross:/go/src/github.com/docker/cli/build /work/build
- run:
name: "Unit Test"
command: |
if [ "$CIRCLE_NODE_INDEX" != "2" ]; then exit; fi
dockerfile=dockerfiles/Dockerfile.dev
echo "COPY . ." >> $dockerfile
docker build -f $dockerfile --tag cli-builder .
docker run cli-builder make test
- run:
name: "Validate Vendor and Code Generation"
command: |
if [ "$CIRCLE_NODE_INDEX" != "3" ]; then exit; fi
dockerfile=dockerfiles/Dockerfile.dev
echo "COPY . ." >> $dockerfile
docker build -f $dockerfile --tag cli-builder .
docker run cli-builder make -B vendor compose-jsonschema
- store_artifacts:
path: /work/build

View File

@ -1,35 +0,0 @@
package checkpoint
import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"golang.org/x/net/context"
)
type fakeClient struct {
client.Client
checkpointCreateFunc func(container string, options types.CheckpointCreateOptions) error
checkpointDeleteFunc func(container string, options types.CheckpointDeleteOptions) error
checkpointListFunc func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error)
}
func (cli *fakeClient) CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error {
if cli.checkpointCreateFunc != nil {
return cli.checkpointCreateFunc(container, options)
}
return nil
}
func (cli *fakeClient) CheckpointDelete(ctx context.Context, container string, options types.CheckpointDeleteOptions) error {
if cli.checkpointDeleteFunc != nil {
return cli.checkpointDeleteFunc(container, options)
}
return nil
}
func (cli *fakeClient) CheckpointList(ctx context.Context, container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
if cli.checkpointListFunc != nil {
return cli.checkpointListFunc(container, options)
}
return []types.Checkpoint{}, nil
}

View File

@ -1,72 +0,0 @@
package checkpoint
import (
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
func TestCheckpointCreateErrors(t *testing.T) {
testCases := []struct {
args []string
checkpointCreateFunc func(container string, options types.CheckpointCreateOptions) error
expectedError string
}{
{
args: []string{"too-few-arguments"},
expectedError: "requires exactly 2 arguments",
},
{
args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 2 arguments",
},
{
args: []string{"foo", "bar"},
checkpointCreateFunc: func(container string, options types.CheckpointCreateOptions) error {
return errors.Errorf("error creating checkpoint for container foo")
},
expectedError: "error creating checkpoint for container foo",
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
checkpointCreateFunc: tc.checkpointCreateFunc,
})
cmd := newCreateCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestCheckpointCreateWithOptions(t *testing.T) {
var containerID, checkpointID, checkpointDir string
var exit bool
cli := test.NewFakeCli(&fakeClient{
checkpointCreateFunc: func(container string, options types.CheckpointCreateOptions) error {
containerID = container
checkpointID = options.CheckpointID
checkpointDir = options.CheckpointDir
exit = options.Exit
return nil
},
})
cmd := newCreateCommand(cli)
checkpoint := "checkpoint-bar"
cmd.SetArgs([]string{"container-foo", checkpoint})
cmd.Flags().Set("leave-running", "true")
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
assert.NoError(t, cmd.Execute())
assert.Equal(t, "container-foo", containerID)
assert.Equal(t, checkpoint, checkpointID)
assert.Equal(t, "/dir/foo", checkpointDir)
assert.Equal(t, false, exit)
assert.Equal(t, checkpoint, strings.TrimSpace(cli.OutBuffer().String()))
}

View File

@ -1,67 +0,0 @@
package checkpoint
import (
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
func TestCheckpointListErrors(t *testing.T) {
testCases := []struct {
args []string
checkpointListFunc func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error)
expectedError string
}{
{
args: []string{},
expectedError: "requires exactly 1 argument",
},
{
args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 1 argument",
},
{
args: []string{"foo"},
checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
return []types.Checkpoint{}, errors.Errorf("error getting checkpoints for container foo")
},
expectedError: "error getting checkpoints for container foo",
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
checkpointListFunc: tc.checkpointListFunc,
})
cmd := newListCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestCheckpointListWithOptions(t *testing.T) {
var containerID, checkpointDir string
cli := test.NewFakeCli(&fakeClient{
checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
containerID = container
checkpointDir = options.CheckpointDir
return []types.Checkpoint{
{Name: "checkpoint-foo"},
}, nil
},
})
cmd := newListCommand(cli)
cmd.SetArgs([]string{"container-foo"})
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
assert.NoError(t, cmd.Execute())
assert.Equal(t, "container-foo", containerID)
assert.Equal(t, "/dir/foo", checkpointDir)
golden.Assert(t, cli.OutBuffer().String(), "checkpoint-list-with-options.golden")
}

View File

@ -1,65 +0,0 @@
package checkpoint
import (
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
func TestCheckpointRemoveErrors(t *testing.T) {
testCases := []struct {
args []string
checkpointDeleteFunc func(container string, options types.CheckpointDeleteOptions) error
expectedError string
}{
{
args: []string{"too-few-arguments"},
expectedError: "requires exactly 2 arguments",
},
{
args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 2 arguments",
},
{
args: []string{"foo", "bar"},
checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error {
return errors.Errorf("error deleting checkpoint")
},
expectedError: "error deleting checkpoint",
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
checkpointDeleteFunc: tc.checkpointDeleteFunc,
})
cmd := newRemoveCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestCheckpointRemoveWithOptions(t *testing.T) {
var containerID, checkpointID, checkpointDir string
cli := test.NewFakeCli(&fakeClient{
checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error {
containerID = container
checkpointID = options.CheckpointID
checkpointDir = options.CheckpointDir
return nil
},
})
cmd := newRemoveCommand(cli)
cmd.SetArgs([]string{"container-foo", "checkpoint-bar"})
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
assert.NoError(t, cmd.Execute())
assert.Equal(t, "container-foo", containerID)
assert.Equal(t, "checkpoint-bar", checkpointID)
assert.Equal(t, "/dir/foo", checkpointDir)
}

View File

@ -1,2 +0,0 @@
CHECKPOINT NAME
checkpoint-foo

View File

@ -1,25 +1,24 @@
package command
import (
"fmt"
"io"
"net"
"net/http"
"os"
"runtime"
"time"
"github.com/docker/cli/cli"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/credentials"
cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/cli/cli/trust"
dopts "github.com/docker/cli/opts"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client"
"github.com/docker/go-connections/sockets"
"github.com/docker/go-connections/tlsconfig"
"github.com/docker/notary"
notaryclient "github.com/docker/notary/client"
"github.com/docker/notary/passphrase"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -41,8 +40,7 @@ type Cli interface {
In() *InStream
SetIn(in *InStream)
ConfigFile() *configfile.ConfigFile
ServerInfo() ServerInfo
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
CredentialsStore(serverAddress string) credentials.Store
}
// DockerCli is an instance the docker command line client.
@ -107,66 +105,106 @@ func (cli *DockerCli) ServerInfo() ServerInfo {
return cli.server
}
// GetAllCredentials returns all of the credentials stored in all of the
// configured credential stores.
func (cli *DockerCli) GetAllCredentials() (map[string]types.AuthConfig, error) {
auths := make(map[string]types.AuthConfig)
for registry := range cli.configFile.CredentialHelpers {
helper := cli.CredentialsStore(registry)
newAuths, err := helper.GetAll()
if err != nil {
return nil, err
}
addAll(auths, newAuths)
}
defaultStore := cli.CredentialsStore("")
newAuths, err := defaultStore.GetAll()
if err != nil {
return nil, err
}
addAll(auths, newAuths)
return auths, nil
}
func addAll(to, from map[string]types.AuthConfig) {
for reg, ac := range from {
to[reg] = ac
}
}
// CredentialsStore returns a new credentials store based
// on the settings provided in the configuration file. Empty string returns
// the default credential store.
func (cli *DockerCli) CredentialsStore(serverAddress string) credentials.Store {
if helper := getConfiguredCredentialStore(cli.configFile, serverAddress); helper != "" {
return credentials.NewNativeStore(cli.configFile, helper)
}
return credentials.NewFileStore(cli.configFile)
}
// getConfiguredCredentialStore returns the credential helper configured for the
// given registry, the default credsStore, or the empty string if neither are
// configured.
func getConfiguredCredentialStore(c *configfile.ConfigFile, serverAddress string) string {
if c.CredentialHelpers != nil && serverAddress != "" {
if helper, exists := c.CredentialHelpers[serverAddress]; exists {
return helper
}
}
return c.CredentialsStore
}
// Initialize the dockerCli runs initialization that must happen after command
// line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
cli.configFile = LoadDefaultConfigFile(cli.err)
var err error
cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
if tlsconfig.IsErrEncryptedKey(err) {
var (
passwd string
giveup bool
)
passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
newClient := func(password string) (client.APIClient, error) {
opts.Common.TLSOptions.Passphrase = password
return NewAPIClientFromFlags(opts.Common, cli.configFile)
for attempts := 0; tlsconfig.IsErrEncryptedKey(err); attempts++ {
// some code and comments borrowed from notary/trustmanager/keystore.go
passwd, giveup, err = passRetriever("private", "encrypted TLS private", false, attempts)
// Check if the passphrase retriever got an error or if it is telling us to give up
if giveup || err != nil {
return errors.Wrap(err, "private key is encrypted, but could not get passphrase")
}
opts.Common.TLSOptions.Passphrase = passwd
cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
}
cli.client, err = getClientWithPassword(passRetriever, newClient)
}
if err != nil {
return err
}
cli.initializeFromClient()
return nil
}
func (cli *DockerCli) initializeFromClient() {
cli.defaultVersion = cli.client.ClientVersion()
ping, err := cli.client.Ping(context.Background())
if err != nil {
// Default to true if we fail to connect to daemon
cli.server = ServerInfo{HasExperimental: true}
if ping.APIVersion != "" {
cli.client.NegotiateAPIVersionPing(ping)
}
return
}
cli.server = ServerInfo{
HasExperimental: ping.Experimental,
OSType: ping.OSType,
}
cli.client.NegotiateAPIVersionPing(ping)
}
func getClientWithPassword(passRetriever notary.PassRetriever, newClient func(password string) (client.APIClient, error)) (client.APIClient, error) {
for attempts := 0; ; attempts++ {
passwd, giveup, err := passRetriever("private", "encrypted TLS private", false, attempts)
if giveup || err != nil {
return nil, errors.Wrap(err, "private key is encrypted, but could not get passphrase")
if ping, err := cli.client.Ping(context.Background()); err == nil {
cli.server = ServerInfo{
HasExperimental: ping.Experimental,
OSType: ping.OSType,
}
apiclient, err := newClient(passwd)
if !tlsconfig.IsErrEncryptedKey(err) {
return apiclient, err
// since the new header was added in 1.25, assume server is 1.24 if header is not present.
if ping.APIVersion == "" {
ping.APIVersion = "1.24"
}
// if server version is lower than the current cli, downgrade
if versions.LessThan(ping.APIVersion, cli.client.ClientVersion()) {
cli.client.UpdateClientVersion(ping.APIVersion)
}
}
}
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
return nil
}
// ServerInfo stores details about the supported features and platform of the
@ -181,6 +219,19 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli {
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err}
}
// LoadDefaultConfigFile attempts to load the default config file and returns
// an initialized ConfigFile struct if none is found.
func LoadDefaultConfigFile(err io.Writer) *configfile.ConfigFile {
configFile, e := cliconfig.Load(cliconfig.Dir())
if e != nil {
fmt.Fprintf(err, "WARNING: Error loading config file:%v\n", e)
}
if !configFile.ContainsAuth() {
credentials.DetectDefaultStore(configFile)
}
return configFile
}
// NewAPIClientFromFlags creates a new APIClient from command line flags
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
@ -234,10 +285,6 @@ func newHTTPClient(host string, tlsOptions *tlsconfig.Options) (*http.Client, er
}
tr := &http.Transport{
TLSClientConfig: config,
DialContext: (&net.Dialer{
KeepAlive: 30 * time.Second,
Timeout: 30 * time.Second,
}).DialContext,
}
proto, addr, _, err := client.ParseHost(host)
if err != nil {

View File

@ -1,198 +0,0 @@
package command
import (
"os"
"testing"
"crypto/x509"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/flags"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
func TestNewAPIClientFromFlags(t *testing.T) {
host := "unix://path"
opts := &flags.CommonOptions{Hosts: []string{host}}
configFile := &configfile.ConfigFile{
HTTPHeaders: map[string]string{
"My-Header": "Custom-Value",
},
}
apiclient, err := NewAPIClientFromFlags(opts, configFile)
require.NoError(t, err)
assert.Equal(t, host, apiclient.DaemonHost())
expectedHeaders := map[string]string{
"My-Header": "Custom-Value",
"User-Agent": UserAgent(),
}
assert.Equal(t, expectedHeaders, apiclient.(*client.Client).CustomHTTPHeaders())
assert.Equal(t, api.DefaultVersion, apiclient.ClientVersion())
}
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
customVersion := "v3.3.3"
defer patchEnvVariable(t, "DOCKER_API_VERSION", customVersion)()
opts := &flags.CommonOptions{}
configFile := &configfile.ConfigFile{}
apiclient, err := NewAPIClientFromFlags(opts, configFile)
require.NoError(t, err)
assert.Equal(t, customVersion, apiclient.ClientVersion())
}
// TODO: use gotestyourself/env.Patch
func patchEnvVariable(t *testing.T, key, value string) func() {
oldValue, ok := os.LookupEnv(key)
require.NoError(t, os.Setenv(key, value))
return func() {
if !ok {
require.NoError(t, os.Unsetenv(key))
return
}
require.NoError(t, os.Setenv(key, oldValue))
}
}
type fakeClient struct {
client.Client
pingFunc func() (types.Ping, error)
version string
negotiated bool
}
func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) {
return c.pingFunc()
}
func (c *fakeClient) ClientVersion() string {
return c.version
}
func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
c.negotiated = true
}
func TestInitializeFromClient(t *testing.T) {
defaultVersion := "v1.55"
var testcases = []struct {
doc string
pingFunc func() (types.Ping, error)
expectedServer ServerInfo
negotiated bool
}{
{
doc: "successful ping",
pingFunc: func() (types.Ping, error) {
return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil
},
expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
negotiated: true,
},
{
doc: "failed ping, no API version",
pingFunc: func() (types.Ping, error) {
return types.Ping{}, errors.New("failed")
},
expectedServer: ServerInfo{HasExperimental: true},
},
{
doc: "failed ping, with API version",
pingFunc: func() (types.Ping, error) {
return types.Ping{APIVersion: "v1.33"}, errors.New("failed")
},
expectedServer: ServerInfo{HasExperimental: true},
negotiated: true,
},
}
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
apiclient := &fakeClient{
pingFunc: testcase.pingFunc,
version: defaultVersion,
}
cli := &DockerCli{client: apiclient}
cli.initializeFromClient()
assert.Equal(t, defaultVersion, cli.defaultVersion)
assert.Equal(t, testcase.expectedServer, cli.server)
assert.Equal(t, testcase.negotiated, apiclient.negotiated)
})
}
}
func TestGetClientWithPassword(t *testing.T) {
expected := "password"
var testcases = []struct {
doc string
password string
retrieverErr error
retrieverGiveup bool
newClientErr error
expectedErr string
}{
{
doc: "successful connect",
password: expected,
},
{
doc: "password retriever exhausted",
retrieverGiveup: true,
retrieverErr: errors.New("failed"),
expectedErr: "private key is encrypted, but could not get passphrase",
},
{
doc: "password retriever error",
retrieverErr: errors.New("failed"),
expectedErr: "failed",
},
{
doc: "newClient error",
newClientErr: errors.New("failed to connect"),
expectedErr: "failed to connect",
},
}
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
passRetriever := func(_, _ string, _ bool, attempts int) (passphrase string, giveup bool, err error) {
// Always return an invalid pass first to test iteration
switch attempts {
case 0:
return "something else", false, nil
default:
return testcase.password, testcase.retrieverGiveup, testcase.retrieverErr
}
}
newClient := func(currentPassword string) (client.APIClient, error) {
if testcase.newClientErr != nil {
return nil, testcase.newClientErr
}
if currentPassword == expected {
return &client.Client{}, nil
}
return &client.Client{}, x509.IncorrectPasswordError
}
_, err := getClientWithPassword(passRetriever, newClient)
if testcase.expectedErr != "" {
testutil.ErrorContains(t, err, testcase.expectedErr)
return
}
assert.NoError(t, err)
})
}
}

View File

@ -17,7 +17,6 @@ import (
"github.com/docker/cli/cli/command/stack"
"github.com/docker/cli/cli/command/swarm"
"github.com/docker/cli/cli/command/system"
"github.com/docker/cli/cli/command/trust"
"github.com/docker/cli/cli/command/volume"
"github.com/spf13/cobra"
)
@ -70,9 +69,6 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
// swarm
swarm.NewSwarmCommand(dockerCli),
// trust
trust.NewTrustCommand(dockerCli),
// volume
volume.NewVolumeCommand(dockerCli),

View File

@ -10,6 +10,7 @@ import (
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/system"
runconfigopts "github.com/docker/docker/runconfig/opts"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
@ -64,7 +65,7 @@ func runConfigCreate(dockerCli command.Cli, options createOptions) error {
spec := swarm.ConfigSpec{
Annotations: swarm.Annotations{
Name: options.name,
Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
},
Data: configData,
}

View File

@ -1,17 +1,18 @@
package config
import (
"bytes"
"io/ioutil"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -26,10 +27,10 @@ func TestConfigCreateErrors(t *testing.T) {
}{
{
args: []string{"too_few"},
expectedError: "requires exactly 2 arguments",
expectedError: "requires exactly 2 argument(s)",
},
{args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 2 arguments",
expectedError: "requires exactly 2 argument(s)",
},
{
args: []string{"name", filepath.Join("testdata", configDataFile)},
@ -40,10 +41,11 @@ func TestConfigCreateErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newConfigCreateCommand(
test.NewFakeCli(&fakeClient{
configCreateFunc: tc.configCreateFunc,
}),
}, buf),
)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
@ -53,6 +55,7 @@ func TestConfigCreateErrors(t *testing.T) {
func TestConfigCreateWithName(t *testing.T) {
name := "foo"
buf := new(bytes.Buffer)
var actual []byte
cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
@ -66,13 +69,14 @@ func TestConfigCreateWithName(t *testing.T) {
ID: "ID-" + spec.Name,
}, nil
},
})
}, buf)
cmd := newConfigCreateCommand(cli)
cmd.SetArgs([]string{name, filepath.Join("testdata", configDataFile)})
assert.NoError(t, cmd.Execute())
golden.Assert(t, string(actual), configDataFile)
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
expected := golden.Get(t, actual, configDataFile)
assert.Equal(t, string(expected), string(actual))
assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String()))
}
func TestConfigCreateWithLabels(t *testing.T) {
@ -82,6 +86,7 @@ func TestConfigCreateWithLabels(t *testing.T) {
}
name := "foo"
buf := new(bytes.Buffer)
cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if spec.Name != name {
@ -96,12 +101,12 @@ func TestConfigCreateWithLabels(t *testing.T) {
ID: "ID-" + spec.Name,
}, nil
},
})
}, buf)
cmd := newConfigCreateCommand(cli)
cmd.SetArgs([]string{name, filepath.Join("testdata", configDataFile)})
cmd.Flags().Set("label", "lbl1=Label-foo")
cmd.Flags().Set("label", "lbl2=Label-bar")
assert.NoError(t, cmd.Execute())
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String()))
}

View File

@ -1,18 +1,19 @@
package config
import (
"bytes"
"fmt"
"io/ioutil"
"testing"
"time"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
"github.com/docker/cli/internal/test/testutil"
"github.com/gotestyourself/gotestyourself/golden"
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/stretchr/testify/assert"
)
@ -52,10 +53,11 @@ func TestConfigInspectErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newConfigInspectCommand(
test.NewFakeCli(&fakeClient{
configInspectFunc: tc.configInspectFunc,
}),
}, buf),
)
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
@ -93,11 +95,17 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{configInspectFunc: tc.configInspectFunc})
cmd := newConfigInspectCommand(cli)
buf := new(bytes.Buffer)
cmd := newConfigInspectCommand(
test.NewFakeCli(&fakeClient{
configInspectFunc: tc.configInspectFunc,
}, buf),
)
cmd.SetArgs(tc.args)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-without-format.%s.golden", tc.name))
actual := buf.String()
expected := golden.Get(t, []byte(actual), fmt.Sprintf("config-inspect-without-format.%s.golden", tc.name))
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
}
@ -127,14 +135,18 @@ func TestConfigInspectWithFormat(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
configInspectFunc: tc.configInspectFunc,
})
cmd := newConfigInspectCommand(cli)
buf := new(bytes.Buffer)
cmd := newConfigInspectCommand(
test.NewFakeCli(&fakeClient{
configInspectFunc: tc.configInspectFunc,
}, buf),
)
cmd.SetArgs(tc.args)
cmd.Flags().Set("format", tc.format)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-with-format.%s.golden", tc.name))
actual := buf.String()
expected := golden.Get(t, []byte(actual), fmt.Sprintf("config-inspect-with-format.%s.golden", tc.name))
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
}
@ -160,14 +172,16 @@ func TestConfigInspectPretty(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
configInspectFunc: tc.configInspectFunc,
})
cmd := newConfigInspectCommand(cli)
buf := new(bytes.Buffer)
cmd := newConfigInspectCommand(
test.NewFakeCli(&fakeClient{
configInspectFunc: tc.configInspectFunc,
}, buf))
cmd.SetArgs([]string{"configID"})
cmd.Flags().Set("pretty", "true")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-pretty.%s.golden", tc.name))
actual := buf.String()
expected := golden.Get(t, []byte(actual), fmt.Sprintf("config-inspect-pretty.%s.golden", tc.name))
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
}

View File

@ -1,27 +1,15 @@
package config
import (
"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/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/spf13/cobra"
"golang.org/x/net/context"
"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,8 +55,6 @@ func runConfigList(dockerCli command.Cli, options listOptions) error {
}
}
sort.Sort(byConfigName(configs))
configCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewConfigFormat(format, options.quiet),

View File

@ -1,19 +1,20 @@
package config
import (
"bytes"
"io/ioutil"
"testing"
"time"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
"github.com/docker/cli/internal/test/testutil"
"github.com/gotestyourself/gotestyourself/golden"
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/stretchr/testify/assert"
)
@ -35,10 +36,11 @@ func TestConfigListErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newConfigListCommand(
test.NewFakeCli(&fakeClient{
configListFunc: tc.configListFunc,
}),
}, buf),
)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
@ -47,36 +49,36 @@ func TestConfigListErrors(t *testing.T) {
}
func TestConfigList(t *testing.T) {
buf := new(bytes.Buffer)
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{
*Config(ConfigID("ID-1-foo"),
ConfigName("1-foo"),
*Config(ConfigID("ID-foo"),
ConfigName("foo"),
ConfigVersion(swarm.Version{Index: 10}),
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
*Config(ConfigID("ID-10-foo"),
ConfigName("10-foo"),
ConfigVersion(swarm.Version{Index: 11}),
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
*Config(ConfigID("ID-2-foo"),
ConfigName("2-foo"),
*Config(ConfigID("ID-bar"),
ConfigName("bar"),
ConfigVersion(swarm.Version{Index: 11}),
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
}, nil
},
})
}, buf)
cli.SetConfigfile(&configfile.ConfigFile{})
cmd := newConfigListCommand(cli)
cmd.SetOutput(buf)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list-sort.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "config-list.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestConfigListWithQuietOption(t *testing.T) {
buf := new(bytes.Buffer)
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{
@ -86,14 +88,18 @@ func TestConfigListWithQuietOption(t *testing.T) {
})),
}, nil
},
})
}, buf)
cli.SetConfigfile(&configfile.ConfigFile{})
cmd := newConfigListCommand(cli)
cmd.Flags().Set("quiet", "true")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-quiet-option.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "config-list-with-quiet-option.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestConfigListWithConfigFormat(t *testing.T) {
buf := new(bytes.Buffer)
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{
@ -103,16 +109,19 @@ func TestConfigListWithConfigFormat(t *testing.T) {
})),
}, nil
},
})
cli.SetConfigFile(&configfile.ConfigFile{
}, buf)
cli.SetConfigfile(&configfile.ConfigFile{
ConfigFormat: "{{ .Name }} {{ .Labels }}",
})
cmd := newConfigListCommand(cli)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-config-format.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "config-list-with-config-format.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestConfigListWithFormat(t *testing.T) {
buf := new(bytes.Buffer)
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{
@ -122,14 +131,17 @@ func TestConfigListWithFormat(t *testing.T) {
})),
}, nil
},
})
}, buf)
cmd := newConfigListCommand(cli)
cmd.Flags().Set("format", "{{ .Name }} {{ .Labels }}")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-format.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "config-list-with-format.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestConfigListWithFilter(t *testing.T) {
buf := new(bytes.Buffer)
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
assert.Equal(t, "foo", options.Filters.Get("name")[0])
@ -149,10 +161,13 @@ func TestConfigListWithFilter(t *testing.T) {
),
}, nil
},
})
}, buf)
cli.SetConfigfile(&configfile.ConfigFile{})
cmd := newConfigListCommand(cli)
cmd.Flags().Set("filter", "name=foo")
cmd.Flags().Set("filter", "label=lbl1=Label-bar")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-filter.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "config-list-with-filter.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}

View File

@ -1,12 +1,13 @@
package config
import (
"bytes"
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -19,7 +20,7 @@ func TestConfigRemoveErrors(t *testing.T) {
}{
{
args: []string{},
expectedError: "requires at least 1 argument.",
expectedError: "requires at least 1 argument(s).",
},
{
args: []string{"foo"},
@ -30,10 +31,11 @@ func TestConfigRemoveErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newConfigRemoveCommand(
test.NewFakeCli(&fakeClient{
configRemoveFunc: tc.configRemoveFunc,
}),
}, buf),
)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
@ -43,22 +45,24 @@ func TestConfigRemoveErrors(t *testing.T) {
func TestConfigRemoveWithName(t *testing.T) {
names := []string{"foo", "bar"}
buf := new(bytes.Buffer)
var removedConfigs []string
cli := test.NewFakeCli(&fakeClient{
configRemoveFunc: func(name string) error {
removedConfigs = append(removedConfigs, name)
return nil
},
})
}, buf)
cmd := newConfigRemoveCommand(cli)
cmd.SetArgs(names)
assert.NoError(t, cmd.Execute())
assert.Equal(t, names, strings.Split(strings.TrimSpace(cli.OutBuffer().String()), "\n"))
assert.Equal(t, names, strings.Split(strings.TrimSpace(buf.String()), "\n"))
assert.Equal(t, names, removedConfigs)
}
func TestConfigRemoveContinueAfterError(t *testing.T) {
names := []string{"foo", "bar"}
buf := new(bytes.Buffer)
var removedConfigs []string
cli := test.NewFakeCli(&fakeClient{
@ -69,11 +73,10 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
}
return nil
},
})
}, buf)
cmd := newConfigRemoveCommand(cli)
cmd.SetArgs(names)
cmd.SetOutput(ioutil.Discard)
assert.EqualError(t, cmd.Execute(), "error removing config: foo")
assert.Equal(t, names, removedConfigs)
}

View File

@ -1,8 +1,8 @@
ID: configID
Name: configName
ID: configID
Name: configName
Labels:
- lbl1=value1
Created at: 0001-01-01 00:00:00 +0000 utc
Updated at: 0001-01-01 00:00:00 +0000 utc
- lbl1=value1
Created at: 0001-01-01 00:00:00+0000 utc
Updated at: 0001-01-01 00:00:00+0000 utc
Data:
payload here

View File

@ -1,7 +1,7 @@
[
{
"ID": "ID-foo",
"Version": {},
"Version": {},
"CreatedAt": "0001-01-01T00:00:00Z",
"UpdatedAt": "0001-01-01T00:00:00Z",
"Spec": {
@ -13,7 +13,7 @@
},
{
"ID": "ID-bar",
"Version": {},
"Version": {},
"CreatedAt": "0001-01-01T00:00:00Z",
"UpdatedAt": "0001-01-01T00:00:00Z",
"Spec": {

View File

@ -1,9 +1,9 @@
[
{
"ID": "ID-foo",
"Version": {},
"CreatedAt": "0001-01-01T00:00:00Z",
"UpdatedAt": "0001-01-01T00:00:00Z",
"Version": {},
"CreatedAt": "0001-01-01T00:00:00Z",
"UpdatedAt": "0001-01-01T00:00:00Z",
"Spec": {
"Name": "foo",
"Labels": null

View File

@ -1,4 +0,0 @@
ID NAME CREATED UPDATED
ID-1-foo 1-foo 2 hours ago About an hour ago
ID-2-foo 2-foo 2 hours ago About an hour ago
ID-10-foo 10-foo 2 hours ago About an hour ago

View File

@ -1,2 +1,2 @@
foo
bar label=label-bar
foo

View File

@ -1,3 +1,3 @@
ID NAME CREATED UPDATED
ID-bar bar 2 hours ago About an hour ago
ID-foo foo 2 hours ago About an hour ago
ID-bar bar 2 hours ago About an hour ago

View File

@ -1,2 +1,2 @@
foo
bar label=label-bar
foo

View File

@ -1,2 +1,2 @@
ID-bar
ID-foo
ID-bar

View File

@ -0,0 +1,3 @@
ID NAME CREATED UPDATED
ID-foo foo 2 hours ago About an hour ago
ID-bar bar 2 hours ago About an hour ago

View File

@ -4,13 +4,12 @@ import (
"io"
"net/http/httputil"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/signal"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
@ -23,26 +22,8 @@ type attachOptions struct {
container string
}
func inspectContainerAndCheckState(ctx context.Context, cli client.APIClient, args string) (*types.ContainerJSON, error) {
c, err := cli.ContainerInspect(ctx, args)
if err != nil {
return nil, err
}
if !c.State.Running {
return nil, errors.New("You cannot attach to a stopped container, start it first")
}
if c.State.Paused {
return nil, errors.New("You cannot attach to a paused container, unpause it first")
}
if c.State.Restarting {
return nil, errors.New("You cannot attach to a restarting container, wait until it is running")
}
return &c, nil
}
// NewAttachCommand creates a new cobra.Command for `docker attach`
func NewAttachCommand(dockerCli command.Cli) *cobra.Command {
func NewAttachCommand(dockerCli *command.DockerCli) *cobra.Command {
var opts attachOptions
cmd := &cobra.Command{
@ -62,15 +43,23 @@ func NewAttachCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
func runAttach(dockerCli command.Cli, opts *attachOptions) error {
func runAttach(dockerCli *command.DockerCli, opts *attachOptions) error {
ctx := context.Background()
client := dockerCli.Client()
c, err := inspectContainerAndCheckState(ctx, client, opts.container)
c, err := client.ContainerInspect(ctx, opts.container)
if err != nil {
return err
}
if !c.State.Running {
return errors.New("You cannot attach to a stopped container, start it first")
}
if c.State.Paused {
return errors.New("You cannot attach to a paused container, unpause it first")
}
if err := dockerCli.In().CheckTty(!opts.noStdin, c.Config.Tty); err != nil {
return err
}
@ -106,21 +95,19 @@ func runAttach(dockerCli command.Cli, opts *attachOptions) error {
}
defer resp.Close()
// If use docker attach command to attach to a stop container, it will return
// "You cannot attach to a stopped container" error, it's ok, but when
// attach to a running container, it(docker attach) use inspect to check
// the container's state, if it pass the state check on the client side,
// and then the container is stopped, docker attach command still attach to
// the container and not exit.
//
// Recheck the container's state to avoid attach block.
_, err = inspectContainerAndCheckState(ctx, client, opts.container)
if err != nil {
return err
}
if c.Config.Tty && dockerCli.Out().IsTerminal() {
resizeTTY(ctx, dockerCli, opts.container)
height, width := dockerCli.Out().GetTtySize()
// To handle the case where a user repeatedly attaches/detaches without resizing their
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
// resize it, then go back to normal. Without this, every attach after the first will
// require the user to manually resize or hit enter.
resizeTtyTo(ctx, client, opts.container, height+1, width+1, false)
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
// to the actual size.
if err := MonitorTtySize(ctx, dockerCli, opts.container, false); err != nil {
logrus.Debugf("Error monitoring TTY size: %s", err)
}
}
streamer := hijackedIOStreamer{
@ -140,36 +127,14 @@ func runAttach(dockerCli command.Cli, opts *attachOptions) error {
if errAttach != nil {
return errAttach
}
return getExitStatus(ctx, dockerCli.Client(), opts.container)
}
func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
height, width := dockerCli.Out().GetTtySize()
// To handle the case where a user repeatedly attaches/detaches without resizing their
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
// resize it, then go back to normal. Without this, every attach after the first will
// require the user to manually resize or hit enter.
resizeTtyTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
// to the actual size.
if err := MonitorTtySize(ctx, dockerCli, containerID, false); err != nil {
logrus.Debugf("Error monitoring TTY size: %s", err)
}
}
func getExitStatus(ctx context.Context, apiclient client.ContainerAPIClient, containerID string) error {
container, err := apiclient.ContainerInspect(ctx, containerID)
_, status, err := getExitCode(ctx, dockerCli, opts.container)
if err != nil {
// If we can't connect, then the daemon probably died.
if !client.IsErrConnectionFailed(err) {
return err
}
return cli.StatusError{StatusCode: -1}
return err
}
status := container.State.ExitCode
if status != 0 {
return cli.StatusError{StatusCode: status}
}
return nil
}

View File

@ -1,117 +0,0 @@
package container
import (
"io/ioutil"
"testing"
"github.com/docker/cli/cli"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestNewAttachCommandErrors(t *testing.T) {
testCases := []struct {
name string
args []string
expectedError string
containerInspectFunc func(img string) (types.ContainerJSON, error)
}{
{
name: "client-error",
args: []string{"5cb5bb5e4a3b"},
expectedError: "something went wrong",
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
return types.ContainerJSON{}, errors.Errorf("something went wrong")
},
},
{
name: "client-stopped",
args: []string{"5cb5bb5e4a3b"},
expectedError: "You cannot attach to a stopped container",
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
c := types.ContainerJSON{}
c.ContainerJSONBase = &types.ContainerJSONBase{}
c.ContainerJSONBase.State = &types.ContainerState{Running: false}
return c, nil
},
},
{
name: "client-paused",
args: []string{"5cb5bb5e4a3b"},
expectedError: "You cannot attach to a paused container",
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
c := types.ContainerJSON{}
c.ContainerJSONBase = &types.ContainerJSONBase{}
c.ContainerJSONBase.State = &types.ContainerState{
Running: true,
Paused: true,
}
return c, nil
},
},
{
name: "client-restarting",
args: []string{"5cb5bb5e4a3b"},
expectedError: "You cannot attach to a restarting container",
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
c := types.ContainerJSON{}
c.ContainerJSONBase = &types.ContainerJSONBase{}
c.ContainerJSONBase.State = &types.ContainerState{
Running: true,
Paused: false,
Restarting: true,
}
return c, nil
},
},
}
for _, tc := range testCases {
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestGetExitStatus(t *testing.T) {
containerID := "the exec id"
expecatedErr := errors.New("unexpected error")
testcases := []struct {
inspectError error
exitCode int
expectedError error
}{
{
inspectError: nil,
exitCode: 0,
},
{
inspectError: expecatedErr,
expectedError: expecatedErr,
},
{
exitCode: 15,
expectedError: cli.StatusError{StatusCode: 15},
},
}
for _, testcase := range testcases {
client := &fakeClient{
inspectFunc: func(id string) (types.ContainerJSON, error) {
assert.Equal(t, containerID, id)
return types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{ExitCode: testcase.exitCode},
},
}, testcase.inspectError
},
}
err := getExitStatus(context.Background(), client, containerID)
assert.Equal(t, testcase.expectedError, err)
}
}

View File

@ -1,73 +0,0 @@
package container
import (
"io"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"golang.org/x/net/context"
)
type fakeClient struct {
client.Client
inspectFunc func(string) (types.ContainerJSON, error)
execInspectFunc func(execID string) (types.ContainerExecInspect, error)
execCreateFunc func(container string, config types.ExecConfig) (types.IDResponse, error)
createContainerFunc func(config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error)
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
infoFunc func() (types.Info, error)
}
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
if f.inspectFunc != nil {
return f.inspectFunc(containerID)
}
return types.ContainerJSON{}, nil
}
func (f *fakeClient) ContainerExecCreate(_ context.Context, container string, config types.ExecConfig) (types.IDResponse, error) {
if f.execCreateFunc != nil {
return f.execCreateFunc(container, config)
}
return types.IDResponse{}, nil
}
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (types.ContainerExecInspect, error) {
if f.execInspectFunc != nil {
return f.execInspectFunc(execID)
}
return types.ContainerExecInspect{}, nil
}
func (f *fakeClient) ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error {
return nil
}
func (f *fakeClient) ContainerCreate(
_ context.Context,
config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
containerName string,
) (container.ContainerCreateCreatedBody, error) {
if f.createContainerFunc != nil {
return f.createContainerFunc(config, hostConfig, networkingConfig, containerName)
}
return container.ContainerCreateCreatedBody{}, nil
}
func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
if f.imageCreateFunc != nil {
return f.imageCreateFunc(parentReference, options)
}
return nil, nil
}
func (f *fakeClient) Info(_ context.Context) (types.Info, error) {
if f.infoFunc != nil {
return f.infoFunc()
}
return types.Info{}, nil
}

View File

@ -113,7 +113,7 @@ func resolveLocalPath(localPath string) (absPath string, err error) {
return
}
return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil
return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
}
func copyFromContainer(ctx context.Context, dockerCli *command.DockerCli, srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) {

View File

@ -113,9 +113,6 @@ type cidFile struct {
}
func (cid *cidFile) Close() error {
if cid.file == nil {
return nil
}
cid.file.Close()
if cid.written {
@ -129,9 +126,6 @@ func (cid *cidFile) Close() error {
}
func (cid *cidFile) Write(id string) error {
if cid.file == nil {
return nil
}
if _, err := cid.file.Write([]byte(id)); err != nil {
return errors.Errorf("Failed to write the container ID to the file: %s", err)
}
@ -140,9 +134,6 @@ func (cid *cidFile) Write(id string) error {
}
func newCIDFile(path string) (*cidFile, error) {
if path == "" {
return &cidFile{}, nil
}
if _, err := os.Stat(path); err == nil {
return nil, errors.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path)
}
@ -162,15 +153,19 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig
stderr := dockerCli.Err()
var (
trustedRef reference.Canonical
namedRef reference.Named
containerIDFile *cidFile
trustedRef reference.Canonical
namedRef reference.Named
)
containerIDFile, err := newCIDFile(hostConfig.ContainerIDFile)
if err != nil {
return nil, err
cidfile := hostConfig.ContainerIDFile
if cidfile != "" {
var err error
if containerIDFile, err = newCIDFile(cidfile); err != nil {
return nil, err
}
defer containerIDFile.Close()
}
defer containerIDFile.Close()
ref, err := reference.ParseAnyReference(config.Image)
if err != nil {
@ -198,7 +193,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig
fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
// we don't want to write to stdout anything apart from container.ID
if err := pullImage(ctx, dockerCli, config.Image, stderr); err != nil {
if err = pullImage(ctx, dockerCli, config.Image, stderr); err != nil {
return nil, err
}
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
@ -220,6 +215,10 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig
for _, warning := range response.Warnings {
fmt.Fprintf(stderr, "WARNING: %s\n", warning)
}
err = containerIDFile.Write(response.ID)
return &response, err
if containerIDFile != nil {
if err = containerIDFile.Write(response.ID); err != nil {
return nil, err
}
}
return &response, nil
}

View File

@ -1,120 +0,0 @@
package container
import (
"context"
"io"
"io/ioutil"
"os"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCIDFileNoOPWithNoFilename(t *testing.T) {
file, err := newCIDFile("")
require.NoError(t, err)
assert.Equal(t, &cidFile{}, file)
assert.NoError(t, file.Write("id"))
assert.NoError(t, file.Close())
}
func TestNewCIDFileWhenFileAlreadyExists(t *testing.T) {
tempfile := fs.NewFile(t, "test-cid-file")
defer tempfile.Remove()
_, err := newCIDFile(tempfile.Path())
testutil.ErrorContains(t, err, "Container ID file found")
}
func TestCIDFileCloseWithNoWrite(t *testing.T) {
tempdir := fs.NewDir(t, "test-cid-file")
defer tempdir.Remove()
path := tempdir.Join("cidfile")
file, err := newCIDFile(path)
require.NoError(t, err)
assert.Equal(t, file.path, path)
assert.NoError(t, file.Close())
_, err = os.Stat(path)
assert.True(t, os.IsNotExist(err))
}
func TestCIDFileCloseWithWrite(t *testing.T) {
tempdir := fs.NewDir(t, "test-cid-file")
defer tempdir.Remove()
path := tempdir.Join("cidfile")
file, err := newCIDFile(path)
require.NoError(t, err)
content := "id"
assert.NoError(t, file.Write(content))
actual, err := ioutil.ReadFile(path)
require.NoError(t, err)
assert.Equal(t, content, string(actual))
assert.NoError(t, file.Close())
_, err = os.Stat(path)
require.NoError(t, err)
}
func TestCreateContainerPullsImageIfMissing(t *testing.T) {
imageName := "does-not-exist-locally"
responseCounter := 0
containerID := "abcdef"
client := &fakeClient{
createContainerFunc: func(
config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
containerName string,
) (container.ContainerCreateCreatedBody, error) {
defer func() { responseCounter++ }()
switch responseCounter {
case 0:
return container.ContainerCreateCreatedBody{}, fakeNotFound{}
case 1:
return container.ContainerCreateCreatedBody{ID: containerID}, nil
default:
return container.ContainerCreateCreatedBody{}, errors.New("unexpected")
}
},
imageCreateFunc: func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader("")), nil
},
infoFunc: func() (types.Info, error) {
return types.Info{IndexServerAddress: "http://indexserver"}, nil
},
}
cli := test.NewFakeCli(client)
config := &containerConfig{
Config: &container.Config{
Image: imageName,
},
HostConfig: &container.HostConfig{},
}
body, err := createContainer(context.Background(), cli, config, "name")
require.NoError(t, err)
expected := container.ContainerCreateCreatedBody{ID: containerID}
assert.Equal(t, expected, *body)
stderr := cli.ErrBuffer().String()
assert.Contains(t, stderr, "Unable to find image 'does-not-exist-locally:latest' locally")
}
type fakeNotFound struct{}
func (f fakeNotFound) NotFound() bool { return true }
func (f fakeNotFound) Error() string { return "error fake not found" }

View File

@ -4,14 +4,13 @@ import (
"fmt"
"io"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
apiclient "github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/docker/docker/pkg/promise"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
@ -23,17 +22,18 @@ type execOptions struct {
detach bool
user string
privileged bool
env opts.ListOpts
container string
command []string
env *opts.ListOpts
}
func newExecOptions() execOptions {
return execOptions{env: opts.NewListOpts(opts.ValidateEnv)}
func newExecOptions() *execOptions {
var values []string
return &execOptions{
env: opts.NewListOptsRef(&values, opts.ValidateEnv),
}
}
// NewExecCommand creates a new cobra.Command for `docker exec`
func NewExecCommand(dockerCli command.Cli) *cobra.Command {
func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command {
options := newExecOptions()
cmd := &cobra.Command{
@ -41,9 +41,9 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
Short: "Run a command in a running container",
Args: cli.RequiresMinArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
options.container = args[0]
options.command = args[1:]
return runExec(dockerCli, options)
container := args[0]
execCmd := args[1:]
return runExec(dockerCli, options, container, execCmd)
},
}
@ -56,14 +56,27 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
flags.BoolVarP(&options.detach, "detach", "d", false, "Detached mode: run command in the background")
flags.StringVarP(&options.user, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
flags.BoolVarP(&options.privileged, "privileged", "", false, "Give extended privileges to the command")
flags.VarP(&options.env, "env", "e", "Set environment variables")
flags.VarP(options.env, "env", "e", "Set environment variables")
flags.SetAnnotation("env", "version", []string{"1.25"})
return cmd
}
func runExec(dockerCli command.Cli, options execOptions) error {
execConfig := parseExec(options, dockerCli.ConfigFile())
// nolint: gocyclo
func runExec(dockerCli *command.DockerCli, options *execOptions, container string, execCmd []string) error {
execConfig, err := parseExec(options, execCmd)
// just in case the ParseExec does not exit
if container == "" || err != nil {
return cli.StatusError{StatusCode: 1}
}
if options.detachKeys != "" {
dockerCli.ConfigFile().DetachKeys = options.detachKeys
}
// Send client escape keys
execConfig.DetachKeys = dockerCli.ConfigFile().DetachKeys
ctx := context.Background()
client := dockerCli.Client()
@ -71,7 +84,7 @@ func runExec(dockerCli command.Cli, options execOptions) error {
// otherwise if we error out we will leak execIDs on the server (and
// there's no easy way to clean those up). But also in order to make "not
// exist" errors take precedence we do a dummy inspect first.
if _, err := client.ContainerInspect(ctx, options.container); err != nil {
if _, err := client.ContainerInspect(ctx, container); err != nil {
return err
}
if !execConfig.Detach {
@ -80,31 +93,37 @@ func runExec(dockerCli command.Cli, options execOptions) error {
}
}
response, err := client.ContainerExecCreate(ctx, options.container, *execConfig)
response, err := client.ContainerExecCreate(ctx, container, *execConfig)
if err != nil {
return err
}
execID := response.ID
if execID == "" {
return errors.New("exec ID empty")
fmt.Fprintln(dockerCli.Out(), "exec ID empty")
return nil
}
// Temp struct for execStart so that we don't need to transfer all the execConfig.
if execConfig.Detach {
execStartCheck := types.ExecStartCheck{
Detach: execConfig.Detach,
Tty: execConfig.Tty,
}
return client.ContainerExecStart(ctx, execID, execStartCheck)
}
return interactiveExec(ctx, dockerCli, execConfig, execID)
}
func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *types.ExecConfig, execID string) error {
if err := client.ContainerExecStart(ctx, execID, execStartCheck); err != nil {
return err
}
// For now don't print this - wait for when we support exec wait()
// fmt.Fprintf(dockerCli.Out(), "%s\n", execID)
return nil
}
// Interactive exec requested.
var (
out, stderr io.Writer
in io.ReadCloser
errCh chan error
)
if execConfig.AttachStdin {
@ -121,31 +140,24 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *typ
}
}
client := dockerCli.Client()
resp, err := client.ContainerExecAttach(ctx, execID, *execConfig)
if err != nil {
return err
}
defer resp.Close()
errCh = promise.Go(func() error {
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: out,
errorStream: stderr,
resp: resp,
tty: execConfig.Tty,
detachKeys: execConfig.DetachKeys,
}
errCh := make(chan error, 1)
go func() {
defer close(errCh)
errCh <- func() error {
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: out,
errorStream: stderr,
resp: resp,
tty: execConfig.Tty,
detachKeys: execConfig.DetachKeys,
}
return streamer.stream(ctx)
}()
}()
return streamer.stream(ctx)
})
if execConfig.Tty && dockerCli.In().IsTerminal() {
if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil {
@ -158,35 +170,42 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *typ
return err
}
return getExecExitStatus(ctx, client, execID)
var status int
if _, status, err = getExecExitCode(ctx, client, execID); err != nil {
return err
}
if status != 0 {
return cli.StatusError{StatusCode: status}
}
return nil
}
func getExecExitStatus(ctx context.Context, client apiclient.ContainerAPIClient, execID string) error {
// getExecExitCode perform an inspect on the exec command. It returns
// the running state and the exit code.
func getExecExitCode(ctx context.Context, client apiclient.ContainerAPIClient, execID string) (bool, int, error) {
resp, err := client.ContainerExecInspect(ctx, execID)
if err != nil {
// If we can't connect, then the daemon probably died.
if !apiclient.IsErrConnectionFailed(err) {
return err
return false, -1, err
}
return cli.StatusError{StatusCode: -1}
return false, -1, nil
}
status := resp.ExitCode
if status != 0 {
return cli.StatusError{StatusCode: status}
}
return nil
return resp.Running, resp.ExitCode, nil
}
// parseExec parses the specified args for the specified command and generates
// an ExecConfig from it.
func parseExec(opts execOptions, configFile *configfile.ConfigFile) *types.ExecConfig {
func parseExec(opts *execOptions, execCmd []string) (*types.ExecConfig, error) {
execConfig := &types.ExecConfig{
User: opts.user,
Privileged: opts.privileged,
Tty: opts.tty,
Cmd: opts.command,
Cmd: execCmd,
Detach: opts.detach,
Env: opts.env.GetAll(),
}
// If -d is not set, attach to everything by default
@ -198,10 +217,9 @@ func parseExec(opts execOptions, configFile *configfile.ConfigFile) *types.ExecC
}
}
if opts.detachKeys != "" {
execConfig.DetachKeys = opts.detachKeys
} else {
execConfig.DetachKeys = configFile.DetachKeys
if opts.env != nil {
execConfig.Env = opts.env.GetAll()
}
return execConfig
return execConfig, nil
}

View File

@ -1,227 +1,116 @@
package container
import (
"io/ioutil"
"testing"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func withDefaultOpts(options execOptions) execOptions {
options.env = opts.NewListOpts(opts.ValidateEnv)
if len(options.command) == 0 {
options.command = []string{"command"}
}
return options
type arguments struct {
options execOptions
execCmd []string
}
func TestParseExec(t *testing.T) {
testcases := []struct {
options execOptions
configFile configfile.ConfigFile
expected types.ExecConfig
}{
valids := map[*arguments]*types.ExecConfig{
{
expected: types.ExecConfig{
Cmd: []string{"command"},
AttachStdout: true,
AttachStderr: true,
},
options: withDefaultOpts(execOptions{}),
execCmd: []string{"command"},
}: {
Cmd: []string{"command"},
AttachStdout: true,
AttachStderr: true,
},
{
expected: types.ExecConfig{
Cmd: []string{"command1", "command2"},
AttachStdout: true,
AttachStderr: true,
},
options: withDefaultOpts(execOptions{
command: []string{"command1", "command2"},
}),
execCmd: []string{"command1", "command2"},
}: {
Cmd: []string{"command1", "command2"},
AttachStdout: true,
AttachStderr: true,
},
{
options: withDefaultOpts(execOptions{
options: execOptions{
interactive: true,
tty: true,
user: "uid",
}),
expected: types.ExecConfig{
User: "uid",
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
Cmd: []string{"command"},
},
execCmd: []string{"command"},
}: {
User: "uid",
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
Cmd: []string{"command"},
},
{
options: withDefaultOpts(execOptions{detach: true}),
expected: types.ExecConfig{
Detach: true,
Cmd: []string{"command"},
options: execOptions{
detach: true,
},
execCmd: []string{"command"},
}: {
AttachStdin: false,
AttachStdout: false,
AttachStderr: false,
Detach: true,
Cmd: []string{"command"},
},
{
options: withDefaultOpts(execOptions{
options: execOptions{
tty: true,
interactive: true,
detach: true,
}),
expected: types.ExecConfig{
Detach: true,
Tty: true,
Cmd: []string{"command"},
},
},
{
options: withDefaultOpts(execOptions{detach: true}),
configFile: configfile.ConfigFile{DetachKeys: "de"},
expected: types.ExecConfig{
Cmd: []string{"command"},
DetachKeys: "de",
Detach: true,
},
},
{
options: withDefaultOpts(execOptions{
detach: true,
detachKeys: "ab",
}),
configFile: configfile.ConfigFile{DetachKeys: "de"},
expected: types.ExecConfig{
Cmd: []string{"command"},
DetachKeys: "ab",
Detach: true,
},
execCmd: []string{"command"},
}: {
AttachStdin: false,
AttachStdout: false,
AttachStderr: false,
Detach: true,
Tty: true,
Cmd: []string{"command"},
},
}
for _, testcase := range testcases {
execConfig := parseExec(testcase.options, &testcase.configFile)
assert.Equal(t, testcase.expected, *execConfig)
}
}
func TestRunExec(t *testing.T) {
var testcases = []struct {
doc string
options execOptions
client fakeClient
expectedError string
expectedOut string
expectedErr string
}{
{
doc: "successful detach",
options: withDefaultOpts(execOptions{
container: "thecontainer",
detach: true,
}),
client: fakeClient{execCreateFunc: execCreateWithID},
},
{
doc: "inspect error",
options: newExecOptions(),
client: fakeClient{
inspectFunc: func(string) (types.ContainerJSON, error) {
return types.ContainerJSON{}, errors.New("failed inspect")
},
},
expectedError: "failed inspect",
},
{
doc: "missing exec ID",
options: newExecOptions(),
expectedError: "exec ID empty",
},
}
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
cli := test.NewFakeCli(&testcase.client)
err := runExec(cli, testcase.options)
if testcase.expectedError != "" {
testutil.ErrorContains(t, err, testcase.expectedError)
} else {
if !assert.NoError(t, err) {
return
}
}
assert.Equal(t, testcase.expectedOut, cli.OutBuffer().String())
assert.Equal(t, testcase.expectedErr, cli.ErrBuffer().String())
})
}
}
func execCreateWithID(_ string, _ types.ExecConfig) (types.IDResponse, error) {
return types.IDResponse{ID: "execid"}, nil
}
func TestGetExecExitStatus(t *testing.T) {
execID := "the exec id"
expecatedErr := errors.New("unexpected error")
testcases := []struct {
inspectError error
exitCode int
expectedError error
}{
{
inspectError: nil,
exitCode: 0,
},
{
inspectError: expecatedErr,
expectedError: expecatedErr,
},
{
exitCode: 15,
expectedError: cli.StatusError{StatusCode: 15},
},
}
for _, testcase := range testcases {
client := &fakeClient{
execInspectFunc: func(id string) (types.ContainerExecInspect, error) {
assert.Equal(t, execID, id)
return types.ContainerExecInspect{ExitCode: testcase.exitCode}, testcase.inspectError
},
for valid, expectedExecConfig := range valids {
execConfig, err := parseExec(&valid.options, valid.execCmd)
if err != nil {
t.Fatal(err)
}
if !compareExecConfig(expectedExecConfig, execConfig) {
t.Fatalf("Expected [%v] for %v, got [%v]", expectedExecConfig, valid, execConfig)
}
err := getExecExitStatus(context.Background(), client, execID)
assert.Equal(t, testcase.expectedError, err)
}
}
func TestNewExecCommandErrors(t *testing.T) {
testCases := []struct {
name string
args []string
expectedError string
containerInspectFunc func(img string) (types.ContainerJSON, error)
}{
{
name: "client-error",
args: []string{"5cb5bb5e4a3b", "-t", "-i", "bash"},
expectedError: "something went wrong",
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
return types.ContainerJSON{}, errors.Errorf("something went wrong")
},
},
func compareExecConfig(config1 *types.ExecConfig, config2 *types.ExecConfig) bool {
if config1.AttachStderr != config2.AttachStderr {
return false
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})
cmd := NewExecCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
if config1.AttachStdin != config2.AttachStdin {
return false
}
if config1.AttachStdout != config2.AttachStdout {
return false
}
if config1.Detach != config2.Detach {
return false
}
if config1.Privileged != config2.Privileged {
return false
}
if config1.Tty != config2.Tty {
return false
}
if config1.User != config2.User {
return false
}
if len(config1.Cmd) != len(config2.Cmd) {
return false
}
for index, value := range config1.Cmd {
if value != config2.Cmd[index] {
return false
}
}
return true
}

View File

@ -6,12 +6,12 @@ import (
"runtime"
"sync"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/docker/pkg/term"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
)
@ -110,7 +110,7 @@ func (h *hijackedIOStreamer) setupInput() (restore func(), err error) {
func (h *hijackedIOStreamer) beginOutputStream(restoreInput func()) <-chan error {
if h.outputStream == nil && h.errorStream == nil {
// There is no need to copy output.
// Ther is no need to copy output.
return nil
}
@ -163,7 +163,7 @@ func (h *hijackedIOStreamer) beginInputStream(restoreInput func()) (doneC <-chan
if err != nil {
// This error will also occur on the receive
// side (from stdout) where it will be
// propagated back to the caller.
// propogated back to the caller.
logrus.Debugf("Error sendStdin: %s", err)
}
}
@ -185,7 +185,6 @@ func setRawTerminal(streams command.Streams) error {
return streams.Out().SetRawTerminal()
}
// nolint: unparam
func restoreTerminal(streams command.Streams, in io.Closer) error {
streams.In().RestoreTerminal()
streams.Out().RestoreTerminal()

View File

@ -7,8 +7,8 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/opts"
"github.com/docker/cli/templates"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/templates"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)

View File

@ -11,20 +11,20 @@ import (
"strings"
"time"
"github.com/docker/cli/cli/compose/loader"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types/container"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/pkg/signal"
runconfigopts "github.com/docker/docker/runconfig/opts"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/pflag"
)
var (
deviceCgroupRuleRegexp = regexp.MustCompile(`^[acb] ([0-9]+|\*):([0-9]+|\*) [rwm]{1,3}$`)
deviceCgroupRuleRegexp = regexp.MustCompile("^[acb] ([0-9]+|\\*):([0-9]+|\\*) [rwm]{1,3}$")
)
// containerOptions is a data object with all the options for creating a container
@ -274,7 +274,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
// Low-level execution (cgroups, namespaces, ...)
flags.StringVar(&copts.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container")
flags.StringVar(&copts.ipcMode, "ipc", "", "IPC mode to use")
flags.StringVar(&copts.ipcMode, "ipc", "", "IPC namespace to use")
flags.StringVar(&copts.isolation, "isolation", "", "Container isolation technology")
flags.StringVar(&copts.pidMode, "pid", "", "PID namespace to use")
flags.Var(&copts.shmSize, "shm-size", "Size of /dev/shm")
@ -333,8 +333,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
volumes := copts.volumes.GetMap()
// add any bind targets to the list of container volumes
for bind := range copts.volumes.GetMap() {
parsed, _ := loader.ParseVolume(bind)
if parsed.Source != "" {
if arr := volumeSplitN(bind, 2); len(arr) > 1 {
// after creating the bind mount we want to delete it from the copts.volumes values because
// we do not want bind mounts being committed to image configs
binds = append(binds, bind)
@ -410,17 +409,22 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
}
// collect all the environment variables for the container
envVariables, err := opts.ReadKVStrings(copts.envFile.GetAll(), copts.env.GetAll())
envVariables, err := runconfigopts.ReadKVStrings(copts.envFile.GetAll(), copts.env.GetAll())
if err != nil {
return nil, err
}
// collect all the labels for the container
labels, err := opts.ReadKVStrings(copts.labelsFile.GetAll(), copts.labels.GetAll())
labels, err := runconfigopts.ReadKVStrings(copts.labelsFile.GetAll(), copts.labels.GetAll())
if err != nil {
return nil, err
}
ipcMode := container.IpcMode(copts.ipcMode)
if !ipcMode.Valid() {
return nil, errors.Errorf("--ipc: invalid IPC mode")
}
pidMode := container.PidMode(copts.pidMode)
if !pidMode.Valid() {
return nil, errors.Errorf("--pid: invalid PID mode")
@ -436,7 +440,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
return nil, errors.Errorf("--userns: invalid USER mode")
}
restartPolicy, err := opts.ParseRestartPolicy(copts.restartPolicy)
restartPolicy, err := runconfigopts.ParseRestartPolicy(copts.restartPolicy)
if err != nil {
return nil, err
}
@ -549,7 +553,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
MacAddress: copts.macAddress,
Entrypoint: entrypoint,
WorkingDir: copts.workingDir,
Labels: opts.ConvertKVStringsToMap(labels),
Labels: runconfigopts.ConvertKVStringsToMap(labels),
Healthcheck: healthConfig,
}
if flags.Changed("stop-signal") {
@ -579,7 +583,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
ExtraHosts: copts.extraHosts.GetAll(),
VolumesFrom: copts.volumesFrom.GetAll(),
NetworkMode: container.NetworkMode(copts.netMode),
IpcMode: container.IpcMode(copts.ipcMode),
IpcMode: ipcMode,
PidMode: pidMode,
UTSMode: utsMode,
UsernsMode: usernsMode,
@ -662,7 +666,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
}
func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) {
loggingOptsMap := opts.ConvertKVStringsToMap(loggingOpts)
loggingOptsMap := runconfigopts.ConvertKVStringsToMap(loggingOpts)
if loggingDriver == "none" && len(loggingOpts) > 0 {
return map[string]string{}, errors.Errorf("invalid logging opts for driver %s", loggingDriver)
}
@ -824,6 +828,67 @@ func validatePath(val string, validator func(string) bool) (string, error) {
return val, nil
}
// volumeSplitN splits raw into a maximum of n parts, separated by a separator colon.
// A separator colon is the last `:` character in the regex `[:\\]?[a-zA-Z]:` (note `\\` is `\` escaped).
// In Windows driver letter appears in two situations:
// a. `^[a-zA-Z]:` (A colon followed by `^[a-zA-Z]:` is OK as colon is the separator in volume option)
// b. A string in the format like `\\?\C:\Windows\...` (UNC).
// Therefore, a driver letter can only follow either a `:` or `\\`
// This allows to correctly split strings such as `C:\foo:D:\:rw` or `/tmp/q:/foo`.
func volumeSplitN(raw string, n int) []string {
var array []string
if len(raw) == 0 || raw[0] == ':' {
// invalid
return nil
}
// numberOfParts counts the number of parts separated by a separator colon
numberOfParts := 0
// left represents the left-most cursor in raw, updated at every `:` character considered as a separator.
left := 0
// right represents the right-most cursor in raw incremented with the loop. Note this
// starts at index 1 as index 0 is already handle above as a special case.
for right := 1; right < len(raw); right++ {
// stop parsing if reached maximum number of parts
if n >= 0 && numberOfParts >= n {
break
}
if raw[right] != ':' {
continue
}
potentialDriveLetter := raw[right-1]
if (potentialDriveLetter >= 'A' && potentialDriveLetter <= 'Z') || (potentialDriveLetter >= 'a' && potentialDriveLetter <= 'z') {
if right > 1 {
beforePotentialDriveLetter := raw[right-2]
// Only `:` or `\\` are checked (`/` could fall into the case of `/tmp/q:/foo`)
if beforePotentialDriveLetter != ':' && beforePotentialDriveLetter != '\\' {
// e.g. `C:` is not preceded by any delimiter, therefore it was not a drive letter but a path ending with `C:`.
array = append(array, raw[left:right])
left = right + 1
numberOfParts++
}
// else, `C:` is considered as a drive letter and not as a delimiter, so we continue parsing.
}
// if right == 1, then `C:` is the beginning of the raw string, therefore `:` is again not considered a delimiter and we continue parsing.
} else {
// if `:` is not preceded by a potential drive letter, then consider it as a delimiter.
array = append(array, raw[left:right])
left = right + 1
numberOfParts++
}
}
// need to take care of the last part
if left < len(raw) {
if n >= 0 && numberOfParts >= n {
// if the maximum number of parts is reached, just append the rest to the last part
// left-1 is at the last `:` that needs to be included since not considered a separator.
array[n-1] += raw[left-1:]
} else {
array = append(array, raw[left:])
}
}
return array
}
// validateAttach validates that the specified string is a valid attach option.
func validateAttach(val string) (string, error) {
s := strings.ToLower(val)

View File

@ -1,6 +1,8 @@
package container
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
@ -9,14 +11,14 @@ import (
"testing"
"time"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types/container"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/runconfig"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidateAttach(t *testing.T) {
@ -43,7 +45,6 @@ func TestValidateAttach(t *testing.T) {
}
}
// nolint: unparam
func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) {
flags := pflag.NewFlagSet("run", pflag.ContinueOnError)
flags.SetOutput(ioutil.Discard)
@ -60,14 +61,16 @@ func parseRun(args []string) (*container.Config, *container.HostConfig, *network
return containerConfig.Config, containerConfig.HostConfig, containerConfig.NetworkingConfig, err
}
func parseMustError(t *testing.T, args string) {
_, _, _, err := parseRun(strings.Split(args+" ubuntu bash", " "))
assert.Error(t, err, args)
func parsetest(t *testing.T, args string) (*container.Config, *container.HostConfig, error) {
config, hostConfig, _, err := parseRun(strings.Split(args+" ubuntu bash", " "))
return config, hostConfig, err
}
func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig) {
config, hostConfig, _, err := parseRun(append(strings.Split(args, " "), "ubuntu", "bash"))
assert.NoError(t, err)
config, hostConfig, err := parsetest(t, args)
if err != nil {
t.Fatal(err)
}
return config, hostConfig
}
@ -83,6 +86,7 @@ func TestParseRunLinks(t *testing.T) {
}
}
// nolint: gocyclo
func TestParseRunAttach(t *testing.T) {
if config, _ := mustParse(t, "-a stdin"); !config.AttachStdin || config.AttachStdout || config.AttachStderr {
t.Fatalf("Error parsing attach flags. Expect only Stdin enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr)
@ -99,21 +103,35 @@ func TestParseRunAttach(t *testing.T) {
if config, _ := mustParse(t, "-i"); !config.AttachStdin || !config.AttachStdout || !config.AttachStderr {
t.Fatalf("Error parsing attach flags. Expect Stdin enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr)
}
}
func TestParseRunWithInvalidArgs(t *testing.T) {
parseMustError(t, "-a")
parseMustError(t, "-a invalid")
parseMustError(t, "-a invalid -a stdout")
parseMustError(t, "-a stdout -a stderr -d")
parseMustError(t, "-a stdin -d")
parseMustError(t, "-a stdout -d")
parseMustError(t, "-a stderr -d")
parseMustError(t, "-d --rm")
if _, _, err := parsetest(t, "-a"); err == nil {
t.Fatal("Error parsing attach flags, `-a` should be an error but is not")
}
if _, _, err := parsetest(t, "-a invalid"); err == nil {
t.Fatal("Error parsing attach flags, `-a invalid` should be an error but is not")
}
if _, _, err := parsetest(t, "-a invalid -a stdout"); err == nil {
t.Fatal("Error parsing attach flags, `-a stdout -a invalid` should be an error but is not")
}
if _, _, err := parsetest(t, "-a stdout -a stderr -d"); err == nil {
t.Fatal("Error parsing attach flags, `-a stdout -a stderr -d` should be an error but is not")
}
if _, _, err := parsetest(t, "-a stdin -d"); err == nil {
t.Fatal("Error parsing attach flags, `-a stdin -d` should be an error but is not")
}
if _, _, err := parsetest(t, "-a stdout -d"); err == nil {
t.Fatal("Error parsing attach flags, `-a stdout -d` should be an error but is not")
}
if _, _, err := parsetest(t, "-a stderr -d"); err == nil {
t.Fatal("Error parsing attach flags, `-a stderr -d` should be an error but is not")
}
if _, _, err := parsetest(t, "-d --rm"); err == nil {
t.Fatal("Error parsing attach flags, `-d --rm` should be an error but is not")
}
}
// nolint: gocyclo
func TestParseWithVolumes(t *testing.T) {
func TestParseRunVolumes(t *testing.T) {
// A single volume
arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
@ -133,19 +151,19 @@ func TestParseWithVolumes(t *testing.T) {
t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[1], config.Volumes)
}
// A single bind mount
// A single bind-mount
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`})
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] {
t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes)
}
// Two bind mounts.
// Two bind-mounts.
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`})
if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
}
// Two bind mounts, first read-only, second read-write.
// Two bind-mounts, first read-only, second read-write.
// TODO Windows: The Windows version uses read-write as that's the only mode it supports. Can change this post TP4
arr, tryit = setupPlatformVolume(
[]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`},
@ -364,36 +382,52 @@ func TestParseDevice(t *testing.T) {
}
func TestParseModes(t *testing.T) {
// ipc ko
if _, _, _, err := parseRun([]string{"--ipc=container:", "img", "cmd"}); err == nil || err.Error() != "--ipc: invalid IPC mode" {
t.Fatalf("Expected an error with message '--ipc: invalid IPC mode', got %v", err)
}
// ipc ok
_, hostconfig, _, err := parseRun([]string{"--ipc=host", "img", "cmd"})
if err != nil {
t.Fatal(err)
}
if !hostconfig.IpcMode.Valid() {
t.Fatalf("Expected a valid IpcMode, got %v", hostconfig.IpcMode)
}
// pid ko
_, _, _, err := parseRun([]string{"--pid=container:", "img", "cmd"})
testutil.ErrorContains(t, err, "--pid: invalid PID mode")
if _, _, _, err := parseRun([]string{"--pid=container:", "img", "cmd"}); err == nil || err.Error() != "--pid: invalid PID mode" {
t.Fatalf("Expected an error with message '--pid: invalid PID mode', got %v", err)
}
// pid ok
_, hostconfig, _, err := parseRun([]string{"--pid=host", "img", "cmd"})
require.NoError(t, err)
_, hostconfig, _, err = parseRun([]string{"--pid=host", "img", "cmd"})
if err != nil {
t.Fatal(err)
}
if !hostconfig.PidMode.Valid() {
t.Fatalf("Expected a valid PidMode, got %v", hostconfig.PidMode)
}
// uts ko
_, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"})
testutil.ErrorContains(t, err, "--uts: invalid UTS mode")
if _, _, _, err := parseRun([]string{"--uts=container:", "img", "cmd"}); err == nil || err.Error() != "--uts: invalid UTS mode" {
t.Fatalf("Expected an error with message '--uts: invalid UTS mode', got %v", err)
}
// uts ok
_, hostconfig, _, err = parseRun([]string{"--uts=host", "img", "cmd"})
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
if !hostconfig.UTSMode.Valid() {
t.Fatalf("Expected a valid UTSMode, got %v", hostconfig.UTSMode)
}
// shm-size ko
expectedErr := `invalid argument "a128m" for --shm-size=a128m: invalid size: 'a128m'`
_, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"})
testutil.ErrorContains(t, err, expectedErr)
if _, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"}); err == nil || err.Error() != expectedErr {
t.Fatalf("Expected an error with message '%v', got %v", expectedErr, err)
}
// shm-size ok
_, hostconfig, _, err = parseRun([]string{"--shm-size=128m", "img", "cmd"})
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
if hostconfig.ShmSize != 134217728 {
t.Fatalf("Expected a valid ShmSize, got %d", hostconfig.ShmSize)
}
@ -582,6 +616,213 @@ func TestParseEntryPoint(t *testing.T) {
}
}
// This tests the cases for binds which are generated through
// DecodeContainerConfig rather than Parse()
// nolint: gocyclo
func TestDecodeContainerConfigVolumes(t *testing.T) {
// Root to root
bindsOrVols, _ := setupPlatformVolume([]string{`/:/`}, []string{os.Getenv("SystemDrive") + `\:c:\`})
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// No destination path
bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:`}, []string{os.Getenv("TEMP") + `\:`})
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// // No destination path or mode
bindsOrVols, _ = setupPlatformVolume([]string{`/tmp::`}, []string{os.Getenv("TEMP") + `\::`})
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// A whole lot of nothing
bindsOrVols = []string{`:`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// A whole lot of nothing with no mode
bindsOrVols = []string{`::`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// Too much including an invalid mode
wTmp := os.Getenv("TEMP")
bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:/tmp:/tmp:/tmp`}, []string{wTmp + ":" + wTmp + ":" + wTmp + ":" + wTmp})
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// Windows specific error tests
if runtime.GOOS == "windows" {
// Volume which does not include a drive letter
bindsOrVols = []string{`\tmp`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// Root to C-Drive
bindsOrVols = []string{os.Getenv("SystemDrive") + `\:c:`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// Container path that does not include a drive letter
bindsOrVols = []string{`c:\windows:\somewhere`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
}
// Linux-specific error tests
if runtime.GOOS != "windows" {
// Just root
bindsOrVols = []string{`/`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// A single volume that looks like a bind mount passed in Volumes.
// This should be handled as a bind mount, not a volume.
vols := []string{`/foo:/bar`}
if config, hostConfig, err := callDecodeContainerConfig(vols, nil); err != nil {
t.Fatal("Volume /foo:/bar should have succeeded as a volume name")
} else if hostConfig.Binds != nil {
t.Fatalf("Error parsing volume flags, /foo:/bar should not mount-bind anything. Received %v", hostConfig.Binds)
} else if _, exists := config.Volumes[vols[0]]; !exists {
t.Fatalf("Error parsing volume flags, /foo:/bar is missing from volumes. Received %v", config.Volumes)
}
}
}
// callDecodeContainerConfig is a utility function used by TestDecodeContainerConfigVolumes
// to call DecodeContainerConfig. It effectively does what a client would
// do when calling the daemon by constructing a JSON stream of a
// ContainerConfigWrapper which is populated by the set of volume specs
// passed into it. It returns a config and a hostconfig which can be
// validated to ensure DecodeContainerConfig has manipulated the structures
// correctly.
func callDecodeContainerConfig(volumes []string, binds []string) (*container.Config, *container.HostConfig, error) {
var (
b []byte
err error
c *container.Config
h *container.HostConfig
)
w := runconfig.ContainerConfigWrapper{
Config: &container.Config{
Volumes: map[string]struct{}{},
},
HostConfig: &container.HostConfig{
NetworkMode: "none",
Binds: binds,
},
}
for _, v := range volumes {
w.Config.Volumes[v] = struct{}{}
}
if b, err = json.Marshal(w); err != nil {
return nil, nil, errors.Errorf("Error on marshal %s", err.Error())
}
c, h, _, err = runconfig.DecodeContainerConfig(bytes.NewReader(b))
if err != nil {
return nil, nil, errors.Errorf("Error parsing %s: %v", string(b), err)
}
if c == nil || h == nil {
return nil, nil, errors.Errorf("Empty config or hostconfig")
}
return c, h, err
}
func TestVolumeSplitN(t *testing.T) {
for _, x := range []struct {
input string
n int
expected []string
}{
{`C:\foo:d:`, -1, []string{`C:\foo`, `d:`}},
{`:C:\foo:d:`, -1, nil},
{`/foo:/bar:ro`, 3, []string{`/foo`, `/bar`, `ro`}},
{`/foo:/bar:ro`, 2, []string{`/foo`, `/bar:ro`}},
{`C:\foo\:/foo`, -1, []string{`C:\foo\`, `/foo`}},
{`d:\`, -1, []string{`d:\`}},
{`d:`, -1, []string{`d:`}},
{`d:\path`, -1, []string{`d:\path`}},
{`d:\path with space`, -1, []string{`d:\path with space`}},
{`d:\pathandmode:rw`, -1, []string{`d:\pathandmode`, `rw`}},
{`c:\:d:\`, -1, []string{`c:\`, `d:\`}},
{`c:\windows\:d:`, -1, []string{`c:\windows\`, `d:`}},
{`c:\windows:d:\s p a c e`, -1, []string{`c:\windows`, `d:\s p a c e`}},
{`c:\windows:d:\s p a c e:RW`, -1, []string{`c:\windows`, `d:\s p a c e`, `RW`}},
{`c:\program files:d:\s p a c e i n h o s t d i r`, -1, []string{`c:\program files`, `d:\s p a c e i n h o s t d i r`}},
{`0123456789name:d:`, -1, []string{`0123456789name`, `d:`}},
{`MiXeDcAsEnAmE:d:`, -1, []string{`MiXeDcAsEnAmE`, `d:`}},
{`name:D:`, -1, []string{`name`, `D:`}},
{`name:D::rW`, -1, []string{`name`, `D:`, `rW`}},
{`name:D::RW`, -1, []string{`name`, `D:`, `RW`}},
{`c:/:d:/forward/slashes/are/good/too`, -1, []string{`c:/`, `d:/forward/slashes/are/good/too`}},
{`c:\Windows`, -1, []string{`c:\Windows`}},
{`c:\Program Files (x86)`, -1, []string{`c:\Program Files (x86)`}},
{``, -1, nil},
{`.`, -1, []string{`.`}},
{`..\`, -1, []string{`..\`}},
{`c:\:..\`, -1, []string{`c:\`, `..\`}},
{`c:\:d:\:xyzzy`, -1, []string{`c:\`, `d:\`, `xyzzy`}},
// Cover directories with one-character name
{`/tmp/x/y:/foo/x/y`, -1, []string{`/tmp/x/y`, `/foo/x/y`}},
} {
res := volumeSplitN(x.input, x.n)
if len(res) < len(x.expected) {
t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res)
}
for i, e := range res {
if e != x.expected[i] {
t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res)
}
}
}
}
func TestValidateDevice(t *testing.T) {
valid := []string{
"/home",

View File

@ -5,20 +5,20 @@ import (
"io"
"net/http/httputil"
"os"
"regexp"
"runtime"
"strings"
"syscall"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/promise"
"github.com/docker/docker/pkg/signal"
"github.com/docker/docker/pkg/term"
"github.com/docker/libnetwork/resolvconf/dns"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/net/context"
@ -77,43 +77,21 @@ func warnOnOomKillDisable(hostConfig container.HostConfig, stderr io.Writer) {
// they are trying to set a DNS to a localhost address
func warnOnLocalhostDNS(hostConfig container.HostConfig, stderr io.Writer) {
for _, dnsIP := range hostConfig.DNS {
if isLocalhost(dnsIP) {
if dns.IsLocalhost(dnsIP) {
fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP)
return
}
}
}
// IPLocalhost is a regex pattern for IPv4 or IPv6 loopback range.
const ipLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)`
var localhostIPRegexp = regexp.MustCompile(ipLocalhost)
// IsLocalhost returns true if ip matches the localhost IP regular expression.
// Used for determining if nameserver settings are being passed which are
// localhost addresses
func isLocalhost(ip string) bool {
return localhostIPRegexp.MatchString(ip)
}
func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, ropts *runOptions, copts *containerOptions) error {
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), copts.env.GetAll())
newEnv := []string{}
for k, v := range proxyConfig {
if v == nil {
newEnv = append(newEnv, k)
} else {
newEnv = append(newEnv, fmt.Sprintf("%s=%s", k, *v))
}
}
copts.env = *opts.NewListOptsRef(&newEnv, nil)
func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions, copts *containerOptions) error {
containerConfig, err := parse(flags, copts)
// just in case the parse does not exit
if err != nil {
reportError(dockerCli.Err(), "run", err.Error(), true)
return cli.StatusError{StatusCode: 125}
}
return runContainer(dockerCli, ropts, copts, containerConfig)
return runContainer(dockerCli, opts, copts, containerConfig)
}
// nolint: gocyclo
@ -169,7 +147,6 @@ func runContainer(dockerCli *command.DockerCli, opts *runOptions, copts *contain
sigc := ForwardAllSignals(ctx, dockerCli, createResponse.ID)
defer signal.StopCatch(sigc)
}
var (
waitDisplayID chan struct{}
errCh chan error
@ -189,11 +166,10 @@ func runContainer(dockerCli *command.DockerCli, opts *runOptions, copts *contain
}
close, err := attachContainer(ctx, dockerCli, &errCh, config, createResponse.ID)
defer close()
if err != nil {
return err
}
defer close()
}
statusChan := waitExitOrRemoved(ctx, dockerCli, createResponse.ID, copts.autoRemove)
@ -290,27 +266,22 @@ func attachContainer(
return nil, errAttach
}
ch := make(chan error, 1)
*errCh = ch
*errCh = promise.Go(func() error {
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: out,
errorStream: cerr,
resp: resp,
tty: config.Tty,
detachKeys: options.DetachKeys,
}
go func() {
ch <- func() error {
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: out,
errorStream: cerr,
resp: resp,
tty: config.Tty,
detachKeys: options.DetachKeys,
}
if errHijack := streamer.stream(ctx); errHijack != nil {
return errHijack
}
return errAttach
}()
}()
if errHijack := streamer.stream(ctx); errHijack != nil {
return errHijack
}
return errAttach
})
return resp.Close, nil
}

View File

@ -9,6 +9,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/promise"
"github.com/docker/docker/pkg/signal"
"github.com/docker/docker/pkg/term"
"github.com/pkg/errors"
@ -102,28 +103,23 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error {
return errAttach
}
defer resp.Close()
cErr := promise.Go(func() error {
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: dockerCli.Out(),
errorStream: dockerCli.Err(),
resp: resp,
tty: c.Config.Tty,
detachKeys: options.DetachKeys,
}
cErr := make(chan error, 1)
go func() {
cErr <- func() error {
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: dockerCli.Out(),
errorStream: dockerCli.Err(),
resp: resp,
tty: c.Config.Tty,
detachKeys: options.DetachKeys,
}
errHijack := streamer.stream(ctx)
if errHijack == nil {
return errAttach
}
return errHijack
}()
}()
errHijack := streamer.stream(ctx)
if errHijack == nil {
return errAttach
}
return errHijack
})
// 3. We should open a channel for receiving status code of the container
// no matter it's detached, removed on daemon side(--rm) or exit normally.
@ -150,12 +146,12 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error {
fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err)
}
}
if attachErr := <-cErr; attachErr != nil {
if attchErr := <-cErr; attchErr != nil {
if _, ok := err.(term.EscapeError); ok {
// The user entered the detach escape sequence.
return nil
}
return attachErr
return attchErr
}
if status := <-statusChan; status != 0 {

View File

@ -21,7 +21,6 @@ import (
type statsOptions struct {
all bool
noStream bool
noTrunc bool
format string
containers []string
}
@ -43,7 +42,6 @@ func NewStatsCommand(dockerCli *command.DockerCli) *cobra.Command {
flags := cmd.Flags()
flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)")
flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result")
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template")
return cmd
}
@ -108,7 +106,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
closeChan <- err
}
for _, container := range cs {
s := formatter.NewContainerStats(container.ID[:12])
s := formatter.NewContainerStats(container.ID[:12], daemonOSType)
if cStats.add(s) {
waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
@ -125,7 +123,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
eh := command.InitEventHandler()
eh.Handle("create", func(e events.Message) {
if opts.all {
s := formatter.NewContainerStats(e.ID[:12])
s := formatter.NewContainerStats(e.ID[:12], daemonOSType)
if cStats.add(s) {
waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
@ -134,7 +132,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
})
eh.Handle("start", func(e events.Message) {
s := formatter.NewContainerStats(e.ID[:12])
s := formatter.NewContainerStats(e.ID[:12], daemonOSType)
if cStats.add(s) {
waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
@ -160,7 +158,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
// Artificially send creation events for the containers we were asked to
// monitor (same code path than we use when monitoring all containers).
for _, name := range opts.containers {
s := formatter.NewContainerStats(name)
s := formatter.NewContainerStats(name, daemonOSType)
if cStats.add(s) {
waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
@ -216,7 +214,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
ccstats = append(ccstats, c.GetStatistics())
}
cStats.mu.Unlock()
if err = formatter.ContainerStatsWrite(statsCtx, ccstats, daemonOSType, !opts.noTrunc); err != nil {
if err = formatter.ContainerStatsWrite(statsCtx, ccstats, daemonOSType); err != nil {
break
}
if len(cStats.cs) == 0 && !showAll {

View File

@ -7,17 +7,18 @@ import (
"sync"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
)
type stats struct {
mu sync.Mutex
cs []*formatter.ContainerStats
ostype string
mu sync.Mutex
cs []*formatter.ContainerStats
}
// daemonOSType is set once we have at least one stat for a container

View File

@ -7,11 +7,11 @@ import (
"runtime"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/signal"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
)
@ -39,7 +39,7 @@ func resizeTtyTo(ctx context.Context, client client.ContainerAPIClient, id strin
}
// MonitorTtySize updates the container tty size when the terminal tty changes size
func MonitorTtySize(ctx context.Context, cli command.Cli, id string, isExec bool) error {
func MonitorTtySize(ctx context.Context, cli *command.DockerCli, id string, isExec bool) error {
resizeTty := func() {
height, width := cli.Out().GetTtySize()
resizeTtyTo(ctx, cli.Client(), id, height, width, isExec)
@ -74,7 +74,7 @@ func MonitorTtySize(ctx context.Context, cli command.Cli, id string, isExec bool
}
// ForwardAllSignals forwards signals to the container
func ForwardAllSignals(ctx context.Context, cli command.Cli, cid string) chan os.Signal {
func ForwardAllSignals(ctx context.Context, cli *command.DockerCli, cid string) chan os.Signal {
sigc := make(chan os.Signal, 128)
signal.CatchAll(sigc)
go func() {

View File

@ -8,6 +8,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/opts"
containertypes "github.com/docker/docker/api/types/container"
runconfigopts "github.com/docker/docker/runconfig/opts"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
@ -81,7 +82,7 @@ func runUpdate(dockerCli *command.DockerCli, options *updateOptions) error {
var restartPolicy containertypes.RestartPolicy
if options.restartPolicy != "" {
restartPolicy, err = opts.ParseRestartPolicy(options.restartPolicy)
restartPolicy, err = runconfigopts.ParseRestartPolicy(options.restartPolicy)
if err != nil {
return err
}

View File

@ -3,13 +3,14 @@ package container
import (
"strconv"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/versions"
"github.com/sirupsen/logrus"
clientapi "github.com/docker/docker/client"
"golang.org/x/net/context"
)
@ -124,6 +125,20 @@ func legacyWaitExitOrRemoved(ctx context.Context, dockerCli *command.DockerCli,
return statusChan
}
// getExitCode performs an inspect on the container. It returns
// the running state and the exit code.
func getExitCode(ctx context.Context, dockerCli *command.DockerCli, containerID string) (bool, int, error) {
c, err := dockerCli.Client().ContainerInspect(ctx, containerID)
if err != nil {
// If we can't connect, then the daemon probably died.
if !clientapi.IsErrConnectionFailed(err) {
return false, -1, err
}
return false, -1, nil
}
return c.State.Running, c.State.ExitCode, nil
}
func parallelOperation(ctx context.Context, containers []string, op func(ctx context.Context, container string) error) chan error {
if len(containers) == 0 {
return nil

View File

@ -3,8 +3,8 @@ package command
import (
"sync"
"github.com/Sirupsen/logrus"
eventtypes "github.com/docker/docker/api/types/events"
"github.com/sirupsen/logrus"
)
// EventHandler is abstract interface for user to customize

View File

@ -2,16 +2,16 @@ package formatter
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/pkg/stringutils"
"github.com/docker/go-units"
units "github.com/docker/go-units"
)
const (
@ -171,16 +171,16 @@ func (c *containerContext) Command() string {
}
func (c *containerContext) CreatedAt() string {
return time.Unix(c.c.Created, 0).String()
return time.Unix(int64(c.c.Created), 0).String()
}
func (c *containerContext) RunningFor() string {
createdAt := time.Unix(c.c.Created, 0)
createdAt := time.Unix(int64(c.c.Created), 0)
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
}
func (c *containerContext) Ports() string {
return DisplayablePorts(c.c.Ports)
return api.DisplayablePorts(c.c.Ports)
}
func (c *containerContext) Status() string {
@ -257,89 +257,3 @@ func (c *containerContext) Networks() string {
return strings.Join(networks, ",")
}
// DisplayablePorts returns formatted string representing open ports of container
// e.g. "0.0.0.0:80->9090/tcp, 9988/tcp"
// it's used by command 'docker ps'
func DisplayablePorts(ports []types.Port) string {
type portGroup struct {
first uint16
last uint16
}
groupMap := make(map[string]*portGroup)
var result []string
var hostMappings []string
var groupMapKeys []string
sort.Sort(byPortInfo(ports))
for _, port := range ports {
current := port.PrivatePort
portKey := port.Type
if port.IP != "" {
if port.PublicPort != current {
hostMappings = append(hostMappings, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type))
continue
}
portKey = fmt.Sprintf("%s/%s", port.IP, port.Type)
}
group := groupMap[portKey]
if group == nil {
groupMap[portKey] = &portGroup{first: current, last: current}
// record order that groupMap keys are created
groupMapKeys = append(groupMapKeys, portKey)
continue
}
if current == (group.last + 1) {
group.last = current
continue
}
result = append(result, formGroup(portKey, group.first, group.last))
groupMap[portKey] = &portGroup{first: current, last: current}
}
for _, portKey := range groupMapKeys {
g := groupMap[portKey]
result = append(result, formGroup(portKey, g.first, g.last))
}
result = append(result, hostMappings...)
return strings.Join(result, ", ")
}
func formGroup(key string, start, last uint16) string {
parts := strings.Split(key, "/")
groupType := parts[0]
var ip string
if len(parts) > 1 {
ip = parts[0]
groupType = parts[1]
}
group := strconv.Itoa(int(start))
if start != last {
group = fmt.Sprintf("%s-%d", group, last)
}
if ip != "" {
group = fmt.Sprintf("%s:%s->%s", ip, group, group)
}
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
}
if r[i].IP != r[j].IP {
return r[i].IP < r[j].IP
}
if r[i].PublicPort != r[j].PublicPort {
return r[i].PublicPort < r[j].PublicPort
}
return r[i].Type < r[j].Type
}

View File

@ -10,9 +10,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestContainerPsContext(t *testing.T) {
@ -228,10 +226,13 @@ size: 0B
Context{Format: NewContainerFormat("{{.Image}}", false, true)},
"ubuntu\nubuntu\n",
},
// Special headers for customized table format
// Special headers for customerized table format
{
Context{Format: NewContainerFormat(`table {{truncate .ID 5}}\t{{json .Image}} {{.RunningFor}}/{{title .Status}}/{{pad .Ports 2 2}}.{{upper .Names}} {{lower .Status}}`, false, true)},
string(golden.Get(t, "container-context-write-special-headers.golden")),
`CONTAINER ID IMAGE CREATED/STATUS/ PORTS .NAMES STATUS
conta "ubuntu" 24 hours ago//.FOOBAR_BAZ
conta "ubuntu" 24 hours ago//.FOOBAR_BAR
`,
},
}
@ -356,11 +357,12 @@ func TestContainerContextWriteJSON(t *testing.T) {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
msg := fmt.Sprintf("Output: line %d: %s", i, line)
t.Logf("Output: line %d: %s", i, line)
var m map[string]interface{}
err := json.Unmarshal([]byte(line), &m)
require.NoError(t, err, msg)
assert.Equal(t, expectedJSONs[i], m, msg)
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedJSONs[i], m)
}
}
@ -375,11 +377,12 @@ func TestContainerContextWriteJSONField(t *testing.T) {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
msg := fmt.Sprintf("Output: line %d: %s", i, line)
t.Logf("Output: line %d: %s", i, line)
var s string
err := json.Unmarshal([]byte(line), &s)
require.NoError(t, err, msg)
assert.Equal(t, containers[i].ID, s, msg)
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Equal(t, containers[i].ID, s)
}
}
@ -408,248 +411,3 @@ func TestContainerBackCompat(t *testing.T) {
buf.Reset()
}
}
type ports struct {
ports []types.Port
expected string
}
// nolint: lll
func TestDisplayablePorts(t *testing.T) {
cases := []ports{
{
[]types.Port{
{
PrivatePort: 9988,
Type: "tcp",
},
},
"9988/tcp"},
{
[]types.Port{
{
PrivatePort: 9988,
Type: "udp",
},
},
"9988/udp",
},
{
[]types.Port{
{
IP: "0.0.0.0",
PrivatePort: 9988,
Type: "tcp",
},
},
"0.0.0.0:0->9988/tcp",
},
{
[]types.Port{
{
PrivatePort: 9988,
PublicPort: 8899,
Type: "tcp",
},
},
"9988/tcp",
},
{
[]types.Port{
{
IP: "4.3.2.1",
PrivatePort: 9988,
PublicPort: 8899,
Type: "tcp",
},
},
"4.3.2.1:8899->9988/tcp",
},
{
[]types.Port{
{
IP: "4.3.2.1",
PrivatePort: 9988,
PublicPort: 9988,
Type: "tcp",
},
},
"4.3.2.1:9988->9988/tcp",
},
{
[]types.Port{
{
PrivatePort: 9988,
Type: "udp",
}, {
PrivatePort: 9988,
Type: "udp",
},
},
"9988/udp, 9988/udp",
},
{
[]types.Port{
{
IP: "1.2.3.4",
PublicPort: 9998,
PrivatePort: 9998,
Type: "udp",
}, {
IP: "1.2.3.4",
PublicPort: 9999,
PrivatePort: 9999,
Type: "udp",
},
},
"1.2.3.4:9998-9999->9998-9999/udp",
},
{
[]types.Port{
{
IP: "1.2.3.4",
PublicPort: 8887,
PrivatePort: 9998,
Type: "udp",
}, {
IP: "1.2.3.4",
PublicPort: 8888,
PrivatePort: 9999,
Type: "udp",
},
},
"1.2.3.4:8887->9998/udp, 1.2.3.4:8888->9999/udp",
},
{
[]types.Port{
{
PrivatePort: 9998,
Type: "udp",
}, {
PrivatePort: 9999,
Type: "udp",
},
},
"9998-9999/udp",
},
{
[]types.Port{
{
IP: "1.2.3.4",
PrivatePort: 6677,
PublicPort: 7766,
Type: "tcp",
}, {
PrivatePort: 9988,
PublicPort: 8899,
Type: "udp",
},
},
"9988/udp, 1.2.3.4:7766->6677/tcp",
},
{
[]types.Port{
{
IP: "1.2.3.4",
PrivatePort: 9988,
PublicPort: 8899,
Type: "udp",
}, {
IP: "1.2.3.4",
PrivatePort: 9988,
PublicPort: 8899,
Type: "tcp",
}, {
IP: "4.3.2.1",
PrivatePort: 2233,
PublicPort: 3322,
Type: "tcp",
},
},
"4.3.2.1:3322->2233/tcp, 1.2.3.4:8899->9988/tcp, 1.2.3.4:8899->9988/udp",
},
{
[]types.Port{
{
PrivatePort: 9988,
PublicPort: 8899,
Type: "udp",
}, {
IP: "1.2.3.4",
PrivatePort: 6677,
PublicPort: 7766,
Type: "tcp",
}, {
IP: "4.3.2.1",
PrivatePort: 2233,
PublicPort: 3322,
Type: "tcp",
},
},
"9988/udp, 4.3.2.1:3322->2233/tcp, 1.2.3.4:7766->6677/tcp",
},
{
[]types.Port{
{
PrivatePort: 80,
Type: "tcp",
}, {
PrivatePort: 1024,
Type: "tcp",
}, {
PrivatePort: 80,
Type: "udp",
}, {
PrivatePort: 1024,
Type: "udp",
}, {
IP: "1.1.1.1",
PublicPort: 80,
PrivatePort: 1024,
Type: "tcp",
}, {
IP: "1.1.1.1",
PublicPort: 80,
PrivatePort: 1024,
Type: "udp",
}, {
IP: "1.1.1.1",
PublicPort: 1024,
PrivatePort: 80,
Type: "tcp",
}, {
IP: "1.1.1.1",
PublicPort: 1024,
PrivatePort: 80,
Type: "udp",
}, {
IP: "2.1.1.1",
PublicPort: 80,
PrivatePort: 1024,
Type: "tcp",
}, {
IP: "2.1.1.1",
PublicPort: 80,
PrivatePort: 1024,
Type: "udp",
}, {
IP: "2.1.1.1",
PublicPort: 1024,
PrivatePort: 80,
Type: "tcp",
}, {
IP: "2.1.1.1",
PublicPort: 1024,
PrivatePort: 80,
Type: "udp",
},
},
"80/tcp, 80/udp, 1024/tcp, 1024/udp, 1.1.1.1:1024->80/tcp, 1.1.1.1:1024->80/udp, 2.1.1.1:1024->80/tcp, 2.1.1.1:1024->80/udp, 1.1.1.1:80->1024/tcp, 1.1.1.1:80->1024/udp, 2.1.1.1:80->1024/tcp, 2.1.1.1:80->1024/udp",
},
}
for _, port := range cases {
actual := DisplayablePorts(port.ports)
assert.Equal(t, port.expected, actual)
}
}

View File

@ -1,10 +1,9 @@
package formatter
import (
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func compareMultipleValues(t *testing.T, value, expected string) {
@ -23,5 +22,7 @@ func compareMultipleValues(t *testing.T, value, expected string) {
keyval := strings.Split(expected, "=")
expMap[keyval[0]] = keyval[1]
}
assert.Equal(t, expMap, entriesMap)
if !reflect.DeepEqual(expMap, entriesMap) {
t.Fatalf("Expected entries: %v, got: %v", expected, value)
}
}

View File

@ -29,12 +29,11 @@ const (
// DiskUsageContext contains disk usage specific information required by the formatter, encapsulate a Context struct.
type DiskUsageContext struct {
Context
Verbose bool
LayersSize int64
Images []*types.ImageSummary
Containers []*types.Container
Volumes []*types.Volume
BuilderSize int64
Verbose bool
LayersSize int64
Images []*types.ImageSummary
Containers []*types.Container
Volumes []*types.Volume
}
func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template, error) {
@ -66,59 +65,49 @@ reclaimable: {{.Reclaimable}}
}
func (ctx *DiskUsageContext) Write() (err error) {
if ctx.Verbose {
return ctx.verboseWrite()
}
ctx.buffer = bytes.NewBufferString("")
ctx.preFormat()
if ctx.Verbose == false {
ctx.buffer = bytes.NewBufferString("")
ctx.preFormat()
tmpl, err := ctx.parseFormat()
if err != nil {
return err
}
err = ctx.contextFormat(tmpl, &diskUsageImagesContext{
totalSize: ctx.LayersSize,
images: ctx.Images,
})
if err != nil {
return err
}
err = ctx.contextFormat(tmpl, &diskUsageContainersContext{
containers: ctx.Containers,
})
if err != nil {
return err
}
err = ctx.contextFormat(tmpl, &diskUsageVolumesContext{
volumes: ctx.Volumes,
})
if err != nil {
return err
}
diskUsageContainersCtx := diskUsageContainersContext{containers: []*types.Container{}}
diskUsageContainersCtx.header = map[string]string{
"Type": typeHeader,
"TotalCount": totalHeader,
"Active": activeHeader,
"Size": sizeHeader,
"Reclaimable": reclaimableHeader,
}
ctx.postFormat(tmpl, &diskUsageContainersCtx)
tmpl, err := ctx.parseFormat()
if err != nil {
return err
}
err = ctx.contextFormat(tmpl, &diskUsageImagesContext{
totalSize: ctx.LayersSize,
images: ctx.Images,
})
if err != nil {
return err
}
err = ctx.contextFormat(tmpl, &diskUsageContainersContext{
containers: ctx.Containers,
})
if err != nil {
return err
}
err = ctx.contextFormat(tmpl, &diskUsageVolumesContext{
volumes: ctx.Volumes,
})
if err != nil {
return err
}
err = ctx.contextFormat(tmpl, &diskUsageBuilderContext{
builderSize: ctx.BuilderSize,
})
if err != nil {
return err
}
diskUsageContainersCtx := diskUsageContainersContext{containers: []*types.Container{}}
diskUsageContainersCtx.header = map[string]string{
"Type": typeHeader,
"TotalCount": totalHeader,
"Active": activeHeader,
"Size": sizeHeader,
"Reclaimable": reclaimableHeader,
}
ctx.postFormat(tmpl, &diskUsageContainersCtx)
return err
}
func (ctx *DiskUsageContext) verboseWrite() (err error) {
// First images
tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat)
if err != nil {
@ -187,9 +176,6 @@ func (ctx *DiskUsageContext) verboseWrite() (err error) {
}
}
ctx.postFormat(tmpl, newVolumeContext())
// And build cache
fmt.Fprintf(ctx.Output, "\nBuild cache usage: %s\n\n", units.HumanSize(float64(ctx.BuilderSize)))
return
}
@ -243,11 +229,12 @@ func (c *diskUsageImagesContext) Reclaimable() string {
if c.totalSize > 0 {
return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/c.totalSize)
}
return units.HumanSize(float64(reclaimable))
return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
}
type diskUsageContainersContext struct {
HeaderContext
verbose bool
containers []*types.Container
}
@ -305,11 +292,12 @@ func (c *diskUsageContainersContext) Reclaimable() string {
return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize)
}
return units.HumanSize(float64(reclaimable))
return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
}
type diskUsageVolumesContext struct {
HeaderContext
verbose bool
volumes []*types.Volume
}
@ -366,34 +354,5 @@ func (c *diskUsageVolumesContext) Reclaimable() string {
return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize)
}
return units.HumanSize(float64(reclaimable))
}
type diskUsageBuilderContext struct {
HeaderContext
builderSize int64
}
func (c *diskUsageBuilderContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *diskUsageBuilderContext) Type() string {
return "Build Cache"
}
func (c *diskUsageBuilderContext) TotalCount() string {
return ""
}
func (c *diskUsageBuilderContext) Active() string {
return ""
}
func (c *diskUsageBuilderContext) Size() string {
return units.HumanSize(float64(c.builderSize))
}
func (c *diskUsageBuilderContext) Reclaimable() string {
return c.Size()
return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
}

View File

@ -4,7 +4,6 @@ import (
"bytes"
"testing"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
)
@ -24,7 +23,6 @@ func TestDiskUsageContextFormatWrite(t *testing.T) {
Images 0 0 0B 0B
Containers 0 0 0B 0B
Local Volumes 0 0 0B 0B
Build Cache 0B 0B
`,
},
{
@ -40,9 +38,6 @@ CONTAINER ID IMAGE COMMAND LOCAL VOLUMES
Local Volumes space usage:
VOLUME NAME LINKS SIZE
Build cache usage: 0B
`,
},
// Errors
@ -75,7 +70,6 @@ Build cache usage: 0B
Images 0 0 0B 0B
Containers 0 0 0B 0B
Local Volumes 0 0 0B 0B
Build Cache 0B 0B
`,
},
{
@ -84,7 +78,11 @@ Build Cache 0B
Format: NewDiskUsageFormat("table {{.Type}}\t{{.Active}}"),
},
},
string(golden.Get(t, "disk-usage-context-write-custom.golden")),
`TYPE ACTIVE
Images 0
Containers 0
Local Volumes 0
`,
},
// Raw Format
{
@ -93,7 +91,25 @@ Build Cache 0B
Format: NewDiskUsageFormat("raw"),
},
},
string(golden.Get(t, "disk-usage-raw-format.golden")),
`type: Images
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Containers
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Local Volumes
total: 0
active: 0
size: 0B
reclaimable: 0B
`,
},
}

View File

@ -7,7 +7,7 @@ import (
"text/tabwriter"
"text/template"
"github.com/docker/cli/templates"
"github.com/docker/docker/pkg/templates"
"github.com/pkg/errors"
)

View File

@ -79,21 +79,21 @@ func (c *historyContext) ID() string {
}
func (c *historyContext) CreatedAt() string {
return time.Unix(c.h.Created, 0).Format(time.RFC3339)
var created string
created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0)))
return created
}
func (c *historyContext) CreatedSince() string {
if !c.human {
return c.CreatedAt()
}
created := units.HumanDuration(time.Now().UTC().Sub(time.Unix(c.h.Created, 0)))
var created string
created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0)))
return created + " ago"
}
func (c *historyContext) CreatedBy() string {
createdBy := strings.Replace(c.h.CreatedBy, "\t", " ", -1)
if c.trunc {
return stringutils.Ellipsis(createdBy, 45)
createdBy = stringutils.Ellipsis(createdBy, 45)
}
return createdBy
}

View File

@ -51,21 +51,17 @@ func TestHistoryContext_ID(t *testing.T) {
}
func TestHistoryContext_CreatedSince(t *testing.T) {
unixTime := time.Now().AddDate(0, 0, -7).Unix()
expected := "7 days ago"
var ctx historyContext
cases := []historyCase{
{
historyContext{
h: image.HistoryResponseItem{Created: time.Now().AddDate(0, 0, -7).Unix()},
h: image.HistoryResponseItem{Created: unixTime},
trunc: false,
human: true,
}, "7 days ago", ctx.CreatedSince,
},
{
historyContext{
h: image.HistoryResponseItem{Created: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()},
trunc: false,
human: false,
}, "2009-11-10T23:00:00Z", ctx.CreatedSince,
}, expected, ctx.CreatedSince,
},
}

View File

@ -79,16 +79,11 @@ func ImageWrite(ctx ImageContext, images []types.ImageSummary) error {
return ctx.Write(newImageContext(), render)
}
// needDigest determines whether the image digest should be ignored or not when writing image context
func needDigest(ctx ImageContext) bool {
return ctx.Digest || ctx.Format.Contains("{{.Digest}}")
}
func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext subContext) error) error {
for _, image := range images {
formatted := []*imageContext{}
images := []*imageContext{}
if isDangling(image) {
formatted = append(formatted, &imageContext{
images = append(images, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: "<none>",
@ -96,9 +91,90 @@ func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subC
digest: "<none>",
})
} else {
formatted = imageFormatTaggedAndDigest(ctx, image)
repoTags := map[string][]string{}
repoDigests := map[string][]string{}
for _, refString := range image.RepoTags {
ref, err := reference.ParseNormalizedNamed(refString)
if err != nil {
continue
}
if nt, ok := ref.(reference.NamedTagged); ok {
familiarRef := reference.FamiliarName(ref)
repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag())
}
}
for _, refString := range image.RepoDigests {
ref, err := reference.ParseNormalizedNamed(refString)
if err != nil {
continue
}
if c, ok := ref.(reference.Canonical); ok {
familiarRef := reference.FamiliarName(ref)
repoDigests[familiarRef] = append(repoDigests[familiarRef], c.Digest().String())
}
}
for repo, tags := range repoTags {
digests := repoDigests[repo]
// Do not display digests as their own row
delete(repoDigests, repo)
if !ctx.Digest {
// Ignore digest references, just show tag once
digests = nil
}
for _, tag := range tags {
if len(digests) == 0 {
images = append(images, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: tag,
digest: "<none>",
})
continue
}
// Display the digests for each tag
for _, dgst := range digests {
images = append(images, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: tag,
digest: dgst,
})
}
}
}
// Show rows for remaining digest only references
for repo, digests := range repoDigests {
// If digests are displayed, show row per digest
if ctx.Digest {
for _, dgst := range digests {
images = append(images, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: "<none>",
digest: dgst,
})
}
} else {
images = append(images, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: "<none>",
})
}
}
}
for _, imageCtx := range formatted {
for _, imageCtx := range images {
if err := format(imageCtx); err != nil {
return err
}
@ -107,82 +183,6 @@ func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subC
return nil
}
func imageFormatTaggedAndDigest(ctx ImageContext, image types.ImageSummary) []*imageContext {
repoTags := map[string][]string{}
repoDigests := map[string][]string{}
images := []*imageContext{}
for _, refString := range image.RepoTags {
ref, err := reference.ParseNormalizedNamed(refString)
if err != nil {
continue
}
if nt, ok := ref.(reference.NamedTagged); ok {
familiarRef := reference.FamiliarName(ref)
repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag())
}
}
for _, refString := range image.RepoDigests {
ref, err := reference.ParseNormalizedNamed(refString)
if err != nil {
continue
}
if c, ok := ref.(reference.Canonical); ok {
familiarRef := reference.FamiliarName(ref)
repoDigests[familiarRef] = append(repoDigests[familiarRef], c.Digest().String())
}
}
addImage := func(repo, tag, digest string) {
image := &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: tag,
digest: digest,
}
images = append(images, image)
}
for repo, tags := range repoTags {
digests := repoDigests[repo]
// Do not display digests as their own row
delete(repoDigests, repo)
if !needDigest(ctx) {
// Ignore digest references, just show tag once
digests = nil
}
for _, tag := range tags {
if len(digests) == 0 {
addImage(repo, tag, "<none>")
continue
}
// Display the digests for each tag
for _, dgst := range digests {
addImage(repo, tag, dgst)
}
}
}
// Show rows for remaining digest only references
for repo, digests := range repoDigests {
// If digests are displayed, show row per digest
if ctx.Digest {
for _, dgst := range digests {
addImage(repo, "<none>", dgst)
}
} else {
addImage(repo, "<none>", "")
}
}
return images
}
type imageContext struct {
HeaderContext
trunc bool
@ -234,12 +234,12 @@ func (c *imageContext) Digest() string {
}
func (c *imageContext) CreatedSince() string {
createdAt := time.Unix(c.i.Created, 0)
createdAt := time.Unix(int64(c.i.Created), 0)
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
}
func (c *imageContext) CreatedAt() string {
return time.Unix(c.i.Created, 0).String()
return time.Unix(int64(c.i.Created), 0).String()
}
func (c *imageContext) Size() string {

View File

@ -55,26 +55,6 @@ func TestImageContext(t *testing.T) {
i: types.ImageSummary{},
digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a",
}, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", ctx.Digest},
{
imageContext{
i: types.ImageSummary{Containers: 10},
}, "10", ctx.Containers,
},
{
imageContext{
i: types.ImageSummary{VirtualSize: 10000},
}, "10kB", ctx.VirtualSize,
},
{
imageContext{
i: types.ImageSummary{SharedSize: 10000},
}, "10kB", ctx.SharedSize,
},
{
imageContext{
i: types.ImageSummary{SharedSize: 5000, VirtualSize: 20000},
}, "15kB", ctx.UniqueSize,
},
}
for _, c := range cases {
@ -82,8 +62,8 @@ func TestImageContext(t *testing.T) {
v := c.call()
if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue)
} else {
assert.Equal(t, c.expValue, v)
} else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
}
}
}
@ -157,14 +137,6 @@ image <none>
},
"REPOSITORY\nimage\nimage\n<none>\n",
},
{
ImageContext{
Context: Context{
Format: NewImageFormat("table {{.Digest}}", true, false),
},
},
"DIGEST\nsha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf\n<none>\n<none>\n",
},
{
ImageContext{
Context: Context{

View File

@ -3,7 +3,6 @@ package formatter
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"testing"
"time"
@ -11,7 +10,6 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNetworkContext(t *testing.T) {
@ -185,11 +183,12 @@ func TestNetworkContextWriteJSON(t *testing.T) {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
msg := fmt.Sprintf("Output: line %d: %s", i, line)
t.Logf("Output: line %d: %s", i, line)
var m map[string]interface{}
err := json.Unmarshal([]byte(line), &m)
require.NoError(t, err, msg)
assert.Equal(t, expectedJSONs[i], m, msg)
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedJSONs[i], m)
}
}
@ -204,10 +203,11 @@ func TestNetworkContextWriteJSONField(t *testing.T) {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
msg := fmt.Sprintf("Output: line %d: %s", i, line)
t.Logf("Output: line %d: %s", i, line)
var s string
err := json.Unmarshal([]byte(line), &s)
require.NoError(t, err, msg)
assert.Equal(t, networks[i].ID, s, msg)
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Equal(t, networks[i].ID, s)
}
}

View File

@ -3,7 +3,6 @@ package formatter
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"testing"
@ -11,7 +10,6 @@ import (
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/stringid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNodeContext(t *testing.T) {
@ -250,11 +248,12 @@ func TestNodeContextWriteJSON(t *testing.T) {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
msg := fmt.Sprintf("Output: line %d: %s", i, line)
t.Logf("Output: line %d: %s", i, line)
var m map[string]interface{}
err := json.Unmarshal([]byte(line), &m)
require.NoError(t, err, msg)
assert.Equal(t, testcase.expected[i], m, msg)
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Equal(t, testcase.expected[i], m)
}
}
}
@ -270,11 +269,12 @@ func TestNodeContextWriteJSONField(t *testing.T) {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
msg := fmt.Sprintf("Output: line %d: %s", i, line)
t.Logf("Output: line %d: %s", i, line)
var s string
err := json.Unmarshal([]byte(line), &s)
require.NoError(t, err, msg)
assert.Equal(t, nodes[i].ID, s, msg)
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Equal(t, nodes[i].ID, s)
}
}

View File

@ -12,7 +12,7 @@ func (d *dummy) Func1() string {
return "Func1"
}
func (d *dummy) func2() string { // nolint: unused
func (d *dummy) func2() string {
return "func2(should not be marshalled)"
}

View File

@ -1,104 +0,0 @@
package formatter
import (
"strconv"
"strings"
registry "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/stringutils"
)
const (
defaultSearchTableFormat = "table {{.Name}}\t{{.Description}}\t{{.StarCount}}\t{{.IsOfficial}}\t{{.IsAutomated}}"
starsHeader = "STARS"
officialHeader = "OFFICIAL"
automatedHeader = "AUTOMATED"
)
// NewSearchFormat returns a Format for rendering using a network Context
func NewSearchFormat(source string) Format {
switch source {
case "":
return defaultSearchTableFormat
case TableFormatKey:
return defaultSearchTableFormat
}
return Format(source)
}
// SearchWrite writes the context
func SearchWrite(ctx Context, results []registry.SearchResult, auto bool, stars int) error {
render := func(format func(subContext subContext) error) error {
for _, result := range results {
// --automated and -s, --stars are deprecated since Docker 1.12
if (auto && !result.IsAutomated) || (stars > result.StarCount) {
continue
}
searchCtx := &searchContext{trunc: ctx.Trunc, s: result}
if err := format(searchCtx); err != nil {
return err
}
}
return nil
}
searchCtx := searchContext{}
searchCtx.header = map[string]string{
"Name": nameHeader,
"Description": descriptionHeader,
"StarCount": starsHeader,
"IsOfficial": officialHeader,
"IsAutomated": automatedHeader,
}
return ctx.Write(&searchCtx, render)
}
type searchContext struct {
HeaderContext
trunc bool
json bool
s registry.SearchResult
}
func (c *searchContext) MarshalJSON() ([]byte, error) {
c.json = true
return marshalJSON(c)
}
func (c *searchContext) Name() string {
return c.s.Name
}
func (c *searchContext) Description() string {
desc := strings.Replace(c.s.Description, "\n", " ", -1)
desc = strings.Replace(desc, "\r", " ", -1)
if c.trunc {
desc = stringutils.Ellipsis(desc, 45)
}
return desc
}
func (c *searchContext) StarCount() string {
return strconv.Itoa(c.s.StarCount)
}
func (c *searchContext) formatBool(value bool) string {
switch {
case value && c.json:
return "true"
case value:
return "[OK]"
case c.json:
return "false"
default:
return ""
}
}
func (c *searchContext) IsOfficial() string {
return c.formatBool(c.s.IsOfficial)
}
func (c *searchContext) IsAutomated() string {
return c.formatBool(c.s.IsAutomated)
}

View File

@ -1,280 +0,0 @@
package formatter
import (
"bytes"
"encoding/json"
"strings"
"testing"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/stringutils"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
)
func TestSearchContext(t *testing.T) {
name := "nginx"
starCount := 5000
var ctx searchContext
cases := []struct {
searchCtx searchContext
expValue string
call func() string
}{
{searchContext{
s: registrytypes.SearchResult{Name: name},
}, name, ctx.Name},
{searchContext{
s: registrytypes.SearchResult{StarCount: starCount},
}, "5000", ctx.StarCount},
{searchContext{
s: registrytypes.SearchResult{IsOfficial: true},
}, "[OK]", ctx.IsOfficial},
{searchContext{
s: registrytypes.SearchResult{IsOfficial: false},
}, "", ctx.IsOfficial},
{searchContext{
s: registrytypes.SearchResult{IsAutomated: true},
}, "[OK]", ctx.IsAutomated},
{searchContext{
s: registrytypes.SearchResult{IsAutomated: false},
}, "", ctx.IsAutomated},
}
for _, c := range cases {
ctx = c.searchCtx
v := c.call()
if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue)
} else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
}
}
}
func TestSearchContextDescription(t *testing.T) {
shortDescription := "Official build of Nginx."
longDescription := "Automated Nginx reverse proxy for docker containers"
descriptionWReturns := "Automated\nNginx reverse\rproxy\rfor docker\ncontainers"
var ctx searchContext
cases := []struct {
searchCtx searchContext
expValue string
call func() string
}{
{searchContext{
s: registrytypes.SearchResult{Description: shortDescription},
trunc: true,
}, shortDescription, ctx.Description},
{searchContext{
s: registrytypes.SearchResult{Description: shortDescription},
trunc: false,
}, shortDescription, ctx.Description},
{searchContext{
s: registrytypes.SearchResult{Description: longDescription},
trunc: false,
}, longDescription, ctx.Description},
{searchContext{
s: registrytypes.SearchResult{Description: longDescription},
trunc: true,
}, stringutils.Ellipsis(longDescription, 45), ctx.Description},
{searchContext{
s: registrytypes.SearchResult{Description: descriptionWReturns},
trunc: false,
}, longDescription, ctx.Description},
{searchContext{
s: registrytypes.SearchResult{Description: descriptionWReturns},
trunc: true,
}, stringutils.Ellipsis(longDescription, 45), ctx.Description},
}
for _, c := range cases {
ctx = c.searchCtx
v := c.call()
if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue)
} else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
}
}
}
func TestSearchContextWrite(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: NewSearchFormat("table")},
string(golden.Get(t, "search-context-write-table.golden")),
},
{
Context{Format: NewSearchFormat("table {{.Name}}")},
`NAME
result1
result2
`,
},
// Custom Format
{
Context{Format: NewSearchFormat("{{.Name}}")},
`result1
result2
`,
},
// Custom Format with CreatedAt
{
Context{Format: NewSearchFormat("{{.Name}} {{.StarCount}}")},
`result1 5000
result2 5
`,
},
}
for _, testcase := range cases {
results := []registrytypes.SearchResult{
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false},
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true},
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := SearchWrite(testcase.context, results, false, 0)
if err != nil {
assert.Error(t, err, testcase.expected)
} else {
assert.Equal(t, out.String(), testcase.expected)
}
}
}
func TestSearchContextWriteAutomated(t *testing.T) {
cases := []struct {
context Context
expected string
}{
// Table format
{
Context{Format: NewSearchFormat("table")},
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result2 Not official 5 [OK]
`,
},
{
Context{Format: NewSearchFormat("table {{.Name}}")},
`NAME
result2
`,
},
}
for _, testcase := range cases {
results := []registrytypes.SearchResult{
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false},
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true},
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := SearchWrite(testcase.context, results, true, 0)
if err != nil {
assert.Error(t, err, testcase.expected)
} else {
assert.Equal(t, out.String(), testcase.expected)
}
}
}
func TestSearchContextWriteStars(t *testing.T) {
cases := []struct {
context Context
expected string
}{
// Table format
{
Context{Format: NewSearchFormat("table")},
string(golden.Get(t, "search-context-write-stars-table.golden")),
},
{
Context{Format: NewSearchFormat("table {{.Name}}")},
`NAME
result1
`,
},
}
for _, testcase := range cases {
results := []registrytypes.SearchResult{
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false},
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true},
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := SearchWrite(testcase.context, results, false, 6)
if err != nil {
assert.Error(t, err, testcase.expected)
} else {
assert.Equal(t, out.String(), testcase.expected)
}
}
}
func TestSearchContextWriteJSON(t *testing.T) {
results := []registrytypes.SearchResult{
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false},
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true},
}
expectedJSONs := []map[string]interface{}{
{"Name": "result1", "Description": "Official build", "StarCount": "5000", "IsOfficial": "true", "IsAutomated": "false"},
{"Name": "result2", "Description": "Not official", "StarCount": "5", "IsOfficial": "false", "IsAutomated": "true"},
}
out := bytes.NewBufferString("")
err := SearchWrite(Context{Format: "{{json .}}", Output: out}, results, false, 0)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
t.Logf("Output: line %d: %s", i, line)
var m map[string]interface{}
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Equal(t, m, expectedJSONs[i])
}
}
func TestSearchContextWriteJSONField(t *testing.T) {
results := []registrytypes.SearchResult{
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false},
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true},
}
out := bytes.NewBufferString("")
err := SearchWrite(Context{Format: "{{json .Name}}", Output: out}, results, false, 0)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
t.Logf("Output: line %d: %s", i, line)
var s string
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Equal(t, s, results[i].Name)
}
}

View File

@ -12,20 +12,19 @@ import (
)
const (
defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.CreatedAt}}\t{{.UpdatedAt}}"
defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}"
secretIDHeader = "ID"
secretCreatedHeader = "CREATED"
secretUpdatedHeader = "UPDATED"
secretInspectPrettyTemplate Format = `ID: {{.ID}}
Name: {{.Name}}
secretInspectPrettyTemplate Format = `ID: {{.ID}}
Name: {{.Name}}
{{- if .Labels }}
Labels:
{{- range $k, $v := .Labels }}
- {{ $k }}{{if $v }}={{ $v }}{{ end }}
{{- end }}{{ end }}
Driver: {{.Driver}}
Created at: {{.CreatedAt}}
Updated at: {{.UpdatedAt}}`
Created at: {{.CreatedAt}}
Updated at: {{.UpdatedAt}}`
)
// NewSecretFormat returns a Format for rendering using a secret Context
@ -62,7 +61,6 @@ func newSecretContext() *secretContext {
sCtx.header = map[string]string{
"ID": secretIDHeader,
"Name": nameHeader,
"Driver": driverHeader,
"CreatedAt": secretCreatedHeader,
"UpdatedAt": secretUpdatedHeader,
"Labels": labelsHeader,
@ -91,13 +89,6 @@ func (c *secretContext) CreatedAt() string {
return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.CreatedAt)) + " ago"
}
func (c *secretContext) Driver() string {
if c.s.Spec.Driver == nil {
return ""
}
return c.s.Spec.Driver.Name
}
func (c *secretContext) UpdatedAt() string {
return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.UpdatedAt)) + " ago"
}
@ -162,13 +153,6 @@ func (ctx *secretInspectContext) Labels() map[string]string {
return ctx.Secret.Spec.Labels
}
func (ctx *secretInspectContext) Driver() string {
if ctx.Secret.Spec.Driver == nil {
return ""
}
return ctx.Secret.Spec.Driver.Name
}
func (ctx *secretInspectContext) CreatedAt() string {
return command.PrettyPrint(ctx.Secret.CreatedAt)
}

View File

@ -28,9 +28,9 @@ func TestSecretContextFormatWrite(t *testing.T) {
},
// Table format
{Context{Format: NewSecretFormat("table", false)},
`ID NAME DRIVER CREATED UPDATED
1 passwords Less than a second ago Less than a second ago
2 id_rsa Less than a second ago Less than a second ago
`ID NAME CREATED UPDATED
1 passwords Less than a second ago Less than a second ago
2 id_rsa Less than a second ago Less than a second ago
`},
{Context{Format: NewSecretFormat("table {{.Name}}", true)},
`NAME

View File

@ -504,10 +504,7 @@ func (c *serviceContext) Replicas() string {
}
func (c *serviceContext) Image() string {
var image string
if c.service.Spec.TaskTemplate.ContainerSpec != nil {
image = c.service.Spec.TaskTemplate.ContainerSpec.Image
}
image := c.service.Spec.TaskTemplate.ContainerSpec.Image
if ref, err := reference.ParseNormalizedNamed(image); err == nil {
// update image string for display, (strips any digest)
if nt, ok := ref.(reference.NamedTagged); ok {
@ -521,11 +518,11 @@ func (c *serviceContext) Image() string {
}
func (c *serviceContext) Ports() string {
if c.service.Endpoint.Ports == nil {
if c.service.Spec.EndpointSpec == nil || c.service.Spec.EndpointSpec.Ports == nil {
return ""
}
ports := []string{}
for _, pConfig := range c.service.Endpoint.Ports {
for _, pConfig := range c.service.Spec.EndpointSpec.Ports {
if pConfig.PublishMode == swarm.PortConfigPublishModeIngress {
ports = append(ports, fmt.Sprintf("*:%d->%d/%s",
pConfig.PublishedPort,

View File

@ -3,14 +3,11 @@ package formatter
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/docker/docker/api/types/swarm"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestServiceContextWrite(t *testing.T) {
@ -60,7 +57,21 @@ bar
// Raw Format
{
Context{Format: NewServiceListFormat("raw", false)},
string(golden.Get(t, "service-context-write-raw.golden")),
`id: id_baz
name: baz
mode: global
replicas: 2/4
image:
ports: *:80->8080/tcp
id: id_bar
name: bar
mode: replicated
replicas: 2/4
image:
ports: *:80->8080/tcp
`,
},
{
Context{Format: NewServiceListFormat("raw", true)},
@ -83,14 +94,14 @@ bar
ID: "id_baz",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "baz"},
},
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
EndpointSpec: &swarm.EndpointSpec{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
},
},
},
},
@ -99,14 +110,14 @@ bar
ID: "id_bar",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "bar"},
},
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
EndpointSpec: &swarm.EndpointSpec{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
},
},
},
},
@ -139,14 +150,14 @@ func TestServiceContextWriteJSON(t *testing.T) {
ID: "id_baz",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "baz"},
},
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
EndpointSpec: &swarm.EndpointSpec{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
},
},
},
},
@ -155,14 +166,14 @@ func TestServiceContextWriteJSON(t *testing.T) {
ID: "id_bar",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "bar"},
},
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
EndpointSpec: &swarm.EndpointSpec{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
},
},
},
},
@ -189,11 +200,12 @@ func TestServiceContextWriteJSON(t *testing.T) {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
msg := fmt.Sprintf("Output: line %d: %s", i, line)
t.Logf("Output: line %d: %s", i, line)
var m map[string]interface{}
err := json.Unmarshal([]byte(line), &m)
require.NoError(t, err, msg)
assert.Equal(t, expectedJSONs[i], m, msg)
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedJSONs[i], m)
}
}
func TestServiceContextWriteJSONField(t *testing.T) {
@ -217,10 +229,11 @@ func TestServiceContextWriteJSONField(t *testing.T) {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
msg := fmt.Sprintf("Output: line %d: %s", i, line)
t.Logf("Output: line %d: %s", i, line)
var s string
err := json.Unmarshal([]byte(line), &s)
require.NoError(t, err, msg)
assert.Equal(t, services[i].Spec.Name, s, msg)
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Equal(t, services[i].Spec.Name, s)
}
}

View File

@ -4,14 +4,13 @@ import (
"fmt"
"sync"
"github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units"
)
const (
winOSType = "windows"
defaultStatsTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}"
winDefaultStatsTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"
defaultStatsTableFormat = "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}"
winDefaultStatsTableFormat = "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"
containerHeader = "CONTAINER"
cpuPercHeader = "CPU %"
@ -110,18 +109,19 @@ func NewStatsFormat(source, osType string) Format {
}
// NewContainerStats returns a new ContainerStats entity and sets in it the given name
func NewContainerStats(container string) *ContainerStats {
return &ContainerStats{StatsEntry: StatsEntry{Container: container}}
func NewContainerStats(container, osType string) *ContainerStats {
return &ContainerStats{
StatsEntry: StatsEntry{Container: container},
}
}
// ContainerStatsWrite renders the context for a list of containers statistics
func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string, trunc bool) error {
func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string) error {
render := func(format func(subContext subContext) error) error {
for _, cstats := range containerStats {
containerStatsCtx := &containerStatsContext{
s: cstats,
os: osType,
trunc: trunc,
s: cstats,
os: osType,
}
if err := format(containerStatsCtx); err != nil {
return err
@ -151,9 +151,8 @@ func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string
type containerStatsContext struct {
HeaderContext
s StatsEntry
os string
trunc bool
s StatsEntry
os string
}
func (c *containerStatsContext) MarshalJSON() ([]byte, error) {
@ -172,9 +171,6 @@ func (c *containerStatsContext) Name() string {
}
func (c *containerStatsContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.s.ID)
}
return c.s.ID
}
@ -190,7 +186,7 @@ func (c *containerStatsContext) MemUsage() string {
return fmt.Sprintf("-- / --")
}
if c.os == winOSType {
return units.BytesSize(c.s.Memory)
return fmt.Sprintf("%s", units.BytesSize(c.s.Memory))
}
return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit))
}

View File

@ -114,7 +114,7 @@ container2 --
}
var out bytes.Buffer
te.context.Output = &out
err := ContainerStatsWrite(te.context, stats, "linux", false)
err := ContainerStatsWrite(te.context, stats, "linux")
if err != nil {
assert.EqualError(t, err, te.expected)
} else {
@ -180,7 +180,7 @@ container2 -- --
}
var out bytes.Buffer
te.context.Output = &out
err := ContainerStatsWrite(te.context, stats, "windows", false)
err := ContainerStatsWrite(te.context, stats, "windows")
if err != nil {
assert.EqualError(t, err, te.expected)
} else {
@ -220,7 +220,7 @@ func TestContainerStatsContextWriteWithNoStats(t *testing.T) {
}
for _, context := range contexts {
ContainerStatsWrite(context.context, []StatsEntry{}, "linux", false)
ContainerStatsWrite(context.context, []StatsEntry{}, "linux")
assert.Equal(t, context.expected, out.String())
// Clean buffer
out.Reset()
@ -258,41 +258,7 @@ func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) {
}
for _, context := range contexts {
ContainerStatsWrite(context.context, []StatsEntry{}, "windows", false)
assert.Equal(t, context.expected, out.String())
// Clean buffer
out.Reset()
}
}
func TestContainerStatsContextWriteTrunc(t *testing.T) {
var out bytes.Buffer
contexts := []struct {
context Context
trunc bool
expected string
}{
{
Context{
Format: "{{.ID}}",
Output: &out,
},
false,
"b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc\n",
},
{
Context{
Format: "{{.ID}}",
Output: &out,
},
true,
"b95a83497c91\n",
},
}
for _, context := range contexts {
ContainerStatsWrite(context.context, []StatsEntry{{ID: "b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc"}}, "linux", context.trunc)
ContainerStatsWrite(context.context, []StatsEntry{}, "windows")
assert.Equal(t, context.expected, out.String())
// Clean buffer
out.Reset()

View File

@ -7,7 +7,6 @@ import (
"testing"
"github.com/docker/docker/api/types/swarm"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
)
@ -34,7 +33,10 @@ taskID2
},
{
Context{Format: NewTaskFormat("table {{.Name}}\t{{.Node}}\t{{.Ports}}", false)},
string(golden.Get(t, "task-context-write-table-custom.golden")),
`NAME NODE PORTS
foobar_baz foo1
foobar_bar foo2
`,
},
{
Context{Format: NewTaskFormat("table {{.Name}}", true)},

View File

@ -1,3 +0,0 @@
CONTAINER ID IMAGE CREATED/STATUS/ PORTS .NAMES STATUS
conta "ubuntu" 24 hours ago//.FOOBAR_BAZ
conta "ubuntu" 24 hours ago//.FOOBAR_BAR

View File

@ -1,5 +0,0 @@
TYPE ACTIVE
Images 0
Containers 0
Local Volumes 0
Build Cache

View File

@ -1,24 +0,0 @@
type: Images
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Containers
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Local Volumes
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Build Cache
total:
active:
size: 0B
reclaimable: 0B

View File

@ -1,2 +0,0 @@
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result1 Official build 5000 [OK]

View File

@ -1,3 +0,0 @@
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result1 Official build 5000 [OK]
result2 Not official 5 [OK]

View File

@ -1,14 +0,0 @@
id: id_baz
name: baz
mode: global
replicas: 2/4
image:
ports: *:80->8080/tcp
id: id_bar
name: bar
mode: replicated
replicas: 2/4
image:
ports: *:80->8080/tcp

View File

@ -1,3 +0,0 @@
NAME NODE PORTS
foobar_baz foo1
foobar_bar foo2

View File

@ -1,150 +0,0 @@
package formatter
import (
"sort"
"strings"
"github.com/docker/docker/pkg/stringid"
)
const (
defaultTrustTagTableFormat = "table {{.SignedTag}}\t{{.Digest}}\t{{.Signers}}"
signedTagNameHeader = "SIGNED TAG"
trustedDigestHeader = "DIGEST"
signersHeader = "SIGNERS"
defaultSignerInfoTableFormat = "table {{.Signer}}\t{{.Keys}}"
signerNameHeader = "SIGNER"
keysHeader = "KEYS"
)
// SignedTagInfo represents all formatted information needed to describe a signed tag:
// Name: name of the signed tag
// Digest: hex encoded digest of the contents
// Signers: list of entities who signed the tag
type SignedTagInfo struct {
Name string
Digest string
Signers []string
}
// SignerInfo represents all formatted information needed to describe a signer:
// Name: name of the signer role
// Keys: the keys associated with the signer
type SignerInfo struct {
Name string
Keys []string
}
// NewTrustTagFormat returns a Format for rendering using a trusted tag Context
func NewTrustTagFormat() Format {
return defaultTrustTagTableFormat
}
// NewSignerInfoFormat returns a Format for rendering a signer role info Context
func NewSignerInfoFormat() Format {
return defaultSignerInfoTableFormat
}
// TrustTagWrite writes the context
func TrustTagWrite(ctx Context, signedTagInfoList []SignedTagInfo) error {
render := func(format func(subContext subContext) error) error {
for _, signedTag := range signedTagInfoList {
if err := format(&trustTagContext{s: signedTag}); err != nil {
return err
}
}
return nil
}
trustTagCtx := trustTagContext{}
trustTagCtx.header = trustTagHeaderContext{
"SignedTag": signedTagNameHeader,
"Digest": trustedDigestHeader,
"Signers": signersHeader,
}
return ctx.Write(&trustTagCtx, render)
}
type trustTagHeaderContext map[string]string
type trustTagContext struct {
HeaderContext
s SignedTagInfo
}
// SignedTag returns the name of the signed tag
func (c *trustTagContext) SignedTag() string {
return c.s.Name
}
// Digest returns the hex encoded digest associated with this signed tag
func (c *trustTagContext) Digest() string {
return c.s.Digest
}
// Signers returns the sorted list of entities who signed this tag
func (c *trustTagContext) Signers() string {
sort.Strings(c.s.Signers)
return strings.Join(c.s.Signers, ", ")
}
// SignerInfoWrite writes the context
func SignerInfoWrite(ctx Context, signerInfoList []SignerInfo) error {
render := func(format func(subContext subContext) error) error {
for _, signerInfo := range signerInfoList {
if err := format(&signerInfoContext{
trunc: ctx.Trunc,
s: signerInfo,
}); err != nil {
return err
}
}
return nil
}
signerInfoCtx := signerInfoContext{}
signerInfoCtx.header = signerInfoHeaderContext{
"Signer": signerNameHeader,
"Keys": keysHeader,
}
return ctx.Write(&signerInfoCtx, render)
}
type signerInfoHeaderContext map[string]string
type signerInfoContext struct {
HeaderContext
trunc bool
s SignerInfo
}
// Keys returns the sorted list of keys associated with the signer
func (c *signerInfoContext) Keys() string {
sort.Strings(c.s.Keys)
truncatedKeys := []string{}
if c.trunc {
for _, keyID := range c.s.Keys {
truncatedKeys = append(truncatedKeys, stringid.TruncateID(keyID))
}
return strings.Join(truncatedKeys, ", ")
}
return strings.Join(c.s.Keys, ", ")
}
// Signer returns the name of the signer
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]
}

View File

@ -1,238 +0,0 @@
package formatter
import (
"bytes"
"testing"
"github.com/docker/docker/pkg/stringid"
"github.com/stretchr/testify/assert"
)
func TestTrustTag(t *testing.T) {
digest := stringid.GenerateRandomID()
trustedTag := "tag"
var ctx trustTagContext
cases := []struct {
trustTagCtx trustTagContext
expValue string
call func() string
}{
{
trustTagContext{
s: SignedTagInfo{Name: trustedTag,
Digest: digest,
Signers: nil,
},
},
digest,
ctx.Digest,
},
{
trustTagContext{
s: SignedTagInfo{Name: trustedTag,
Digest: digest,
Signers: nil,
},
},
trustedTag,
ctx.SignedTag,
},
// Empty signers makes a row with empty string
{
trustTagContext{
s: SignedTagInfo{Name: trustedTag,
Digest: digest,
Signers: nil,
},
},
"",
ctx.Signers,
},
{
trustTagContext{
s: SignedTagInfo{Name: trustedTag,
Digest: digest,
Signers: []string{"alice", "bob", "claire"},
},
},
"alice, bob, claire",
ctx.Signers,
},
// alphabetic signing on Signers
{
trustTagContext{
s: SignedTagInfo{Name: trustedTag,
Digest: digest,
Signers: []string{"claire", "bob", "alice"},
},
},
"alice, bob, claire",
ctx.Signers,
},
}
for _, c := range cases {
ctx = c.trustTagCtx
v := c.call()
if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
}
}
}
func TestTrustTagContextWrite(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: NewTrustTagFormat(),
},
`SIGNED TAG DIGEST SIGNERS
tag1 deadbeef alice
tag2 aaaaaaaa alice, bob
tag3 bbbbbbbb
`,
},
}
for _, testcase := range cases {
signedTags := []SignedTagInfo{
{Name: "tag1", Digest: "deadbeef", Signers: []string{"alice"}},
{Name: "tag2", Digest: "aaaaaaaa", Signers: []string{"alice", "bob"}},
{Name: "tag3", Digest: "bbbbbbbb", Signers: []string{}},
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := TrustTagWrite(testcase.context, signedTags)
if err != nil {
assert.EqualError(t, err, testcase.expected)
} else {
assert.Equal(t, testcase.expected, out.String())
}
}
}
// With no trust data, the TrustTagWrite will print an empty table:
// it's up to the caller to decide whether or not to print this versus an error
func TestTrustTagContextEmptyWrite(t *testing.T) {
emptyCase := struct {
context Context
expected string
}{
Context{
Format: NewTrustTagFormat(),
},
`SIGNED TAG DIGEST SIGNERS
`,
}
emptySignedTags := []SignedTagInfo{}
out := bytes.NewBufferString("")
emptyCase.context.Output = out
err := TrustTagWrite(emptyCase.context, emptySignedTags)
assert.NoError(t, err)
assert.Equal(t, emptyCase.expected, out.String())
}
func TestSignerInfoContextEmptyWrite(t *testing.T) {
emptyCase := struct {
context Context
expected string
}{
Context{
Format: NewSignerInfoFormat(),
},
`SIGNER KEYS
`,
}
emptySignerInfo := []SignerInfo{}
out := bytes.NewBufferString("")
emptyCase.context.Output = out
err := SignerInfoWrite(emptyCase.context, emptySignerInfo)
assert.NoError(t, err)
assert.Equal(t, emptyCase.expected, out.String())
}
func TestSignerInfoContextWrite(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: NewSignerInfoFormat(),
Trunc: true,
},
`SIGNER KEYS
alice key11, key12
bob key21
eve foobarbazqux, key31, key32
`,
},
// No truncation
{
Context{
Format: NewSignerInfoFormat(),
},
`SIGNER KEYS
alice key11, key12
bob key21
eve foobarbazquxquux, key31, key32
`,
},
}
for _, testcase := range cases {
signerInfo := SignerInfoList{
{Name: "alice", Keys: []string{"key11", "key12"}},
{Name: "bob", Keys: []string{"key21"}},
{Name: "eve", Keys: []string{"key31", "key32", "foobarbazquxquux"}},
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := SignerInfoWrite(testcase.context, signerInfo)
if err != nil {
assert.EqualError(t, err, testcase.expected)
} else {
assert.Equal(t, testcase.expected, out.String())
}
}
}

View File

@ -3,14 +3,12 @@ package formatter
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestVolumeContext(t *testing.T) {
@ -155,11 +153,12 @@ func TestVolumeContextWriteJSON(t *testing.T) {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
msg := fmt.Sprintf("Output: line %d: %s", i, line)
t.Logf("Output: line %d: %s", i, line)
var m map[string]interface{}
err := json.Unmarshal([]byte(line), &m)
require.NoError(t, err, msg)
assert.Equal(t, expectedJSONs[i], m, msg)
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedJSONs[i], m)
}
}
@ -174,10 +173,11 @@ func TestVolumeContextWriteJSONField(t *testing.T) {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
msg := fmt.Sprintf("Output: line %d: %s", i, line)
t.Logf("Output: line %d: %s", i, line)
var s string
err := json.Unmarshal([]byte(line), &s)
require.NoError(t, err, msg)
assert.Equal(t, volumes[i].Name, s, msg)
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Equal(t, volumes[i].Name, s)
}
}

View File

@ -5,7 +5,7 @@ import (
"github.com/docker/docker/api/types/swarm"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"

View File

@ -21,14 +21,13 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/docker/docker/pkg/urlutil"
runconfigopts "github.com/docker/docker/runconfig/opts"
units "github.com/docker/go-units"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
@ -63,7 +62,6 @@ type buildOptions struct {
squash bool
target string
imageIDFile string
stream bool
}
// dockerfileFromStdin returns true when the user specified that the Dockerfile
@ -78,20 +76,16 @@ func (o buildOptions) contextFromStdin() bool {
return o.context == "-"
}
func newBuildOptions() buildOptions {
// NewBuildCommand creates a new `docker build` command
func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command {
ulimits := make(map[string]*units.Ulimit)
return buildOptions{
options := buildOptions{
tags: opts.NewListOpts(validateTag),
buildArgs: opts.NewListOpts(opts.ValidateEnv),
ulimits: opts.NewUlimitOpt(&ulimits),
labels: opts.NewListOpts(opts.ValidateEnv),
extraHosts: opts.NewListOpts(opts.ValidateExtraHost),
}
}
// NewBuildCommand creates a new `docker build` command
func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
options := newBuildOptions()
cmd := &cobra.Command{
Use: "build [OPTIONS] PATH | URL | -",
@ -140,10 +134,6 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
flags.SetAnnotation("squash", "experimental", nil)
flags.SetAnnotation("squash", "version", []string{"1.25"})
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"})
return cmd
}
@ -164,7 +154,7 @@ func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error {
}
// nolint: gocyclo
func runBuild(dockerCli command.Cli, options buildOptions) error {
func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
var (
buildCtx io.ReadCloser
dockerfileCtx io.ReadCloser
@ -174,7 +164,6 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
relDockerfile string
progBuff io.Writer
buildBuff io.Writer
remote string
)
if options.dockerfileFromStdin() {
@ -200,7 +189,6 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
switch {
case options.contextFromStdin():
// buildCtx is tar archive. if stdin was dockerfile then it is wrapped
buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName)
case isLocalDir(specifiedContext):
contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName)
@ -224,8 +212,7 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
contextDir = tempDir
}
// read from a directory into tar archive
if buildCtx == nil && !options.stream {
if buildCtx == nil {
excludes, err := build.ReadDockerignore(contextDir)
if err != nil {
return err
@ -242,61 +229,38 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
}
excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, options.dockerfileFromStdin())
compression := archive.Uncompressed
if options.compress {
compression = archive.Gzip
}
buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
Compression: compression,
ExcludePatterns: excludes,
ChownOpts: &idtools.IDPair{UID: 0, GID: 0},
})
if err != nil {
return err
}
}
// replace Dockerfile if it was added from stdin and there is archive context
if dockerfileCtx != nil && buildCtx != nil {
// replace Dockerfile if added dynamically
if dockerfileCtx != nil {
buildCtx, relDockerfile, err = build.AddDockerfileToBuildContext(dockerfileCtx, buildCtx)
if err != nil {
return err
}
}
// if streaming and dockerfile was not from stdin then read from file
// to the same reader that is usually stdin
if options.stream && dockerfileCtx == nil {
dockerfileCtx, err = os.Open(relDockerfile)
if err != nil {
return errors.Wrapf(err, "failed to open %s", relDockerfile)
}
defer dockerfileCtx.Close()
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := context.Background()
var resolvedTags []*resolvedTag
if command.IsTrusted() {
translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) {
return TrustedReference(ctx, dockerCli, ref, nil)
}
// if there is a tar wrapper, the dockerfile needs to be replaced inside it
if buildCtx != nil {
// Wrap the tar archive to replace the Dockerfile entry with the rewritten
// Dockerfile which uses trusted pulls.
buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, translator, &resolvedTags)
} else if dockerfileCtx != nil {
// if there was not archive context still do the possible replacements in Dockerfile
newDockerfile, _, err := rewriteDockerfileFrom(ctx, dockerfileCtx, translator)
if err != nil {
return err
}
dockerfileCtx = ioutil.NopCloser(bytes.NewBuffer(newDockerfile))
}
}
if options.compress {
buildCtx, err = build.Compress(buildCtx)
if err != nil {
return err
}
// Wrap the tar archive to replace the Dockerfile entry with the rewritten
// Dockerfile which uses trusted pulls.
buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, translator, &resolvedTags)
}
// Setup an upload progress bar
@ -305,46 +269,9 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
progressOutput = &lastProgressOutput{output: progressOutput}
}
// if up to this point nothing has set the context then we must have have
// another way for sending it(streaming) and set the context to the Dockerfile
if dockerfileCtx != nil && buildCtx == nil {
buildCtx = dockerfileCtx
}
var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
s, err := trySession(dockerCli, contextDir)
if err != nil {
return err
}
var body io.Reader
if buildCtx != nil && !options.stream {
body = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
}
// add context stream to the session
if options.stream && s != nil {
syncDone := make(chan error) // used to signal first progress reporting completed.
// progress would also send errors but don't need it here as errors
// are handled by session.Run() and ImageBuild()
if err := addDirToSession(s, contextDir, progressOutput, syncDone); err != nil {
return err
}
buf := newBufferedWriter(syncDone, buildBuff)
defer func() {
select {
case <-buf.flushed:
case <-ctx.Done():
}
}()
buildBuff = buf
remote = clientSessionRemote
body = buildCtx
}
configFile := dockerCli.ConfigFile()
authConfigs, _ := configFile.GetAllCredentials()
authConfigs, _ := dockerCli.GetAllCredentials()
buildOptions := types.ImageBuildOptions{
Memory: options.memory.Value(),
MemorySwap: options.memorySwap.Value(),
@ -364,27 +291,15 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
Dockerfile: relDockerfile,
ShmSize: options.shmSize.Value(),
Ulimits: options.ulimits.GetList(),
BuildArgs: configFile.ParseProxyConfig(dockerCli.Client().DaemonHost(), options.buildArgs.GetAll()),
BuildArgs: runconfigopts.ConvertKVStringsToMapWithNil(options.buildArgs.GetAll()),
AuthConfigs: authConfigs,
Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
CacheFrom: options.cacheFrom,
SecurityOpt: options.securityOpt,
NetworkMode: options.networkMode,
Squash: options.squash,
ExtraHosts: options.extraHosts.GetAll(),
Target: options.target,
RemoteContext: remote,
}
if s != nil {
go func() {
logrus.Debugf("running session: %v", s.ID())
if err := s.Run(ctx, dockerCli.Client().DialSession); err != nil {
logrus.Error(err)
cancel() // cancel progress context
}
}()
buildOptions.SessionID = s.ID()
}
response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
@ -392,7 +307,6 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
if options.quiet {
fmt.Fprintf(dockerCli.Err(), "%s", progBuff)
}
cancel()
return err
}
defer response.Body.Close()

View File

@ -1,25 +1,25 @@
package build
import (
"archive/tar"
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"archive/tar"
"bytes"
"time"
"github.com/docker/docker/builder/remotecontext/git"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/fileutils"
"github.com/docker/docker/pkg/gitutils"
"github.com/docker/docker/pkg/httputils"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/pools"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/docker/docker/pkg/stringid"
@ -29,8 +29,6 @@ import (
const (
// DefaultDockerfileName is the Default filename with Docker commands, read by docker build
DefaultDockerfileName string = "Dockerfile"
// archiveHeaderSize is the number of bytes in an archive header
archiveHeaderSize = 512
)
// ValidateContextDirectory checks if all the contents of the directory
@ -87,12 +85,12 @@ func ValidateContextDirectory(srcPath string, excludes []string) error {
func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) {
buf := bufio.NewReader(r)
magic, err := buf.Peek(archiveHeaderSize)
magic, err := buf.Peek(archive.HeaderSize)
if err != nil && err != io.EOF {
return nil, "", errors.Errorf("failed to peek context header from STDIN: %v", err)
}
if IsArchive(magic) {
if archive.IsArchive(magic) {
return ioutils.NewReadCloserWrapper(buf, func() error { return r.Close() }), dockerfileName, nil
}
@ -136,18 +134,6 @@ func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCl
}
// IsArchive checks for the magic bytes of a tar or any supported compression
// algorithm.
func IsArchive(header []byte) bool {
compression := archive.DetectCompression(header)
if compression != archive.Uncompressed {
return true
}
r := tar.NewReader(bytes.NewBuffer(header))
_, err := r.Next()
return err == nil
}
// GetContextFromGitURL uses a Git URL as context for a `docker build`. The
// git repo is cloned into a temporary directory used as the context directory.
// Returns the absolute path to the temporary context directory, the relative
@ -157,7 +143,7 @@ func GetContextFromGitURL(gitURL, dockerfileName string) (string, string, error)
if _, err := exec.LookPath("git"); err != nil {
return "", "", errors.Wrapf(err, "unable to find 'git'")
}
absContextDir, err := git.Clone(gitURL)
absContextDir, err := gitutils.Clone(gitURL)
if err != nil {
return "", "", errors.Wrapf(err, "unable to 'git clone' to temporary context directory")
}
@ -175,7 +161,7 @@ func GetContextFromGitURL(gitURL, dockerfileName string) (string, string, error)
// Returns the tar archive used for the context and a path of the
// dockerfile inside the tar.
func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.ReadCloser, string, error) {
response, err := getWithStatusError(remoteURL)
response, err := httputils.Download(remoteURL)
if err != nil {
return nil, "", errors.Errorf("unable to download remote context %s: %v", remoteURL, err)
}
@ -187,24 +173,6 @@ func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.Read
return GetContextFromReader(ioutils.NewReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName)
}
// getWithStatusError does an http.Get() and returns an error if the
// status code is 4xx or 5xx.
func getWithStatusError(url string) (resp *http.Response, err error) {
if resp, err = http.Get(url); err != nil {
return nil, err
}
if resp.StatusCode < 400 {
return resp, nil
}
msg := fmt.Sprintf("failed to GET %s with status %s", url, resp.Status)
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, errors.Wrapf(err, msg+": error reading body")
}
return nil, errors.Errorf(msg+": %s", bytes.TrimSpace(body))
}
// GetContextFromLocalDir uses the given local directory as context for a
// `docker build`. Returns the absolute path to the local context directory,
// the relative path of the dockerfile in that context directory, and a non-nil
@ -376,27 +344,3 @@ func AddDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCl
})
return buildCtx, randomName, nil
}
// Compress the build context for sending to the API
func Compress(buildCtx io.ReadCloser) (io.ReadCloser, error) {
pipeReader, pipeWriter := io.Pipe()
go func() {
compressWriter, err := archive.CompressStream(pipeWriter, archive.Gzip)
if err != nil {
pipeWriter.CloseWithError(err)
}
defer buildCtx.Close()
if _, err := pools.Copy(compressWriter, buildCtx); err != nil {
pipeWriter.CloseWithError(
errors.Wrap(err, "failed to compress context"))
compressWriter.Close()
return
}
compressWriter.Close()
pipeWriter.Close()
}()
return pipeReader, nil
}

View File

@ -11,8 +11,8 @@ import (
"strings"
"testing"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -266,35 +266,3 @@ func chdir(t *testing.T, dir string) func() {
require.NoError(t, os.Chdir(dir))
return func() { require.NoError(t, os.Chdir(workingDirectory)) }
}
func TestIsArchive(t *testing.T) {
var testcases = []struct {
doc string
header []byte
expected bool
}{
{
doc: "nil is not a valid header",
header: nil,
expected: false,
},
{
doc: "invalid header bytes",
header: []byte{0x00, 0x01, 0x02},
expected: false,
},
{
doc: "header for bzip2 archive",
header: []byte{0x42, 0x5A, 0x68},
expected: true,
},
{
doc: "header for 7zip archive is not supported",
header: []byte{0x50, 0x4b, 0x03, 0x04},
expected: false,
},
}
for _, testcase := range testcases {
assert.Equal(t, testcase.expected, IsArchive(testcase.header), testcase.doc)
}
}

View File

@ -1,153 +0,0 @@
package image
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sync"
"time"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image/build"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/pkg/progress"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/filesync"
"github.com/pkg/errors"
"golang.org/x/time/rate"
)
const clientSessionRemote = "client-session"
func isSessionSupported(dockerCli command.Cli) bool {
return dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31")
}
func trySession(dockerCli command.Cli, contextDir string) (*session.Session, error) {
var s *session.Session
if isSessionSupported(dockerCli) {
sharedKey, err := getBuildSharedKey(contextDir)
if err != nil {
return nil, errors.Wrap(err, "failed to get build shared key")
}
s, err = session.NewSession(filepath.Base(contextDir), sharedKey)
if err != nil {
return nil, errors.Wrap(err, "failed to create session")
}
}
return s, nil
}
func addDirToSession(session *session.Session, contextDir string, progressOutput progress.Output, done chan error) error {
excludes, err := build.ReadDockerignore(contextDir)
if err != nil {
return err
}
p := &sizeProgress{out: progressOutput, action: "Streaming build context to Docker daemon"}
workdirProvider := filesync.NewFSSyncProvider([]filesync.SyncedDir{
{Dir: contextDir, Excludes: excludes},
})
session.Allow(workdirProvider)
// this will be replaced on parallel build jobs. keep the current
// progressbar for now
if snpc, ok := workdirProvider.(interface {
SetNextProgressCallback(func(int, bool), chan error)
}); ok {
snpc.SetNextProgressCallback(p.update, done)
}
return nil
}
type sizeProgress struct {
out progress.Output
action string
limiter *rate.Limiter
}
func (sp *sizeProgress) update(size int, last bool) {
if sp.limiter == nil {
sp.limiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 1)
}
if last || sp.limiter.Allow() {
sp.out.WriteProgress(progress.Progress{Action: sp.action, Current: int64(size), LastUpdate: last})
}
}
type bufferedWriter struct {
done chan error
io.Writer
buf *bytes.Buffer
flushed chan struct{}
mu sync.Mutex
}
func newBufferedWriter(done chan error, w io.Writer) *bufferedWriter {
bw := &bufferedWriter{done: done, Writer: w, buf: new(bytes.Buffer), flushed: make(chan struct{})}
go func() {
<-done
bw.flushBuffer()
}()
return bw
}
func (bw *bufferedWriter) Write(dt []byte) (int, error) {
select {
case <-bw.done:
bw.flushBuffer()
return bw.Writer.Write(dt)
default:
return bw.buf.Write(dt)
}
}
func (bw *bufferedWriter) flushBuffer() {
bw.mu.Lock()
select {
case <-bw.flushed:
default:
bw.Writer.Write(bw.buf.Bytes())
close(bw.flushed)
}
bw.mu.Unlock()
}
func getBuildSharedKey(dir string) (string, error) {
// build session is hash of build dir with node based randomness
s := sha256.Sum256([]byte(fmt.Sprintf("%s:%s", tryNodeIdentifier(), dir)))
return hex.EncodeToString(s[:]), nil
}
func tryNodeIdentifier() (out string) {
out = cliconfig.Dir() // return config dir as default on permission error
if err := os.MkdirAll(cliconfig.Dir(), 0700); err == nil {
sessionFile := filepath.Join(cliconfig.Dir(), ".buildNodeID")
if _, err := os.Lstat(sessionFile); err != nil {
if os.IsNotExist(err) { // create a new file with stored randomness
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return
}
if err := ioutil.WriteFile(sessionFile, []byte(hex.EncodeToString(b)), 0600); err != nil {
return
}
}
}
dt, err := ioutil.ReadFile(sessionFile)
if err == nil {
return string(dt)
}
}
return
}

View File

@ -1,143 +0,0 @@
package image
import (
"bytes"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"sort"
"syscall"
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/gotestyourself/gotestyourself/skip"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
func TestRunBuildResetsUidAndGidInContext(t *testing.T) {
skip.IfCondition(t, runtime.GOOS == "windows", "uid and gid not relevant on windows")
dest := fs.NewDir(t, "test-build-context-dest")
defer dest.Remove()
fakeImageBuild := func(_ context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
assert.NoError(t, archive.Untar(context, dest.Path(), nil))
body := new(bytes.Buffer)
return types.ImageBuildResponse{Body: ioutil.NopCloser(body)}, nil
}
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeImageBuild})
dir := fs.NewDir(t, "test-build-context",
fs.WithFile("foo", "some content", fs.AsUser(65534, 65534)),
fs.WithFile("Dockerfile", `
FROM alpine:3.6
COPY foo bar /
`),
)
defer dir.Remove()
options := newBuildOptions()
options.context = dir.Path()
err := runBuild(cli, options)
require.NoError(t, err)
files, err := ioutil.ReadDir(dest.Path())
require.NoError(t, err)
for _, fileInfo := range files {
assert.Equal(t, uint32(0), fileInfo.Sys().(*syscall.Stat_t).Uid)
assert.Equal(t, uint32(0), fileInfo.Sys().(*syscall.Stat_t).Gid)
}
}
func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) {
dest, err := ioutil.TempDir("", "test-build-compress-dest")
require.NoError(t, err)
defer os.RemoveAll(dest)
var dockerfileName string
fakeImageBuild := func(_ context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
buffer := new(bytes.Buffer)
tee := io.TeeReader(context, buffer)
assert.NoError(t, archive.Untar(tee, dest, nil))
dockerfileName = options.Dockerfile
header := buffer.Bytes()[:10]
assert.Equal(t, archive.Gzip, archive.DetectCompression(header))
body := new(bytes.Buffer)
return types.ImageBuildResponse{Body: ioutil.NopCloser(body)}, nil
}
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeImageBuild})
dockerfile := bytes.NewBufferString(`
FROM alpine:3.6
COPY foo /
`)
cli.SetIn(command.NewInStream(ioutil.NopCloser(dockerfile)))
dir, err := ioutil.TempDir("", "test-build-compress")
require.NoError(t, err)
defer os.RemoveAll(dir)
ioutil.WriteFile(filepath.Join(dir, "foo"), []byte("some content"), 0644)
options := newBuildOptions()
options.compress = true
options.dockerfileName = "-"
options.context = dir
err = runBuild(cli, options)
require.NoError(t, err)
files, err := ioutil.ReadDir(dest)
require.NoError(t, err)
actual := []string{}
for _, fileInfo := range files {
actual = append(actual, fileInfo.Name())
}
sort.Strings(actual)
assert.Equal(t, []string{dockerfileName, ".dockerignore", "foo"}, actual)
}
// TestRunBuildFromLocalGitHubDirNonExistingRepo tests that build contexts
// starting with `github.com/` are special-cased, and the build command attempts
// to clone the remote repo.
func TestRunBuildFromGitHubSpecialCase(t *testing.T) {
cmd := NewBuildCommand(test.NewFakeCli(nil))
cmd.SetArgs([]string{"github.com/docker/no-such-repository"})
cmd.SetOutput(ioutil.Discard)
err := cmd.Execute()
assert.Error(t, err)
assert.Contains(t, err.Error(), "unable to prepare context: unable to 'git clone'")
}
// TestRunBuildFromLocalGitHubDirNonExistingRepo tests that a local directory
// starting with `github.com` takes precedence over the `github.com` special
// case.
func TestRunBuildFromLocalGitHubDir(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "docker-build-from-local-dir-")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
buildDir := filepath.Join(tmpDir, "github.com", "docker", "no-such-repository")
err = os.MkdirAll(buildDir, 0777)
require.NoError(t, err)
err = ioutil.WriteFile(filepath.Join(buildDir, "Dockerfile"), []byte("FROM busybox\n"), 0644)
require.NoError(t, err)
client := test.NewFakeCli(&fakeClient{})
cmd := NewBuildCommand(client)
cmd.SetArgs([]string{buildDir})
cmd.SetOutput(ioutil.Discard)
err = cmd.Execute()
require.NoError(t, err)
}

View File

@ -27,7 +27,6 @@ type fakeClient struct {
imageInspectFunc func(image string) (types.ImageInspect, []byte, error)
imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
imageHistoryFunc func(image string) ([]image.HistoryResponseItem, error)
imageBuildFunc func(context.Context, io.Reader, types.ImageBuildOptions) (types.ImageBuildResponse, error)
}
func (cli *fakeClient) ImageTag(_ context.Context, image, ref string) error {
@ -115,10 +114,3 @@ func (cli *fakeClient) ImageHistory(_ context.Context, img string) ([]image.Hist
}
return []image.HistoryResponseItem{{ID: img, Created: time.Now().Unix()}}, nil
}
func (cli *fakeClient) ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
if cli.imageBuildFunc != nil {
return cli.imageBuildFunc(ctx, context, options)
}
return types.ImageBuildResponse{Body: ioutil.NopCloser(strings.NewReader(""))}, nil
}

View File

@ -8,7 +8,8 @@ import (
)
// NewImageCommand returns a cobra command for `image` subcommands
func NewImageCommand(dockerCli command.Cli) *cobra.Command {
// nolint: interfacer
func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command {
cmd := &cobra.Command{
Use: "image",
Short: "Manage images",

View File

@ -1,15 +1,17 @@
package image
import (
"bytes"
"fmt"
"io/ioutil"
"regexp"
"testing"
"time"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types/image"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -24,7 +26,7 @@ func TestNewHistoryCommandErrors(t *testing.T) {
{
name: "wrong-args",
args: []string{},
expectedError: "requires exactly 1 argument.",
expectedError: "requires exactly 1 argument(s).",
},
{
name: "client-error",
@ -36,7 +38,8 @@ func TestNewHistoryCommandErrors(t *testing.T) {
},
}
for _, tc := range testCases {
cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}))
buf := new(bytes.Buffer)
cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}, buf))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -87,17 +90,19 @@ func TestNewHistoryCommandSuccess(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc})
cmd := NewHistoryCommand(cli)
buf := new(bytes.Buffer)
cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}, buf))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.NoError(t, err)
actual := cli.OutBuffer().String()
actual := buf.String()
if tc.outputRegex == "" {
golden.Assert(t, actual, fmt.Sprintf("history-command-success.%s.golden", tc.name))
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("history-command-success.%s.golden", tc.name))[:])
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
} else {
assert.Regexp(t, tc.outputRegex, actual)
match, _ := regexp.MatchString(tc.outputRegex, actual)
assert.True(t, match)
}
}
}

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