diff --git a/api/server/image.go b/api/server/image.go index 55b66c1db8..3a751e8045 100644 --- a/api/server/image.go +++ b/api/server/image.go @@ -233,10 +233,15 @@ func (s *Server) deleteImages(version version.Version, w http.ResponseWriter, r } name := vars["name"] - force := boolValue(r, "force") - noprune := boolValue(r, "noprune") - list, err := s.daemon.ImageDelete(name, force, noprune) + if name == "" { + return fmt.Errorf("image name cannot be blank") + } + + force := boolValue(r, "force") + prune := !boolValue(r, "noprune") + + list, err := s.daemon.ImageDelete(name, force, prune) if err != nil { return err } diff --git a/daemon/image_delete.go b/daemon/image_delete.go index 6b34d4dcbb..9e918b326a 100644 --- a/daemon/image_delete.go +++ b/daemon/image_delete.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types" "github.com/docker/docker/graph/tags" "github.com/docker/docker/image" @@ -13,165 +12,329 @@ import ( "github.com/docker/docker/utils" ) -// ImageDelete removes the image from the filesystem. -// FIXME: remove ImageDelete's dependency on Daemon, then move to graph/ -func (daemon *Daemon) ImageDelete(name string, force, noprune bool) ([]types.ImageDelete, error) { - list := []types.ImageDelete{} - if err := daemon.imgDeleteHelper(name, &list, true, force, noprune); err != nil { +// ImageDelete deletes the image referenced by the given imageRef from this +// daemon. The given imageRef can be an image ID, ID prefix, or a repository +// reference (with an optional tag or digest, defaulting to the tag name +// "latest"). There is differing behavior depending on whether the given +// imageRef is a repository reference or not. +// +// If the given imageRef is a repository reference then that repository +// reference will be removed. However, if there exists any containers which +// were created using the same image reference then the repository reference +// cannot be removed unless either there are other repository references to the +// same image or force is true. Following removal of the repository reference, +// the referenced image itself will attempt to be deleted as described below +// but quietly, meaning any image delete conflicts will cause the image to not +// be deleted and the conflict will not be reported. +// +// There may be conflicts preventing deletion of an image and these conflicts +// are divided into two categories grouped by their severity: +// +// Hard Conflict: +// - a pull or build using the image. +// - any descendent image. +// - any running container using the image. +// +// Soft Conflict: +// - any stopped container using the image. +// - any repository tag or digest references to the image. +// +// The image cannot be removed if there are any hard conflicts and can be +// removed if there are soft conflicts only if force is true. +// +// If prune is true, ancestor images will each attempt to be deleted quietly, +// meaning any delete conflicts will cause the image to not be deleted and the +// conflict will not be reported. +// +// FIXME: remove ImageDelete's dependency on Daemon, then move to the graph +// package. This would require that we no longer need the daemon to determine +// whether images are being used by a stopped or running container. +func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.ImageDelete, error) { + records := []types.ImageDelete{} + + img, err := daemon.Repositories().LookupImage(imageRef) + if err != nil { return nil, err } - if len(list) == 0 { - return nil, fmt.Errorf("Conflict, %s wasn't deleted", name) - } - return list, nil -} - -func (daemon *Daemon) imgDeleteHelper(name string, list *[]types.ImageDelete, first, force, noprune bool) error { - if name == "" { - return fmt.Errorf("Image name can not be blank") - } - - var repoName, tag string - repoAndTags := make(map[string][]string) - - // FIXME: please respect DRY and centralize repo+tag parsing in a single central place! -- shykes - repoName, tag = parsers.ParseRepositoryTag(name) - if tag == "" { - tag = tags.DefaultTag - } - - img, err := daemon.Repositories().LookupImage(name) - if err != nil { - if r, _ := daemon.Repositories().Get(repoName); r != nil { - return fmt.Errorf("No such image: %s", utils.ImageReference(repoName, tag)) - } - return fmt.Errorf("No such image: %s", name) - } - - if strings.Contains(img.ID, name) { - repoName = "" - tag = "" - } - - byParents := daemon.Graph().ByParent() - repos := daemon.Repositories().ByID()[img.ID] - - //If delete by id, see if the id belong only to one repository - deleteByID := repoName == "" - if deleteByID { - for _, repoAndTag := range repos { - parsedRepo, parsedTag := parsers.ParseRepositoryTag(repoAndTag) - if repoName == "" || repoName == parsedRepo { - repoName = parsedRepo - if parsedTag != "" { - repoAndTags[repoName] = append(repoAndTags[repoName], parsedTag) - } - } else if repoName != parsedRepo && !force && first { - // the id belongs to multiple repos, like base:latest and user:test, - // in that case return conflict - return fmt.Errorf("Conflict, cannot delete image %s because it is tagged in multiple repositories, use -f to force", name) - } else { - //the id belongs to multiple repos, with -f just delete all - repoName = parsedRepo - if parsedTag != "" { - repoAndTags[repoName] = append(repoAndTags[repoName], parsedTag) - } + var removedRepositoryRef bool + if !isImageIDPrefix(img.ID, imageRef) { + // A repository reference was given and should be removed + // first. We can only remove this reference if either force is + // true, there are multiple repository references to this + // image, or there are no containers using the given reference. + if !(force || daemon.imageHasMultipleRepositoryReferences(img.ID)) { + if container := daemon.getContainerUsingImage(img.ID); container != nil { + // If we removed the repository reference then + // this image would remain "dangling" and since + // we really want to avoid that the client must + // explicitly force its removal. + return nil, fmt.Errorf("conflict: unable to remove repository reference %q (must force) - container %s is using its referenced image %s", imageRef, stringid.TruncateID(container.ID), stringid.TruncateID(img.ID)) } } + + parsedRef, err := daemon.removeImageRef(imageRef) + if err != nil { + return nil, err + } + + untaggedRecord := types.ImageDelete{Untagged: parsedRef} + + daemon.EventsService.Log("untag", img.ID, "") + records = append(records, untaggedRecord) + + removedRepositoryRef = true } else { - repoAndTags[repoName] = append(repoAndTags[repoName], tag) + // If an ID reference was given AND there is exactly one + // repository reference to the image then we will want to + // remove that reference. + // FIXME: Is this the behavior we want? + repoRefs := daemon.Repositories().ByID()[img.ID] + if len(repoRefs) == 1 { + parsedRef, err := daemon.removeImageRef(repoRefs[0]) + if err != nil { + return nil, err + } + + untaggedRecord := types.ImageDelete{Untagged: parsedRef} + + daemon.EventsService.Log("untag", img.ID, "") + records = append(records, untaggedRecord) + } } - if !first && len(repoAndTags) > 0 { + return records, daemon.imageDeleteHelper(img, &records, force, prune, removedRepositoryRef) +} + +// isImageIDPrefix returns whether the given possiblePrefix is a prefix of the +// given imageID. +func isImageIDPrefix(imageID, possiblePrefix string) bool { + return strings.HasPrefix(imageID, possiblePrefix) +} + +// imageHasMultipleRepositoryReferences returns whether there are multiple +// repository references to the given imageID. +func (daemon *Daemon) imageHasMultipleRepositoryReferences(imageID string) bool { + return len(daemon.Repositories().ByID()[imageID]) > 1 +} + +// getContainerUsingImage returns a container that was created using the given +// imageID. Returns nil if there is no such container. +func (daemon *Daemon) getContainerUsingImage(imageID string) *Container { + for _, container := range daemon.List() { + if container.ImageID == imageID { + return container + } + } + + return nil +} + +// removeImageRef attempts to parse and remove the given image reference from +// this daemon's store of repository tag/digest references. The given +// repositoryRef must not be an image ID but a repository name followed by an +// optional tag or digest reference. If tag or digest is omitted, the default +// tag is used. Returns the resolved image reference and an error. +func (daemon *Daemon) removeImageRef(repositoryRef string) (string, error) { + repository, ref := parsers.ParseRepositoryTag(repositoryRef) + if ref == "" { + ref = tags.DefaultTag + } + + // Ignore the boolean value returned, as far as we're concerned, this + // is an idempotent operation and it's okay if the reference didn't + // exist in the first place. + _, err := daemon.Repositories().Delete(repository, ref) + + return utils.ImageReference(repository, ref), err +} + +// removeAllReferencesToImageID attempts to remove every reference to the given +// imgID from this daemon's store of repository tag/digest references. Returns +// on the first encountered error. Removed references are logged to this +// daemon's event service. An "Untagged" types.ImageDelete is added to the +// given list of records. +func (daemon *Daemon) removeAllReferencesToImageID(imgID string, records *[]types.ImageDelete) error { + imageRefs := daemon.Repositories().ByID()[imgID] + + for _, imageRef := range imageRefs { + parsedRef, err := daemon.removeImageRef(imageRef) + if err != nil { + return err + } + + untaggedRecord := types.ImageDelete{Untagged: parsedRef} + + daemon.EventsService.Log("untag", imgID, "") + *records = append(*records, untaggedRecord) + } + + return nil +} + +// ImageDeleteConflict holds a soft or hard conflict and an associated error. +// Implements the error interface. +type imageDeleteConflict struct { + hard bool + imgID string + message string +} + +func (idc *imageDeleteConflict) Error() string { + var forceMsg string + if idc.hard { + forceMsg = "cannot be forced" + } else { + forceMsg = "must be forced" + } + + return fmt.Sprintf("conflict: unable to delete %s (%s) - %s", stringid.TruncateID(idc.imgID), forceMsg, idc.message) +} + +// imageDeleteHelper attempts to delete the given image from this daemon. If +// the image has any hard delete conflicts (child images or running containers +// using the image) then it cannot be deleted. If the image has any soft delete +// conflicts (any tags/digests referencing the image or any stopped container +// using the image) then it can only be deleted if force is true. If the delete +// succeeds and prune is true, the parent images are also deleted if they do +// not have any soft or hard delete conflicts themselves. Any deleted images +// and untagged references are appended to the given records. If any error or +// conflict is encountered, it will be returned immediately without deleting +// the image. If quiet is true, any encountered conflicts will be ignored and +// the function will return nil immediately without deleting the image. +func (daemon *Daemon) imageDeleteHelper(img *image.Image, records *[]types.ImageDelete, force, prune, quiet bool) error { + // First, determine if this image has any conflicts. Ignore soft conflicts + // if force is true. + if conflict := daemon.checkImageDeleteConflict(img, force); conflict != nil { + if quiet && !daemon.imageIsDangling(img) { + // Ignore conflicts UNLESS the image is "dangling" in + // which case we want the user to know. + return nil + } + + // There was a conflict and it's either a hard conflict OR we are not + // forcing deletion on soft conflicts. + return conflict + } + + // Delete all repository tag/digest references to this image. + if err := daemon.removeAllReferencesToImageID(img.ID, records); err != nil { + return err + } + + if err := daemon.Graph().Delete(img.ID); err != nil { + return err + } + + daemon.EventsService.Log("delete", img.ID, "") + *records = append(*records, types.ImageDelete{Deleted: img.ID}) + + if !prune || img.Parent == "" { return nil } - if len(repos) <= 1 || deleteByID { - if err := daemon.canDeleteImage(img.ID, force); err != nil { - return err - } + // We need to prune the parent image. This means delete it if there are + // no tags/digests referencing it and there are no containers using it ( + // either running or stopped). + parentImg, err := daemon.Graph().Get(img.Parent) + if err != nil { + return fmt.Errorf("unable to get parent image: %v", err) } - // Untag the current image - for repoName, tags := range repoAndTags { - for _, tag := range tags { - tagDeleted, err := daemon.Repositories().Delete(repoName, tag) - if err != nil { - return err - } - if tagDeleted { - *list = append(*list, types.ImageDelete{ - Untagged: utils.ImageReference(repoName, tag), - }) - daemon.EventsService.Log("untag", img.ID, "") - } - } - } - tags := daemon.Repositories().ByID()[img.ID] - if (len(tags) <= 1 && repoName == "") || len(tags) == 0 { - if len(byParents[img.ID]) == 0 { - if err := daemon.Repositories().DeleteAll(img.ID); err != nil { - return err - } - if err := daemon.Graph().Delete(img.ID); err != nil { - return err - } - *list = append(*list, types.ImageDelete{ - Deleted: img.ID, - }) - daemon.EventsService.Log("delete", img.ID, "") - if img.Parent != "" && !noprune { - err := daemon.imgDeleteHelper(img.Parent, list, false, force, noprune) - if first { - return err - } - - } - - } - } - return nil + // Do not force prunings, but do so quietly (stopping on any encountered + // conflicts). + return daemon.imageDeleteHelper(parentImg, records, false, true, true) } -func (daemon *Daemon) canDeleteImage(imgID string, force bool) error { - if daemon.Graph().IsHeld(imgID) { - return fmt.Errorf("Conflict, cannot delete because %s is held by an ongoing pull or build", stringid.TruncateID(imgID)) +// checkImageDeleteConflict determines whether there are any conflicts +// preventing deletion of the given image from this daemon. A hard conflict is +// any image which has the given image as a parent or any running container +// using the image. A soft conflict is any tags/digest referencing the given +// image or any stopped container using the image. If ignoreSoftConflicts is +// true, this function will not check for soft conflict conditions. +func (daemon *Daemon) checkImageDeleteConflict(img *image.Image, ignoreSoftConflicts bool) *imageDeleteConflict { + // Check for hard conflicts first. + if conflict := daemon.checkImageDeleteHardConflict(img); conflict != nil { + return conflict } + + // Then check for soft conflicts. + if ignoreSoftConflicts { + // Don't bother checking for soft conflicts. + return nil + } + + return daemon.checkImageDeleteSoftConflict(img) +} + +func (daemon *Daemon) checkImageDeleteHardConflict(img *image.Image) *imageDeleteConflict { + // Check if the image ID is being used by a pull or build. + if daemon.Graph().IsHeld(img.ID) { + return &imageDeleteConflict{ + hard: true, + imgID: img.ID, + message: "image is held by an ongoing pull or build", + } + } + + // Check if the image has any descendent images. + if daemon.Graph().HasChildren(img) { + return &imageDeleteConflict{ + hard: true, + imgID: img.ID, + message: "image has dependent child images", + } + } + + // Check if any running container is using the image. for _, container := range daemon.List() { - if container.ImageID == "" { - // This technically should never happen, but if the container - // has no ImageID then log the situation and move on. - // If we allowed processing to continue then the code later - // on would fail with a "Prefix can't be empty" error even - // though the bad container has nothing to do with the image - // we're trying to delete. - logrus.Errorf("Container %q has no image associated with it!", container.ID) + if !container.IsRunning() { + // Skip this until we check for soft conflicts later. continue } - parent, err := daemon.Repositories().LookupImage(container.ImageID) - if err != nil { - if daemon.Graph().IsNotExist(err, container.ImageID) { - continue - } - return err - } - if err := daemon.graph.WalkHistory(parent, func(p image.Image) error { - if imgID == p.ID { - if container.IsRunning() { - if force { - return fmt.Errorf("Conflict, cannot force delete %s because the running container %s is using it, stop it and retry", stringid.TruncateID(imgID), stringid.TruncateID(container.ID)) - } - return fmt.Errorf("Conflict, cannot delete %s because the running container %s is using it, stop it and use -f to force", stringid.TruncateID(imgID), stringid.TruncateID(container.ID)) - } else if !force { - return fmt.Errorf("Conflict, cannot delete %s because the container %s is using it, use -f to force", stringid.TruncateID(imgID), stringid.TruncateID(container.ID)) - } + if container.ImageID == img.ID { + return &imageDeleteConflict{ + imgID: img.ID, + hard: true, + message: fmt.Sprintf("image is being used by running container %s", stringid.TruncateID(container.ID)), } - return nil - }); err != nil { - return err } } + return nil } + +func (daemon *Daemon) checkImageDeleteSoftConflict(img *image.Image) *imageDeleteConflict { + // Check if any repository tags/digest reference this image. + if daemon.Repositories().HasReferences(img) { + return &imageDeleteConflict{ + imgID: img.ID, + message: "image is referenced in one or more repositories", + } + } + + // Check if any stopped containers reference this image. + for _, container := range daemon.List() { + if container.IsRunning() { + // Skip this as it was checked above in hard conflict conditions. + continue + } + + if container.ImageID == img.ID { + return &imageDeleteConflict{ + imgID: img.ID, + message: fmt.Sprintf("image is being used by stopped container %s", stringid.TruncateID(container.ID)), + } + } + } + + return nil +} + +// imageIsDangling returns whether the given image is "dangling" which means +// that there are no repository references to the given image and it has no +// child images. +func (daemon *Daemon) imageIsDangling(img *image.Image) bool { + return !(daemon.Repositories().HasReferences(img) || daemon.Graph().HasChildren(img)) +} diff --git a/graph/graph.go b/graph/graph.go index 442725e221..be61ebf0b4 100644 --- a/graph/graph.go +++ b/graph/graph.go @@ -404,6 +404,11 @@ func (graph *Graph) ByParent() map[string][]*image.Image { return byParent } +// HasChildren returns whether the given image has any child images. +func (graph *Graph) HasChildren(img *image.Image) bool { + return len(graph.ByParent()[img.ID]) > 0 +} + // Retain keeps the images and layers that are in the pulling chain so that // they are not deleted. If not retained, they may be deleted by rmi. func (graph *Graph) Retain(sessionID string, layerIDs ...string) { diff --git a/graph/tags.go b/graph/tags.go index f477cd2d8b..293ac33186 100644 --- a/graph/tags.go +++ b/graph/tags.go @@ -188,6 +188,12 @@ func (store *TagStore) ByID() map[string][]string { return byID } +// HasReferences returns whether or not the given image is referenced in one or +// more repositories. +func (store *TagStore) HasReferences(img *image.Image) bool { + return len(store.ByID()[img.ID]) > 0 +} + // ImageName returns name of an image, given the image's ID. func (store *TagStore) ImageName(id string) string { if names, exists := store.ByID()[id]; exists && len(names) > 0 { diff --git a/integration-cli/docker_cli_rmi_test.go b/integration-cli/docker_cli_rmi_test.go index f9b0fca6b7..6b1cc1de10 100644 --- a/integration-cli/docker_cli_rmi_test.go +++ b/integration-cli/docker_cli_rmi_test.go @@ -106,7 +106,7 @@ func (s *DockerSuite) TestRmiImgIDMultipleTag(c *check.C) { // first checkout without force it fails out, _, err = dockerCmdWithError("rmi", imgID) - expected := fmt.Sprintf("Conflict, cannot delete %s because the running container %s is using it, stop it and use -f to force", imgID[:12], containerID[:12]) + expected := fmt.Sprintf("conflict: unable to delete %s (cannot be forced) - image is being used by running container %s", imgID[:12], containerID[:12]) if err == nil || !strings.Contains(out, expected) { c.Fatalf("rmi tagged in multiple repos should have failed without force: %s, %v, expected: %s", out, err, expected) } @@ -148,7 +148,7 @@ func (s *DockerSuite) TestRmiImgIDForce(c *check.C) { // first checkout without force it fails out, _, err = dockerCmdWithError("rmi", imgID) - if err == nil || !strings.Contains(out, fmt.Sprintf("Conflict, cannot delete image %s because it is tagged in multiple repositories, use -f to force", imgID)) { + if err == nil || !strings.Contains(out, "(must be forced) - image is referenced in one or more repositories") { c.Fatalf("rmi tagged in multiple repos should have failed without force:%s, %v", out, err) } @@ -172,7 +172,7 @@ func (s *DockerSuite) TestRmiImageIDForceWithRunningContainersAndMultipleTags(c dockerCmd(c, "run", "-d", imgID, "top") out, _, err := dockerCmdWithError("rmi", "-f", imgID) - if err == nil || !strings.Contains(out, "stop it and retry") { + if err == nil || !strings.Contains(out, "(cannot be forced) - image is being used by running container") { c.Log(out) c.Fatalf("rmi -f should not delete image with running containers") } @@ -251,10 +251,10 @@ func (s *DockerSuite) TestRmiBlank(c *check.C) { if err == nil { c.Fatal("Should have failed to delete '' image") } - if strings.Contains(out, "No such image") { + if strings.Contains(out, "no such id") { c.Fatalf("Wrong error message generated: %s", out) } - if !strings.Contains(out, "Image name can not be blank") { + if !strings.Contains(out, "image name cannot be blank") { c.Fatalf("Expected error message not generated: %s", out) } @@ -262,7 +262,7 @@ func (s *DockerSuite) TestRmiBlank(c *check.C) { if err == nil { c.Fatal("Should have failed to delete '' image") } - if !strings.Contains(out, "No such image") { + if !strings.Contains(out, "no such id") { c.Fatalf("Expected error message not generated: %s", out) } } @@ -287,8 +287,59 @@ func (s *DockerSuite) TestRmiContainerImageNotFound(c *check.C) { // Try to remove the image of the running container and see if it fails as expected. out, _, err := dockerCmdWithError("rmi", "-f", imageIds[0]) - if err == nil || !strings.Contains(out, "is using it") { + if err == nil || !strings.Contains(out, "image is being used by running container") { c.Log(out) c.Fatal("The image of the running container should not be removed.") } } + +// #13422 +func (s *DockerSuite) TestRmiUntagHistoryLayer(c *check.C) { + image := "tmp1" + // Build a image for testing. + dockerfile := `FROM busybox +MAINTAINER foo +RUN echo 0 #layer0 +RUN echo 1 #layer1 +RUN echo 2 #layer2 +` + _, err := buildImage(image, dockerfile, false) + c.Assert(err, check.IsNil) + + out, _ := dockerCmd(c, "history", "-q", image) + ids := strings.Split(out, "\n") + idToTag := ids[2] + + // Tag layer0 to "tmp2". + newTag := "tmp2" + dockerCmd(c, "tag", idToTag, newTag) + // Create a container based on "tmp1". + dockerCmd(c, "run", "-d", image, "true") + + // See if the "tmp2" can be untagged. + out, _ = dockerCmd(c, "rmi", newTag) + if d := strings.Count(out, "Untagged: "); d != 1 { + c.Log(out) + c.Fatalf("Expected 1 untagged entry got %d: %q", d, out) + } + + // Now let's add the tag again and create a container based on it. + dockerCmd(c, "tag", idToTag, newTag) + out, _ = dockerCmd(c, "run", "-d", newTag, "true") + cid := strings.TrimSpace(out) + + // At this point we have 2 containers, one based on layer2 and another based on layer0. + // Try to untag "tmp2" without the -f flag. + out, _, err = dockerCmdWithError("rmi", newTag) + if err == nil || !strings.Contains(out, cid[:12]) || !strings.Contains(out, "(must force)") { + c.Log(out) + c.Fatalf("%q should not be untagged without the -f flag", newTag) + } + + // Add the -f flag and test again. + out, _ = dockerCmd(c, "rmi", "-f", newTag) + if !strings.Contains(out, fmt.Sprintf("Untagged: %s:latest", newTag)) { + c.Log(out) + c.Fatalf("%q should be allowed to untag with the -f flag", newTag) + } +}