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 26, 2019
1 parent 856c60a commit e3085b2
Show file tree
Hide file tree
Showing 8 changed files with 373 additions and 0 deletions.
53 changes: 53 additions & 0 deletions e2e/images_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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, cleanup := dockerCli.createTestCmd()
defer cleanup()
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)
})
}
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
}
126 changes: 126 additions & 0 deletions internal/commands/image/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
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
}
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"
"github.com/docker/cli/cli/config"
Expand Down Expand Up @@ -79,6 +80,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
}

0 comments on commit e3085b2

Please sign in to comment.