Browse code

Merge branch '610-improve_rmi-feature'

* Runtime: improved image removal to garbage-collect unreferenced parents
- Runtime: fixed image removal to cleanly remove tags and repositories

Solomon Hykes authored on 2013/06/13 12:30:07
Showing 8 changed files
... ...
@@ -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
 	}