Browse code

rebase master

Victor Vieux authored on 2013/06/14 02:58:06
Showing 20 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,18 @@
0
+
1
+## FIXME
2
+
3
+This file is a loose collection of things to improve in the codebase, for the internal
4
+use of the maintainers.
5
+
6
+They are not big enough to be in the roadmap, not user-facing enough to be github issues,
7
+and not important enough to be discussed in the mailing list.
8
+
9
+They are just like FIXME comments in the source code, except we're not sure where in the source
10
+to put them - so we put them here :)
11
+
12
+
13
+* Merge Runtime, Server and Builder into Runtime
14
+* Run linter on codebase
15
+* Unify build commands and regular commands
16
+* Move source code into src/ subdir for clarity
17
+* Clean up the Makefile, it's a mess
... ...
@@ -3,4 +3,9 @@ Copyright 2012-2013 dotCloud, inc.
3 3
 
4 4
 This product includes software developed at dotCloud, inc. (http://www.dotcloud.com).
5 5
 
6
-This product contains software (https://github.com/kr/pty) developed by Keith Rarick, licensed under the MIT License.
7 6
\ No newline at end of file
7
+This product contains software (https://github.com/kr/pty) developed by Keith Rarick, licensed under the MIT License.
8
+
9
+Transfers of Docker shall be in accordance with applicable export controls of any country and all other applicable
10
+legal requirements.  Docker shall not be distributed or downloaded to or in Cuba, Iran, North Korea, Sudan or Syria
11
+and shall not be distributed or downloaded to any person on the Denied Persons List administered by the U.S.
12
+Department of Commerce.
... ...
@@ -373,5 +373,8 @@ Standard Container Specification
373 373
 
374 374
 ### Legal
375 375
 
376
-Transfers Docker shall be in accordance with any applicable export control or other legal requirements.
376
+Transfers of Docker shall be in accordance with applicable export controls of any country and all other applicable
377
+legal requirements.  Docker shall not be distributed or downloaded to or in Cuba, Iran, North Korea, Sudan or Syria
378
+and shall not be distributed or downloaded to any person on the Denied Persons List administered by the U.S.
379
+Department of Commerce.
377 380
 
... ...
@@ -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 {
... ...
@@ -500,14 +502,30 @@ func deleteContainers(srv *Server, version float64, w http.ResponseWriter, r *ht
500 500
 }
501 501
 
502 502
 func deleteImages(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
503
+	if err := parseForm(r); err != nil {
504
+		return err
505
+	}
503 506
 	if vars == nil {
504 507
 		return fmt.Errorf("Missing parameter")
505 508
 	}
506 509
 	name := vars["name"]
507
-	if err := srv.ImageDelete(name); err != nil {
510
+	imgs, err := srv.ImageDelete(name, version > 1.1)
511
+	if err != nil {
508 512
 		return err
509 513
 	}
510
-	w.WriteHeader(http.StatusNoContent)
514
+	if imgs != nil {
515
+		if len(*imgs) != 0 {
516
+			b, err := json.Marshal(imgs)
517
+			if err != nil {
518
+				return err
519
+			}
520
+			writeJSON(w, b)
521
+		} else {
522
+			return fmt.Errorf("Conflict, %s wasn't deleted", name)
523
+		}
524
+	} else {
525
+		w.WriteHeader(http.StatusNoContent)
526
+	}
511 527
 	return nil
512 528
 }
513 529
 
... ...
@@ -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
... ...
@@ -1266,8 +1266,63 @@ func TestGetEnabledCors(t *testing.T) {
1266 1266
 }
1267 1267
 
1268 1268
 func TestDeleteImages(t *testing.T) {
1269
-	//FIXME: Implement this test
1270
-	t.Log("Test not implemented")
1269
+	runtime, err := newTestRuntime()
1270
+	if err != nil {
1271
+		t.Fatal(err)
1272
+	}
1273
+	defer nuke(runtime)
1274
+
1275
+	srv := &Server{runtime: runtime}
1276
+
1277
+	if err := srv.runtime.repositories.Set("test", "test", unitTestImageName, true); err != nil {
1278
+		t.Fatal(err)
1279
+	}
1280
+
1281
+	images, err := srv.Images(false, "")
1282
+	if err != nil {
1283
+		t.Fatal(err)
1284
+	}
1285
+
1286
+	if len(images) != 2 {
1287
+		t.Errorf("Excepted 2 images, %d found", len(images))
1288
+	}
1289
+
1290
+	req, err := http.NewRequest("DELETE", "/images/test:test", nil)
1291
+	if err != nil {
1292
+		t.Fatal(err)
1293
+	}
1294
+
1295
+	r := httptest.NewRecorder()
1296
+	if err := deleteImages(srv, APIVERSION, r, req, map[string]string{"name": "test:test"}); err != nil {
1297
+		t.Fatal(err)
1298
+	}
1299
+	if r.Code != http.StatusOK {
1300
+		t.Fatalf("%d OK expected, received %d\n", http.StatusOK, r.Code)
1301
+	}
1302
+
1303
+	var outs []APIRmi
1304
+	if err := json.Unmarshal(r.Body.Bytes(), &outs); err != nil {
1305
+		t.Fatal(err)
1306
+	}
1307
+	if len(outs) != 1 {
1308
+		t.Fatalf("Expected %d event (untagged), got %d", 1, len(outs))
1309
+	}
1310
+	images, err = srv.Images(false, "")
1311
+	if err != nil {
1312
+		t.Fatal(err)
1313
+	}
1314
+
1315
+	if len(images) != 1 {
1316
+		t.Errorf("Excepted 1 image, %d found", len(images))
1317
+	}
1318
+
1319
+	/*	if c := runtime.Get(container.Id); c != nil {
1320
+			t.Fatalf("The container as not been deleted")
1321
+		}
1322
+
1323
+		if _, err := os.Stat(path.Join(container.rwPath(), "test")); err == nil {
1324
+			t.Fatalf("The test file has not been deleted")
1325
+		} */
1271 1326
 }
1272 1327
 
1273 1328
 // Mocked types for tests
... ...
@@ -581,11 +581,22 @@ func (cli *DockerCli) CmdRmi(args ...string) error {
581 581
 	}
582 582
 
583 583
 	for _, name := range cmd.Args() {
584
-		_, _, err := cli.call("DELETE", "/images/"+name, nil)
584
+		body, _, err := cli.call("DELETE", "/images/"+name, nil)
585 585
 		if err != nil {
586
-			fmt.Printf("%s", err)
586
+			fmt.Fprintf(os.Stderr, "%s", err)
587 587
 		} else {
588
-			fmt.Println(name)
588
+			var outs []APIRmi
589
+			err = json.Unmarshal(body, &outs)
590
+			if err != nil {
591
+				return err
592
+			}
593
+			for _, out := range outs {
594
+				if out.Deleted != "" {
595
+					fmt.Println("Deleted:", out.Deleted)
596
+				} else {
597
+					fmt.Println("Untagged:", out.Untagged)
598
+				}
599
+			}
589 600
 		}
590 601
 	}
591 602
 	return nil
... ...
@@ -355,6 +355,18 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s
355 355
 				errors <- err
356 356
 			}()
357 357
 		}
358
+	} else {
359
+		go func() {
360
+			if stdinCloser != nil {
361
+				defer stdinCloser.Close()
362
+			}
363
+
364
+			if cStdout, err := container.StdoutPipe(); err != nil {
365
+				utils.Debugf("Error stdout pipe")
366
+			} else {
367
+				io.Copy(&utils.NopWriter{}, cStdout)
368
+			}
369
+		}()
358 370
 	}
359 371
 	if stderr != nil {
360 372
 		nJobs += 1
... ...
@@ -381,7 +393,20 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s
381 381
 				errors <- err
382 382
 			}()
383 383
 		}
384
+	} else {
385
+		go func() {
386
+			if stdinCloser != nil {
387
+				defer stdinCloser.Close()
388
+			}
389
+
390
+			if cStderr, err := container.StderrPipe(); err != nil {
391
+				utils.Debugf("Error stdout pipe")
392
+			} else {
393
+				io.Copy(&utils.NopWriter{}, cStderr)
394
+			}
395
+		}()
384 396
 	}
