Removing tagged images change in behavior

An image name is really just a tag.  When an image has multiple tags, we should be
able to "delete" the one of its tags without harm. In this case, the "delete' is
really a form of Untag (removing the tag from the image).

If an image has multiple tags and the user tries to delete by ID without force, this
should be denied because when you delete by ID there is no distinguishing it like
image tags.

Signed-off-by: baude <bbaude@redhat.com>

Closes: #528
Approved by: mheon
This commit is contained in:
baude
2018-03-21 13:08:32 -05:00
committed by Atomic Bot
parent 2cc51dbb1c
commit d364d41e1b
6 changed files with 167 additions and 25 deletions

View File

@ -75,14 +75,14 @@ func rmiCmd(c *cli.Context) error {
return errors.Errorf("no valid images to delete") return errors.Errorf("no valid images to delete")
} }
for _, img := range imagesToDelete { for _, img := range imagesToDelete {
err := runtime.RemoveImage(img, c.Bool("force")) msg, err := runtime.RemoveImage(img, c.Bool("force"))
if err != nil { if err != nil {
if lastError != nil { if lastError != nil {
fmt.Fprintln(os.Stderr, lastError) fmt.Fprintln(os.Stderr, lastError)
} }
lastError = err lastError = err
} else { } else {
fmt.Println(img.ID()) fmt.Println(msg)
} }
} }
return lastError return lastError

View File

@ -685,3 +685,60 @@ func Import(path, reference string, writer io.Writer, signingOptions SigningOpti
} }
return runtime.NewFromLocal(reference) return runtime.NewFromLocal(reference)
} }
// MatchRepoTag takes a string and tries to match it against an
// image's repotags
func (i *Image) MatchRepoTag(input string) (string, error) {
results := make(map[int][]string)
var maxCount int
// first check if we have an exact match with the input
if util.StringInSlice(input, i.Names()) {
return input, nil
}
// next check if we are missing the tag
dcImage, err := decompose(input)
if err != nil {
return "", err
}
for _, repoName := range i.Names() {
count := 0
dcRepoName, err := decompose(repoName)
if err != nil {
return "", err
}
if dcRepoName.registry == dcImage.registry && dcImage.registry != "" {
count++
}
if dcRepoName.name == dcImage.name && dcImage.name != "" {
count++
} else if splitString(dcRepoName.name) == splitString(dcImage.name) {
count++
}
if dcRepoName.tag == dcImage.tag {
count++
}
results[count] = append(results[count], repoName)
if count > maxCount {
maxCount = count
}
}
if maxCount == 0 {
return "", errors.Errorf("unable to match user input to any specific repotag")
}
if len(results[maxCount]) > 1 {
return "", errors.Errorf("user input matched multiple repotags for the image")
}
return results[maxCount][0], nil
}
// splitString splits input string by / and returns the last array item
func splitString(input string) string {
split := strings.Split(input, "/")
return split[len(split)-1]
}
// InputIsID returns a bool if the user input for an image
// is the image's partial or full id
func (i *Image) InputIsID() bool {
return strings.HasPrefix(i.ID(), i.InputName)
}

View File

@ -136,3 +136,52 @@ func TestImage_New(t *testing.T) {
// Shutdown the runtime and remove the temporary storage // Shutdown the runtime and remove the temporary storage
cleanup(workdir, ir) cleanup(workdir, ir)
} }
// TestImage_MatchRepoTag tests the various inputs we need to match
// against an image's reponames
func TestImage_MatchRepoTag(t *testing.T) {
//Set up
workdir, err := mkWorkDir()
assert.NoError(t, err)
so := storage.StoreOptions{
RunRoot: workdir,
GraphRoot: workdir,
}
ir, err := NewImageRuntimeFromOptions(so)
assert.NoError(t, err)
newImage, err := ir.New("busybox", "", "", os.Stdout, nil, SigningOptions{})
assert.NoError(t, err)
err = newImage.TagImage("foo:latest")
assert.NoError(t, err)
err = newImage.TagImage("foo:bar")
assert.NoError(t, err)
// Tests start here.
for _, name := range bbNames {
repoTag, err := newImage.MatchRepoTag(name)
assert.NoError(t, err)
assert.Equal(t, "docker.io/library/busybox:latest", repoTag)
}
// Test against tagged images of busybox
// foo should resolve to foo:latest
repoTag, err := newImage.MatchRepoTag("foo")
assert.NoError(t, err)
assert.Equal(t, "foo:latest", repoTag)
// foo:bar should resolve to foo:bar
repoTag, err = newImage.MatchRepoTag("foo:bar")
assert.NoError(t, err)
assert.Equal(t, "foo:bar", repoTag)
// Shutdown the runtime and remove the temporary storage
cleanup(workdir, ir)
}
// Test_splitString tests the splitString function in image that
// takes input and splits on / and returns the last array item
func Test_splitString(t *testing.T) {
assert.Equal(t, splitString("foo/bar"), "bar")
assert.Equal(t, splitString("a/foo/bar"), "bar")
assert.Equal(t, splitString("bar"), "bar")
}

View File

