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

Commit aa2de75

Browse files
committed
New command to list application images
For now we show the reference, name and version. Signed-off-by: Djordje Lukic <[email protected]>
1 parent 695509b commit aa2de75

File tree

10 files changed

+434
-66
lines changed

10 files changed

+434
-66
lines changed

e2e/helper_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88
"testing"
99
"time"
1010

11+
"github.com/docker/app/internal"
1112
"gotest.tools/assert"
13+
"gotest.tools/fs"
1214
"gotest.tools/icmd"
1315
)
1416

@@ -21,6 +23,69 @@ func readFile(t *testing.T, path string) string {
2123
return strings.Replace(string(content), "\r", "", -1)
2224
}
2325

26+
func runWithDindSwarmAndRegistry(t *testing.T, todo func(dindSwarmAndRegistryInfo)) {
27+
cmd, cleanup := dockerCli.createTestCmd()
28+
defer cleanup()
29+
30+
registryPort := findAvailablePort()
31+
tmpDir := fs.NewDir(t, t.Name())
32+
defer tmpDir.Remove()
33+
34+
cmd.Env = append(cmd.Env, "DOCKER_TARGET_CONTEXT=swarm-target-context")
35+
36+
// The dind doesn't have the cnab-app-base image so we save it in order to load it later
37+
saveCmd := icmd.Cmd{Command: dockerCli.Command("save", fmt.Sprintf("docker/cnab-app-base:%s", internal.Version), "-o", tmpDir.Join("cnab-app-base.tar.gz"))}
38+
icmd.RunCmd(saveCmd).Assert(t, icmd.Success)
39+
40+
// we have a difficult constraint here:
41+
// - the registry must be reachable from the client side (for cnab-to-oci, which does not use the docker daemon to access the registry)
42+
// - the registry must be reachable from the dind daemon on the same address/port
43+
// Solution found is: fix the port of the registry to be the same internally and externally
44+
// and run the dind container in the same network namespace: this way 127.0.0.1:<registry-port> both resolves to the registry from the client and from dind
45+
46+
swarm := NewContainer("docker:18.09-dind", 2375, "--insecure-registry", fmt.Sprintf("127.0.0.1:%d", registryPort))
47+
swarm.Start(t, "--expose", strconv.FormatInt(int64(registryPort), 10),
48+
"-p", fmt.Sprintf("%d:%d", registryPort, registryPort),
49+
"-p", "2375")
50+
defer swarm.Stop(t)
51+
52+
registry := NewContainer("registry:2", registryPort)
53+
registry.StartWithContainerNetwork(t, swarm, "-e", "REGISTRY_VALIDATION_MANIFESTS_URLS_ALLOW=[^http]",
54+
"-e", fmt.Sprintf("REGISTRY_HTTP_ADDR=0.0.0.0:%d", registryPort))
55+
defer registry.StopNoFail()
56+
57+
// We need two contexts:
58+
// - one for `docker` so that it connects to the dind swarm created before
59+
// - the target context for the invocation image to install within the swarm
60+
cmd.Command = dockerCli.Command("context", "create", "swarm-context", "--docker", fmt.Sprintf(`"host=tcp://%s"`, swarm.GetAddress(t)), "--default-stack-orchestrator", "swarm")
61+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
62+
63+
// When creating a context on a Windows host we cannot use
64+
// the unix socket but it's needed inside the invocation image.
65+
// The workaround is to create a context with an empty host.
66+
// This host will default to the unix socket inside the
67+
// invocation image
68+
cmd.Command = dockerCli.Command("context", "create", "swarm-target-context", "--docker", "host=", "--default-stack-orchestrator", "swarm")
69+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
70+
71+
// Initialize the swarm
72+
cmd.Env = append(cmd.Env, "DOCKER_CONTEXT=swarm-context")
73+
cmd.Command = dockerCli.Command("swarm", "init")
74+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
75+
// Load the needed base cnab image into the swarm docker engine
76+
cmd.Command = dockerCli.Command("load", "-i", tmpDir.Join("cnab-app-base.tar.gz"))
77+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
78+
79+
info := dindSwarmAndRegistryInfo{
80+
configuredCmd: cmd,
81+
registryAddress: registry.GetAddress(t),
82+
swarmAddress: swarm.GetAddress(t),
83+
stopRegistry: registry.StopNoFail,
84+
registryLogs: registry.Logs(t),
85+
}
86+
todo(info)
87+
}
88+
2489
// Container represents a docker container
2590
type Container struct {
2691
image string

e2e/images_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package e2e
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
"regexp"
7+
"testing"
8+
9+
"gotest.tools/assert"
10+
"gotest.tools/fs"
11+
"gotest.tools/icmd"
12+
)
13+
14+
var (
15+
reg = regexp.MustCompile("Digest is (.*).")
16+
expected = `REPOSITORY TAG APP NAME
17+
%s push-pull
18+
a-simple-app latest simple
19+
b-simple-app latest simple
20+
`
21+
)
22+
23+
func TestImageList(t *testing.T) {
24+
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
25+
cmd := info.configuredCmd
26+
dir := fs.NewDir(t, "")
27+
defer dir.Remove()
28+
29+
// Push an application so that we can later pull it by digest
30+
cmd.Command = dockerCli.Command("app", "push", "--tag", info.registryAddress+"/c-myapp", "--insecure-registries="+info.registryAddress, filepath.Join("testdata", "push-pull", "push-pull.dockerapp"))
31+
r := icmd.RunCmd(cmd).Assert(t, icmd.Success)
32+
33+
// Get the digest from the output of the pull command
34+
out := r.Stdout()
35+
matches := reg.FindAllStringSubmatch(out, 1)
36+
digest := matches[0][1]
37+
38+
// Pull the app by digest
39+
cmd.Command = dockerCli.Command("app", "pull", "--insecure-registries="+info.registryAddress, info.registryAddress+"/c-myapp@"+digest)
40+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
41+
42+
cmd.Command = dockerCli.Command("app", "bundle", filepath.Join("testdata", "simple", "simple.dockerapp"), "--tag", "b-simple-app", "--output", dir.Join("simple-bundle.json"))
43+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
44+
cmd.Command = dockerCli.Command("app", "bundle", filepath.Join("testdata", "simple", "simple.dockerapp"), "--tag", "a-simple-app", "--output", dir.Join("simple-bundle.json"))
45+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
46+
47+
expectedOutput := fmt.Sprintf(expected, info.registryAddress+"/c-myapp")
48+
cmd.Command = dockerCli.Command("app", "image", "ls")
49+
result := icmd.RunCmd(cmd).Assert(t, icmd.Success)
50+
assert.Equal(t, result.Stdout(), expectedOutput)
51+
})
52+
}

