Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit 6fd4fc8

Browse files
author
Vincent Demeester
authored
Merge pull request #234 from Dimrok/feature/ls
Add ls command.
2 parents 20545ed + bd36668 commit 6fd4fc8

11 files changed

Lines changed: 252 additions & 60 deletions

File tree

Jenkinsfile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,5 +112,3 @@ pipeline {
112112
}
113113
}
114114
}
115-
116-

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ lint: ## run linter(s)
3434

3535
test-e2e: bin/$(BIN_NAME) ## run end-to-end tests
3636
@echo "Running e2e tests..."
37-
$(GO_TEST) ./e2e/
37+
$(GO_TEST) -v ./e2e/
3838

3939
test-unit: ## run unit tests
4040
@echo "Running unit tests..."

cmd/docker-app/ls.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package main
2+
3+
import (
4+
"github.com/docker/app/internal/image"
5+
"github.com/docker/cli/cli"
6+
"github.com/spf13/cobra"
7+
)
8+
9+
type listOptions struct {
10+
quiet bool
11+
}
12+
13+
func lsCmd() *cobra.Command {
14+
var opts listOptions
15+
cmd := &cobra.Command{
16+
Use: "ls [<app-name>:[<tag>]]",
17+
Short: "List applications.",
18+
Args: cli.RequiresMaxArgs(1),
19+
RunE: func(cmd *cobra.Command, args []string) error {
20+
return image.List(firstOrEmpty(args), opts.quiet)
21+
},
22+
}
23+
cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "Only show numeric IDs")
24+
return cmd
25+
}