@ -834,18 +834,18 @@ func (r *Runtime) UntagImage(image *storage.Image, tag string) (string, error) {
// RemoveImage deletes an image from local storage // RemoveImage deletes an image from local storage
// Images being used by running containers can only be removed if force=true // Images being used by running containers can only be removed if force=true
func (r *Runtime) RemoveImage(image *image.Image, force bool) error { func (r *Runtime) RemoveImage(image *image.Image, force bool) (string, error) {
r.lock.Lock() r.lock.Lock()
defer r.lock.Unlock() defer r.lock.Unlock()
if !r.valid { if !r.valid {
return ErrRuntimeStopped return "", ErrRuntimeStopped
} }
// Get all containers, filter to only those using the image, and remove those containers // Get all containers, filter to only those using the image, and remove those containers
ctrs, err := r.state.AllContainers() ctrs, err := r.state.AllContainers()
if err != nil { if err != nil {
return err return "", err
} }
imageCtrs := []*Container{} imageCtrs := []*Container{}
for _, ctr := range ctrs { for _, ctr := range ctrs {
@ -857,29 +857,33 @@ func (r *Runtime) RemoveImage(image *image.Image, force bool) error {
if force { if force {
for _, ctr := range imageCtrs { for _, ctr := range imageCtrs {
if err := r.removeContainer(ctr, true); err != nil { if err := r.removeContainer(ctr, true); err != nil {
return errors.Wrapf(err, "error removing image %s: container %s using image could not be removed", image.ID, ctr.ID()) return "", errors.Wrapf(err, "error removing image %s: container %s using image could not be removed", image.ID, ctr.ID())
} }
} }
} else { } else {
return fmt.Errorf("could not remove image %s as it is being used by %d containers", image.ID, len(imageCtrs)) return "", fmt.Errorf("could not remove image %s as it is being used by %d containers", image.ID, len(imageCtrs))
} }
} }
if len(image.Names()) > 1 && !force { if len(image.Names()) > 1 && !image.InputIsID() {
return fmt.Errorf("unable to delete %s (must force) - image is referred to in multiple tags", image.ID) // If the image has multiple reponames, we do not technically delete
} // the image. we figure out which repotag the user is trying to refer
// to and untag it.
if len(image.Names()) > 1 && force { repoName, err := image.MatchRepoTag(image.InputName)
// If it is forced, we have to untag the image so that it can be deleted if err != nil {
if err = r.store.SetNames(image.ID(), image.Names()[:0]); err != nil { return "", err
return err
} }
if err := image.UntagImage(repoName); err != nil {
return "", err
}
return fmt.Sprintf("Untagged: %s", repoName), nil
} else if len(image.Names()) > 1 && image.InputIsID() && !force {
// If the user requests to delete an image by ID and the image has multiple
// reponames and no force is applied, we error out.
return "", fmt.Errorf("unable to delete %s (must force) - image is referred to in multiple tags", image.ID())
} }
if err = image.Remove(force); err != nil { return image.ID(), image.Remove(force)
return err
}
return nil
} }
// GetImage retrieves an image matching the given name or hash from system // GetImage retrieves an image matching the given name or hash from system

View File

@ -35,20 +35,16 @@ var _ = Describe("Podman push", func() {
session = podmanTest.Podman([]string{"rmi", ALPINE}) session = podmanTest.Podman([]string{"rmi", ALPINE})
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Not(Equal(0))) Expect(session.ExitCode()).To(Equal(0))
session = podmanTest.Podman([]string{"rmi", "busybox:test"}) session = podmanTest.Podman([]string{"rmi", "busybox:test"})
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Not(Equal(0)))
session = podmanTest.Podman([]string{"rmi", "-f", "busybox:test"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0)) Expect(session.ExitCode()).To(Equal(0))
}) })
// push to oci-archive, docker-archive, and dir are tested in pull_test.go // push to oci-archive, docker-archive, and dir are tested in pull_test.go
It("podman push to containers/storage", func() { It("podman push to dir", func() {
session := podmanTest.Podman([]string{"push", "--remove-signatures", ALPINE, "dir:/tmp/busybox"}) session := podmanTest.Podman([]string{"push", "--remove-signatures", ALPINE, "dir:/tmp/busybox"})
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0)) Expect(session.ExitCode()).To(Equal(0))

View File

@ -67,4 +67,40 @@ var _ = Describe("Podman rmi", func() {
}) })
It("podman rmi tagged image", func() {
setup := podmanTest.Podman([]string{"images", "-q", ALPINE})
setup.WaitWithDefaultTimeout()
Expect(setup.ExitCode()).To(Equal(0))
session := podmanTest.Podman([]string{"tag", "alpine", "foo:bar", "foo"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
result := podmanTest.Podman([]string{"images", "-q", "foo"})
result.WaitWithDefaultTimeout()
Expect(result.ExitCode()).To(Equal(0))
Expect(result.LineInOuputContains(setup.OutputToString())).To(BeTrue())
})
It("podman rmi image with tags by ID cannot be done without force", func() {
setup := podmanTest.Podman([]string{"images", "-q", ALPINE})
setup.WaitWithDefaultTimeout()
Expect(setup.ExitCode()).To(Equal(0))
alpineId := setup.OutputToString()
session := podmanTest.Podman([]string{"tag", "alpine", "foo:bar", "foo"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
// Trying without --force should fail
result := podmanTest.Podman([]string{"rmi", alpineId})
result.WaitWithDefaultTimeout()
Expect(result.ExitCode()).ToNot(Equal(0))
// With --force it should work
resultForce := podmanTest.Podman([]string{"rmi", "-f", alpineId})
resultForce.WaitWithDefaultTimeout()
Expect(resultForce.ExitCode()).To(Equal(0))
})
}) })