diff --git a/api/client.go b/api/client.go index eb345ae40b..7acf63f9ca 100644 --- a/api/client.go +++ b/api/client.go @@ -781,6 +781,7 @@ func (cli *DockerCli) CmdPort(args ...string) error { // 'docker rmi IMAGE' removes all images with the name IMAGE func (cli *DockerCli) CmdRmi(args ...string) error { cmd := cli.Subcmd("rmi", "IMAGE [IMAGE...]", "Remove one or more images") + force := cmd.Bool([]string{"f", "-force"}, false, "Force") if err := cmd.Parse(args); err != nil { return nil } @@ -789,9 +790,14 @@ func (cli *DockerCli) CmdRmi(args ...string) error { return nil } + v := url.Values{} + if *force { + v.Set("force", "1") + } + var encounteredError error for _, name := range cmd.Args() { - body, _, err := readBody(cli.call("DELETE", "/images/"+name, nil, false)) + body, _, err := readBody(cli.call("DELETE", "/images/"+name+"?"+v.Encode(), nil, false)) if err != nil { fmt.Fprintf(cli.err, "%s\n", err) encounteredError = fmt.Errorf("Error: failed to remove one or more images") diff --git a/api/server.go b/api/server.go index 0dd9d966e6..03f451fe8b 100644 --- a/api/server.go +++ b/api/server.go @@ -631,7 +631,7 @@ func deleteImages(eng *engine.Engine, version float64, w http.ResponseWriter, r } var job = eng.Job("image_delete", vars["name"]) streamJSON(job, w, false) - job.SetenvBool("autoPrune", version > 1.1) + job.Setenv("force", r.Form.Get("force")) return job.Run() } diff --git a/integration/api_test.go b/integration/api_test.go index 5779e6b226..abbc1a1c59 100644 --- a/integration/api_test.go +++ b/integration/api_test.go @@ -1175,6 +1175,8 @@ func TestGetEnabledCors(t *testing.T) { func TestDeleteImages(t *testing.T) { eng := NewTestEngine(t) + //we expect errors, so we disable stderr + eng.Stderr = ioutil.Discard defer mkRuntimeFromEngine(eng, t).Nuke() initialImages := getImages(eng, t, true, "") diff --git a/integration/commands_test.go b/integration/commands_test.go index a3359ec631..0cf7fda660 100644 --- a/integration/commands_test.go +++ b/integration/commands_test.go @@ -1031,7 +1031,10 @@ func TestContainerOrphaning(t *testing.T) { buildSomething(template2, imageName) // remove the second image by name - resp, err := srv.DeleteImage(imageName, true) + resp := engine.NewTable("", 0) + if err := srv.DeleteImage(imageName, resp, true, false); err == nil { + t.Fatal("Expected error, got none") + } // see if we deleted the first image (and orphaned the container) for _, i := range resp.Data { diff --git a/integration/server_test.go b/integration/server_test.go index 2234bcab08..8600e78573 100644 --- a/integration/server_test.go +++ b/integration/server_test.go @@ -35,7 +35,7 @@ func TestImageTagImageDelete(t *testing.T) { t.Errorf("Expected %d images, %d found", nExpected, nActual) } - if _, err := srv.DeleteImage("utest/docker:tag2", true); err != nil { + if err := srv.DeleteImage("utest/docker:tag2", engine.NewTable("", 0), true, false); err != nil { t.Fatal(err) } @@ -47,7 +47,7 @@ func TestImageTagImageDelete(t *testing.T) { t.Errorf("Expected %d images, %d found", nExpected, nActual) } - if _, err := srv.DeleteImage("utest:5000/docker:tag3", true); err != nil { + if err := srv.DeleteImage("utest:5000/docker:tag3", engine.NewTable("", 0), true, false); err != nil { t.Fatal(err) } @@ -56,7 +56,7 @@ func TestImageTagImageDelete(t *testing.T) { nExpected = len(initialImages.Data[0].GetList("RepoTags")) + 1 nActual = len(images.Data[0].GetList("RepoTags")) - if _, err := srv.DeleteImage("utest:tag1", true); err != nil { + if err := srv.DeleteImage("utest:tag1", engine.NewTable("", 0), true, false); err != nil { t.Fatal(err) } @@ -447,8 +447,7 @@ func TestRmi(t *testing.T) { t.Fatalf("Expected 2 new images, found %d.", images.Len()-initialImages.Len()) } - _, err = srv.DeleteImage(imageID, true) - if err != nil { + if err = srv.DeleteImage(imageID, engine.NewTable("", 0), true, false); err != nil { t.Fatal(err) } @@ -683,8 +682,8 @@ func TestDeleteTagWithExistingContainers(t *testing.T) { } // Try to remove the tag - imgs, err := srv.DeleteImage("utest:tag1", true) - if err != nil { + imgs := engine.NewTable("", 0) + if err := srv.DeleteImage("utest:tag1", imgs, true, false); err != nil { t.Fatal(err) } diff --git a/server.go b/server.go index 190ccbcc4b..91266c23b9 100644 --- a/server.go +++ b/server.go @@ -2,7 +2,6 @@ package docker import ( "encoding/json" - "errors" "fmt" "github.com/dotcloud/docker/archive" "github.com/dotcloud/docker/auth" @@ -1810,102 +1809,27 @@ func (srv *Server) ContainerDestroy(job *engine.Job) engine.Status { return engine.StatusOK } -var ErrImageReferenced = errors.New("Image referenced by a repository") - -func (srv *Server) deleteImageAndChildren(id string, imgs *engine.Table, byParents map[string][]*Image) error { - // If the image is referenced by a repo, do not delete - if len(srv.runtime.repositories.ByID()[id]) != 0 { - return ErrImageReferenced - } - // If the image is not referenced but has children, go recursive - referenced := false - for _, img := range byParents[id] { - if err := srv.deleteImageAndChildren(img.ID, imgs, byParents); err != nil { - if err != ErrImageReferenced { - return err - } - referenced = true - } - } - if referenced { - return ErrImageReferenced - } - - // If the image is not referenced and has no children, remove it - byParents, err := srv.runtime.graph.ByParent() - if err != nil { - return err - } - if len(byParents[id]) == 0 && srv.canDeleteImage(id) == nil { - if err := srv.runtime.repositories.DeleteAll(id); err != nil { - return err - } - err := srv.runtime.graph.Delete(id) - if err != nil { - return err - } - out := &engine.Env{} - out.Set("Deleted", id) - imgs.Add(out) - srv.LogEvent("delete", id, "") - return nil - } - return nil -} - -func (srv *Server) deleteImageParents(img *Image, imgs *engine.Table) error { - if img.Parent != "" { - parent, err := srv.runtime.graph.Get(img.Parent) - if err != nil { - return err - } - byParents, err := srv.runtime.graph.ByParent() - if err != nil { - return err - } - // Remove all children images - if err := srv.deleteImageAndChildren(img.Parent, imgs, byParents); err != nil { - return err - } - return srv.deleteImageParents(parent, imgs) - } - return nil -} - -func (srv *Server) DeleteImage(name string, autoPrune bool) (*engine.Table, error) { +func (srv *Server) DeleteImage(name string, imgs *engine.Table, first, force bool) error { var ( repoName, tag string img, err = srv.runtime.repositories.LookupImage(name) - imgs = engine.NewTable("", 0) tags = []string{} ) if err != nil { - return nil, fmt.Errorf("No such image: %s", name) - } - - // FIXME: What does autoPrune mean ? - if !autoPrune { - if err := srv.runtime.graph.Delete(img.ID); err != nil { - return nil, fmt.Errorf("Cannot delete image %s: %s", name, err) - } - return nil, nil + return fmt.Errorf("No such image: %s", name) } if !strings.Contains(img.ID, name) { repoName, tag = utils.ParseRepositoryTag(name) + if tag == "" { + tag = DEFAULTTAG + } } - // If we have a repo and the image is not referenced anywhere else - // then just perform an untag and do not validate. - // - // i.e. only validate if we are performing an actual delete and not - // an untag op - if repoName != "" && len(srv.runtime.repositories.ByID()[img.ID]) == 1 { - // Prevent deletion if image is used by a container - if err := srv.canDeleteImage(img.ID); err != nil { - return nil, err - } + byParents, err := srv.runtime.graph.ByParent() + if err != nil { + return err } //If delete by id, see if the id belong only to one repository @@ -1917,10 +1841,10 @@ func (srv *Server) DeleteImage(name string, autoPrune bool) (*engine.Table, erro if parsedTag != "" { tags = append(tags, parsedTag) } - } else if repoName != parsedRepo { + } else if repoName != parsedRepo && !force { // the id belongs to multiple repos, like base:latest and user:test, // in that case return conflict - return nil, fmt.Errorf("Conflict, cannot delete image %s because it is tagged in multiple repositories", utils.TruncateID(img.ID)) + return fmt.Errorf("Conflict, cannot delete image %s because it is tagged in multiple repositories, use -f to force", name) } } } else { @@ -1931,37 +1855,51 @@ func (srv *Server) DeleteImage(name string, autoPrune bool) (*engine.Table, erro for _, tag := range tags { tagDeleted, err := srv.runtime.repositories.Delete(repoName, tag) if err != nil { - return nil, err + return err } if tagDeleted { out := &engine.Env{} - out.Set("Untagged", img.ID) + out.Set("Untagged", repoName+":"+tag) imgs.Add(out) srv.LogEvent("untag", img.ID, "") } } + tags = srv.runtime.repositories.ByID()[img.ID] + if (len(tags) <= 1 && repoName == "") || len(tags) == 0 { + if len(byParents[img.ID]) == 0 { + if err := srv.canDeleteImage(img.ID); err != nil { + return err + } + if err := srv.runtime.repositories.DeleteAll(img.ID); err != nil { + return err + } + err := srv.runtime.graph.Delete(img.ID) + if err != nil { + return err + } + out := &engine.Env{} + out.Set("Deleted", img.ID) + imgs.Add(out) + srv.LogEvent("delete", img.ID, "") + if img.Parent != "" { + err := srv.DeleteImage(img.Parent, imgs, false, force) + if first { + return err + } - if len(srv.runtime.repositories.ByID()[img.ID]) == 0 { - if err := srv.deleteImageAndChildren(img.ID, imgs, nil); err != nil { - if err != ErrImageReferenced { - return imgs, err - } - } else if err := srv.deleteImageParents(img, imgs); err != nil { - if err != ErrImageReferenced { - return imgs, err } + } } - return imgs, nil + return nil } func (srv *Server) ImageDelete(job *engine.Job) engine.Status { if n := len(job.Args); n != 1 { return job.Errorf("Usage: %s IMAGE", job.Name) } - - imgs, err := srv.DeleteImage(job.Args[0], job.GetenvBool("autoPrune")) - if err != nil { + var imgs = engine.NewTable("", 0) + if err := srv.DeleteImage(job.Args[0], imgs, true, job.GetenvBool("force")); err != nil { return job.Error(err) } if len(imgs.Data) == 0 {