397
+
385 398
 	return utils.Go(func() error {
386 399
 		if cStdout != nil {
387 400
 			defer cStdout.Close()
... ...
@@ -35,6 +35,9 @@ The client should send it's authConfig as POST on each call of /images/(name)/pu
35 35
 .. http:get:: /auth is now deprecated
36 36
 .. http:post:: /auth only checks the configuration but doesn't store it on the server
37 37
 
38
+Deleting an image is now improved, will only untag the image if it has chidrens and remove all the untagged parents if has any.
39
+.. http:post:: /images/<name>/delete now returns a JSON with the list of images deleted/untagged
40
+
38 41
 
39 42
 :doc:`docker_remote_api_v1.1`
40 43
 *****************************
... ...
@@ -60,13 +63,15 @@ Uses json stream instead of HTML hijack, it looks like this:
60 60
 	   {"error":"Invalid..."}
61 61
 	   ...
62 62
 
63
-:doc:`docker_remote_api_v1.0`
64
-*****************************
65 63
 
66 64
 docker v0.3.4 8d73740_
67 65
 
66
+What's new
67
+----------
68
+
68 69
 Initial version
69 70
 
71
+
70 72
 .. _a8ae398: https://github.com/dotcloud/docker/commit/a8ae398bf52e97148ee7bd0d5868de2e15bd297f
71 73
 .. _8d73740: https://github.com/dotcloud/docker/commit/8d73740343778651c09160cde9661f5f387b36f4
72 74
 
... ...
@@ -1,3 +1,4 @@
1
+
1 2
 :title: Remote API v1.1
2 3
 :description: API Documentation for Docker
3 4
 :keywords: API, Docker, rcli, REST, documentation
... ...
@@ -744,6 +745,7 @@ Tag an image into a repository
744 744
 	:statuscode 200: no error
745 745
 	:statuscode 400: bad parameter
746 746
 	:statuscode 404: no such image
747
+	:statuscode 409: conflict
747 748
         :statuscode 500: server error
748 749
 
749 750
 
... ...
@@ -745,6 +745,7 @@ Tag an image into a repository
745 745
 	:statuscode 200: no error
746 746
 	:statuscode 400: bad parameter
747 747
 	:statuscode 404: no such image
748
+	:statuscode 409: conflict
748 749
         :statuscode 500: server error
749 750
 
750 751
 
... ...
@@ -765,10 +766,18 @@ Remove an image
765 765
 
766 766
         .. sourcecode:: http
767 767
 
768
-           HTTP/1.1 204 OK
768
+	   HTTP/1.1 200 OK
769
+	   Content-type: application/json
770
+
771
+	   [
772
+	    {"Untagged":"3e2f21a89f"},
773
+	    {"Deleted":"3e2f21a89f"},
774
+	    {"Deleted":"53b4f83ac9"}
775
+	   ]
769 776
 
770 777
 	:statuscode 204: no error
771 778
         :statuscode 404: no such image
779
+	:statuscode 409: conflict
772 780
         :statuscode 500: server error
773 781
 
774 782
 
... ...
@@ -19,10 +19,15 @@ Examples
19 19
 
20 20
     docker build .
21 21
 
22
-This will take the local Dockerfile
22
+| This will read the Dockerfile from the current directory. It will also send any other files and directories found in the current directory to the docker daemon.
23
+| The contents of this directory would be used by ADD commands found within the Dockerfile.
24
+| This will send a lot of data to the docker daemon if the current directory contains a lot of data.
25
+| If the absolute path is provided instead of '.', only the files and directories required by the ADD commands from the Dockerfile will be added to the context and transferred to the docker daemon.
26
+|
23 27
 
24 28
 .. code-block:: bash
25 29
 
26 30
     docker build -
27 31
 
28
-This will read a Dockerfile form Stdin without context
32
+| This will read a Dockerfile from Stdin without context. Due to the lack of a context, no contents of any local directory will be sent to the docker daemon.
33
+| ADD doesn't work when running in this mode due to the absence of the context, thus having no source files to copy to the container.
... ...
@@ -86,3 +86,20 @@ Production-ready
86 86
 Docker is still alpha software, and not suited for production.
87 87
 We are working hard to get there, and we are confident that it will be possible within a few months.
88 88
 
89
+
90
+Advanced port redirections
91
+--------------------------
92
+
93
+Docker currently supports 2 flavors of port redirection: STATIC->STATIC (eg. "redirect public port 80 to private port 80")
94
+and RANDOM->STATIC (eg. "redirect any public port to private port 80").
95
+
96
+With these 2 flavors, docker can support the majority of backend programs out there. But some applications have more exotic
97
+requirements, generally to implement custom clustering techniques. These applications include Hadoop, MongoDB, Riak, RabbitMQ,
98
+Disco, and all programs relying on Erlang's OTP.
99
+
100
+To support these applications, Docker needs to support more advanced redirection flavors, including:
101
+
102
+* RANDOM->RANDOM
103
+* STATIC1->STATIC2
104
+
105
+These flavors should be implemented without breaking existing semantics, if at all possible.
... ...
@@ -13,7 +13,7 @@ run	apt-get update
13 13
 # Packages required to checkout, build and upload docker
14 14
 run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q s3cmd
15 15
 run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q curl
16
-run	curl -s -o /go.tar.gz https://go.googlecode.com/files/go1.1.linux-amd64.tar.gz
16
+run	curl -s -o /go.tar.gz https://go.googlecode.com/files/go1.1.1.linux-amd64.tar.gz
17 17
 run	tar -C /usr/local -xzf /go.tar.gz
18 18
 run	echo "export PATH=/usr/local/go/bin:$PATH" > /.bashrc
19 19
 run	echo "export PATH=/usr/local/go/bin:$PATH" > /.bash_profile
... ...
@@ -126,6 +126,8 @@ func MountAUFS(ro []string, rw string, target string) error {
126 126
 	}
127 127
 	branches := fmt.Sprintf("br:%v:%v", rwBranch, roBranches)
128 128
 
129
+	branches += ",xino=/dev/shm/aufs.xino"
130
+
129 131
 	//if error, try to load aufs kernel module
130 132
 	if err := mount("none", target, "aufs", 0, branches); err != nil {
131 133
 		log.Printf("Kernel does not support AUFS, trying to load the AUFS module with modprobe...")
... ...
@@ -17,7 +17,7 @@ import (
17 17
 )
18 18
 
19 19
 const unitTestImageName string = "docker-ut"
20
-
20
+const unitTestImageId string = "e9aa60c60128cad1"
21 21
 const unitTestStoreBase string = "/var/lib/docker/unit-tests"
22 22
 
23 23
 func nuke(runtime *Runtime) error {
... ...
@@ -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
 	}
... ...
@@ -151,14 +197,20 @@ func (store *TagStore) Get(repoName string) (Repository, error) {
151 151
 	return nil, nil
152 152
 }
153 153
 
154
-func (store *TagStore) GetImage(repoName, tag string) (*Image, error) {
154
+func (store *TagStore) GetImage(repoName, tagOrId string) (*Image, error) {
155 155
 	repo, err := store.Get(repoName)
156 156
 	if err != nil {
157 157
 		return nil, err
158 158
 	} else if repo == nil {
159 159
 		return nil, nil
160 160
 	}
161
-	if revision, exists := repo[tag]; exists {
161
+	//go through all the tags, to see if tag is in fact an ID
162
+	for _, revision := range repo {
163
+		if strings.HasPrefix(revision, tagOrId) {
164
+			return store.graph.Get(revision)
165
+		}
166
+	}
167
+	if revision, exists := repo[tagOrId]; exists {
162 168
 		return store.graph.Get(revision)
163 169
 	}
164 170
 	return nil, nil
165 171
new file mode 100644
... ...
@@ -0,0 +1,49 @@
0
+package docker
1
+
2
+import (
3
+	"testing"
4
+)
5
+
6
+func TestLookupImage(t *testing.T) {
7
+	runtime, err := newTestRuntime()
8
+	if err != nil {
9
+		t.Fatal(err)
10
+	}
11
+	defer nuke(runtime)
12
+
13
+	if img, err := runtime.repositories.LookupImage(unitTestImageName); err != nil {
14
+		t.Fatal(err)
15
+	} else if img == nil {
16
+		t.Errorf("Expected 1 image, none found")
17
+	}
18
+
19
+	if img, err := runtime.repositories.LookupImage(unitTestImageName + ":" + DEFAULTTAG); err != nil {
20
+		t.Fatal(err)
21
+	} else if img == nil {
22
+		t.Errorf("Expected 1 image, none found")
23
+	}
24
+
25
+	if img, err := runtime.repositories.LookupImage(unitTestImageName + ":" + "fail"); err == nil {
26
+		t.Errorf("Expected error, none found")
27
+	} else if img != nil {
28
+		t.Errorf("Expected 0 image, 1 found")
29
+	}
30
+
31
+	if img, err := runtime.repositories.LookupImage("fail:fail"); err == nil {
32
+		t.Errorf("Expected error, none found")
33
+	} else if img != nil {
34
+		t.Errorf("Expected 0 image, 1 found")
35
+	}
36
+
37
+	if img, err := runtime.repositories.LookupImage(unitTestImageId); err != nil {
38
+		t.Fatal(err)
39
+	} else if img == nil {
40
+		t.Errorf("Expected 1 image, none found")
41
+	}
42
+
43
+	if img, err := runtime.repositories.LookupImage(unitTestImageName + ":" + unitTestImageId); err != nil {
44
+		t.Fatal(err)
45
+	} else if img == nil {
46
+		t.Errorf("Expected 1 image, none found")
47
+	}
48
+}