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

New command to list application images #639

Merged
merged 1 commit into from
Sep 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As runWithDindSwarmAndRegistry is defined in e2e/pushpull_test.go maybe we can move the method in e2e/helper_test.go. This way this test only depends on the helper and not on an other test file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

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
}