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

Commit

Permalink
Introduce --force option on image rm command
Browse files Browse the repository at this point in the history
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
  • Loading branch information
ndeloof committed Nov 22, 2019
1 parent b621499 commit c236c31
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 20 deletions.
33 changes: 33 additions & 0 deletions e2e/images_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ package e2e

import (
"bufio"
"fmt"
"io/ioutil"
"path/filepath"
"regexp"
"strings"
"testing"

"github.com/opencontainers/go-digest"

"gotest.tools/fs"

"gotest.tools/assert"
"gotest.tools/icmd"
)
Expand Down Expand Up @@ -90,6 +96,33 @@ my.registry:5000/c-myapp latest <none> [a-f0-9]{12}
})
}

func TestImageRmForce(t *testing.T) {
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
cmd := info.configuredCmd
iidfile := fs.NewFile(t, "iid").Path()

cmd.Command = dockerCli.Command("app", "build", "--no-resolve-image", "--tag", "a-simple-app", "--iidfile", iidfile, filepath.Join("testdata", "simple"))
icmd.RunCmd(cmd).Assert(t, icmd.Success)
cmd.Command = dockerCli.Command("app", "image", "tag", "a-simple-app", "b-simple-app")
icmd.RunCmd(cmd).Assert(t, icmd.Success)
cmd.Command = dockerCli.Command("app", "image", "tag", "a-simple-app", "c-simple-app")
icmd.RunCmd(cmd).Assert(t, icmd.Success)

bytes, err := ioutil.ReadFile(iidfile)
assert.NilError(t, err)

imageID := digest.Digest(bytes).Encoded()
cmd.Command = dockerCli.Command("app", "image", "rm", imageID)
icmd.RunCmd(cmd).Assert(t, icmd.Expected{
ExitCode: 1,
Err: fmt.Sprintf("Error: unable to delete %q - App is referenced in multiple repositories", imageID),
})

cmd.Command = dockerCli.Command("app", "image", "rm", "--force", imageID)
icmd.RunCmd(cmd).Assert(t, icmd.Success)
})
}

