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 25, 2019
1 parent 3baa67c commit bf21fbb
Show file tree
Hide file tree
Showing 9 changed files with 336 additions and 0 deletions.
26 changes: 26 additions & 0 deletions e2e/images_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package e2e

import (
"path/filepath"
"testing"

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

func TestImageList(t *testing.T) {
cmd, cleanup := dockerCli.createTestCmd()
defer cleanup()
dir := fs.NewDir(t, "")
defer dir.Remove()

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)

cmd.Command = dockerCli.Command("app", "image", "ls")
result := icmd.RunCmd(cmd).Assert(t, icmd.Success)
golden.Assert(t, result.Stdout(), "expected-image-ls.golden")
}
3 changes: 3 additions & 0 deletions e2e/testdata/expected-image-ls.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
REFERENCE NAME VERSION
a-simple-app:latest simple 1.1.0-beta1
b-simple-app:latest simple 1.1.0-beta1
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
}
113 changes: 113 additions & 0 deletions internal/commands/image/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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 {
return runList(dockerCli)
},
}

return cmd
}

func runList(dockerCli command.Cli) error {
appstore, err := store.NewApplicationStore(config.Dir())
if err != nil {
return err
}

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

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
}

packages[i] = pkg{
ref: ref,
bundle: b,
}
}

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
}{
{"REFERENCE", func(p pkg) string {
return reference.FamiliarString(p.ref)
}},
{"NAME", func(p pkg) string { return p.bundle.Name }},
{"VERSION", func(p pkg) string { return p.bundle.Version }},
}
)

type pkg struct {
ref reference.Named
bundle *bundle.Bundle
}
2 changes: 2 additions & 0 deletions internal/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"

"github.com/docker/app/internal"
"github.com/docker/app/internal/commands/image"

"github.com/docker/app/internal/store"
"github.com/docker/cli/cli/command"
Expand Down Expand Up @@ -71,6 +72,7 @@ func addCommands(cmd *cobra.Command, dockerCli command.Cli) {
bundleCmd(dockerCli),
pushCmd(dockerCli),
pullCmd(dockerCli),
image.Cmd(dockerCli),
)
}

Expand Down
86 changes: 86 additions & 0 deletions internal/store/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package store
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"

"github.com/docker/app/internal/log"
Expand All @@ -21,6 +23,7 @@ import (
type BundleStore interface {
Store(ref reference.Named, bndle *bundle.Bundle) error
Read(ref reference.Named) (*bundle.Bundle, error)
List() ([]reference.Named, error)

LookupOrPullBundle(ref reference.Named, pullRef bool, config *configfile.ConfigFile, insecureRegistries []string) (*bundle.Bundle, error)
}
Expand Down Expand Up @@ -59,6 +62,41 @@ func (b *bundleStore) Read(ref reference.Named) (*bundle.Bundle, error) {
return &bndle, nil
}

// Returns the list of all bundles present in the bundle store
func (b *bundleStore) List() ([]reference.Named, error) {
var references []reference.Named
if err := filepath.Walk(b.path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

if info.IsDir() {
return nil
}

if !strings.HasSuffix(info.Name(), ".json") {
return fmt.Errorf("unknown file %q in bundle store", path)
}

ref, err := b.pathToReference(path)
if err != nil {
return err
}

references = append(references, ref)

return nil
}); err != nil {
return nil, err
}

sort.Slice(references, func(i, j int) bool {
return references[i].Name() < references[j].Name()
})

return references, nil
}

// LookupOrPullBundle will fetch the given bundle from the local
// bundle store, or if it is missing from the registry, and returns
// it. Always pulls if pullRef is true. If it pulls then the local
Expand Down Expand Up @@ -110,3 +148,51 @@ func (b *bundleStore) storePath(ref reference.Named) (string, error) {

return storeDir + ".json", nil
}

