* Runtime: improved image removal to garbage-collect unreferenced parents
- Runtime: fixed image removal to cleanly remove tags and repositories
| ... | ... |
@@ -45,6 +45,8 @@ func httpError(w http.ResponseWriter, err error) {
|
| 45 | 45 |
http.Error(w, err.Error(), http.StatusNotFound) |
| 46 | 46 |
} else if strings.HasPrefix(err.Error(), "Bad parameter") {
|
| 47 | 47 |
http.Error(w, err.Error(), http.StatusBadRequest) |
| 48 |
+ } else if strings.HasPrefix(err.Error(), "Conflict") {
|
|
| 49 |
+ http.Error(w, err.Error(), http.StatusConflict) |
|
| 48 | 50 |
} else if strings.HasPrefix(err.Error(), "Impossible") {
|
| 49 | 51 |
http.Error(w, err.Error(), http.StatusNotAcceptable) |
| 50 | 52 |
} else {
|
| ... | ... |
@@ -481,14 +483,30 @@ func deleteContainers(srv *Server, version float64, w http.ResponseWriter, r *ht |
| 481 | 481 |
} |
| 482 | 482 |
|
| 483 | 483 |
func deleteImages(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
| 484 |
+ if err := parseForm(r); err != nil {
|
|
| 485 |
+ return err |
|
| 486 |
+ } |
|
| 484 | 487 |
if vars == nil {
|
| 485 | 488 |
return fmt.Errorf("Missing parameter")
|
| 486 | 489 |
} |
| 487 | 490 |
name := vars["name"] |
| 488 |
- if err := srv.ImageDelete(name); err != nil {
|
|
| 491 |
+ imgs, err := srv.ImageDelete(name, version > 1.0) |
|
| 492 |
+ if err != nil {
|
|
| 489 | 493 |
return err |
| 490 | 494 |
} |
| 491 |
- w.WriteHeader(http.StatusNoContent) |
|
| 495 |
+ if imgs != nil {
|
|
| 496 |
+ if len(*imgs) != 0 {
|
|
| 497 |
+ b, err := json.Marshal(imgs) |
|
| 498 |
+ if err != nil {
|
|
| 499 |
+ return err |
|
| 500 |
+ } |
|
| 501 |
+ writeJSON(w, b) |
|
| 502 |
+ } else {
|
|
| 503 |
+ return fmt.Errorf("Conflict, %s wasn't deleted", name)
|
|
| 504 |
+ } |
|
| 505 |
+ } else {
|
|
| 506 |
+ w.WriteHeader(http.StatusNoContent) |
|
| 507 |
+ } |
|
| 492 | 508 |
return nil |
| 493 | 509 |
} |
| 494 | 510 |
|
| ... | ... |
@@ -23,6 +23,11 @@ type APIInfo struct {
|
| 23 | 23 |
SwapLimit bool `json:",omitempty"` |
| 24 | 24 |
} |
| 25 | 25 |
|
| 26 |
+type APIRmi struct {
|
|
| 27 |
+ Deleted string `json:",omitempty"` |
|
| 28 |
+ Untagged string `json:",omitempty"` |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 26 | 31 |
type APIContainers struct {
|
| 27 | 32 |
ID string `json:"Id"` |
| 28 | 33 |
Image string |
| ... | ... |
@@ -1307,8 +1307,63 @@ func TestGetEnabledCors(t *testing.T) {
|
| 1307 | 1307 |
} |
| 1308 | 1308 |
|
| 1309 | 1309 |
func TestDeleteImages(t *testing.T) {
|
| 1310 |
- //FIXME: Implement this test |
|
| 1311 |
- t.Log("Test not implemented")
|
|
| 1310 |
+ runtime, err := newTestRuntime() |
|
| 1311 |
+ if err != nil {
|
|
| 1312 |
+ t.Fatal(err) |
|
| 1313 |
+ } |
|
| 1314 |
+ defer nuke(runtime) |
|
| 1315 |
+ |
|
| 1316 |
+ srv := &Server{runtime: runtime}
|
|
| 1317 |
+ |
|
| 1318 |
+ if err := srv.runtime.repositories.Set("test", "test", unitTestImageName, true); err != nil {
|
|
| 1319 |
+ t.Fatal(err) |
|
| 1320 |
+ } |
|
| 1321 |
+ |
|
| 1322 |
+ images, err := srv.Images(false, "") |
|
| 1323 |
+ if err != nil {
|
|
| 1324 |
+ t.Fatal(err) |
|
| 1325 |
+ } |
|
| 1326 |
+ |
|
| 1327 |
+ if len(images) != 2 {
|
|
| 1328 |
+ t.Errorf("Excepted 2 images, %d found", len(images))
|
|
| 1329 |
+ } |
|
| 1330 |
+ |
|
| 1331 |
+ req, err := http.NewRequest("DELETE", "/images/test:test", nil)
|
|
| 1332 |
+ if err != nil {
|
|
| 1333 |
+ t.Fatal(err) |
|
| 1334 |
+ } |
|
| 1335 |
+ |
|
| 1336 |
+ r := httptest.NewRecorder() |
|
| 1337 |
+ if err := deleteImages(srv, APIVERSION, r, req, map[string]string{"name": "test:test"}); err != nil {
|
|
| 1338 |
+ t.Fatal(err) |
|
| 1339 |
+ } |
|
| 1340 |
+ if r.Code != http.StatusOK {
|
|
| 1341 |
+ t.Fatalf("%d OK expected, received %d\n", http.StatusOK, r.Code)
|
|
| 1342 |
+ } |
|
| 1343 |
+ |
|
| 1344 |
+ var outs []APIRmi |
|
| 1345 |
+ if err := json.Unmarshal(r.Body.Bytes(), &outs); err != nil {
|
|
| 1346 |
+ t.Fatal(err) |
|
| 1347 |
+ } |
|
| 1348 |
+ if len(outs) != 1 {
|
|
| 1349 |
+ t.Fatalf("Expected %d event (untagged), got %d", 1, len(outs))
|
|
| 1350 |
+ } |
|
| 1351 |
+ images, err = srv.Images(false, "") |
|
| 1352 |
+ if err != nil {
|
|
| 1353 |
+ t.Fatal(err) |
|
| 1354 |
+ } |
|
| 1355 |
+ |
|
| 1356 |
+ if len(images) != 1 {
|
|
| 1357 |
+ t.Errorf("Excepted 1 image, %d found", len(images))
|
|
| 1358 |
+ } |
|
| 1359 |
+ |
|
| 1360 |
+ /* if c := runtime.Get(container.Id); c != nil {
|
|
| 1361 |
+ t.Fatalf("The container as not been deleted")
|
|
| 1362 |
+ } |
|
| 1363 |
+ |
|
| 1364 |
+ if _, err := os.Stat(path.Join(container.rwPath(), "test")); err == nil {
|
|
| 1365 |
+ t.Fatalf("The test file has not been deleted")
|
|
| 1366 |
+ } */ |
|
| 1312 | 1367 |
} |
| 1313 | 1368 |
|
| 1314 | 1369 |
// Mocked types for tests |
| ... | ... |
@@ -590,11 +590,22 @@ func (cli *DockerCli) CmdRmi(args ...string) error {
|
| 590 | 590 |
} |
| 591 | 591 |
|
| 592 | 592 |
for _, name := range cmd.Args() {
|
| 593 |
- _, _, err := cli.call("DELETE", "/images/"+name, nil)
|
|
| 593 |
+ body, _, err := cli.call("DELETE", "/images/"+name, nil)
|
|
| 594 | 594 |
if err != nil {
|
| 595 |
- fmt.Printf("%s", err)
|
|
| 595 |
+ fmt.Fprintf(os.Stderr, "%s", err) |
|
| 596 | 596 |
} else {
|
| 597 |
- fmt.Println(name) |
|
| 597 |
+ var outs []APIRmi |
|
| 598 |
+ err = json.Unmarshal(body, &outs) |
|
| 599 |
+ if err != nil {
|
|
| 600 |
+ return err |
|
| 601 |
+ } |
|
| 602 |
+ for _, out := range outs {
|
|
| 603 |
+ if out.Deleted != "" {
|
|
| 604 |
+ fmt.Println("Deleted:", out.Deleted)
|
|
| 605 |
+ } else {
|
|
| 606 |
+ fmt.Println("Untagged:", out.Untagged)
|
|
| 607 |
+ } |
|
| 608 |
+ } |
|
| 598 | 609 |
} |
| 599 | 610 |
} |
| 600 | 611 |
return nil |
| ... | ... |
@@ -777,6 +777,7 @@ Tag an image into a repository |
| 777 | 777 |
:statuscode 200: no error |
| 778 | 778 |
:statuscode 400: bad parameter |
| 779 | 779 |
:statuscode 404: no such image |
| 780 |
+ :statuscode 409: conflict |
|
| 780 | 781 |
:statuscode 500: server error |
| 781 | 782 |
|
| 782 | 783 |
|
| ... | ... |
@@ -793,14 +794,30 @@ Remove an image |
| 793 | 793 |
|
| 794 | 794 |
DELETE /images/test HTTP/1.1 |
| 795 | 795 |
|
| 796 |
- **Example response**: |
|
| 796 |
+ **Example response v1.0**: |
|
| 797 | 797 |
|
| 798 | 798 |
.. sourcecode:: http |
| 799 | 799 |
|
| 800 | 800 |
HTTP/1.1 204 OK |
| 801 | 801 |
|
| 802 |
+ **Example response v1.1**: |
|
| 803 |
+ |
|
| 804 |
+ .. sourcecode:: http |
|
| 805 |
+ |
|
| 806 |
+ HTTP/1.1 200 OK |
|
| 807 |
+ Content-type: application/json |
|
| 808 |
+ |
|
| 809 |
+ [ |
|
| 810 |
+ {"Untagged":"3e2f21a89f"},
|
|
| 811 |
+ {"Deleted":"3e2f21a89f"},
|
|
| 812 |
+ {"Deleted":"53b4f83ac9"}
|
|
| 813 |
+ ] |
|
| 814 |
+ |
|
| 815 |
+ :query force: 1/True/true or 0/False/false, default false |
|
| 816 |
+ :statuscode 200: no error |
|
| 802 | 817 |
:statuscode 204: no error |
| 803 | 818 |
:statuscode 404: no such image |
| 819 |
+ :statuscode 409: conflict |
|
| 804 | 820 |
:statuscode 500: server error |
| 805 | 821 |
|
| 806 | 822 |
|
| ... | ... |
@@ -1,6 +1,7 @@ |
| 1 | 1 |
package docker |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "errors" |
|
| 4 | 5 |
"fmt" |
| 5 | 6 |
"github.com/dotcloud/docker/auth" |
| 6 | 7 |
"github.com/dotcloud/docker/registry" |
| ... | ... |
@@ -717,17 +718,112 @@ func (srv *Server) ContainerDestroy(name string, removeVolume bool) error {
|
| 717 | 717 |
return nil |
| 718 | 718 |
} |
| 719 | 719 |
|
| 720 |
-func (srv *Server) ImageDelete(name string) error {
|
|
| 721 |
- img, err := srv.runtime.repositories.LookupImage(name) |
|
| 720 |
+var ErrImageReferenced = errors.New("Image referenced by a repository")
|
|
| 721 |
+ |
|
| 722 |
+func (srv *Server) deleteImageAndChildren(id string, imgs *[]APIRmi) error {
|
|
| 723 |
+ // If the image is referenced by a repo, do not delete |
|
| 724 |
+ if len(srv.runtime.repositories.ByID()[id]) != 0 {
|
|
| 725 |
+ return ErrImageReferenced |
|
| 726 |
+ } |
|
| 727 |
+ |
|
| 728 |
+ // If the image is not referenced but has children, go recursive |
|
| 729 |
+ referenced := false |
|
| 730 |
+ byParents, err := srv.runtime.graph.ByParent() |
|
| 722 | 731 |
if err != nil {
|
| 723 |
- return fmt.Errorf("No such image: %s", name)
|
|
| 732 |
+ return err |
|
| 724 | 733 |
} |
| 725 |
- if err := srv.runtime.graph.Delete(img.ID); err != nil {
|
|
| 726 |
- return fmt.Errorf("Error deleting image %s: %s", name, err.Error())
|
|
| 734 |
+ for _, img := range byParents[id] {
|
|
| 735 |
+ if err := srv.deleteImageAndChildren(img.ID, imgs); err != nil {
|
|
| 736 |
+ if err != ErrImageReferenced {
|
|
| 737 |
+ return err |
|
| 738 |
+ } |
|
| 739 |
+ referenced = true |
|
| 740 |
+ } |
|
| 741 |
+ } |
|
| 742 |
+ if referenced {
|
|
| 743 |
+ return ErrImageReferenced |
|
| 744 |
+ } |
|
| 745 |
+ |
|
| 746 |
+ // If the image is not referenced and has no children, remove it |
|
| 747 |
+ byParents, err = srv.runtime.graph.ByParent() |
|
| 748 |
+ if err != nil {
|
|
| 749 |
+ return err |
|
| 750 |
+ } |
|
| 751 |
+ if len(byParents[id]) == 0 {
|
|
| 752 |
+ if err := srv.runtime.repositories.DeleteAll(id); err != nil {
|
|
| 753 |
+ return err |
|
| 754 |
+ } |
|
| 755 |
+ err := srv.runtime.graph.Delete(id) |
|
| 756 |
+ if err != nil {
|
|
| 757 |
+ return err |
|
| 758 |
+ } |
|
| 759 |
+ *imgs = append(*imgs, APIRmi{Deleted: utils.TruncateID(id)})
|
|
| 760 |
+ return nil |
|
| 727 | 761 |
} |
| 728 | 762 |
return nil |
| 729 | 763 |
} |
| 730 | 764 |
|
| 765 |
+func (srv *Server) deleteImageParents(img *Image, imgs *[]APIRmi) error {
|
|
| 766 |
+ if img.Parent != "" {
|
|
| 767 |
+ parent, err := srv.runtime.graph.Get(img.Parent) |
|
| 768 |
+ if err != nil {
|
|
| 769 |
+ return err |
|
| 770 |
+ } |
|
| 771 |
+ // Remove all children images |
|
| 772 |
+ if err := srv.deleteImageAndChildren(img.Parent, imgs); err != nil {
|
|
| 773 |
+ return err |
|
| 774 |
+ } |
|
| 775 |
+ return srv.deleteImageParents(parent, imgs) |
|
| 776 |
+ } |
|
| 777 |
+ return nil |
|
| 778 |
+} |
|
| 779 |
+ |
|
| 780 |
+func (srv *Server) deleteImage(img *Image, repoName, tag string) (*[]APIRmi, error) {
|
|
| 781 |
+ //Untag the current image |
|
| 782 |
+ var imgs []APIRmi |
|
| 783 |
+ tagDeleted, err := srv.runtime.repositories.Delete(repoName, tag) |
|
| 784 |
+ if err != nil {
|
|
| 785 |
+ return nil, err |
|
| 786 |
+ } |
|
| 787 |
+ if tagDeleted {
|
|
| 788 |
+ imgs = append(imgs, APIRmi{Untagged: img.ShortID()})
|
|
| 789 |
+ } |
|
| 790 |
+ if len(srv.runtime.repositories.ByID()[img.ID]) == 0 {
|
|
| 791 |
+ if err := srv.deleteImageAndChildren(img.ID, &imgs); err != nil {
|
|
| 792 |
+ if err != ErrImageReferenced {
|
|
| 793 |
+ return &imgs, err |
|
| 794 |
+ } |
|
| 795 |
+ } else if err := srv.deleteImageParents(img, &imgs); err != nil {
|
|
| 796 |
+ if err != ErrImageReferenced {
|
|
| 797 |
+ return &imgs, err |
|
| 798 |
+ } |
|
| 799 |
+ } |
|
| 800 |
+ } |
|
| 801 |
+ return &imgs, nil |
|
| 802 |
+} |
|
| 803 |
+ |
|
| 804 |
+func (srv *Server) ImageDelete(name string, autoPrune bool) (*[]APIRmi, error) {
|
|
| 805 |
+ img, err := srv.runtime.repositories.LookupImage(name) |
|
| 806 |
+ if err != nil {
|
|
| 807 |
+ return nil, fmt.Errorf("No such image: %s", name)
|
|
| 808 |
+ } |
|
| 809 |
+ if !autoPrune {
|
|
| 810 |
+ if err := srv.runtime.graph.Delete(img.ID); err != nil {
|
|
| 811 |
+ return nil, fmt.Errorf("Error deleting image %s: %s", name, err.Error())
|
|
| 812 |
+ } |
|
| 813 |
+ return nil, nil |
|
| 814 |
+ } |
|
| 815 |
+ |
|
| 816 |
+ var tag string |
|
| 817 |
+ if strings.Contains(name, ":") {
|
|
| 818 |
+ nameParts := strings.Split(name, ":") |
|
| 819 |
+ name = nameParts[0] |
|
| 820 |
+ tag = nameParts[1] |
|
| 821 |
+ } |
|
| 822 |
+ |
|
| 823 |
+ return srv.deleteImage(img, name, tag) |
|
| 824 |
+} |
|
| 825 |
+ |
|
| 731 | 826 |
func (srv *Server) ImageGetCached(imgId string, config *Config) (*Image, error) {
|
| 732 | 827 |
|
| 733 | 828 |
// Retrieve all images |
| ... | ... |
@@ -4,6 +4,58 @@ import ( |
| 4 | 4 |
"testing" |
| 5 | 5 |
) |
| 6 | 6 |
|
| 7 |
+func TestContainerTagImageDelete(t *testing.T) {
|
|
| 8 |
+ runtime, err := newTestRuntime() |
|
| 9 |
+ if err != nil {
|
|
| 10 |
+ t.Fatal(err) |
|
| 11 |
+ } |
|
| 12 |
+ defer nuke(runtime) |
|
| 13 |
+ |
|
| 14 |
+ srv := &Server{runtime: runtime}
|
|
| 15 |
+ |
|
| 16 |
+ if err := srv.runtime.repositories.Set("utest", "tag1", unitTestImageName, false); err != nil {
|
|
| 17 |
+ t.Fatal(err) |
|
| 18 |
+ } |
|
| 19 |
+ if err := srv.runtime.repositories.Set("utest/docker", "tag2", unitTestImageName, false); err != nil {
|
|
| 20 |
+ t.Fatal(err) |
|
| 21 |
+ } |
|
| 22 |
+ |
|
| 23 |
+ images, err := srv.Images(false, "") |
|
| 24 |
+ if err != nil {
|
|
| 25 |
+ t.Fatal(err) |
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ if len(images) != 3 {
|
|
| 29 |
+ t.Errorf("Excepted 3 images, %d found", len(images))
|
|
| 30 |
+ } |
|
| 31 |
+ |
|
| 32 |
+ if _, err := srv.ImageDelete("utest/docker:tag2", true); err != nil {
|
|
| 33 |
+ t.Fatal(err) |
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ images, err = srv.Images(false, "") |
|
| 37 |
+ if err != nil {
|
|
| 38 |
+ t.Fatal(err) |
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ if len(images) != 2 {
|
|
| 42 |
+ t.Errorf("Excepted 2 images, %d found", len(images))
|
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 45 |
+ if _, err := srv.ImageDelete("utest:tag1", true); err != nil {
|
|
| 46 |
+ t.Fatal(err) |
|
| 47 |
+ } |
|
| 48 |
+ |
|
| 49 |
+ images, err = srv.Images(false, "") |
|
| 50 |
+ if err != nil {
|
|
| 51 |
+ t.Fatal(err) |
|
| 52 |
+ } |
|
| 53 |
+ |
|
| 54 |
+ if len(images) != 1 {
|
|
| 55 |
+ t.Errorf("Excepted 1 image, %d found", len(images))
|
|
| 56 |
+ } |
|
| 57 |
+} |
|
| 58 |
+ |
|
| 7 | 59 |
func TestCreateRm(t *testing.T) {
|
| 8 | 60 |
runtime, err := newTestRuntime() |
| 9 | 61 |
if err != nil {
|
| ... | ... |
@@ -110,6 +110,52 @@ func (store *TagStore) ImageName(id string) string {
|
| 110 | 110 |
return utils.TruncateID(id) |
| 111 | 111 |
} |
| 112 | 112 |
|
| 113 |
+func (store *TagStore) DeleteAll(id string) error {
|
|
| 114 |
+ names, exists := store.ByID()[id] |
|
| 115 |
+ if !exists || len(names) == 0 {
|
|
| 116 |
+ return nil |
|
| 117 |
+ } |
|
| 118 |
+ for _, name := range names {
|
|
| 119 |
+ if strings.Contains(name, ":") {
|
|
| 120 |
+ nameParts := strings.Split(name, ":") |
|
| 121 |
+ if _, err := store.Delete(nameParts[0], nameParts[1]); err != nil {
|
|
| 122 |
+ return err |
|
| 123 |
+ } |
|
| 124 |
+ } else {
|
|
| 125 |
+ if _, err := store.Delete(name, ""); err != nil {
|
|
| 126 |
+ return err |
|
| 127 |
+ } |
|
| 128 |
+ } |
|
| 129 |
+ } |
|
| 130 |
+ return nil |
|
| 131 |
+} |
|
| 132 |
+ |
|
| 133 |
+func (store *TagStore) Delete(repoName, tag string) (bool, error) {
|
|
| 134 |
+ deleted := false |
|
| 135 |
+ if err := store.Reload(); err != nil {
|
|
| 136 |
+ return false, err |
|
| 137 |
+ } |
|
| 138 |
+ if r, exists := store.Repositories[repoName]; exists {
|
|
| 139 |
+ if tag != "" {
|
|
| 140 |
+ if _, exists2 := r[tag]; exists2 {
|
|
| 141 |
+ delete(r, tag) |
|
| 142 |
+ if len(r) == 0 {
|
|
| 143 |
+ delete(store.Repositories, repoName) |
|
| 144 |
+ } |
|
| 145 |
+ deleted = true |
|
| 146 |
+ } else {
|
|
| 147 |
+ return false, fmt.Errorf("No such tag: %s:%s", repoName, tag)
|
|
| 148 |
+ } |
|
| 149 |
+ } else {
|
|
| 150 |
+ delete(store.Repositories, repoName) |
|
| 151 |
+ deleted = true |
|
| 152 |
+ } |
|
| 153 |
+ } else {
|
|
| 154 |
+ fmt.Errorf("No such repository: %s", repoName)
|
|
| 155 |
+ } |
|
| 156 |
+ return deleted, store.Save() |
|
| 157 |
+} |
|
| 158 |
+ |
|
| 113 | 159 |
func (store *TagStore) Set(repoName, tag, imageName string, force bool) error {
|
| 114 | 160 |
img, err := store.LookupImage(imageName) |
| 115 | 161 |
if err != nil {
|
| ... | ... |
@@ -133,7 +179,7 @@ func (store *TagStore) Set(repoName, tag, imageName string, force bool) error {
|
| 133 | 133 |
} else {
|
| 134 | 134 |
repo = make(map[string]string) |
| 135 | 135 |
if old, exists := store.Repositories[repoName]; exists && !force {
|
| 136 |
- return fmt.Errorf("Tag %s:%s is already set to %s", repoName, tag, old)
|
|
| 136 |
+ return fmt.Errorf("Conflict: Tag %s:%s is already set to %s", repoName, tag, old)
|
|
| 137 | 137 |
} |
| 138 | 138 |
store.Repositories[repoName] = repo |
| 139 | 139 |
} |