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

Commit 7a4683d

Browse files
committed
Introduce --force option on image rm command
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent e3a9aed commit 7a4683d

File tree

6 files changed

+118
-19
lines changed

6 files changed

+118
-19
lines changed

e2e/images_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ package e2e
22

33
import (
44
"bufio"
5+
"fmt"
6+
"io/ioutil"
57
"path/filepath"
68
"regexp"
79
"strings"
810
"testing"
911

12+
"github.com/opencontainers/go-digest"
13+
14+
"gotest.tools/fs"
15+
1016
"gotest.tools/assert"
1117
"gotest.tools/icmd"
1218
)
@@ -90,6 +96,33 @@ my.registry:5000/c-myapp latest <none> [a-f0-9]{12}
9096
})
9197
}
9298

99+
func TestImageRmForce(t *testing.T) {
100+
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
101+
cmd := info.configuredCmd
102+
iidfile := fs.NewFile(t, "iid").Path()
103+
104+
cmd.Command = dockerCli.Command("app", "build", "--no-resolve-image", "--tag", "a-simple-app", "--iidfile", iidfile, filepath.Join("testdata", "simple"))
105+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
106+
cmd.Command = dockerCli.Command("app", "image", "tag", "a-simple-app", "b-simple-app")
107+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
108+
cmd.Command = dockerCli.Command("app", "image", "tag", "a-simple-app", "c-simple-app")
109+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
110+
111+
bytes, err := ioutil.ReadFile(iidfile)
112+
assert.NilError(t, err)
113+
114+
imageID := digest.Digest(bytes).Encoded()
115+
cmd.Command = dockerCli.Command("app", "image", "rm", imageID)
116+
icmd.RunCmd(cmd).Assert(t, icmd.Expected{
117+
ExitCode: 1,
118+
Err: fmt.Sprintf("Error: unable to delete %q - App is referenced in multiple repositories", imageID),
119+
})
120+
121+
cmd.Command = dockerCli.Command("app", "image", "rm", "--force", imageID)
122+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
123+
})
124+
}
125+
93126
func TestImageRm(t *testing.T) {
94127
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
95128
cmd := info.configuredCmd

internal/commands/image/list_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func (b *bundleStoreStubForListCmd) List() ([]reference.Reference, error) {
4141
return b.refList, nil
4242
}
4343

44-
func (b *bundleStoreStubForListCmd) Remove(ref reference.Reference) error {
44+
func (b *bundleStoreStubForListCmd) Remove(ref reference.Reference, force bool) error {
4545
return nil
4646
}
4747

internal/commands/image/rm.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@ import (
1515
const rmExample = `- $ docker app image rm myapp
1616
- $ docker app image rm myapp:1.0.0
1717
- $ docker app image rm myrepo/myapp@sha256:c0de...
18-
- $ docker app image rm 34be4a0c5f50`
18+
- $ docker app image rm 34be4a0c5f50
19+
- $ docker app image rm --force 34be4a0c5f50`
20+
21+
type rmOptions struct {
22+
force bool
23+
}
1924

2025
func rmCmd() *cobra.Command {
21-
return &cobra.Command{
26+
options := rmOptions{}
27+
cmd := &cobra.Command{
2228
Short: "Remove an App image",
2329
Use: "rm APP_IMAGE [APP_IMAGE...]",
2430
Aliases: []string{"remove"},
@@ -37,7 +43,7 @@ func rmCmd() *cobra.Command {
3743

3844
errs := []string{}
3945
for _, arg := range args {
40-
if err := runRm(bundleStore, arg); err != nil {
46+
if err := runRm(bundleStore, arg, options); err != nil {
4147
errs = append(errs, fmt.Sprintf("Error: %s", err))
4248
}
4349
}
@@ -47,15 +53,17 @@ func rmCmd() *cobra.Command {
4753
return nil
4854
},
4955
}
56+
cmd.Flags().BoolVarP(&options.force, "force", "f", false, "")
57+
return cmd
5058
}
5159

52-
func runRm(bundleStore store.BundleStore, app string) error {
60+
func runRm(bundleStore store.BundleStore, app string, options rmOptions) error {
5361
ref, err := bundleStore.LookUp(app)
5462
if err != nil {
5563
return err
5664
}
5765

58-
if err := bundleStore.Remove(ref); err != nil {
66+
if err := bundleStore.Remove(ref, options.force); err != nil {
5967
return err
6068
}
6169

internal/commands/image/tag_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func (b *bundleStoreStub) List() ([]reference.Reference, error) {
5050
return nil, nil
5151
}
5252

53-
func (b *bundleStoreStub) Remove(ref reference.Reference) error {
53+
func (b *bundleStoreStub) Remove(ref reference.Reference, force bool) error {
5454
return nil
5555
}
5656

internal/store/bundle.go

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package store
22

33
import (
44
"fmt"
5+
"io"
56
"os"
67
"path/filepath"
78
"sort"
@@ -20,7 +21,7 @@ type BundleStore interface {
2021
Store(ref reference.Reference, bndl *relocated.Bundle) (reference.Digested, error)
2122
Read(ref reference.Reference) (*relocated.Bundle, error)
2223
List() ([]reference.Reference, error)
23-
Remove(ref reference.Reference) error
24+
Remove(ref reference.Reference, force bool) error
2425
LookUp(refOrID string) (reference.Reference, error)
2526
}
2627

@@ -111,15 +112,30 @@ func (b *bundleStore) List() ([]reference.Reference, error) {
111112
}
112113

113114
// Remove removes a bundle from the bundle store.
114-
func (b *bundleStore) Remove(ref reference.Reference) error {
115+
func (b *bundleStore) Remove(ref reference.Reference, force bool) error {
115116
if id, ok := ref.(ID); ok {
116-
if len(b.refsMap[id]) == 0 {
117+
refs := b.refsMap[id]
118+
if len(refs) == 0 {
117119
return fmt.Errorf("no such image %q", reference.FamiliarString(ref))
118-
} else if len(b.refsMap[id]) > 1 {
120+
} else if len(refs) > 1 {
121+
var failure error
122+
if force {
123+
toDelete := append([]reference.Reference{}, refs...)
124+
for _, r := range toDelete {
125+
if err := b.doRemove(r); err != nil {
126+
failure = err
127+
}
128+
}
129+
return failure
130+
}
119131
return fmt.Errorf("unable to delete %q - App is referenced in multiple repositories", reference.FamiliarString(ref))
120132
}
121-
ref = b.refsMap[id][0]
133+
ref = refs[0]
122134
}
135+
return b.doRemove(ref)
136+
}
137+
138+
func (b *bundleStore) doRemove(ref reference.Reference) error {
123139
path, err := b.storePath(ref)
124140
if err != nil {
125141
return err
@@ -128,7 +144,36 @@ func (b *bundleStore) Remove(ref reference.Reference) error {
128144
return errors.New("no such image " + reference.FamiliarString(ref))
129145
}
130146
b.refsMap.removeRef(ref)
131-
return os.RemoveAll(path)
147+
148+
if err := os.RemoveAll(path); err != nil {
149+
return nil
150+
}
151+
return cleanupParentTree(path)
152+
}
153+
154+
func cleanupParentTree(path string) error {
155+
for {
156+
path = filepath.Dir(path)
157+
if empty, err := isEmpty(path); err != nil || !empty {
158+
return err
159+
}
160+
if err := os.RemoveAll(path); err != nil {
161+
return nil
162+
}
163+
}
164+
}
165+
166+
func isEmpty(path string) (bool, error) {
167+
f, err := os.Open(path)
168+
if err != nil {
169+
return false, err
170+
}
171+
defer f.Close()
172+
if _, err = f.Readdir(1); err == io.EOF {
173+
// dir is empty
174+
return true, nil
175+
}
176+
return false, nil
132177
}
133178

134179
func (b *bundleStore) LookUp(refOrID string) (reference.Reference, error) {

internal/store/bundle_test.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ func TestRemove(t *testing.T) {
269269
}
270270

271271
t.Run("error on unknown", func(t *testing.T) {
272-
err := bundleStore.Remove(parseRefOrDie(t, "my-repo/some-bundle:1.0.0"))
272+
err := bundleStore.Remove(parseRefOrDie(t, "my-repo/some-bundle:1.0.0"), false)
273273
assert.Equal(t, err.Error(), "no such image my-repo/some-bundle:1.0.0")
274274
})
275275

@@ -278,15 +278,15 @@ func TestRemove(t *testing.T) {
278278
assert.NilError(t, err)
279279
assert.Equal(t, len(bundles), 2)
280280

281-
err = bundleStore.Remove(refs[0])
281+
err = bundleStore.Remove(refs[0], false)
282282

283283
// Once removed there should be none left
284284
assert.NilError(t, err)
285285
bundles, err = bundleStore.List()
286286
assert.NilError(t, err)
287287
assert.Equal(t, len(bundles), 1)
288288

289-
err = bundleStore.Remove(refs[1])
289+
err = bundleStore.Remove(refs[1], false)
290290
assert.NilError(t, err)
291291

292292
bundles, err = bundleStore.List()
@@ -307,7 +307,7 @@ func TestRemoveById(t *testing.T) {
307307
idRef, err := FromBundle(relocated.FromBundle(&bundle.Bundle{Name: "not-stored-bundle-name"}))
308308
assert.NilError(t, err)
309309

310-
err = bundleStore.Remove(idRef)
310+
err = bundleStore.Remove(idRef, false)
311311
assert.Equal(t, err.Error(), fmt.Sprintf("no such image %q", reference.FamiliarString(idRef)))
312312
})
313313

@@ -320,10 +320,23 @@ func TestRemoveById(t *testing.T) {
320320
_, err = bundleStore.Store(parseRefOrDie(t, "my-repo/a-bundle:my-tag"), bndl)
321321
assert.NilError(t, err)
322322

323-
err = bundleStore.Remove(idRef)
323+
err = bundleStore.Remove(idRef, false)
324324
assert.Equal(t, err.Error(), fmt.Sprintf("unable to delete %q - App is referenced in multiple repositories", reference.FamiliarString(idRef)))
325325
})
326326

327+
t.Run("success on multiple repositories but force", func(t *testing.T) {
328+
bndl := relocated.FromBundle(&bundle.Bundle{Name: "bundle-name"})
329+
idRef, err := FromBundle(bndl)
330+
assert.NilError(t, err)
331+
_, err = bundleStore.Store(idRef, bndl)
332+
assert.NilError(t, err)
333+
_, err = bundleStore.Store(parseRefOrDie(t, "my-repo/a-bundle:my-tag"), bndl)
334+
assert.NilError(t, err)
335+
336+
err = bundleStore.Remove(idRef, true)
337+
assert.NilError(t, err)
338+
})
339+
327340
t.Run("success when only one reference exists", func(t *testing.T) {
328341
bndl := relocated.FromBundle(&bundle.Bundle{Name: "other-bundle-name"})
329342
ref := parseRefOrDie(t, "my-repo/other-bundle:my-tag")
@@ -332,7 +345,7 @@ func TestRemoveById(t *testing.T) {
332345
idRef, err := FromBundle(bndl)
333346
assert.NilError(t, err)
334347

335-
err = bundleStore.Remove(idRef)
348+
err = bundleStore.Remove(idRef, false)
336349
assert.NilError(t, err)
337350
bundles, err := bundleStore.List()
338351
assert.NilError(t, err)

0 commit comments

Comments
 (0)