func (b *bundleStore) pathToReference(path string) (reference.Named, error) {
// Clean the path and remove the local bundle store path
cleanpath := filepath.ToSlash(path)
cleanpath = strings.TrimPrefix(cleanpath, filepath.ToSlash(b.path)+"/")

// get the hierarchy of directories, so we can get digest algorithm or tag
paths := strings.Split(cleanpath, "/")
if len(paths) < 3 {
return nil, fmt.Errorf("invalid path %q in the bundle store", path)
}

// path must point to a json file
if !strings.Contains(paths[len(paths)-1], ".json") {
return nil, fmt.Errorf("invalid path %q, not referencing a CNAB bundle in json format", path)
}

// remove the json suffix from the filename
paths[len(paths)-1] = strings.TrimSuffix(paths[len(paths)-1], ".json")

name, err := reconstructNamedReference(path, paths)
if err != nil {
return nil, err
}

return reference.ParseNamed(name)
}

func reconstructNamedReference(path string, paths []string) (string, error) {
name, paths := strings.Replace(paths[0], "_", ":", 1), paths[1:]
for i, p := range paths {
switch p {
case "_tags":
if i != len(paths)-2 {
return "", fmt.Errorf("invalid path %q in the bundle store", path)
}
return fmt.Sprintf("%s:%s", name, paths[i+1]), nil
case "_digests":
if i != len(paths)-3 {
return "", fmt.Errorf("invalid path %q in the bundle store", path)
}
return fmt.Sprintf("%s@%s:%s", name, paths[i+1], paths[i+2]), nil
default:
name += "/" + p
}
}
return name, nil
}
82 changes: 82 additions & 0 deletions internal/store/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,85 @@ func TestStorePath(t *testing.T) {
})
}
}

func TestPathToReference(t *testing.T) {
bundleStore := &bundleStore{path: "base-dir"}

for _, tc := range []struct {
Name string
Path string
ExpectedError string
ExpectedName string
}{
{
Name: "error on invalid path",
Path: "invalid",
ExpectedError: `invalid path "invalid" in the bundle store`,
}, {
Name: "error if file is not json",
Path: "registry/repo/name/_tags/file.xml",
ExpectedError: `invalid path "registry/repo/name/_tags/file.xml", not referencing a CNAB bundle in json format`,
}, {
Name: "return a reference from tagged",
Path: "docker.io/library/foo/_tags/latest.json",
ExpectedName: "docker.io/library/foo",
}, {
Name: "return a reference from digested",
Path: "docker.io/library/foo/_digests/sha256/" + testSha + ".json",
ExpectedName: "docker.io/library/foo",
},
} {
t.Run(tc.Name, func(t *testing.T) {
ref, err := bundleStore.pathToReference(tc.Path)

if tc.ExpectedError != "" {
assert.Equal(t, err.Error(), tc.ExpectedError)
} else {
assert.NilError(t, err)
}

if tc.ExpectedName != "" {
assert.Equal(t, ref.Name(), tc.ExpectedName)
}
})
}
}

func TestList(t *testing.T) {
dockerConfigDir := fs.NewDir(t, t.Name(), fs.WithMode(0755))
defer dockerConfigDir.Remove()
appstore, err := NewApplicationStore(dockerConfigDir.Path())
assert.NilError(t, err)
bundleStore, err := appstore.BundleStore()
assert.NilError(t, err)

refs := []reference.Named{
parseRefOrDie(t, "my-repo/my-bundle:my-tag"),
parseRefOrDie(t, "my-repo/my-bundle@sha256:"+testSha),
}

t.Run("returns 0 bundles on empty store", func(t *testing.T) {
bundles, err := bundleStore.List()
assert.NilError(t, err)
assert.Equal(t, len(bundles), 0)
})

bndl := &bundle.Bundle{Name: "bundle-name"}
for _, ref := range refs {
err = bundleStore.Store(ref, bndl)
assert.NilError(t, err)
}

t.Run("Returns all bundles", func(t *testing.T) {
bundles, err := bundleStore.List()
assert.NilError(t, err)
assert.Equal(t, len(bundles), 2)
})

t.Run("Returns the bundles sorted by name", func(t *testing.T) {
bundles, err := bundleStore.List()
assert.NilError(t, err)
assert.Equal(t, bundles[0].Name(), "docker.io/my-repo/my-bundle")
assert.Equal(t, bundles[1].Name(), "docker.io/my-repo/my-bundle")
})
}

0 comments on commit bf21fbb

Please sign in to comment.