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

Commit 5b857bc

Browse files
authored
Merge pull request #648 from rumpl/feat-image-rm
Add command to remove application images
2 parents 20b28fa + 3385987 commit 5b857bc

File tree

5 files changed

+163
-14
lines changed

5 files changed

+163
-14
lines changed

e2e/images_test.go

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,58 @@ b-simple-app latest simple
2020
`
2121
)
2222

23+
func insertBundles(t *testing.T, cmd icmd.Cmd, dir *fs.Dir, info dindSwarmAndRegistryInfo) string {
24+
// Push an application so that we can later pull it by digest
25+
cmd.Command = dockerCli.Command("app", "push", "--tag", info.registryAddress+"/c-myapp", filepath.Join("testdata", "push-pull", "push-pull.dockerapp"))
26+
r := icmd.RunCmd(cmd).Assert(t, icmd.Success)
27+
28+
// Get the digest from the output of the pull command
29+
out := r.Stdout()
30+
matches := reg.FindAllStringSubmatch(out, 1)
31+
digest := matches[0][1]
32+
33+
// Pull the app by digest
34+
cmd.Command = dockerCli.Command("app", "pull", info.registryAddress+"/c-myapp@"+digest)
35+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
36+
37+
cmd.Command = dockerCli.Command("app", "bundle", filepath.Join("testdata", "simple", "simple.dockerapp"), "--tag", "b-simple-app", "--output", dir.Join("simple-bundle.json"))
38+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
39+
cmd.Command = dockerCli.Command("app", "bundle", filepath.Join("testdata", "simple", "simple.dockerapp"), "--tag", "a-simple-app", "--output", dir.Join("simple-bundle.json"))
40+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
41+
42+
return digest
43+
}
44+
2345
func TestImageList(t *testing.T) {
2446
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
2547
cmd := info.configuredCmd
2648
dir := fs.NewDir(t, "")
2749
defer dir.Remove()
2850

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", filepath.Join("testdata", "push-pull", "push-pull.dockerapp"))
31-
r := icmd.RunCmd(cmd).Assert(t, icmd.Success)
51+
insertBundles(t, cmd, dir, info)
52+
53+
expectedOutput := fmt.Sprintf(expected, info.registryAddress+"/c-myapp")
54+
cmd.Command = dockerCli.Command("app", "image", "ls")
55+
result := icmd.RunCmd(cmd).Assert(t, icmd.Success)
56+
assert.Equal(t, result.Stdout(), expectedOutput)
57+
})
58+
}
59+
60+
func TestImageRm(t *testing.T) {
61+
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
62+
cmd := info.configuredCmd
63+
dir := fs.NewDir(t, "")
64+
defer dir.Remove()
3265

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]
66+
digest := insertBundles(t, cmd, dir, info)
3767

38-
// Pull the app by digest
39-
cmd.Command = dockerCli.Command("app", "pull", info.registryAddress+"/c-myapp@"+digest)
68+
cmd.Command = dockerCli.Command("app", "image", "rm", info.registryAddress+"/c-myapp@"+digest)
4069
icmd.RunCmd(cmd).Assert(t, icmd.Success)
4170

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"))
71+
cmd.Command = dockerCli.Command("app", "image", "rm", "a-simple-app:latest", "b-simple-app:latest")
4572
icmd.RunCmd(cmd).Assert(t, icmd.Success)
4673

47-
expectedOutput := fmt.Sprintf(expected, info.registryAddress+"/c-myapp")
74+
expectedOutput := "REPOSITORY TAG APP NAME\n"
4875
cmd.Command = dockerCli.Command("app", "image", "ls")
4976
result := icmd.RunCmd(cmd).Assert(t, icmd.Success)
5077
assert.Equal(t, result.Stdout(), expectedOutput)

internal/commands/image/command.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ func Cmd(dockerCli command.Cli) *cobra.Command {
1212
Use: "image",
1313
}
1414

15-
cmd.AddCommand(listCmd(dockerCli))
15+
cmd.AddCommand(
16+
listCmd(dockerCli),
17+
rmCmd(),
18+
)
1619

1720
return cmd
1821
}

internal/commands/image/rm.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package image
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/docker/app/internal/store"
9+
"github.com/docker/cli/cli"
10+
"github.com/docker/cli/cli/config"
11+
"github.com/docker/distribution/reference"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
func rmCmd() *cobra.Command {
16+
return &cobra.Command{
17+
Use: "rm [APP_IMAGE] [APP_IMAGE...]",
18+
Short: "Remove an application image",
19+
Args: cli.RequiresMinArgs(1),
20+
RunE: func(cmd *cobra.Command, args []string) error {
21+
appstore, err := store.NewApplicationStore(config.Dir())
22+
if err != nil {
23+
return err
24+
}
25+
26+
bundleStore, err := appstore.BundleStore()
27+
if err != nil {
28+
return err
29+
}
30+
31+
errs := []string{}
32+
for _, arg := range args {
33+
if err := runRm(bundleStore, arg); err != nil {
34+
errs = append(errs, fmt.Sprintf("Error: %s", err))
35+
}
36+
}
37+
if len(errs) > 0 {
38+
return errors.New(strings.Join(errs, "\n"))
39+
}
40+
return nil
41+
},
42+
}
43+
}
44+
45+
func runRm(bundleStore store.BundleStore, app string) error {
46+
ref, err := reference.ParseNormalizedNamed(app)
47+
if err != nil {
48+
return err
49+
}
50+
51+
err = bundleStore.Remove(ref)
52+
if err != nil {
53+
return err
54+
}
55+
56+
fmt.Println("Deleted: " + ref.String())
57+
return nil
58+
}

internal/store/bundle.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type BundleStore interface {
2424
Store(ref reference.Named, bndle *bundle.Bundle) error
2525
Read(ref reference.Named) (*bundle.Bundle, error)
2626
List() ([]reference.Named, error)
27+
Remove(ref reference.Named) error
2728

2829
LookupOrPullBundle(ref reference.Named, pullRef bool, config *configfile.ConfigFile, insecureRegistries []string) (*bundle.Bundle, error)
2930
}
@@ -97,6 +98,20 @@ func (b *bundleStore) List() ([]reference.Named, error) {
9798
return references, nil
9899
}
99100

101+
// Remove removes a bundle from the bundle store.
102+
func (b *bundleStore) Remove(ref reference.Named) error {
103+
path, err := b.storePath(ref)
104+
if err != nil {
105+
return err
106+
}
107+
108+
if _, err := os.Stat(path); os.IsNotExist(err) {
109+
return errors.New("no such image " + ref.String())
110+
}
111+
112+
return os.Remove(path)
113+
}
114+
100115
// LookupOrPullBundle will fetch the given bundle from the local
101116
// bundle store, or if it is missing from the registry, and returns
102117
// it. Always pulls if pullRef is true. If it pulls then the local

internal/store/bundle_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,49 @@ func TestList(t *testing.T) {
244244
assert.Equal(t, len(bundles), 2)
245245
})
246246
}
247+
248+
func TestRemove(t *testing.T) {
249+
dockerConfigDir := fs.NewDir(t, t.Name(), fs.WithMode(0755))
250+
defer dockerConfigDir.Remove()
251+
appstore, err := NewApplicationStore(dockerConfigDir.Path())
252+
assert.NilError(t, err)
253+
bundleStore, err := appstore.BundleStore()
254+
assert.NilError(t, err)
255+
256+
refs := []reference.Named{
257+
parseRefOrDie(t, "my-repo/a-bundle:my-tag"),
258+
parseRefOrDie(t, "my-repo/b-bundle@sha256:"+testSha),
259+
}
260+
261+
bndl := &bundle.Bundle{Name: "bundle-name"}
262+
for _, ref := range refs {
263+
err = bundleStore.Store(ref, bndl)
264+
assert.NilError(t, err)
265+
}
266+
267+
t.Run("error on unknown", func(t *testing.T) {
268+
err := bundleStore.Remove(parseRefOrDie(t, "my-repo/some-bundle:1.0.0"))
269+
assert.Equal(t, err.Error(), "no such image docker.io/my-repo/some-bundle:1.0.0")
270+
})
271+
272+
t.Run("remove tagged and digested", func(t *testing.T) {
273+
bundles, err := bundleStore.List()
274+
assert.NilError(t, err)
275+
assert.Equal(t, len(bundles), 2)
276+
277+
err = bundleStore.Remove(refs[0])
278+
279+
// Once removed there should be none left
280+
assert.NilError(t, err)
281+
bundles, err = bundleStore.List()
282+
assert.NilError(t, err)
283+
assert.Equal(t, len(bundles), 1)
284+
285+
err = bundleStore.Remove(refs[1])
286+
assert.NilError(t, err)
287+
288+
bundles, err = bundleStore.List()
289+
assert.NilError(t, err)
290+
assert.Equal(t, len(bundles), 0)
291+
})
292+
}

0 commit comments

Comments
 (0)