func TestImageRm(t *testing.T) {
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
cmd := info.configuredCmd
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/image/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (b *bundleStoreStubForListCmd) List() ([]reference.Reference, error) {
return b.refList, nil
}

func (b *bundleStoreStubForListCmd) Remove(ref reference.Reference) error {
func (b *bundleStoreStubForListCmd) Remove(ref reference.Reference, force bool) error {
return nil
}

Expand Down
18 changes: 13 additions & 5 deletions internal/commands/image/rm.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@ import (
const rmExample = `- $ docker app image rm myapp
- $ docker app image rm myapp:1.0.0
- $ docker app image rm myrepo/myapp@sha256:c0de...
- $ docker app image rm 34be4a0c5f50`
- $ docker app image rm 34be4a0c5f50
- $ docker app image rm --force 34be4a0c5f50`

type rmOptions struct {
force bool
}

func rmCmd() *cobra.Command {
return &cobra.Command{
options := rmOptions{}
cmd := &cobra.Command{
Short: "Remove an App image",
Use: "rm APP_IMAGE [APP_IMAGE...]",
Aliases: []string{"remove"},
Expand All @@ -37,7 +43,7 @@ func rmCmd() *cobra.Command {

errs := []string{}
for _, arg := range args {
if err := runRm(bundleStore, arg); err != nil {
if err := runRm(bundleStore, arg, options); err != nil {
errs = append(errs, fmt.Sprintf("Error: %s", err))
}
}
Expand All @@ -47,15 +53,17 @@ func rmCmd() *cobra.Command {
return nil
},
}
cmd.Flags().BoolVarP(&options.force, "force", "f", false, "")
return cmd
}

func runRm(bundleStore store.BundleStore, app string) error {
func runRm(bundleStore store.BundleStore, app string, options rmOptions) error {
ref, err := bundleStore.LookUp(app)
if err != nil {
return err
}

if err := bundleStore.Remove(ref); err != nil {
if err := bundleStore.Remove(ref, options.force); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion internal/commands/image/tag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (b *bundleStoreStub) List() ([]reference.Reference, error) {
return nil, nil
}

func (b *bundleStoreStub) Remove(ref reference.Reference) error {
func (b *bundleStoreStub) Remove(ref reference.Reference, force bool) error {
return nil
}

Expand Down
59 changes: 52 additions & 7 deletions internal/store/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package store

import (
"fmt"
"io"
"os"
"path/filepath"
"sort"
Expand All @@ -20,7 +21,7 @@ type BundleStore interface {
Store(ref reference.Reference, bndl *relocated.Bundle) (reference.Digested, error)
Read(ref reference.Reference) (*relocated.Bundle, error)
List() ([]reference.Reference, error)
Remove(ref reference.Reference) error
Remove(ref reference.Reference, force bool) error
LookUp(refOrID string) (reference.Reference, error)
}

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

// Remove removes a bundle from the bundle store.
func (b *bundleStore) Remove(ref reference.Reference) error {
func (b *bundleStore) Remove(ref reference.Reference, force bool) error {
if id, ok := ref.(ID); ok {
if len(b.refsMap[id]) == 0 {
refs := b.refsMap[id]
if len(refs) == 0 {
return fmt.Errorf("no such image %q", reference.FamiliarString(ref))
} else if len(b.refsMap[id]) > 1 {
return fmt.Errorf("unable to delete %q - App is referenced in multiple repositories", reference.FamiliarString(ref))
} else if len(refs) > 1 {
if force {
toDelete := append([]reference.Reference{}, refs...)
for _, r := range toDelete {
if err := b.doRemove(r); err != nil {
return err
}
}
return nil
} else {
return fmt.Errorf("unable to delete %q - App is referenced in multiple repositories", reference.FamiliarString(ref))
}
}
ref = b.refsMap[id][0]
ref = refs[0]
}
return b.doRemove(ref)
}

func (b *bundleStore) doRemove(ref reference.Reference) error {
path, err := b.storePath(ref)
if err != nil {
return err
Expand All @@ -128,7 +144,36 @@ func (b *bundleStore) Remove(ref reference.Reference) error {
return errors.New("no such image " + reference.FamiliarString(ref))
}
b.refsMap.removeRef(ref)
return os.RemoveAll(path)

if err := os.RemoveAll(path); err != nil {
return nil
}
return cleanupParentTree(path)
}

func cleanupParentTree(path string) error {
for {
path = filepath.Dir(path)
if empty, err := isEmpty(path); err != nil || !empty {
return err
}
if err := os.RemoveAll(path); err != nil {
return nil
}
}
}

func isEmpty(path string) (bool, error) {
f, err := os.Open(path)
if err != nil {
return false, err
}
if _, err = f.Readdir(1); err == io.EOF {
// dir is empty
return true, nil
}
defer f.Close()
return false, nil
}

func (b *bundleStore) LookUp(refOrID string) (reference.Reference, error) {
Expand Down
25 changes: 19 additions & 6 deletions internal/store/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ func TestRemove(t *testing.T) {
}

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

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

err = bundleStore.Remove(refs[0])
err = bundleStore.Remove(refs[0], false)

// Once removed there should be none left
assert.NilError(t, err)
bundles, err = bundleStore.List()
assert.NilError(t, err)
assert.Equal(t, len(bundles), 1)

err = bundleStore.Remove(refs[1])
err = bundleStore.Remove(refs[1], false)
assert.NilError(t, err)

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

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

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

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

t.Run("success on multiple repositories but force", func(t *testing.T) {
bndl := relocated.FromBundle(&bundle.Bundle{Name: "bundle-name"})
idRef, err := FromBundle(bndl)
assert.NilError(t, err)
_, err = bundleStore.Store(idRef, bndl)
assert.NilError(t, err)
_, err = bundleStore.Store(parseRefOrDie(t, "my-repo/a-bundle:my-tag"), bndl)
assert.NilError(t, err)

err = bundleStore.Remove(idRef, true)
assert.NilError(t, err)
})

t.Run("success when only one reference exists", func(t *testing.T) {
bndl := relocated.FromBundle(&bundle.Bundle{Name: "other-bundle-name"})
ref := parseRefOrDie(t, "my-repo/other-bundle:my-tag")
Expand All @@ -332,7 +345,7 @@ func TestRemoveById(t *testing.T) {
idRef, err := FromBundle(bndl)
assert.NilError(t, err)

err = bundleStore.Remove(idRef)
err = bundleStore.Remove(idRef, false)
assert.NilError(t, err)
bundles, err := bundleStore.List()
assert.NilError(t, err)
Expand Down

0 comments on commit c236c31

Please sign in to comment.