e2e/pushpull_test.go

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,10 @@ import (
88
"net"
99
"net/http"
1010
"path/filepath"
11-
"strconv"
1211
"strings"
1312
"testing"
1413
"time"
1514

16-
"github.com/docker/app/internal"
1715
"github.com/docker/cnab-to-oci/converter"
1816
"github.com/docker/distribution/manifest/manifestlist"
1917
"github.com/opencontainers/go-digest"
@@ -32,70 +30,6 @@ type dindSwarmAndRegistryInfo struct {
3230
registryLogs func() string
3331
}
3432

35-
func runWithDindSwarmAndRegistry(t *testing.T, todo func(dindSwarmAndRegistryInfo)) {
36-
cmd, cleanup := dockerCli.createTestCmd()
37-
defer cleanup()
38-
39-
registryPort := findAvailablePort()
40-
tmpDir := fs.NewDir(t, t.Name())
41-
defer tmpDir.Remove()
42-
43-
cmd.Env = append(cmd.Env, "DOCKER_TARGET_CONTEXT=swarm-target-context")
44-
45-
// The dind doesn't have the cnab-app-base image so we save it in order to load it later
46-
saveCmd := icmd.Cmd{Command: dockerCli.Command("save", fmt.Sprintf("docker/cnab-app-base:%s", internal.Version), "-o", tmpDir.Join("cnab-app-base.tar.gz"))}
47-
icmd.RunCmd(saveCmd).Assert(t, icmd.Success)
48-
49-
// we have a difficult constraint here:
50-
// - the registry must be reachable from the client side (for cnab-to-oci, which does not use the docker daemon to access the registry)
51-
// - the registry must be reachable from the dind daemon on the same address/port
52-
// Solution found is: fix the port of the registry to be the same internally and externally
53-
// and run the dind container in the same network namespace: this way 127.0.0.1:<registry-port> both resolves to the registry from the client and from dind
54-
55-
swarm := NewContainer("docker:18.09-dind", 2375, "--insecure-registry", fmt.Sprintf("127.0.0.1:%d", registryPort))
56-
swarm.Start(t, "--expose", strconv.FormatInt(int64(registryPort), 10),
57-
"-p", fmt.Sprintf("%d:%d", registryPort, registryPort),
58-
"-p", "2375")
59-
defer swarm.Stop(t)
60-
61-
registry := NewContainer("registry:2", registryPort)
62-
registry.StartWithContainerNetwork(t, swarm, "-e", "REGISTRY_VALIDATION_MANIFESTS_URLS_ALLOW=[^http]",
63-
"-e", fmt.Sprintf("REGISTRY_HTTP_ADDR=0.0.0.0:%d", registryPort))
64-
defer registry.StopNoFail()
65-
66-
// We need two contexts:
67-
// - one for `docker` so that it connects to the dind swarm created before
68-
// - the target context for the invocation image to install within the swarm
69-
cmd.Command = dockerCli.Command("context", "create", "swarm-context", "--docker", fmt.Sprintf(`"host=tcp://%s"`, swarm.GetAddress(t)), "--default-stack-orchestrator", "swarm")
70-
icmd.RunCmd(cmd).Assert(t, icmd.Success)
71-
72-
// When creating a context on a Windows host we cannot use
73-
// the unix socket but it's needed inside the invocation image.
74-
// The workaround is to create a context with an empty host.
75-
// This host will default to the unix socket inside the
76-
// invocation image
77-
cmd.Command = dockerCli.Command("context", "create", "swarm-target-context", "--docker", "host=", "--default-stack-orchestrator", "swarm")
78-
icmd.RunCmd(cmd).Assert(t, icmd.Success)
79-
80-
// Initialize the swarm
81-
cmd.Env = append(cmd.Env, "DOCKER_CONTEXT=swarm-context")
82-
cmd.Command = dockerCli.Command("swarm", "init")
83-
icmd.RunCmd(cmd).Assert(t, icmd.Success)
84-
// Load the needed base cnab image into the swarm docker engine
85-
cmd.Command = dockerCli.Command("load", "-i", tmpDir.Join("cnab-app-base.tar.gz"))
86-
icmd.RunCmd(cmd).Assert(t, icmd.Success)
87-
88-
info := dindSwarmAndRegistryInfo{
89-
configuredCmd: cmd,
90-
registryAddress: registry.GetAddress(t),
91-
swarmAddress: swarm.GetAddress(t),
92-
stopRegistry: registry.StopNoFail,
93-
registryLogs: registry.Logs(t),
94-
}
95-
todo(info)
96-
97-
}
98-
9933
func TestPushArchs(t *testing.T) {
10034
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
10135
testCases := []struct {

e2e/testdata/plugin-usage-experimental.golden

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ A tool to build and manage Docker Applications.
66
Options:
77
--version Print version information
88

9+
Management Commands:
10+
image Manage application images
11+
912
Commands:
1013
bundle Create a CNAB invocation image and `bundle.json` for the application
1114
init Initialize Docker Application definition

e2e/testdata/plugin-usage.golden

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ A tool to build and manage Docker Applications.
66
Options:
77
--version Print version information
88

9+
Management Commands:
10+
image Manage application images
11+
912
Commands:
1013
bundle Create a CNAB invocation image and `bundle.json` for the application
1114
init Initialize Docker Application definition

internal/commands/image/command.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package image
2+
3+
import (
4+
"github.com/docker/cli/cli/command"
5+
"github.com/spf13/cobra"
6+
)
7+
8+
// Cmd is the image top level command
9+
func Cmd(dockerCli command.Cli) *cobra.Command {
10+
cmd := &cobra.Command{
11+
Short: "Manage application images",
12+
Use: "image",
13+
}
14+
15+
cmd.AddCommand(listCmd(dockerCli))
16+
17+
return cmd
18+
}

internal/commands/image/list.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package image
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"strings"
7+
"text/tabwriter"
8+
9+
"github.com/deislabs/cnab-go/bundle"
10+
"github.com/docker/app/internal/store"
11+
"github.com/docker/cli/cli/command"
12+
"github.com/docker/cli/cli/config"
13+
"github.com/docker/distribution/reference"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
func listCmd(dockerCli command.Cli) *cobra.Command {
18+
cmd := &cobra.Command{
19+
Short: "List application images",
20+
Use: "ls",
21+
Aliases: []string{"list"},
22+
RunE: func(cmd *cobra.Command, args []string) error {
23+
appstore, err := store.NewApplicationStore(config.Dir())
24+
if err != nil {
25+
return err
26+
}
27+
28+
bundleStore, err := appstore.BundleStore()
29+
if err != nil {
30+
return err
31+
}
32+
33+
return runList(dockerCli, bundleStore)
34+
},
35+
}
36+
37+
return cmd
38+
}
39+
40+
func runList(dockerCli command.Cli, bundleStore store.BundleStore) error {
41+
bundles, err := bundleStore.List()
42+
if err != nil {
43+
return err
44+
}
45+
46+
pkgs, err := getPackages(bundleStore, bundles)
47+
if err != nil {
48+
return err
49+
}
50+
51+
return printImages(dockerCli, pkgs)
52+
}
53+
54+
func getPackages(bundleStore store.BundleStore, references []reference.Named) ([]pkg, error) {
55+
packages := make([]pkg, len(references))
56+
for i, ref := range references {
57+
b, err := bundleStore.Read(ref)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
pk := pkg{
63+
bundle: b,
64+
ref: ref,
65+
}
66+
67+
if r, ok := ref.(reference.NamedTagged); ok {
68+
pk.taggedRef = r
69+
}
70+
71+
packages[i] = pk
72+
}
73+
74+
return packages, nil
75+
}
76+
77+
func printImages(dockerCli command.Cli, refs []pkg) error {
78+
w := tabwriter.NewWriter(dockerCli.Out(), 0, 0, 1, ' ', 0)
79+
80+
printHeaders(w)
81+
for _, ref := range refs {
82+
printValues(w, ref)
83+
}
84+
85+
return w.Flush()
86+
}
87+
88+
func printHeaders(w io.Writer) {
89+
var headers []string
90+
for _, column := range listColumns {
91+
headers = append(headers, column.header)
92+
}
93+
fmt.Fprintln(w, strings.Join(headers, "\t"))
94+
}
95+
96+
func printValues(w io.Writer, ref pkg) {
97+
var values []string
98+
for _, column := range listColumns {
99+
values = append(values, column.value(ref))
100+
}
101+
fmt.Fprintln(w, strings.Join(values, "\t"))
102+
}
103+
104+
var (
105+
listColumns = []struct {
106+
header string
107+
value func(p pkg) string
108+
}{
109+
{"REPOSITORY", func(p pkg) string {
110+
return reference.FamiliarName(p.ref)
111+
}},
112+
{"TAG", func(p pkg) string {
113+
if p.taggedRef != nil {
114+
return p.taggedRef.Tag()
115+
}
116+
return ""
117+
}},
118+
{"APP NAME", func(p pkg) string {
119+
return p.bundle.Name
120+
}},
121+
}
122+
)
123+
124+
type pkg struct {
125+
ref reference.Named
126+
taggedRef reference.NamedTagged
127+
bundle *bundle.Bundle
128+
}

0 commit comments

Comments
 (0)