Skip to content
This repository has been archived by the owner on Jun 13, 2021. It is now read-only.

Commit

Permalink
New command to list application images
Browse files Browse the repository at this point in the history
For now we show the reference, name and version.

Signed-off-by: Djordje Lukic <djordje.lukic@docker.com>
  • Loading branch information
rumpl committed Sep 27, 2019
1 parent 695509b commit aa2de75
Show file tree
Hide file tree
Showing 10 changed files with 434 additions and 66 deletions.
65 changes: 65 additions & 0 deletions e2e/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"testing"
"time"

"github.com/docker/app/internal"
"gotest.tools/assert"
"gotest.tools/fs"
"gotest.tools/icmd"
)

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

func runWithDindSwarmAndRegistry(t *testing.T, todo func(dindSwarmAndRegistryInfo)) {
cmd, cleanup := dockerCli.createTestCmd()
defer cleanup()

registryPort := findAvailablePort()
tmpDir := fs.NewDir(t, t.Name())
defer tmpDir.Remove()

cmd.Env = append(cmd.Env, "DOCKER_TARGET_CONTEXT=swarm-target-context")

// The dind doesn't have the cnab-app-base image so we save it in order to load it later
saveCmd := icmd.Cmd{Command: dockerCli.Command("save", fmt.Sprintf("docker/cnab-app-base:%s", internal.Version), "-o", tmpDir.Join("cnab-app-base.tar.gz"))}
icmd.RunCmd(saveCmd).Assert(t, icmd.Success)

// we have a difficult constraint here:
// - the registry must be reachable from the client side (for cnab-to-oci, which does not use the docker daemon to access the registry)
// - the registry must be reachable from the dind daemon on the same address/port
// Solution found is: fix the port of the registry to be the same internally and externally
// 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

swarm := NewContainer("docker:18.09-dind", 2375, "--insecure-registry", fmt.Sprintf("127.0.0.1:%d", registryPort))
swarm.Start(t, "--expose", strconv.FormatInt(int64(registryPort), 10),
"-p", fmt.Sprintf("%d:%d", registryPort, registryPort),
"-p", "2375")
defer swarm.Stop(t)

registry := NewContainer("registry:2", registryPort)
registry.StartWithContainerNetwork(t, swarm, "-e", "REGISTRY_VALIDATION_MANIFESTS_URLS_ALLOW=[^http]",
"-e", fmt.Sprintf("REGISTRY_HTTP_ADDR=0.0.0.0:%d", registryPort))
defer registry.StopNoFail()

// We need two contexts:
// - one for `docker` so that it connects to the dind swarm created before
// - the target context for the invocation image to install within the swarm
cmd.Command = dockerCli.Command("context", "create", "swarm-context", "--docker", fmt.Sprintf(`"host=tcp://%s"`, swarm.GetAddress(t)), "--default-stack-orchestrator", "swarm")
icmd.RunCmd(cmd).Assert(t, icmd.Success)

// When creating a context on a Windows host we cannot use
// the unix socket but it's needed inside the invocation image.
// The workaround is to create a context with an empty host.
// This host will default to the unix socket inside the
// invocation image
cmd.Command = dockerCli.Command("context", "create", "swarm-target-context", "--docker", "host=", "--default-stack-orchestrator", "swarm")
icmd.RunCmd(cmd).Assert(t, icmd.Success)

// Initialize the swarm
cmd.Env = append(cmd.Env, "DOCKER_CONTEXT=swarm-context")
cmd.Command = dockerCli.Command("swarm", "init")
icmd.RunCmd(cmd).Assert(t, icmd.Success)
// Load the needed base cnab image into the swarm docker engine
cmd.Command = dockerCli.Command("load", "-i", tmpDir.Join("cnab-app-base.tar.gz"))
icmd.RunCmd(cmd).Assert(t, icmd.Success)

info := dindSwarmAndRegistryInfo{
configuredCmd: cmd,
registryAddress: registry.GetAddress(t),
swarmAddress: swarm.GetAddress(t),
stopRegistry: registry.StopNoFail,
registryLogs: registry.Logs(t),
}
todo(info)
}