cmd/docker-app/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ var (
1515
helmCmd(),
1616
initCmd(),
1717
inspectCmd(),
18+
lsCmd(),
1819
pushCmd(),
1920
renderCmd(),
2021
saveCmd(),

docker.Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ test-unit: build_dev_image ## run unit tests
6262
docker run --rm $(DEV_IMAGE_NAME) make test-unit
6363

6464
test-e2e: build_dev_image ## run end-to-end tests
65-
docker run -v /var/run:/var/run:ro --rm $(DEV_IMAGE_NAME) make bin/$(BIN_NAME) test-e2e
65+
docker run -v /var/run:/var/run:ro --rm --network="host" $(DEV_IMAGE_NAME) make bin/$(BIN_NAME) test-e2e
6666

6767
COV_LABEL := com.docker.app.cov-run=$(TAG)
6868
coverage: build_dev_image ## run tests with coverage
6969
@$(call mkdir,_build)
70-
docker run -v /var/run:/var/run:ro --name $(COV_CTNR_NAME) -tid $(DEV_IMAGE_NAME) make COMMIT=${COMMIT} TAG=${TAG} coverage
70+
docker run -v /var/run:/var/run:ro --name $(COV_CTNR_NAME) --network="host" -tid $(DEV_IMAGE_NAME) make COMMIT=${COMMIT} TAG=${TAG} coverage
7171
docker logs -f $(COV_CTNR_NAME)
7272
docker cp $(COV_CTNR_NAME):$(PKG_PATH)/_build/cov/ ./_build/ci-cov
7373
docker rm $(COV_CTNR_NAME)

e2e/binary_test.go

Lines changed: 3 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"os/exec"
1010
"path/filepath"
1111
"runtime"
12-
"strconv"
1312
"strings"
1413
"testing"
1514

@@ -21,51 +20,6 @@ import (
2120
"gotest.tools/icmd"
2221
)
2322

24-
type registry struct {
25-
port int
26-
container string
27-
}
28-
29-
func startRegistry() (*registry, error) {
30-
r := &registry{}
31-
err := r.Start()
32-
return r, err
33-
}
34-
35-
// Start starts a new docker registry on a random port
36-
func (r *registry) Start() error {
37-
cmd := exec.Command("docker", "run", "--rm", "-d", "-P", "registry:2")
38-
output, err := cmd.Output()
39-
r.container = strings.Trim(string(output), " \r\n")
40-
return err
41-
}
42-
43-
// Stop terminates this registry
44-
func (r *registry) Stop() error {
45-
cmd := exec.Command("docker", "stop", r.container)
46-
_, err := cmd.CombinedOutput()
47-
return err
48-
}
49-
50-
// Port returns the host port this registry listens on
51-
func (r *registry) Port() (int, error) {
52-
if r.port != 0 {
53-
return r.port, nil
54-
}
55-
cmd := exec.Command("docker", "port", r.container, "5000")
56-
output, err := cmd.CombinedOutput()
57-
if err != nil {
58-
fmt.Println(string(output))
59-
return 0, err
60-
}
61-
sport := strings.Split(string(output), ":")[1]
62-
p, err := strconv.ParseInt(strings.Trim(sport, " \r\n"), 10, 32)
63-
if err == nil {
64-
r.port = int(p)
65-
}
66-
return r.port, err
67-
}
68-
6923
var (
7024
dockerApp = ""
7125
hasExperimental = false
@@ -350,12 +304,9 @@ func TestSplitMergeBinary(t *testing.T) {
350304

351305
func TestImageBinary(t *testing.T) {
352306
dockerApp, _ := getBinary(t)
353-
r, err := startRegistry()
354-
assert.NilError(t, err)
355-
defer r.Stop()
356-
port, err := r.Port()
357-
assert.NilError(t, err)
358-
registry := fmt.Sprintf("localhost:%v", port)
307+
r := startRegistry(t)
308+
defer r.stop(t)
309+
registry := r.getAddress(t)
359310
defer func() {
360311
// no way to match both in one command
361312
cmd1 := exec.Command("docker", "image", "ls", "--format", "{{.ID}}", "--filter", "reference=*/*envvariables*")

e2e/container.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package e2e
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os/exec"
7+
"strconv"
8+
"strings"
9+
"testing"
10+
"time"
11+
12+
"gotest.tools/assert"
13+
)
14+
15+
type container struct {
16+
image string
17+
privatePort int
18+
address string
19+
container string
20+
}
21+
22+
func startRegistry(t *testing.T) *container {
23+
c := &container{image: "registry:2", privatePort: 5000}
24+
c.start(t)
25+
return c
26+
}
27+
28+
func startDind(t *testing.T) *container {
29+
c := &container{image: "docker:dind", privatePort: 2375}
30+
c.start(t)
31+
return c
32+
}
33+
34+
// Start starts a new docker container on a random port
35+
func (c *container) start(t *testing.T) {
36+
cmd := exec.Command("docker", "run", "--rm", "--privileged", "-d", "-P", c.image)
37+
output := runCmd(t, cmd)
38+
c.container = strings.Trim(output, " \r\n")
39+
time.Sleep(time.Second * 3)
40+
}
41+
42+
// Stop terminates this container
43+
func (c *container) stop(t *testing.T) {
44+
runCmd(t, exec.Command("docker", "stop", c.container))
45+
}
46+
47+
// getAddress returns the host:port this container listens on
48+
func (c *container) getAddress(t *testing.T) string {
49+
if c.address != "" {
50+
return c.address
51+
}
52+
cmd := exec.Command("docker", "port", c.container, strconv.Itoa(c.privatePort))
53+
output := runCmd(t, cmd)
54+
c.address = fmt.Sprintf("127.0.0.1:%v", strings.Trim(strings.Split(output, ":")[1], " \r\n"))
55+
return c.address
56+
}
57+
58+
func runCmd(t *testing.T, cmd *exec.Cmd) string {
59+
var outputBuf, errBuf bytes.Buffer
60+
cmd.Stdout = &outputBuf
61+
cmd.Stderr = &errBuf
62+
err := cmd.Run()
63+
assert.NilError(t, err, errBuf.String())
64+
return outputBuf.String()
65+
}

e2e/list_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package e2e
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
"testing"
8+
"time"
9+
10+
"github.com/pkg/errors"
11+
"gotest.tools/assert"
12+
"gotest.tools/fs"
13+
"gotest.tools/icmd"
14+
)
15+
16+
func lineByLineComparator(t *testing.T, actual string, length int, expectedLines map[int]func(string) error) {
17+
t.Helper()
18+
lines := strings.Split(actual, "\n")
19+
assert.Equal(t, len(lines), length)
20+
if len(expectedLines) == 0 {
21+
return
22+
}
23+
for i, line := range lines {
24+
cmp, ok := expectedLines[i]
25+
if !ok {
26+
continue
27+
}
28+
if err := cmp(line); err != nil {
29+
t.Errorf("line %d: %s", i, err)
30+
}
31+
}
32+
if t.Failed() {
33+
t.Log(actual)
34+
}
35+
}
36+
37+
func prefix(expected string) func(string) error {
38+
return func(actual string) error {
39+
if strings.HasPrefix(actual, expected) {
40+
return nil
41+
}
42+
return errors.Errorf("expected %q to start with %q", actual, expected)
43+
}
44+
}
45+
46+
func equals(expected string) func(string) error {
47+
return func(actual string) error {
48+
if expected == actual {
49+
return nil
50+
}
51+
return errors.Errorf("got %q, expected %q", actual, expected)
52+
}
53+
}
54+
55+
func extractImageID(t *testing.T, line string) string {
56+
fields := strings.Fields(line)
57+
assert.Assert(t, len(fields) > 3)
58+
return fields[2]
59+
}
60+
61+
type commandConfig struct {
62+
env string
63+
exe string
64+
dir string
65+
t *testing.T
66+
}
67+
68+
func (c *commandConfig) run(args ...string) *icmd.Result {
69+
c.t.Helper()
70+
cmd := icmd.Command(c.exe, args...)
71+
cmd.Env = append(os.Environ(), c.env)
72+
cmd.Dir = c.dir
73+
result := icmd.RunCmd(cmd)
74+
result.Assert(c.t, icmd.Success)
75+
return result
76+
}
77+
78+
func TestLsCmd(t *testing.T) {
79+
app, _ := getBinary(t)
80+
dind := startDind(t)
81+
defer dind.stop(t)
82+
dockerApp := &commandConfig{
83+
env: fmt.Sprintf("DOCKER_HOST=%v", dind.getAddress(t)),
84+
exe: app,
85+
t: t,
86+
dir: fs.NewDir(t, "test_docker_app_ls_cmd").Path(),
87+
}
88+
result := dockerApp.run("ls")
89+
lineByLineComparator(t, result.Stdout(), 2, map[int]func(string) error{
90+
0: equals("REPOSITORY TAG IMAGE ID CREATED SIZE"),
91+
1: equals(""),
92+
})
93+
// Create two apps.
94+
dockerApp.run("init", "ls_myapp")
95+
dockerApp.run("save", "ls_myapp.dockerapp")
96+
// We need to sleep between creation to avoid timestamp collision.
97+
time.Sleep(1 * time.Second)
98+
dockerApp.run("init", "anotherapp")
99+
dockerApp.run("save", "anotherapp.dockerapp")
100+
// We need to sleep again to avoid: CREATED `Less than a second ago`, otherwise, the header size is not constant.
101+
time.Sleep(1 * time.Second)
102+
103+
// Except the output to contain both applications.
104+
result = dockerApp.run("ls")
105+
lineByLineComparator(t, result.Stdout(), 4, map[int]func(string) error{
106+
0: equals("REPOSITORY TAG IMAGE ID CREATED SIZE"),
107+
1: prefix("anotherapp.dockerapp 0.1.0"),
108+
2: prefix("ls_myapp.dockerapp 0.1.0"),
109+
3: equals(""),
110+
})
111+
112+
// Except quiet flag to return IDs only.
113+
var ids []string
114+
for _, line := range strings.Split(result.Stdout(), "\n")[1:2] {
115+
ids = append(ids, extractImageID(t, line))
116+
}
117+
result = dockerApp.run("ls", "--quiet")
118+
assert.DeepEqual(t, strings.Split(result.Stdout(), "\n")[0:1], ids)
119+
120+
// Except the output to contain only ls_myapp.
121+
result = dockerApp.run("ls", "ls_myapp.dockerapp")
122+
lineByLineComparator(t, result.Stdout(), 3, map[int]func(string) error{
123+
0: equals("REPOSITORY TAG IMAGE ID CREATED SIZE"),
124+
1: prefix("ls_myapp.dockerapp 0.1.0"),
125+
2: equals(""),
126+
})
127+
128+
// Except quiet flag to return only one ID.
129+
id := extractImageID(t, strings.Split(result.Stdout(), "\n")[1])
130+
result = dockerApp.run("ls", "-q", "ls_myapp.dockerapp")
131+
assert.Equal(t, result.Stdout(), id+"\n")
132+
}

internal/image/image.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os/exec"
77
"path/filepath"
88

9+
"github.com/docker/app/internal"
910
"github.com/docker/app/internal/renderer"
1011
)
1112

@@ -62,3 +63,20 @@ func Load(appname string, services []string) error {
6263
}
6364
return nil
6465
}
66+
67+
// List images with the label specific to applications.
68+
func List(appname string, quiet bool) error {
69+
args := []string{
70+
"image", "ls", "--filter", fmt.Sprintf("label=%s", internal.ImageLabel),
71+
}
72+
if quiet {
73+
args = append(args, "-q")
74+
}
75+
args = append(args, []string{"--", appname}...)
76+
77+
cmd := exec.Command("docker", args...)
78+
cmd.Stdout = os.Stdout
79+
cmd.Stderr = os.Stderr
80+
81+
return cmd.Run()
82+
}

internal/names.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import (
88
)
99

1010
const (
11-
// AppExtension is the extension used by an application
11+
// AppExtension is the extension used by an application.
1212
AppExtension = ".dockerapp"
13+
// The label used to distinguish applications from Docker images.
14+
ImageLabel = "com.docker.application"
1315
)
1416

1517
var appNameRe, _ = regexp.Compile("^[a-zA-Z][a-zA-Z0-9_-]+$")

0 commit comments

Comments
 (0)