// Container represents a docker container
type Container struct {
image string
Expand Down
52 changes: 52 additions & 0 deletions e2e/images_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package e2e

import (
"fmt"
"path/filepath"
"regexp"
"testing"

"gotest.tools/assert"
"gotest.tools/fs"
"gotest.tools/icmd"
)

var (
reg = regexp.MustCompile("Digest is (.*).")
expected = `REPOSITORY TAG APP NAME
%s push-pull
a-simple-app latest simple
b-simple-app latest simple
`
)

func TestImageList(t *testing.T) {
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
cmd := info.configuredCmd
dir := fs.NewDir(t, "")
defer dir.Remove()

// Push an application so that we can later pull it by digest
cmd.Command = dockerCli.Command("app", "push", "--tag", info.registryAddress+"/c-myapp", "--insecure-registries="+info.registryAddress, filepath.Join("testdata", "push-pull", "push-pull.dockerapp"))
r := icmd.RunCmd(cmd).Assert(t, icmd.Success)

// Get the digest from the output of the pull command
out := r.Stdout()
matches := reg.FindAllStringSubmatch(out, 1)
digest := matches[0][1]

// Pull the app by digest
cmd.Command = dockerCli.Command("app", "pull", "--insecure-registries="+info.registryAddress, info.registryAddress+"/c-myapp@"+digest)
icmd.RunCmd(cmd).Assert(t, icmd.Success)

cmd.Command = dockerCli.Command("app", "bundle", filepath.Join("testdata", "simple", "simple.dockerapp"), "--tag", "b-simple-app", "--output", dir.Join("simple-bundle.json"))
icmd.RunCmd(cmd).Assert(t, icmd.Success)
cmd.Command = dockerCli.Command("app", "bundle", filepath.Join("testdata", "simple", "simple.dockerapp"), "--tag", "a-simple-app", "--output", dir.Join("simple-bundle.json"))
icmd.RunCmd(cmd).Assert(t, icmd.Success)

expectedOutput := fmt.Sprintf(expected, info.registryAddress+"/c-myapp")
cmd.Command = dockerCli.Command("app", "image", "ls")
result := icmd.RunCmd(cmd).Assert(t, icmd.Success)
assert.Equal(t, result.Stdout(), expectedOutput)
})
}
66 changes: 0 additions & 66 deletions e2e/pushpull_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ import (
"net"
"net/http"
"path/filepath"
"strconv"
"strings"
"testing"
"time"

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

func runWithDindSwarmAndRegistry(t *testing.T, todo func(dindSwarmAndRegistryInfo)) {
cmd, cleanup := dockerCli.createTestCmd()
defer cleanup()

registryPort := findAvailablePort()
tmpDir := fs.NewDir(t, t.Name())
defer tmpDir.Remove()

cmd.Env = append(cmd.Env, "DOCKER_TARGET_CONTEXT=swarm-target-context")

// The dind doesn't have the cnab-app-base image so we save it in order to load it later
saveCmd := icmd.Cmd{Command: dockerCli.Command("save", fmt.Sprintf("docker/cnab-app-base:%s", internal.Version), "-o", tmpDir.Join("cnab-app-base.tar.gz"))}
icmd.RunCmd(saveCmd).Assert(t, icmd.Success)

// we have a difficult constraint here:
// - the registry must be reachable from the client side (for cnab-to-oci, which does not use the docker daemon to access the registry)
// - the registry must be reachable from the dind daemon on the same address/port
// Solution found is: fix the port of the registry to be the same internally and externally
// 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

swarm := NewContainer("docker:18.09-dind", 2375, "--insecure-registry", fmt.Sprintf("127.0.0.1:%d", registryPort))
swarm.Start(t, "--expose", strconv.FormatInt(int64(registryPort), 10),
"-p", fmt.Sprintf("%d:%d", registryPort, registryPort),
"-p", "2375")
defer swarm.Stop(t)

registry := NewContainer("registry:2", registryPort)
registry.StartWithContainerNetwork(t, swarm, "-e", "REGISTRY_VALIDATION_MANIFESTS_URLS_ALLOW=[^http]",
"-e", fmt.Sprintf("REGISTRY_HTTP_ADDR=0.0.0.0:%d", registryPort))
defer registry.StopNoFail()

// We need two contexts:
// - one for `docker` so that it connects to the dind swarm created before
// - the target context for the invocation image to install within the swarm
cmd.Command = dockerCli.Command("context", "create", "swarm-context", "--docker", fmt.Sprintf(`"host=tcp://%s"`, swarm.GetAddress(t)), "--default-stack-orchestrator", "swarm")
icmd.RunCmd(cmd).Assert(t, icmd.Success)

// When creating a context on a Windows host we cannot use
// the unix socket but it's needed inside the invocation image.
// The workaround is to create a context with an empty host.
// This host will default to the unix socket inside the
// invocation image
cmd.Command = dockerCli.Command("context", "create", "swarm-target-context", "--docker", "host=", "--default-stack-orchestrator", "swarm")
icmd.RunCmd(cmd).Assert(t, icmd.Success)

// Initialize the swarm
cmd.Env = append(cmd.Env, "DOCKER_CONTEXT=swarm-context")
cmd.Command = dockerCli.Command("swarm", "init")
icmd.RunCmd(cmd).Assert(t, icmd.Success)
// Load the needed base cnab image into the swarm docker engine
cmd.Command = dockerCli.Command("load", "-i", tmpDir.Join("cnab-app-base.tar.gz"))
icmd.RunCmd(cmd).Assert(t, icmd.Success)

info := dindSwarmAndRegistryInfo{
configuredCmd: cmd,
registryAddress: registry.GetAddress(t),
swarmAddress: swarm.GetAddress(t),
stopRegistry: registry.StopNoFail,
registryLogs: registry.Logs(t),
}
todo(info)

}

func TestPushArchs(t *testing.T) {
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
testCases := []struct {
Expand Down
3 changes: 3 additions & 0 deletions e2e/testdata/plugin-usage-experimental.golden
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ A tool to build and manage Docker Applications.
Options:
--version Print version information

Management Commands:
image Manage application images

Commands:
bundle Create a CNAB invocation image and `bundle.json` for the application
init Initialize Docker Application definition
Expand Down
3 changes: 3 additions & 0 deletions e2e/testdata/plugin-usage.golden
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ A tool to build and manage Docker Applications.
Options:
--version Print version information

Management Commands:
image Manage application images

Commands:
bundle Create a CNAB invocation image and `bundle.json` for the application
init Initialize Docker Application definition
Expand Down
18 changes: 18 additions & 0 deletions internal/commands/image/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package image

import (
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)

// Cmd is the image top level command
func Cmd(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Short: "Manage application images",
Use: "image",
}

cmd.AddCommand(listCmd(dockerCli))

return cmd
}
128 changes: 128 additions & 0 deletions internal/commands/image/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package image

import (
"fmt"
"io"
"strings"
"text/tabwriter"

"github.com/deislabs/cnab-go/bundle"
"github.com/docker/app/internal/store"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"github.com/docker/distribution/reference"
"github.com/spf13/cobra"
)

func listCmd(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Short: "List application images",
Use: "ls",
Aliases: []string{"list"},
RunE: func(cmd *cobra.Command, args []string) error {
appstore, err := store.NewApplicationStore(config.Dir())
if err != nil {
return err
}

bundleStore, err := appstore.BundleStore()
if err != nil {
return err
}

return runList(dockerCli, bundleStore)
},
}

return cmd
}

func runList(dockerCli command.Cli, bundleStore store.BundleStore) error {
bundles, err := bundleStore.List()
if err != nil {
return err
}

pkgs, err := getPackages(bundleStore, bundles)
if err != nil {
return err
}

return printImages(dockerCli, pkgs)
}

func getPackages(bundleStore store.BundleStore, references []reference.Named) ([]pkg, error) {
packages := make([]pkg, len(references))
for i, ref := range references {
b, err := bundleStore.Read(ref)
if err != nil {
return nil, err
}

pk := pkg{
bundle: b,
ref: ref,
}

if r, ok := ref.(reference.NamedTagged); ok {
pk.taggedRef = r
}

packages[i] = pk
}

return packages, nil
}

func printImages(dockerCli command.Cli, refs []pkg) error {
w := tabwriter.NewWriter(dockerCli.Out(), 0, 0, 1, ' ', 0)

printHeaders(w)
for _, ref := range refs {
printValues(w, ref)
}

return w.Flush()
}

func printHeaders(w io.Writer) {
var headers []string
for _, column := range listColumns {
headers = append(headers, column.header)
}
fmt.Fprintln(w, strings.Join(headers, "\t"))
}

func printValues(w io.Writer, ref pkg) {
var values []string
for _, column := range listColumns {
values = append(values, column.value(ref))
}
fmt.Fprintln(w, strings.Join(values, "\t"))
}

var (
listColumns = []struct {
header string
value func(p pkg) string
}{
{"REPOSITORY", func(p pkg) string {
return reference.FamiliarName(p.ref)
}},
{"TAG", func(p pkg) string {
if p.taggedRef != nil {
return p.taggedRef.Tag()
}
return ""
}},
{"APP NAME", func(p pkg) string {
return p.bundle.Name
}},
}
)

type pkg struct {
ref reference.Named
taggedRef reference.NamedTagged
bundle *bundle.Bundle
}

0 comments on commit aa2de75

Please sign in to comment.