Browse code

Add images & image repositories

Add initial support for images & image repositories.

Add registry webhook to support adding images and tags
to image repositories when a registry's image repository
is updated.

Andy Goldstein authored on 2014/09/03 23:47:57
Showing 27 changed files
... ...
@@ -100,7 +100,6 @@ The Kubernetes APIs are exposed at `http://localhost:8080/api/v1beta1/*`:
100 100
 Several experimental API objects are being prototyped, and should be available soon at:
101 101
 
102 102
 * `http://localhost:8080/osapi/v1beta1/images`
103
-* `http://localhost:8080/osapi/v1beta1/imagesByRepository`
104 103
 * `http://localhost:8080/osapi/v1beta1/imageRepositories`
105 104
 * `http://localhost:8080/osapi/v1beta1/builds`
106 105
 * `http://localhost:8080/osapi/v1beta1/buildConfigs`
107 106
new file mode 100644
... ...
@@ -0,0 +1,13 @@
0
+{
1
+  "id": "test",
2
+  "kind": "ImageRepository",
3
+  "apiVersion": "v1beta1",
4
+  "dockerImageRepository": "openshift/ruby-19-centos",
5
+  "tags": {
6
+    "latest": "foo",
7
+    "another": "bar",
8
+  },
9
+  "labels": {
10
+    "color": "blue"
11
+  }
12
+}
0 13
new file mode 100644
... ...
@@ -0,0 +1,88 @@
0
+{
1
+  "id": "test",
2
+  "kind": "Image",
3
+  "version": "v1beta1",
4
+  "dockerImageReference": "openshift/ruby-19-centos:latest",
5
+  "metadata": {
6
+    "Architecture": "amd64",
7
+    "Author": "Michal Fojtik \u003cmfojtik@redhat.com\u003e",
8
+    "Comment": "",
9
+    "Config": {
10
+        "AttachStderr": false,
11
+        "AttachStdin": false,
12
+        "AttachStdout": false,
13
+        "Cmd": [
14
+            "/opt/ruby/bin/usage"
15
+        ],
16
+        "CpuShares": 0,
17
+        "Cpuset": "",
18
+        "Domainname": "",
19
+        "Entrypoint": null,
20
+        "Env": [
21
+            "HOME=/opt/ruby",
22
+            "PATH=/opt/ruby/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
23
+            "STI_SCRIPTS_URL=https://raw.githubusercontent.com/openshift/ruby-19-centos/master/.sti/bin",
24
+            "APP_ROOT=."
25
+        ],
26
+        "ExposedPorts": {
27
+            "9292/tcp": {}
28
+        },
29
+        "Hostname": "df1704c4368e",
30
+        "Image": "dde5ee6a036d5d2c69240413fa8f3bb9bb1fea25166996eadf42e2f113736401",
31
+        "Memory": 0,
32
+        "MemorySwap": 0,
33
+        "NetworkDisabled": false,
34
+        "OnBuild": [],
35
+        "OpenStdin": false,
36
+        "PortSpecs": null,
37
+        "StdinOnce": false,
38
+        "Tty": false,
39
+        "User": "ruby",
40
+        "Volumes": null,
41
+        "WorkingDir": "/opt/ruby/src"
42
+    },
43
+    "Container": "72ce367abbc2862232b5e5e3485e51f096983e4e28b8dbefba9a6fd5ce0d6e48",
44
+    "ContainerConfig": {
45
+        "AttachStderr": false,
46
+        "AttachStdin": false,
47
+        "AttachStdout": false,
48
+        "Cmd": [
49
+            "/bin/sh",
50
+            "-c",
51
+            "#(nop) CMD [/opt/ruby/bin/usage]"
52
+        ],
53
+"CpuShares": 0,
54
+        "Cpuset": "",
55
+        "Domainname": "",
56
+        "Entrypoint": null,
57
+        "Env": [
58
+            "HOME=/opt/ruby",
59
+            "PATH=/opt/ruby/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
60
+            "STI_SCRIPTS_URL=https://raw.githubusercontent.com/openshift/ruby-19-centos/master/.sti/bin",
61
+            "APP_ROOT=."
62
+        ],
63
+        "ExposedPorts": {
64
+            "9292/tcp": {}
65
+        },
66
+        "Hostname": "df1704c4368e",
67
+        "Image": "dde5ee6a036d5d2c69240413fa8f3bb9bb1fea25166996eadf42e2f113736401",
68
+        "Memory": 0,
69
+        "MemorySwap": 0,
70
+        "NetworkDisabled": false,
71
+        "OnBuild": [],
72
+        "OpenStdin": false,
73
+        "PortSpecs": null,
74
+        "StdinOnce": false,
75
+        "Tty": false,
76
+        "User": "ruby",
77
+        "Volumes": null,
78
+        "WorkingDir": "/opt/ruby/src"
79
+    },
80
+    "Created": "2014-08-08T14:36:01.303084707Z",
81
+    "DockerVersion": "1.1.2-dev",
82
+    "Id": "7dbbbc6cb29d5abc29b722c06d5209a499fa97cd655c59793540d00933ab4e45",
83
+    "Os": "linux",
84
+    "Parent": "dde5ee6a036d5d2c69240413fa8f3bb9bb1fea25166996eadf42e2f113736401",
85
+    "Size": 0
86
+  }
87
+}
0 88
new file mode 100644
... ...
@@ -0,0 +1,94 @@
0
+{
1
+  "kind": "ImageRepositoryMapping",
2
+  "version": "v1beta1",
3
+  "dockerImageRepository": "openshift/ruby-19-centos",
4
+  "image": {
5
+    "kind": "Image",
6
+    "version": "v1beta1",
7
+    "id": "abcd1234",
8
+    "dockerImageReference": "openshift/ruby-19-centos:latest",
9
+    "metadata": {
10
+      "Architecture": "amd64",
11
+      "Author": "Michal Fojtik \u003cmfojtik@redhat.com\u003e",
12
+      "Comment": "",
13
+      "Config": {
14
+          "AttachStderr": false,
15
+          "AttachStdin": false,
16
+          "AttachStdout": false,
17
+          "Cmd": [
18
+              "/opt/ruby/bin/usage"
19
+          ],
20
+          "CpuShares": 0,
21
+          "Cpuset": "",
22
+          "Domainname": "",
23
+          "Entrypoint": null,
24
+          "Env": [
25
+              "HOME=/opt/ruby",
26
+              "PATH=/opt/ruby/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
27
+              "STI_SCRIPTS_URL=https://raw.githubusercontent.com/openshift/ruby-19-centos/master/.sti/bin",
28
+              "APP_ROOT=."
29
+          ],
30
+          "ExposedPorts": {
31
+              "9292/tcp": {}
32
+          },
33
+          "Hostname": "df1704c4368e",
34
+          "Image": "dde5ee6a036d5d2c69240413fa8f3bb9bb1fea25166996eadf42e2f113736401",
35
+          "Memory": 0,
36
+          "MemorySwap": 0,
37
+          "NetworkDisabled": false,
38
+          "OnBuild": [],
39
+          "OpenStdin": false,
40
+          "PortSpecs": null,
41
+          "StdinOnce": false,
42
+          "Tty": false,
43
+          "User": "ruby",
44
+          "Volumes": null,
45
+          "WorkingDir": "/opt/ruby/src"
46
+      },
47
+      "Container": "72ce367abbc2862232b5e5e3485e51f096983e4e28b8dbefba9a6fd5ce0d6e48",
48
+      "ContainerConfig": {
49
+          "AttachStderr": false,
50
+          "AttachStdin": false,
51
+          "AttachStdout": false,
52
+          "Cmd": [
53
+              "/bin/sh",
54
+              "-c",
55
+              "#(nop) CMD [/opt/ruby/bin/usage]"
56
+          ],
57
+  "CpuShares": 0,
58
+          "Cpuset": "",
59
+          "Domainname": "",
60
+          "Entrypoint": null,
61
+          "Env": [
62
+              "HOME=/opt/ruby",
63
+              "PATH=/opt/ruby/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
64
+              "STI_SCRIPTS_URL=https://raw.githubusercontent.com/openshift/ruby-19-centos/master/.sti/bin",
65
+              "APP_ROOT=."
66
+          ],
67
+          "ExposedPorts": {
68
+              "9292/tcp": {}
69
+          },
70
+          "Hostname": "df1704c4368e",
71
+          "Image": "dde5ee6a036d5d2c69240413fa8f3bb9bb1fea25166996eadf42e2f113736401",
72
+          "Memory": 0,
73
+          "MemorySwap": 0,
74
+          "NetworkDisabled": false,
75
+          "OnBuild": [],
76
+          "OpenStdin": false,
77
+          "PortSpecs": null,
78
+          "StdinOnce": false,
79
+          "Tty": false,
80
+          "User": "ruby",
81
+          "Volumes": null,
82
+          "WorkingDir": "/opt/ruby/src"
83
+      },
84
+      "Created": "2014-08-08T14:36:01.303084707Z",
85
+      "DockerVersion": "1.1.2-dev",
86
+      "Id": "7dbbbc6cb29d5abc29b722c06d5209a499fa97cd655c59793540d00933ab4e45",
87
+      "Os": "linux",
88
+      "Parent": "dde5ee6a036d5d2c69240413fa8f3bb9bb1fea25166996eadf42e2f113736401",
89
+      "Size": 0
90
+    }
91
+  },
92
+  "tag": "sometag"
93
+}
... ...
@@ -53,3 +53,19 @@ echo "kube(services): ok"
53 53
 ${KUBE_CMD} list minions
54 54
 ${KUBE_CMD} get minions/127.0.0.1
55 55
 echo "kube(minions): ok"
56
+
57
+${KUBE_CMD} list images
58
+${KUBE_CMD} -c examples/image/test-image.json create images
59
+${KUBE_CMD} delete images/test
60
+echo "kube(images): ok"
61
+
62
+${KUBE_CMD} list imageRepositories
63
+${KUBE_CMD} -c examples/image/test-image-repository.json create imageRepositories
64
+${KUBE_CMD} delete imageRepositories/test
65
+echo "kube(imageRepositories): ok"
66
+
67
+${KUBE_CMD} -c examples/image/test-image-repository.json create imageRepositories
68
+${KUBE_CMD} -c examples/image/test-mapping.json create imageRepositoryMappings
69
+${KUBE_CMD} list images
70
+${KUBE_CMD} list imageRepositories
71
+echo "kube(imageRepositoryMappings): ok"
... ...
@@ -3,13 +3,19 @@ package client
3 3
 import (
4 4
 	kubeclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
5 5
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
6
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
6 7
 	buildapi "github.com/openshift/origin/pkg/build/api"
7 8
 	_ "github.com/openshift/origin/pkg/build/api/v1beta1"
9
+	imageapi "github.com/openshift/origin/pkg/image/api"
10
+	_ "github.com/openshift/origin/pkg/image/api/v1beta1"
8 11
 )
9 12
 
10 13
 // Interface exposes methods on OpenShift resources.
11 14
 type Interface interface {
12 15
 	BuildInterface
16
+	ImageInterface
17
+	ImageRepositoryInterface
18
+	ImageRepositoryMappingInterface
13 19
 }
14 20
 
15 21
 // BuildInterface exposes methods on Build resources.
... ...
@@ -18,6 +24,27 @@ type BuildInterface interface {
18 18
 	UpdateBuild(buildapi.Build) (buildapi.Build, error)
19 19
 }
20 20
 
21
+// ImageInterface exposes methods on Image resources.
22
+type ImageInterface interface {
23
+	ListImages(selector labels.Selector) (imageapi.ImageList, error)
24
+	GetImage(id string) (imageapi.Image, error)
25
+	CreateImage(imageapi.Image) (imageapi.Image, error)
26
+}
27
+
28
+// ImageRepositoryInterface exposes methods on ImageRepository resources.
29
+type ImageRepositoryInterface interface {
30
+	ListImageRepositories(selector labels.Selector) (imageapi.ImageRepositoryList, error)
31
+	GetImageRepository(id string) (imageapi.ImageRepository, error)
32
+	WatchImageRepositories(field, label labels.Selector, resourceVersion uint64) (watch.Interface, error)
33
+	CreateImageRepository(repo imageapi.ImageRepository) (imageapi.ImageRepository, error)
34
+	UpdateImageRepository(repo imageapi.ImageRepository) (imageapi.ImageRepository, error)
35
+}
36
+
37
+// ImageRepositoryMappingInterface exposes methods on ImageRepositoryMapping resources.
38
+type ImageRepositoryMappingInterface interface {
39
+	CreateImageRepositoryMapping(mapping imageapi.ImageRepositoryMapping) error
40
+}
41
+
21 42
 // Client is an OpenShift client object
22 43
 type Client struct {
23 44
 	*kubeclient.RESTClient
... ...
@@ -43,3 +70,52 @@ func (c *Client) UpdateBuild(build buildapi.Build) (result buildapi.Build, err e
43 43
 	err = c.Put().Path("builds").Path(build.ID).Body(build).Do().Into(&result)
44 44
 	return
45 45
 }
46
+
47
+func (c *Client) ListImages(selector labels.Selector) (result imageapi.ImageList, err error) {
48
+	err = c.Get().Path("images").SelectorParam("labels", selector).Do().Into(&result)
49
+	return
50
+}
51
+
52
+func (c *Client) GetImage(id string) (result imageapi.Image, err error) {
53
+	err = c.Get().Path("images").Path(id).Do().Into(&result)
54
+	return
55
+}
56
+
57
+func (c *Client) CreateImage(image imageapi.Image) (result imageapi.Image, err error) {
58
+	err = c.Post().Path("images").Body(image).Do().Into(&result)
59
+	return
60
+}
61
+
62
+func (c *Client) ListImageRepositories(selector labels.Selector) (result imageapi.ImageRepositoryList, err error) {
63
+	err = c.Get().Path("imageRepositories").SelectorParam("labels", selector).Do().Into(&result)
64
+	return
65
+}
66
+
67
+func (c *Client) GetImageRepository(id string) (result imageapi.ImageRepository, err error) {
68
+	err = c.Get().Path("imageRepositories").Path(id).Do().Into(&result)
69
+	return
70
+}
71
+
72
+func (c *Client) WatchImageRepositories(field, label labels.Selector, resourceVersion uint64) (watch.Interface, error) {
73
+	return c.Get().
74
+		Path("watch").
75
+		Path("imageRepositories").
76
+		UintParam("resourceVersion", resourceVersion).
77
+		SelectorParam("labels", label).
78
+		SelectorParam("fields", field).
79
+		Watch()
80
+}
81
+
82
+func (c *Client) CreateImageRepository(repo imageapi.ImageRepository) (result imageapi.ImageRepository, err error) {
83
+	err = c.Post().Path("imageRepositories").Body(repo).Do().Into(&result)
84
+	return
85
+}
86
+
87
+func (c *Client) UpdateImageRepository(repo imageapi.ImageRepository) (result imageapi.ImageRepository, err error) {
88
+	err = c.Put().Path("imageRepositories").Path(repo.ID).Body(repo).Do().Into(&result)
89
+	return
90
+}
91
+
92
+func (c *Client) CreateImageRepositoryMapping(mapping imageapi.ImageRepositoryMapping) error {
93
+	return c.Post().Path("imageRepositoryMappings").Body(mapping).Do().Error()
94
+}
46 95
new file mode 100644
... ...
@@ -0,0 +1,57 @@
0
+package image
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+	"strings"
6
+
7
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/kubecfg"
8
+	"github.com/openshift/origin/pkg/image/api"
9
+)
10
+
11
+var imageColumns = []string{"ID", "Docker Ref"}
12
+var imageRepositoryColumns = []string{"ID", "Docker Repo", "Tags"}
13
+
14
+// RegisterPrintHandlers registers HumanReadablePrinter handlers for image and image repository resources.
15
+func RegisterPrintHandlers(printer *kubecfg.HumanReadablePrinter) {
16
+	printer.Handler(imageColumns, printImage)
17
+	printer.Handler(imageColumns, printImageList)
18
+	printer.Handler(imageRepositoryColumns, printImageRepository)
19
+	printer.Handler(imageRepositoryColumns, printImageRepositoryList)
20
+}
21
+
22
+func printImage(image *api.Image, w io.Writer) error {
23
+	_, err := fmt.Fprintf(w, "%s\t%s\n", image.ID, image.DockerImageReference)
24
+	return err
25
+}
26
+
27
+func printImageList(images *api.ImageList, w io.Writer) error {
28
+	for _, image := range images.Items {
29
+		if err := printImage(&image, w); err != nil {
30
+			return err
31
+		}
32
+	}
33
+	return nil
34
+}
35
+
36
+func printImageRepository(repo *api.ImageRepository, w io.Writer) error {
37
+	tags := ""
38
+	if len(repo.Tags) > 0 {
39
+		var t []string
40
+		for tag, _ := range repo.Tags {
41
+			t = append(t, tag)
42
+		}
43
+		tags = strings.Join(t, ",")
44
+	}
45
+	_, err := fmt.Fprintf(w, "%s\t%s\t%s\n", repo.ID, repo.DockerImageRepository, tags)
46
+	return err
47
+}
48
+
49
+func printImageRepositoryList(repos *api.ImageRepositoryList, w io.Writer) error {
50
+	for _, repo := range repos.Items {
51
+		if err := printImageRepository(&repo, w); err != nil {
52
+			return err
53
+		}
54
+	}
55
+	return nil
56
+}
... ...
@@ -39,6 +39,8 @@ import (
39 39
 	buildapi "github.com/openshift/origin/pkg/build/api"
40 40
 	osclient "github.com/openshift/origin/pkg/client"
41 41
 	"github.com/openshift/origin/pkg/cmd/client/build"
42
+	"github.com/openshift/origin/pkg/cmd/client/image"
43
+	imageapi "github.com/openshift/origin/pkg/image/api"
42 44
 )
43 45
 
44 46
 type RESTClient interface {
... ...
@@ -86,12 +88,15 @@ func usage(name string) string {
86 86
 }
87 87
 
88 88
 var parser = kubecfg.NewParser(map[string]interface{}{
89
-	"pods":                   api.Pod{},
90
-	"services":               api.Service{},
91
-	"replicationControllers": api.ReplicationController{},
92
-	"minions":                api.Minion{},
93
-	"builds":                 buildapi.Build{},
94
-	"buildConfigs":           buildapi.BuildConfig{},
89
+	"pods":                    api.Pod{},
90
+	"services":                api.Service{},
91
+	"replicationControllers":  api.ReplicationController{},
92
+	"minions":                 api.Minion{},
93
+	"builds":                  buildapi.Build{},
94
+	"buildConfigs":            buildapi.BuildConfig{},
95
+	"images":                  imageapi.Image{},
96
+	"imageRepositories":       imageapi.ImageRepository{},
97
+	"imageRepositoryMappings": imageapi.ImageRepositoryMapping{},
95 98
 })
96 99
 
97 100
 func prettyWireStorage() string {
... ...
@@ -209,12 +214,15 @@ func (c *KubeConfig) Run() {
209 209
 
210 210
 	method := c.Arg(0)
211 211
 	clients := map[string]RESTClient{
212
-		"minions":                kubeClient.RESTClient,
213
-		"pods":                   kubeClient.RESTClient,
214
-		"services":               kubeClient.RESTClient,
215
-		"replicationControllers": kubeClient.RESTClient,
216
-		"builds":                 client.RESTClient,
217
-		"buildConfigs":           client.RESTClient,
212
+		"minions":                 kubeClient.RESTClient,
213
+		"pods":                    kubeClient.RESTClient,
214
+		"services":                kubeClient.RESTClient,
215
+		"replicationControllers":  kubeClient.RESTClient,
216
+		"builds":                  client.RESTClient,
217
+		"buildConfigs":            client.RESTClient,
218
+		"images":                  client.RESTClient,
219
+		"imageRepositories":       client.RESTClient,
220
+		"imageRepositoryMappings": client.RESTClient,
218 221
 	}
219 222
 
220 223
 	matchFound := c.executeAPIRequest(method, clients) || c.executeControllerRequest(method, kubeClient)
... ...
@@ -415,7 +423,10 @@ func (c *KubeConfig) executeControllerRequest(method string, client *kubeclient.
415 415
 
416 416
 func humanReadablePrinter() *kubecfg.HumanReadablePrinter {
417 417
 	printer := kubecfg.NewHumanReadablePrinter()
418
-	build.RegisterPrintHandlers(printer)
418
+
419 419
 	// Add Handler calls here to support additional types
420
+	build.RegisterPrintHandlers(printer)
421
+	image.RegisterPrintHandlers(printer)
422
+
420 423
 	return printer
421 424
 }
... ...
@@ -31,24 +31,38 @@ import (
31 31
 	buildconfigregistry "github.com/openshift/origin/pkg/build/registry/buildconfig"
32 32
 	"github.com/openshift/origin/pkg/build/strategy"
33 33
 	osclient "github.com/openshift/origin/pkg/client"
34
+	"github.com/openshift/origin/pkg/image"
35
+	_ "github.com/openshift/origin/pkg/image/api/v1beta1"
34 36
 	"github.com/spf13/cobra"
35 37
 )
36 38
 
37 39
 func NewCommandStartAllInOne(name string) *cobra.Command {
38
-	return &cobra.Command{
40
+	cfg := &Config{}
41
+
42
+	cmd := &cobra.Command{
39 43
 		Use:   name,
40 44
 		Short: "Launch in all-in-one mode",
41 45
 		Run: func(c *cobra.Command, args []string) {
42
-			startAllInOne()
46
+			cfg.startAllInOne()
43 47
 		},
44 48
 	}
49
+
50
+	flag := cmd.Flags()
51
+	flag.StringVar(&cfg.ListenAddr, "listenAddr", "127.0.0.1:8080", "The OpenShift server listen address.")
52
+
53
+	return cmd
45 54
 }
46 55
 
47
-func startAllInOne() {
56
+type Config struct {
57
+	ListenAddr string
58
+}
59
+
60
+func (c *Config) startAllInOne() {
48 61
 	minionHost := "127.0.0.1"
49 62
 	minionPort := 10250
50 63
 	rootDirectory := path.Clean("/var/lib/openshift")
51
-	osAddr := "127.0.0.1:8080"
64
+	osAddr := c.ListenAddr
65
+
52 66
 	osPrefix := "/osapi/v1beta1"
53 67
 	kubePrefix := "/api/v1beta1"
54 68
 	kubeClient, err := kubeclient.New("http://"+osAddr, nil)
... ...
@@ -117,10 +131,15 @@ func startAllInOne() {
117 117
 		kubelet.ListenAndServeKubeletServer(k, cfg.Channel("http"), minionHost, uint(minionPort))
118 118
 	}, 0)
119 119
 
120
+	imageRegistry := image.NewEtcdRegistry(etcdClient)
121
+
120 122
 	// initialize OpenShift API
121 123
 	storage := map[string]apiserver.RESTStorage{
122
-		"builds":       buildregistry.NewStorage(build.NewEtcdRegistry(etcdClient)),
123
-		"buildConfigs": buildconfigregistry.NewStorage(build.NewEtcdRegistry(etcdClient)),
124
+		"builds":                  buildregistry.NewStorage(build.NewEtcdRegistry(etcdClient)),
125
+		"buildConfigs":            buildconfigregistry.NewStorage(build.NewEtcdRegistry(etcdClient)),
126
+		"images":                  image.NewImageStorage(imageRegistry),
127
+		"imageRepositories":       image.NewImageRepositoryStorage(imageRegistry),
128
+		"imageRepositoryMappings": image.NewImageRepositoryMappingStorage(imageRegistry, imageRegistry),
124 129
 	}
125 130
 
126 131
 	osMux := http.NewServeMux()
127 132
new file mode 100644
... ...
@@ -0,0 +1,13 @@
0
+package api
1
+
2
+import "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
3
+
4
+func init() {
5
+	runtime.AddKnownTypes("",
6
+		Image{},
7
+		ImageList{},
8
+		ImageRepository{},
9
+		ImageRepositoryList{},
10
+		ImageRepositoryMapping{},
11
+	)
12
+}
0 13
new file mode 100644
... ...
@@ -0,0 +1,47 @@
0
+package api
1
+
2
+import (
3
+	kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
4
+	"github.com/fsouza/go-dockerclient"
5
+)
6
+
7
+// ImageList is a list of Image objects.
8
+type ImageList struct {
9
+	kubeapi.JSONBase `json:",inline" yaml:",inline"`
10
+	Items            []Image `json:"items,omitempty" yaml:"items,omitempty"`
11
+}
12
+
13
+// Image is an immutable representation of a Docker image and metadata at a point in time.
14
+type Image struct {
15
+	kubeapi.JSONBase     `json:",inline" yaml:",inline"`
16
+	Labels               map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
17
+	DockerImageReference string            `json:"dockerImageReference,omitempty" yaml:"dockerImageReference,omitempty"`
18
+	Metadata             docker.Image      `json:"metadata,omitempty" yaml:"metadata,omitempty"`
19
+}
20
+
21
+// ImageRepositoryList is a list of ImageRepository objects.
22
+type ImageRepositoryList struct {
23
+	kubeapi.JSONBase `json:",inline" yaml:",inline"`
24
+	Items            []ImageRepository `json:"items,omitempty" yaml:"items,omitempty"`
25
+}
26
+
27
+// ImageRepository stores a mapping of tags to images, metadata overrides that are applied
28
+// when images are tagged in a repository, and an optional reference to a Docker image
29
+// repository on a registry.
30
+type ImageRepository struct {
31
+	kubeapi.JSONBase      `json:",inline" yaml:",inline"`
32
+	Labels                map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
33
+	DockerImageRepository string            `json:"dockerImageRepository,omitempty" yaml:"dockerImageRepository,omitempty"`
34
+	Tags                  map[string]string `json:"tags,omitempty" yaml:"tags,omitempty"`
35
+}
36
+
37
+// TODO add metadata overrides
38
+
39
+// ImageRepositoryMapping represents a mapping from a single tag to a Docker image as
40
+// well as the reference to the Docker image repository the image came from.
41
+type ImageRepositoryMapping struct {
42
+	kubeapi.JSONBase      `json:",inline" yaml:",inline"`
43
+	DockerImageRepository string `json:"dockerImageRepository" yaml:"dockerImageRepository"`
44
+	Image                 Image  `json:"image" yaml:"image"`
45
+	Tag                   string `json:"tag" yaml:"tag"`
46
+}
0 47
new file mode 100644
... ...
@@ -0,0 +1,13 @@
0
+package v1beta1
1
+
2
+import "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
3
+
4
+func init() {
5
+	runtime.AddKnownTypes("v1beta1",
6
+		Image{},
7
+		ImageList{},
8
+		ImageRepository{},
9
+		ImageRepositoryList{},
10
+		ImageRepositoryMapping{},
11
+	)
12
+}
0 13
new file mode 100644
... ...
@@ -0,0 +1,47 @@
0
+package v1beta1
1
+
2
+import (
3
+	kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
4
+	"github.com/fsouza/go-dockerclient"
5
+)
6
+
7
+// ImageList is a list of Image objects.
8
+type ImageList struct {
9
+	kubeapi.JSONBase `json:",inline" yaml:",inline"`
10
+	Items            []Image `json:"items,omitempty" yaml:"items,omitempty"`
11
+}
12
+
13
+// Image is an immutable representation of a Docker image and metadata at a point in time.
14
+type Image struct {
15
+	kubeapi.JSONBase     `json:",inline" yaml:",inline"`
16
+	Labels               map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
17
+	DockerImageReference string            `json:"dockerImageReference,omitempty" yaml:"dockerImageReference,omitempty"`
18
+	Metadata             docker.Image      `json:"metadata,omitempty" yaml:"metadata,omitempty"`
19
+}
20
+
21
+// ImageRepositoryList is a list of ImageRepository objects.
22
+type ImageRepositoryList struct {
23
+	kubeapi.JSONBase `json:",inline" yaml:",inline"`
24
+	Items            []ImageRepository `json:"items,omitempty" yaml:"items,omitempty"`
25
+}
26
+
27
+// ImageRepository stores a mapping of tags to images, metadata overrides that are applied
28
+// when images are tagged in a repository, and an optional reference to a Docker image
29
+// repository on a registry.
30
+type ImageRepository struct {
31
+	kubeapi.JSONBase      `json:",inline" yaml:",inline"`
32
+	Labels                map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
33
+	DockerImageRepository string            `json:"dockerImageRepository,omitempty" yaml:"dockerImageRepository,omitempty"`
34
+	Tags                  map[string]string `json:"tags,omitempty" yaml:"tags,omitempty"`
35
+}
36
+
37
+// TODO add metadata overrides
38
+
39
+// ImageRepositoryMapping represents a mapping from a single tag to a Docker image as
40
+// well as the reference to the Docker image repository the image came from.
41
+type ImageRepositoryMapping struct {
42
+	kubeapi.JSONBase      `json:",inline" yaml:",inline"`
43
+	DockerImageRepository string `json:"dockerImageRepository" yaml:"dockerImageRepository"`
44
+	Image                 Image  `json:"image" yaml:"image"`
45
+	Tag                   string `json:"tag" yaml:"tag"`
46
+}
0 47
new file mode 100644
... ...
@@ -0,0 +1,3 @@
0
+// Package image provides support for images, image repositories, and image repository mappings,
1
+// including RESTStorage implementations and registries.
2
+package image
0 3
new file mode 100644
... ...
@@ -0,0 +1,152 @@
0
+package image
1
+
2
+import (
3
+	"errors"
4
+
5
+	apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
6
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
7
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
8
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
10
+	"github.com/golang/glog"
11
+	"github.com/openshift/origin/pkg/image/api"
12
+)
13
+
14
+// EtcdRegistry implements ImageRegistry and ImageRepositoryRegistry backed by etcd.
15
+type EtcdRegistry struct {
16
+	tools.EtcdHelper
17
+}
18
+
19
+// NewEtcdRegistry returns a new EtcdRegistry.
20
+func NewEtcdRegistry(client tools.EtcdClient) *EtcdRegistry {
21
+	registry := &EtcdRegistry{
22
+		EtcdHelper: tools.EtcdHelper{
23
+			client,
24
+			runtime.Codec,
25
+			runtime.ResourceVersioner,
26
+		},
27
+	}
28
+
29
+	return registry
30
+}
31
+
32
+// ListImages retrieves a list of images that match selector.
33
+func (r *EtcdRegistry) ListImages(selector labels.Selector) (*api.ImageList, error) {
34
+	list := api.ImageList{}
35
+	err := r.ExtractList("/images", &list.Items, &list.ResourceVersion)
36
+	if err != nil {
37
+		return nil, err
38
+	}
39
+	filtered := []api.Image{}
40
+	for _, item := range list.Items {
41
+		if selector.Matches(labels.Set(item.Labels)) {
42
+			filtered = append(filtered, item)
43
+		}
44
+	}
45
+	list.Items = filtered
46
+	return &list, nil
47
+}
48
+
49
+func makeImageKey(id string) string {
50
+	return "/images/" + id
51
+}
52
+
53
+// GetImage retrieves a specific image
54
+func (r *EtcdRegistry) GetImage(id string) (*api.Image, error) {
55
+	var image api.Image
56
+	if err := r.ExtractObj(makeImageKey(id), &image, false); err != nil {
57
+		return nil, err
58
+	}
59
+	return &image, nil
60
+}
61
+
62
+// CreateImage creates a new image
63
+func (r *EtcdRegistry) CreateImage(image api.Image) error {
64
+	err := r.CreateObj(makeImageKey(image.ID), &image)
65
+	if tools.IsEtcdNodeExist(err) {
66
+		return apierrors.NewAlreadyExists("image", image.ID)
67
+	}
68
+	return err
69
+}
70
+
71
+// UpdateImage updates an existing image
72
+func (r *EtcdRegistry) UpdateImage(image api.Image) error {
73
+	return errors.New("not supported")
74
+}
75
+
76
+// DeleteImage deletes an existing image
77
+func (r *EtcdRegistry) DeleteImage(id string) error {
78
+	key := makeImageKey(id)
79
+	err := r.Delete(key, false)
80
+	if tools.IsEtcdNotFound(err) {
81
+		return apierrors.NewNotFound("image", id)
82
+	}
83
+	return err
84
+}
85
+
86
+// ListImageRepositories retrieves a list of ImageRepositories that match selector.
87
+func (r *EtcdRegistry) ListImageRepositories(selector labels.Selector) (*api.ImageRepositoryList, error) {
88
+	list := api.ImageRepositoryList{}
89
+	err := r.ExtractList("/imageRepositories", &list.Items, &list.ResourceVersion)
90
+	if err != nil {
91
+		return nil, err
92
+	}
93
+	filtered := []api.ImageRepository{}
94
+	for _, item := range list.Items {
95
+		if selector.Matches(labels.Set(item.Labels)) {
96
+			filtered = append(filtered, item)
97
+		}
98
+	}
99
+	list.Items = filtered
100
+	return &list, nil
101
+}
102
+
103
+func makeImageRepositoryKey(id string) string {
104
+	return "/imageRepositories/" + id
105
+}
106
+
107
+// GetImageRepository retrieves an ImageRepository by id.
108
+func (r *EtcdRegistry) GetImageRepository(id string) (*api.ImageRepository, error) {
109
+	var repo api.ImageRepository
110
+	if err := r.ExtractObj(makeImageRepositoryKey(id), &repo, false); err != nil {
111
+		return nil, err
112
+	}
113
+	return &repo, nil
114
+}
115
+
116
+// WatchImageRepositories begins watching for new, changed, or deleted ImageRepositories.
117
+func (r *EtcdRegistry) WatchImageRepositories(resourceVersion uint64, filter func(repo *api.ImageRepository) bool) (watch.Interface, error) {
118
+	return r.WatchList("/imageRepositories", resourceVersion, func(obj interface{}) bool {
119
+		repo, ok := obj.(*api.ImageRepository)
120
+		if !ok {
121
+			glog.Errorf("Unexpected object during image repository watch: %#v", obj)
122
+			return false
123
+		}
124
+		return filter(repo)
125
+	})
126
+}
127
+
128
+// CreateImageRepository registers the given ImageRepository.
129
+func (r *EtcdRegistry) CreateImageRepository(repo api.ImageRepository) error {
130
+	err := r.CreateObj(makeImageRepositoryKey(repo.ID), &repo)
131
+	if err != nil && tools.IsEtcdNodeExist(err) {
132
+		return apierrors.NewAlreadyExists("imageRepository", repo.ID)
133
+	}
134
+
135
+	return err
136
+}
137
+
138
+// UpdateImageRepository replaces an existing ImageRepository in the registry with the given ImageRepository.
139
+func (r *EtcdRegistry) UpdateImageRepository(repo api.ImageRepository) error {
140
+	return r.SetObj(makeImageRepositoryKey(repo.ID), &repo)
141
+}
142
+
143
+// DeleteImageRepository deletes an ImageRepository by id.
144
+func (r *EtcdRegistry) DeleteImageRepository(id string) error {
145
+	imageRepositoryKey := makeImageRepositoryKey(id)
146
+	err := r.Delete(imageRepositoryKey, false)
147
+	if err != nil && tools.IsEtcdNotFound(err) {
148
+		return apierrors.NewNotFound("imageRepository", id)
149
+	}
150
+	return err
151
+}
0 152
new file mode 100644
... ...
@@ -0,0 +1,594 @@
0
+package image
1
+
2
+import (
3
+	"fmt"
4
+	"reflect"
5
+	"testing"
6
+
7
+	kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
8
+	kubeerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
10
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
11
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
12
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
13
+	"github.com/coreos/go-etcd/etcd"
14
+	"github.com/fsouza/go-dockerclient"
15
+	"github.com/openshift/origin/pkg/image/api"
16
+	_ "github.com/openshift/origin/pkg/image/api/v1beta1"
17
+)
18
+
19
+func NewTestEtcdRegistry(client tools.EtcdClient) *EtcdRegistry {
20
+	return NewEtcdRegistry(client)
21
+}
22
+
23
+func TestEtcdListImagesEmpty(t *testing.T) {
24
+	fakeClient := tools.NewFakeEtcdClient(t)
25
+	key := "/images"
26
+	fakeClient.Data[key] = tools.EtcdResponseWithError{
27
+		R: &etcd.Response{
28
+			Node: &etcd.Node{
29
+				Nodes: []*etcd.Node{},
30
+			},
31
+		},
32
+		E: nil,
33
+	}
34
+	registry := NewTestEtcdRegistry(fakeClient)
35
+	images, err := registry.ListImages(labels.Everything())
36
+	if err != nil {
37
+		t.Errorf("unexpected error: %v", err)
38
+	}
39
+
40
+	if len(images.Items) != 0 {
41
+		t.Errorf("Unexpected images list: %#v", images)
42
+	}
43
+}
44
+
45
+func TestEtcdListImagesError(t *testing.T) {
46
+	fakeClient := tools.NewFakeEtcdClient(t)
47
+	key := "/images"
48
+	fakeClient.Data[key] = tools.EtcdResponseWithError{
49
+		R: &etcd.Response{
50
+			Node: nil,
51
+		},
52
+		E: fmt.Errorf("some error"),
53
+	}
54
+	registry := NewTestEtcdRegistry(fakeClient)
55
+	images, err := registry.ListImages(labels.Everything())
56
+	if err == nil {
57
+		t.Error("unexpected nil error")
58
+	}
59
+
60
+	if images != nil {
61
+		t.Errorf("Unexpected non-nil images: %#v", images)
62
+	}
63
+}
64
+
65
+func TestEtcdListImagesEverything(t *testing.T) {
66
+	fakeClient := tools.NewFakeEtcdClient(t)
67
+	key := "/images"
68
+	fakeClient.Data[key] = tools.EtcdResponseWithError{
69
+		R: &etcd.Response{
70
+			Node: &etcd.Node{
71
+				Nodes: []*etcd.Node{
72
+					{
73
+						Value: runtime.EncodeOrDie(api.Image{JSONBase: kubeapi.JSONBase{ID: "foo"}}),
74
+					},
75
+					{
76
+						Value: runtime.EncodeOrDie(api.Image{JSONBase: kubeapi.JSONBase{ID: "bar"}}),
77
+					},
78
+				},
79
+			},
80
+		},
81
+		E: nil,
82
+	}
83
+	registry := NewTestEtcdRegistry(fakeClient)
84
+	images, err := registry.ListImages(labels.Everything())
85
+	if err != nil {
86
+		t.Errorf("unexpected error: %v", err)
87
+	}
88
+
89
+	if len(images.Items) != 2 || images.Items[0].ID != "foo" || images.Items[1].ID != "bar" {
90
+		t.Errorf("Unexpected images list: %#v", images)
91
+	}
92
+}
93
+
94
+func TestEtcdListImagesFiltered(t *testing.T) {
95
+	fakeClient := tools.NewFakeEtcdClient(t)
96
+	key := "/images"
97
+	fakeClient.Data[key] = tools.EtcdResponseWithError{
98
+		R: &etcd.Response{
99
+			Node: &etcd.Node{
100
+				Nodes: []*etcd.Node{
101
+					{
102
+						Value: runtime.EncodeOrDie(api.Image{
103
+							JSONBase: kubeapi.JSONBase{ID: "foo"},
104
+							Labels:   map[string]string{"env": "prod"},
105
+						}),
106
+					},
107
+					{
108
+						Value: runtime.EncodeOrDie(api.Image{
109
+							JSONBase: kubeapi.JSONBase{ID: "bar"},
110
+							Labels:   map[string]string{"env": "dev"},
111
+						}),
112
+					},
113
+				},
114
+			},
115
+		},
116
+		E: nil,
117
+	}
118
+	registry := NewTestEtcdRegistry(fakeClient)
119
+	images, err := registry.ListImages(labels.SelectorFromSet(labels.Set{"env": "dev"}))
120
+	if err != nil {
121
+		t.Errorf("unexpected error: %v", err)
122
+	}
123
+
124
+	if len(images.Items) != 1 || images.Items[0].ID != "bar" {
125
+		t.Errorf("Unexpected images list: %#v", images)
126
+	}
127
+}
128
+
129
+func TestEtcdGetImage(t *testing.T) {
130
+	fakeClient := tools.NewFakeEtcdClient(t)
131
+	fakeClient.Set("/images/foo", runtime.EncodeOrDie(api.Image{JSONBase: kubeapi.JSONBase{ID: "foo"}}), 0)
132
+	registry := NewTestEtcdRegistry(fakeClient)
133
+	image, err := registry.GetImage("foo")
134
+	if err != nil {
135
+		t.Errorf("unexpected error: %v", err)
136
+	}
137
+
138
+	if image.ID != "foo" {
139
+		t.Errorf("Unexpected image: %#v", image)
140
+	}
141
+}
142
+
143
+func TestEtcdGetImageNotFound(t *testing.T) {
144
+	fakeClient := tools.NewFakeEtcdClient(t)
145
+	fakeClient.Data["/images/foo"] = tools.EtcdResponseWithError{
146
+		R: &etcd.Response{
147
+			Node: nil,
148
+		},
149
+		E: tools.EtcdErrorNotFound,
150
+	}
151
+	registry := NewTestEtcdRegistry(fakeClient)
152
+	image, err := registry.GetImage("foo")
153
+	if err == nil {
154
+		t.Errorf("Unexpected non-error.")
155
+	}
156
+	if image != nil {
157
+		t.Errorf("Unexpected image: %#v", image)
158
+	}
159
+}
160
+
161
+func TestEtcdCreateImage(t *testing.T) {
162
+	fakeClient := tools.NewFakeEtcdClient(t)
163
+	fakeClient.TestIndex = true
164
+	fakeClient.Data["/images/foo"] = tools.EtcdResponseWithError{
165
+		R: &etcd.Response{
166
+			Node: nil,
167
+		},
168
+		E: tools.EtcdErrorNotFound,
169
+	}
170
+	registry := NewTestEtcdRegistry(fakeClient)
171
+	err := registry.CreateImage(api.Image{
172
+		JSONBase: kubeapi.JSONBase{
173
+			ID: "foo",
174
+		},
175
+		DockerImageReference: "openshift/ruby-19-centos",
176
+		Metadata: docker.Image{
177
+			ID: "abc123",
178
+		},
179
+	})
180
+	if err != nil {
181
+		t.Fatalf("unexpected error: %v", err)
182
+	}
183
+
184
+	resp, err := fakeClient.Get("/images/foo", false, false)
185
+	if err != nil {
186
+		t.Fatalf("Unexpected error %v", err)
187
+	}
188
+	var image api.Image
189
+	err = runtime.DecodeInto([]byte(resp.Node.Value), &image)
190
+	if err != nil {
191
+		t.Errorf("unexpected error: %v", err)
192
+	}
193
+
194
+	if image.ID != "foo" {
195
+		t.Errorf("Unexpected image: %#v %s", image, resp.Node.Value)
196
+	}
197
+
198
+	if e, a := "openshift/ruby-19-centos", image.DockerImageReference; e != a {
199
+		t.Errorf("Expected %v, got %v", e, a)
200
+	}
201
+
202
+	if e, a := "abc123", image.Metadata.ID; e != a {
203
+		t.Errorf("Expected %v, got %v", e, a)
204
+	}
205
+}
206
+
207
+func TestEtcdCreateImageAlreadyExists(t *testing.T) {
208
+	fakeClient := tools.NewFakeEtcdClient(t)
209
+	fakeClient.Data["/images/foo"] = tools.EtcdResponseWithError{
210
+		R: &etcd.Response{
211
+			Node: &etcd.Node{
212
+				Value: runtime.EncodeOrDie(api.Image{JSONBase: kubeapi.JSONBase{ID: "foo"}}),
213
+			},
214
+		},
215
+		E: nil,
216
+	}
217
+	registry := NewTestEtcdRegistry(fakeClient)
218
+	err := registry.CreateImage(api.Image{
219
+		JSONBase: kubeapi.JSONBase{
220
+			ID: "foo",
221
+		},
222
+	})
223
+	if err == nil {
224
+		t.Error("Unexpected non-error")
225
+	}
226
+	if !kubeerrors.IsAlreadyExists(err) {
227
+		t.Errorf("Expected 'already exists' error, got %#v", err)
228
+	}
229
+}
230
+
231
+func TestEtcdUpdateImage(t *testing.T) {
232
+	fakeClient := tools.NewFakeEtcdClient(t)
233
+	registry := NewTestEtcdRegistry(fakeClient)
234
+	err := registry.UpdateImage(api.Image{})
235
+	if err == nil {
236
+		t.Error("Unexpected non-error")
237
+	}
238
+}
239
+
240
+func TestEtcdDeleteImageNotFound(t *testing.T) {
241
+	fakeClient := tools.NewFakeEtcdClient(t)
242
+	fakeClient.Err = tools.EtcdErrorNotFound
243
+	registry := NewTestEtcdRegistry(fakeClient)
244
+	err := registry.DeleteImage("foo")
245
+	if err == nil {
246
+		t.Error("Unexpected non-error")
247
+	}
248
+	if !kubeerrors.IsNotFound(err) {
249
+		t.Errorf("Expected 'not found' error, got %#v", err)
250
+	}
251
+}
252
+
253
+func TestEtcdDeleteImageError(t *testing.T) {
254
+	fakeClient := tools.NewFakeEtcdClient(t)
255
+	fakeClient.Err = fmt.Errorf("Some error")
256
+	registry := NewTestEtcdRegistry(fakeClient)
257
+	err := registry.DeleteImage("foo")
258
+	if err == nil {
259
+		t.Error("Unexpected non-error")
260
+	}
261
+}
262
+
263
+func TestEtcdDeleteImageOK(t *testing.T) {
264
+	fakeClient := tools.NewFakeEtcdClient(t)
265
+	registry := NewTestEtcdRegistry(fakeClient)
266
+	key := "/images/foo"
267
+	err := registry.DeleteImage("foo")
268
+	if err != nil {
269
+		t.Errorf("Unexpected error: %#v", err)
270
+	}
271
+	if len(fakeClient.DeletedKeys) != 1 {
272
+		t.Errorf("Expected 1 delete, found %#v", fakeClient.DeletedKeys)
273
+	} else if fakeClient.DeletedKeys[0] != key {
274
+		t.Errorf("Unexpected key: %s, expected %s", fakeClient.DeletedKeys[0], key)
275
+	}
276
+}
277
+
278
+func TestEtcdListImagesRepositoriesEmpty(t *testing.T) {
279
+	fakeClient := tools.NewFakeEtcdClient(t)
280
+	key := "/imageRepositories"
281
+	fakeClient.Data[key] = tools.EtcdResponseWithError{
282
+		R: &etcd.Response{
283
+			Node: &etcd.Node{
284
+				Nodes: []*etcd.Node{},
285
+			},
286
+		},
287
+		E: nil,
288
+	}
289
+	registry := NewTestEtcdRegistry(fakeClient)
290
+	repos, err := registry.ListImageRepositories(labels.Everything())
291
+	if err != nil {
292
+		t.Errorf("unexpected error: %v", err)
293
+	}
294
+
295
+	if len(repos.Items) != 0 {
296
+		t.Errorf("Unexpected image repositories list: %#v", repos)
297
+	}
298
+}
299
+
300
+func TestEtcdListImageRepositoriesError(t *testing.T) {
301
+	fakeClient := tools.NewFakeEtcdClient(t)
302
+	key := "/imageRepositories"
303
+	fakeClient.Data[key] = tools.EtcdResponseWithError{
304
+		R: &etcd.Response{
305
+			Node: nil,
306
+		},
307
+		E: fmt.Errorf("some error"),
308
+	}
309
+	registry := NewTestEtcdRegistry(fakeClient)
310
+	repos, err := registry.ListImageRepositories(labels.Everything())
311
+	if err == nil {
312
+		t.Error("unexpected nil error")
313
+	}
314
+
315
+	if repos != nil {
316
+		t.Errorf("Unexpected non-nil repos: %#v", repos)
317
+	}
318
+}
319
+
320
+func TestEtcdListImageRepositoriesEverything(t *testing.T) {
321
+	fakeClient := tools.NewFakeEtcdClient(t)
322
+	key := "/imageRepositories"
323
+	fakeClient.Data[key] = tools.EtcdResponseWithError{
324
+		R: &etcd.Response{
325
+			Node: &etcd.Node{
326
+				Nodes: []*etcd.Node{
327
+					{
328
+						Value: runtime.EncodeOrDie(api.ImageRepository{JSONBase: kubeapi.JSONBase{ID: "foo"}}),
329
+					},
330
+					{
331
+						Value: runtime.EncodeOrDie(api.ImageRepository{JSONBase: kubeapi.JSONBase{ID: "bar"}}),
332
+					},
333
+				},
334
+			},
335
+		},
336
+		E: nil,
337
+	}
338
+	registry := NewTestEtcdRegistry(fakeClient)
339
+	repos, err := registry.ListImageRepositories(labels.Everything())
340
+	if err != nil {
341
+		t.Errorf("unexpected error: %v", err)
342
+	}
343
+
344
+	if len(repos.Items) != 2 || repos.Items[0].ID != "foo" || repos.Items[1].ID != "bar" {
345
+		t.Errorf("Unexpected images list: %#v", repos)
346
+	}
347
+}
348
+
349
+func TestEtcdListImageRepositoriesFiltered(t *testing.T) {
350
+	fakeClient := tools.NewFakeEtcdClient(t)
351
+	key := "/imageRepositories"
352
+	fakeClient.Data[key] = tools.EtcdResponseWithError{
353
+		R: &etcd.Response{
354
+			Node: &etcd.Node{
355
+				Nodes: []*etcd.Node{
356
+					{
357
+						Value: runtime.EncodeOrDie(api.ImageRepository{
358
+							JSONBase: kubeapi.JSONBase{ID: "foo"},
359
+							Labels:   map[string]string{"env": "prod"},
360
+						}),
361
+					},
362
+					{
363
+						Value: runtime.EncodeOrDie(api.ImageRepository{
364
+							JSONBase: kubeapi.JSONBase{ID: "bar"},
365
+							Labels:   map[string]string{"env": "dev"},
366
+						}),
367
+					},
368
+				},
369
+			},
370
+		},
371
+		E: nil,
372
+	}
373
+	registry := NewTestEtcdRegistry(fakeClient)
374
+	repos, err := registry.ListImageRepositories(labels.SelectorFromSet(labels.Set{"env": "dev"}))
375
+	if err != nil {
376
+		t.Errorf("unexpected error: %v", err)
377
+	}
378
+
379
+	if len(repos.Items) != 1 || repos.Items[0].ID != "bar" {
380
+		t.Errorf("Unexpected repos list: %#v", repos)
381
+	}
382
+}
383
+
384
+func TestEtcdGetImageRepository(t *testing.T) {
385
+	fakeClient := tools.NewFakeEtcdClient(t)
386
+	fakeClient.Set("/imageRepositories/foo", runtime.EncodeOrDie(api.ImageRepository{JSONBase: kubeapi.JSONBase{ID: "foo"}}), 0)
387
+	registry := NewTestEtcdRegistry(fakeClient)
388
+	repo, err := registry.GetImageRepository("foo")
389
+	if err != nil {
390
+		t.Errorf("unexpected error: %v", err)
391
+	}
392
+
393
+	if repo.ID != "foo" {
394
+		t.Errorf("Unexpected repo: %#v", repo)
395
+	}
396
+}
397
+
398
+func TestEtcdGetImageRepositoryNotFound(t *testing.T) {
399
+	fakeClient := tools.NewFakeEtcdClient(t)
400
+	fakeClient.Data["/imageRepositories/foo"] = tools.EtcdResponseWithError{
401
+		R: &etcd.Response{
402
+			Node: nil,
403
+		},
404
+		E: tools.EtcdErrorNotFound,
405
+	}
406
+	registry := NewTestEtcdRegistry(fakeClient)
407
+	repo, err := registry.GetImageRepository("foo")
408
+	if err == nil {
409
+		t.Errorf("Unexpected non-error.")
410
+	}
411
+	if repo != nil {
412
+		t.Errorf("Unexpected non-nil repo: %#v", repo)
413
+	}
414
+}
415
+
416
+func TestEtcdCreateImageRepository(t *testing.T) {
417
+	fakeClient := tools.NewFakeEtcdClient(t)
418
+	fakeClient.TestIndex = true
419
+	fakeClient.Data["/imageRepositories/foo"] = tools.EtcdResponseWithError{
420
+		R: &etcd.Response{
421
+			Node: nil,
422
+		},
423
+		E: tools.EtcdErrorNotFound,
424
+	}
425
+	registry := NewTestEtcdRegistry(fakeClient)
426
+	err := registry.CreateImageRepository(api.ImageRepository{
427
+		JSONBase: kubeapi.JSONBase{
428
+			ID: "foo",
429
+		},
430
+		Labels:                map[string]string{"a": "b"},
431
+		DockerImageRepository: "c/d",
432
+		Tags: map[string]string{"t1": "v1"},
433
+	})
434
+	if err != nil {
435
+		t.Fatalf("unexpected error: %v", err)
436
+	}
437
+
438
+	resp, err := fakeClient.Get("/imageRepositories/foo", false, false)
439
+	if err != nil {
440
+		t.Fatalf("Unexpected error %v", err)
441
+	}
442
+	var repo api.ImageRepository
443
+	err = runtime.DecodeInto([]byte(resp.Node.Value), &repo)
444
+	if err != nil {
445
+		t.Errorf("unexpected error: %v", err)
446
+	}
447
+
448
+	if repo.ID != "foo" {
449
+		t.Errorf("Unexpected repo: %#v %s", repo, resp.Node.Value)
450
+	}
451
+
452
+	if len(repo.Labels) != 1 || repo.Labels["a"] != "b" {
453
+		t.Errorf("Unexpected labels: %#v", repo.Labels)
454
+	}
455
+
456
+	if repo.DockerImageRepository != "c/d" {
457
+		t.Errorf("Unexpected docker image repo: %s", repo.DockerImageRepository)
458
+	}
459
+
460
+	if len(repo.Tags) != 1 || repo.Tags["t1"] != "v1" {
461
+		t.Errorf("Unexpected tags: %#v", repo.Tags)
462
+	}
463
+}
464
+
465
+func TestEtcdCreateImageRepositoryAlreadyExists(t *testing.T) {
466
+	fakeClient := tools.NewFakeEtcdClient(t)
467
+	fakeClient.Data["/imageRepositories/foo"] = tools.EtcdResponseWithError{
468
+		R: &etcd.Response{
469
+			Node: &etcd.Node{
470
+				Value: runtime.EncodeOrDie(api.ImageRepository{JSONBase: kubeapi.JSONBase{ID: "foo"}}),
471
+			},
472
+		},
473
+		E: nil,
474
+	}
475
+	registry := NewTestEtcdRegistry(fakeClient)
476
+	err := registry.CreateImageRepository(api.ImageRepository{
477
+		JSONBase: kubeapi.JSONBase{
478
+			ID: "foo",
479
+		},
480
+	})
481
+	if err == nil {
482
+		t.Error("Unexpected non-error")
483
+	}
484
+	if !kubeerrors.IsAlreadyExists(err) {
485
+		t.Errorf("Expected 'already exists' error, got %#v", err)
486
+	}
487
+}
488
+
489
+func TestEtcdUpdateImageRepository(t *testing.T) {
490
+	fakeClient := tools.NewFakeEtcdClient(t)
491
+	fakeClient.TestIndex = true
492
+
493
+	resp, _ := fakeClient.Set("/imageRepositories/foo", runtime.EncodeOrDie(api.ImageRepository{JSONBase: kubeapi.JSONBase{ID: "foo"}}), 0)
494
+	registry := NewTestEtcdRegistry(fakeClient)
495
+	err := registry.UpdateImageRepository(api.ImageRepository{
496
+		JSONBase:              kubeapi.JSONBase{ID: "foo", ResourceVersion: resp.Node.ModifiedIndex},
497
+		DockerImageRepository: "some/repo",
498
+	})
499
+	if err != nil {
500
+		t.Errorf("unexpected error: %v", err)
501
+	}
502
+
503
+	repo, err := registry.GetImageRepository("foo")
504
+	if repo.DockerImageRepository != "some/repo" {
505
+		t.Errorf("Unexpected repo: %#v", repo)
506
+	}
507
+}
508
+
509
+func TestEtcdDeleteImageRepositoryNotFound(t *testing.T) {
510
+	fakeClient := tools.NewFakeEtcdClient(t)
511
+	fakeClient.Err = tools.EtcdErrorNotFound
512
+	registry := NewTestEtcdRegistry(fakeClient)
513
+	err := registry.DeleteImageRepository("foo")
514
+	if err == nil {
515
+		t.Error("Unexpected non-error")
516
+	}
517
+	if !kubeerrors.IsNotFound(err) {
518
+		t.Errorf("Expected 'not found' error, got %#v", err)
519
+	}
520
+}
521
+
522
+func TestEtcdDeleteImageRepositoryError(t *testing.T) {
523
+	fakeClient := tools.NewFakeEtcdClient(t)
524
+	fakeClient.Err = fmt.Errorf("Some error")
525
+	registry := NewTestEtcdRegistry(fakeClient)
526
+	err := registry.DeleteImageRepository("foo")
527
+	if err == nil {
528
+		t.Error("Unexpected non-error")
529
+	}
530
+}
531
+
532
+func TestEtcdDeleteImageRepositoryOK(t *testing.T) {
533
+	fakeClient := tools.NewFakeEtcdClient(t)
534
+	registry := NewTestEtcdRegistry(fakeClient)
535
+	key := "/imageRepositories/foo"
536
+	err := registry.DeleteImageRepository("foo")
537
+	if err != nil {
538
+		t.Errorf("Unexpected error: %#v", err)
539
+	}
540
+	if len(fakeClient.DeletedKeys) != 1 {
541
+		t.Errorf("Expected 1 delete, found %#v", fakeClient.DeletedKeys)
542
+	} else if fakeClient.DeletedKeys[0] != key {
543
+		t.Errorf("Unexpected key: %s, expected %s", fakeClient.DeletedKeys[0], key)
544
+	}
545
+}
546
+
547
+func TestEtcdWatchImageRepositories(t *testing.T) {
548
+	fakeClient := tools.NewFakeEtcdClient(t)
549
+	registry := NewTestEtcdRegistry(fakeClient)
550
+	filterFields := labels.SelectorFromSet(labels.Set{"ID": "foo"})
551
+
552
+	watching, err := registry.WatchImageRepositories(1, func(repo *api.ImageRepository) bool {
553
+		fields := labels.Set{
554
+			"ID": repo.ID,
555
+		}
556
+		return filterFields.Matches(fields)
557
+	})
558
+	if err != nil {
559
+		t.Fatalf("unexpected error: %v", err)
560
+	}
561
+	fakeClient.WaitForWatchCompletion()
562
+
563
+	repo := &api.ImageRepository{JSONBase: kubeapi.JSONBase{ID: "foo"}}
564
+	repoBytes, _ := runtime.Codec.Encode(repo)
565
+	fakeClient.WatchResponse <- &etcd.Response{
566
+		Action: "set",
567
+		Node: &etcd.Node{
568
+			Value: string(repoBytes),
569
+		},
570
+	}
571
+
572
+	event := <-watching.ResultChan()
573
+	if e, a := watch.Added, event.Type; e != a {
574
+		t.Errorf("Expected %v, got %v", e, a)
575
+	}
576
+	if e, a := repo, event.Object; !reflect.DeepEqual(e, a) {
577
+		t.Errorf("Expected %v, got %v", e, a)
578
+	}
579
+
580
+	select {
581
+	case _, ok := <-watching.ResultChan():
582
+		if !ok {
583
+			t.Errorf("watching channel should be open")
584
+		}
585
+	default:
586
+	}
587
+
588
+	fakeClient.WatchInjectError <- nil
589
+	if _, ok := <-watching.ResultChan(); ok {
590
+		t.Errorf("watching channel should be closed")
591
+	}
592
+	watching.Stop()
593
+}
0 594
new file mode 100644
... ...
@@ -0,0 +1,114 @@
0
+package image
1
+
2
+import (
3
+	"errors"
4
+	"fmt"
5
+
6
+	baseapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
7
+	kubeerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
8
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
10
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
11
+	"github.com/openshift/origin/pkg/image/api"
12
+)
13
+
14
+// ImageRepositoryMappingStorage implements the RESTStorage interface in terms of an ImageRegistry and ImageRepositoryRegistry.
15
+// It Only supports the Create method and is used to simply adding a new Image and tag to an ImageRepository.
16
+type ImageRepositoryMappingStorage struct {
17
+	imageRegistry           ImageRegistry
18
+	imageRepositoryRegistry ImageRepositoryRegistry
19
+}
20
+
21
+// NewImageRepositoryMappingStorage returns a new ImageRepositoryMappingStorage.
22
+func NewImageRepositoryMappingStorage(imageRegistry ImageRegistry, imageRepositoryRegistry ImageRepositoryRegistry) apiserver.RESTStorage {
23
+	return &ImageRepositoryMappingStorage{imageRegistry, imageRepositoryRegistry}
24
+}
25
+
26
+// New returns a new ImageRepositoryMapping for use with Create.
27
+func (s *ImageRepositoryMappingStorage) New() interface{} {
28
+	return &api.ImageRepositoryMapping{}
29
+}
30
+
31
+// Get is not supported.
32
+func (s *ImageRepositoryMappingStorage) Get(id string) (interface{}, error) {
33
+	return nil, errors.New("not supported")
34
+}
35
+
36
+// List is not supported.
37
+func (s *ImageRepositoryMappingStorage) List(selector labels.Selector) (interface{}, error) {
38
+	return nil, errors.New("not supported")
39
+}
40
+
41
+// Create registers a new image (if it doesn't exist) and updates the specified ImageRepository's tags.
42
+func (s *ImageRepositoryMappingStorage) Create(obj interface{}) (<-chan interface{}, error) {
43
+	mapping, ok := obj.(*api.ImageRepositoryMapping)
44
+	if !ok {
45
+		return nil, fmt.Errorf("not an image repository mapping: %#v", obj)
46
+	}
47
+
48
+	repo, err := s.findImageRepository(mapping.DockerImageRepository)
49
+	if err != nil {
50
+		return nil, err
51
+	}
52
+	if repo == nil {
53
+		return nil, fmt.Errorf("Unable to locate an image repository for '%s'", mapping.DockerImageRepository)
54
+	}
55
+
56
+	if errs := ValidateImageRepositoryMapping(mapping); len(errs) > 0 {
57
+		return nil, kubeerrors.NewInvalid("imageRepositoryMapping", mapping.ID, errs)
58
+	}
59
+
60
+	image := mapping.Image
61
+
62
+	image.CreationTimestamp = util.Now()
63
+
64
+	//TODO apply metadata overrides
65
+
66
+	if repo.Tags == nil {
67
+		repo.Tags = make(map[string]string)
68
+	}
69
+	repo.Tags[mapping.Tag] = image.DockerImageReference
70
+
71
+	return apiserver.MakeAsync(func() (interface{}, error) {
72
+		err = s.imageRegistry.CreateImage(image)
73
+		if err != nil && !kubeerrors.IsAlreadyExists(err) {
74
+			return nil, err
75
+		}
76
+
77
+		err = s.imageRepositoryRegistry.UpdateImageRepository(*repo)
78
+		if err != nil {
79
+			return nil, err
80
+		}
81
+
82
+		return &baseapi.Status{Status: baseapi.StatusSuccess}, nil
83
+	}), nil
84
+}
85
+
86
+// findImageRepository retrieves an ImageRepository whose DockerImageRepository matches dockerRepo.
87
+func (s *ImageRepositoryMappingStorage) findImageRepository(dockerRepo string) (*api.ImageRepository, error) {
88
+	//TODO make this more efficient
89
+	list, err := s.imageRepositoryRegistry.ListImageRepositories(labels.Everything())
90
+	if err != nil {
91
+		return nil, err
92
+	}
93
+
94
+	var repo *api.ImageRepository
95
+	for _, r := range list.Items {
96
+		if dockerRepo == r.DockerImageRepository {
97
+			repo = &r
98
+			break
99
+		}
100
+	}
101
+
102
+	return repo, nil
103
+}
104
+
105
+// Update is not supported.
106
+func (s *ImageRepositoryMappingStorage) Update(obj interface{}) (<-chan interface{}, error) {
107
+	return nil, errors.New("not supported")
108
+}
109
+
110
+// Delete is not supported.
111
+func (s *ImageRepositoryMappingStorage) Delete(id string) (<-chan interface{}, error) {
112
+	return nil, errors.New("not supported")
113
+}
0 114
new file mode 100644
... ...
@@ -0,0 +1,200 @@
0
+package image
1
+
2
+import (
3
+	"fmt"
4
+	"reflect"
5
+	"strings"
6
+	"testing"
7
+
8
+	kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
10
+	"github.com/fsouza/go-dockerclient"
11
+	"github.com/openshift/origin/pkg/image/api"
12
+	"github.com/openshift/origin/pkg/image/imagetest"
13
+)
14
+
15
+func TestGetImageRepositoryMapping(t *testing.T) {
16
+	imageRegistry := imagetest.NewImageRegistry()
17
+	imageRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
18
+	storage := &ImageRepositoryMappingStorage{imageRegistry, imageRepositoryRegistry}
19
+
20
+	obj, err := storage.Get("foo")
21
+	if obj != nil {
22
+		t.Errorf("Unexpected non-nil object %#v", obj)
23
+	}
24
+	if err == nil || strings.Index(err.Error(), "not supported") == -1 {
25
+		t.Errorf("Expected 'not supported' error, got %#v", err)
26
+	}
27
+}
28
+
29
+func TestListImageRepositoryMappings(t *testing.T) {
30
+	imageRegistry := imagetest.NewImageRegistry()
31
+	imageRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
32
+	storage := &ImageRepositoryMappingStorage{imageRegistry, imageRepositoryRegistry}
33
+
34
+	list, err := storage.List(labels.Everything())
35
+	if list != nil {
36
+		t.Errorf("Unexpected non-nil list %#v", list)
37
+	}
38
+	if err == nil || strings.Index(err.Error(), "not supported") == -1 {
39
+		t.Errorf("Expected 'not supported' error, got %#v", err)
40
+	}
41
+}
42
+
43
+func TestDeleteImageRepositoryMapping(t *testing.T) {
44
+	imageRegistry := imagetest.NewImageRegistry()
45
+	imageRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
46
+	storage := &ImageRepositoryMappingStorage{imageRegistry, imageRepositoryRegistry}
47
+
48
+	channel, err := storage.Delete("repo1")
49
+	if channel != nil {
50
+		t.Errorf("Unexpected non-nil channel %#v", channel)
51
+	}
52
+	if err == nil || strings.Index(err.Error(), "not supported") == -1 {
53
+		t.Errorf("Expected 'not supported' error, got %#v", err)
54
+	}
55
+}
56
+
57
+func TestUpdateImageRepositoryMapping(t *testing.T) {
58
+	imageRegistry := imagetest.NewImageRegistry()
59
+	imageRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
60
+	storage := &ImageRepositoryMappingStorage{imageRegistry, imageRepositoryRegistry}
61
+
62
+	channel, err := storage.Update("repo1")
63
+	if channel != nil {
64
+		t.Errorf("Unexpected non-nil channel %#v", channel)
65
+	}
66
+	if err == nil || strings.Index(err.Error(), "not supported") == -1 {
67
+		t.Errorf("Expected 'not supported' error, got %#v", err)
68
+	}
69
+}
70
+
71
+func TestCreateImageRepositoryMappingBadObject(t *testing.T) {
72
+	imageRegistry := imagetest.NewImageRegistry()
73
+	imageRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
74
+	storage := &ImageRepositoryMappingStorage{imageRegistry, imageRepositoryRegistry}
75
+
76
+	channel, err := storage.Create("bad object")
77
+	if channel != nil {
78
+		t.Errorf("Unexpected non-nil channel %#v", channel)
79
+	}
80
+	if err == nil || strings.Index(err.Error(), "not an image repository mapping") == -1 {
81
+		t.Errorf("Expected 'not an image repository mapping' error, got %#v", err)
82
+	}
83
+}
84
+
85
+func TestCreateImageRepositoryMappingFindError(t *testing.T) {
86
+	imageRegistry := imagetest.NewImageRegistry()
87
+	imageRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
88
+	imageRepositoryRegistry.Err = fmt.Errorf("123")
89
+	storage := &ImageRepositoryMappingStorage{imageRegistry, imageRepositoryRegistry}
90
+
91
+	mapping := api.ImageRepositoryMapping{
92
+		DockerImageRepository: "localhost:5000/someproject/somerepo",
93
+		Image: api.Image{
94
+			JSONBase: kubeapi.JSONBase{
95
+				ID: "imageID1",
96
+			},
97
+			DockerImageReference: "localhost:5000/someproject/somerepo:imageID1",
98
+		},
99
+		Tag: "latest",
100
+	}
101
+
102
+	channel, err := storage.Create(&mapping)
103
+	if channel != nil {
104
+		t.Errorf("Unexpected non-nil channel %#v", channel)
105
+	}
106
+	if err == nil || err.Error() != "123" {
107
+		t.Errorf("Expected 'unable to locate' error, got %#v", err)
108
+	}
109
+}
110
+
111
+func TestCreateImageRepositoryMappingNotFound(t *testing.T) {
112
+	imageRegistry := imagetest.NewImageRegistry()
113
+	imageRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
114
+	imageRepositoryRegistry.ImageRepositories = &api.ImageRepositoryList{
115
+		Items: []api.ImageRepository{
116
+			{
117
+				JSONBase: kubeapi.JSONBase{
118
+					ID: "repo1",
119
+				},
120
+				DockerImageRepository: "localhost:5000/test/repo",
121
+			},
122
+		},
123
+	}
124
+	storage := &ImageRepositoryMappingStorage{imageRegistry, imageRepositoryRegistry}
125
+
126
+	mapping := api.ImageRepositoryMapping{
127
+		DockerImageRepository: "localhost:5000/someproject/somerepo",
128
+		Image: api.Image{
129
+			JSONBase: kubeapi.JSONBase{
130
+				ID: "imageID1",
131
+			},
132
+			DockerImageReference: "localhost:5000/someproject/somerepo:imageID1",
133
+		},
134
+		Tag: "latest",
135
+	}
136
+
137
+	channel, err := storage.Create(&mapping)
138
+	if channel != nil {
139
+		t.Errorf("Unexpected non-nil channel %#v", channel)
140
+	}
141
+	if err == nil || strings.Index(err.Error(), "Unable to locate an image repository") == -1 {
142
+		t.Errorf("Expected 'unable to locate' error, got %#v", err)
143
+	}
144
+}
145
+
146
+func TestCreateImageRepositoryMapping(t *testing.T) {
147
+	imageRegistry := imagetest.NewImageRegistry()
148
+	imageRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
149
+	imageRepositoryRegistry.ImageRepositories = &api.ImageRepositoryList{
150
+		Items: []api.ImageRepository{
151
+			{
152
+				JSONBase: kubeapi.JSONBase{
153
+					ID: "repo1",
154
+				},
155
+				DockerImageRepository: "localhost:5000/someproject/somerepo",
156
+			},
157
+		},
158
+	}
159
+	storage := &ImageRepositoryMappingStorage{imageRegistry, imageRepositoryRegistry}
160
+
161
+	mapping := api.ImageRepositoryMapping{
162
+		DockerImageRepository: "localhost:5000/someproject/somerepo",
163
+		Image: api.Image{
164
+			JSONBase: kubeapi.JSONBase{
165
+				ID: "imageID1",
166
+			},
167
+			DockerImageReference: "localhost:5000/someproject/somerepo:imageID1",
168
+			Metadata: docker.Image{
169
+				Config: &docker.Config{
170
+					Cmd:          []string{"ls", "/"},
171
+					Env:          []string{"a=1"},
172
+					ExposedPorts: map[docker.Port]struct{}{"1234/tcp": {}},
173
+					Memory:       1234,
174
+					CpuShares:    99,
175
+					WorkingDir:   "/workingDir",
176
+				},
177
+			},
178
+		},
179
+		Tag: "latest",
180
+	}
181
+	ch, err := storage.Create(&mapping)
182
+	if err != nil {
183
+		t.Errorf("Unexpected error creating mapping: %#v", err)
184
+	}
185
+
186
+	out := <-ch
187
+	t.Logf("out = '%#v'", out)
188
+
189
+	image, err := imageRegistry.GetImage("imageID1")
190
+	if err != nil {
191
+		t.Errorf("Unexpected error retrieving image: %#v", err)
192
+	}
193
+	if e, a := mapping.Image.DockerImageReference, image.DockerImageReference; e != a {
194
+		t.Errorf("Expected %s, got %s", e, a)
195
+	}
196
+	if !reflect.DeepEqual(mapping.Image.Metadata, image.Metadata) {
197
+		t.Errorf("Expected %#v, got %#v", mapping.Image, image)
198
+	}
199
+}
0 200
new file mode 100644
... ...
@@ -0,0 +1,109 @@
0
+package image
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"code.google.com/p/go-uuid/uuid"
6
+
7
+	baseapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
8
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
10
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
11
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
12
+	"github.com/openshift/origin/pkg/image/api"
13
+)
14
+
15
+// ImageRepositoryStorage implements the RESTStorage interface in terms of an ImageRepositoryRegistry.
16
+type ImageRepositoryStorage struct {
17
+	registry ImageRepositoryRegistry
18
+}
19
+
20
+// NewImageRepositoryStorage returns a new ImageRepositoryStorage.
21
+func NewImageRepositoryStorage(registry ImageRepositoryRegistry) apiserver.RESTStorage {
22
+	return &ImageRepositoryStorage{registry}
23
+}
24
+
25
+// New returns a new ImageRepository for use with Create and Update.
26
+func (s *ImageRepositoryStorage) New() interface{} {
27
+	return &api.ImageRepository{}
28
+}
29
+
30
+// Get retrieves an ImageRepository by id.
31
+func (s *ImageRepositoryStorage) Get(id string) (interface{}, error) {
32
+	repo, err := s.registry.GetImageRepository(id)
33
+	if err != nil {
34
+		return nil, err
35
+	}
36
+	return repo, nil
37
+}
38
+
39
+// List retrieves a list of ImageRepositories that match selector.
40
+func (s *ImageRepositoryStorage) List(selector labels.Selector) (interface{}, error) {
41
+	imageRepositories, err := s.registry.ListImageRepositories(selector)
42
+	if err != nil {
43
+		return nil, err
44
+	}
45
+	return imageRepositories, err
46
+}
47
+
48
+// Watch begins watching for new, changed, or deleted ImageRepositories.
49
+func (s *ImageRepositoryStorage) Watch(label, field labels.Selector, resourceVersion uint64) (watch.Interface, error) {
50
+	return s.registry.WatchImageRepositories(resourceVersion, func(repo *api.ImageRepository) bool {
51
+		fields := labels.Set{
52
+			"ID": repo.ID,
53
+			"DockerImageRepository": repo.DockerImageRepository,
54
+		}
55
+		return label.Matches(labels.Set(repo.Labels)) && field.Matches(fields)
56
+	})
57
+}
58
+
59
+// Create registers the given ImageRepository.
60
+func (s *ImageRepositoryStorage) Create(obj interface{}) (<-chan interface{}, error) {
61
+	repo, ok := obj.(*api.ImageRepository)
62
+	if !ok {
63
+		return nil, fmt.Errorf("not an image repository: %#v", obj)
64
+	}
65
+
66
+	if len(repo.ID) == 0 {
67
+		repo.ID = uuid.NewUUID().String()
68
+	}
69
+
70
+	if repo.Tags == nil {
71
+		repo.Tags = make(map[string]string)
72
+	}
73
+
74
+	repo.CreationTimestamp = util.Now()
75
+
76
+	return apiserver.MakeAsync(func() (interface{}, error) {
77
+		if err := s.registry.CreateImageRepository(*repo); err != nil {
78
+			return nil, err
79
+		}
80
+		return s.Get(repo.ID)
81
+	}), nil
82
+}
83
+
84
+// Update replaces an existing ImageRepository in the registry with the given ImageRepository.
85
+func (s *ImageRepositoryStorage) Update(obj interface{}) (<-chan interface{}, error) {
86
+	repo, ok := obj.(*api.ImageRepository)
87
+	if !ok {
88
+		return nil, fmt.Errorf("not an image repository: %#v", obj)
89
+	}
90
+	if len(repo.ID) == 0 {
91
+		return nil, fmt.Errorf("id is unspecified: %#v", repo)
92
+	}
93
+
94
+	return apiserver.MakeAsync(func() (interface{}, error) {
95
+		err := s.registry.UpdateImageRepository(*repo)
96
+		if err != nil {
97
+			return nil, err
98
+		}
99
+		return s.Get(repo.ID)
100
+	}), nil
101
+}
102
+
103
+// Delete asynchronously deletes an ImageRepository specified by its id.
104
+func (s *ImageRepositoryStorage) Delete(id string) (<-chan interface{}, error) {
105
+	return apiserver.MakeAsync(func() (interface{}, error) {
106
+		return &baseapi.Status{Status: baseapi.StatusSuccess}, s.registry.DeleteImageRepository(id)
107
+	}), nil
108
+}
0 109
new file mode 100644
... ...
@@ -0,0 +1,254 @@
0
+package image
1
+
2
+import (
3
+	"fmt"
4
+	"reflect"
5
+	"strings"
6
+	"testing"
7
+
8
+	kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
10
+	"github.com/openshift/origin/pkg/image/api"
11
+	"github.com/openshift/origin/pkg/image/imagetest"
12
+)
13
+
14
+func TestGetImageRepositoryError(t *testing.T) {
15
+	mockRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
16
+	mockRepositoryRegistry.Err = fmt.Errorf("test error")
17
+	storage := ImageRepositoryStorage{registry: mockRepositoryRegistry}
18
+
19
+	image, err := storage.Get("image1")
20
+	if image != nil {
21
+		t.Errorf("Unexpected non-nil image: %#v", image)
22
+	}
23
+	if err != mockRepositoryRegistry.Err {
24
+		t.Errorf("Expected %#v, got %#v", mockRepositoryRegistry.Err, err)
25
+	}
26
+}
27
+
28
+func TestGetImageRepositoryOK(t *testing.T) {
29
+	mockRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
30
+	mockRepositoryRegistry.ImageRepository = &api.ImageRepository{
31
+		JSONBase:              kubeapi.JSONBase{ID: "foo"},
32
+		DockerImageRepository: "openshift/ruby-19-centos",
33
+	}
34
+	storage := ImageRepositoryStorage{registry: mockRepositoryRegistry}
35
+
36
+	repo, err := storage.Get("foo")
37
+	if repo == nil {
38
+		t.Errorf("Unexpected nil repo: %#v", repo)
39
+	}
40
+	if err != nil {
41
+		t.Errorf("Unexpected non-nil error: %#v", err)
42
+	}
43
+	if e, a := mockRepositoryRegistry.ImageRepository, repo; !reflect.DeepEqual(e, a) {
44
+		t.Errorf("Expected %#v, got %#v", e, a)
45
+	}
46
+}
47
+
48
+func TestListImageRepositoriesError(t *testing.T) {
49
+	mockRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
50
+	mockRepositoryRegistry.Err = fmt.Errorf("test error")
51
+
52
+	storage := ImageRepositoryStorage{
53
+		registry: mockRepositoryRegistry,
54
+	}
55
+
56
+	imageRepositories, err := storage.List(nil)
57
+	if err != mockRepositoryRegistry.Err {
58
+		t.Errorf("Expected %#v, Got %#v", mockRepositoryRegistry.Err, err)
59
+	}
60
+
61
+	if imageRepositories != nil {
62
+		t.Errorf("Unexpected non-nil imageRepositories list: %#v", imageRepositories)
63
+	}
64
+}
65
+
66
+func TestListImageRepositoriesEmptyList(t *testing.T) {
67
+	mockRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
68
+	mockRepositoryRegistry.ImageRepositories = &api.ImageRepositoryList{
69
+		Items: []api.ImageRepository{},
70
+	}
71
+
72
+	storage := ImageRepositoryStorage{
73
+		registry: mockRepositoryRegistry,
74
+	}
75
+
76
+	imageRepositories, err := storage.List(labels.Everything())
77
+	if err != nil {
78
+		t.Errorf("Unexpected non-nil error: %#v", err)
79
+	}
80
+
81
+	if len(imageRepositories.(*api.ImageRepositoryList).Items) != 0 {
82
+		t.Errorf("Unexpected non-zero imageRepositories list: %#v", imageRepositories)
83
+	}
84
+}
85
+
86
+func TestListImageRepositoriesPopulatedList(t *testing.T) {
87
+	mockRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
88
+	mockRepositoryRegistry.ImageRepositories = &api.ImageRepositoryList{
89
+		Items: []api.ImageRepository{
90
+			{
91
+				JSONBase: kubeapi.JSONBase{
92
+					ID: "foo",
93
+				},
94
+			},
95
+			{
96
+				JSONBase: kubeapi.JSONBase{
97
+					ID: "bar",
98
+				},
99
+			},
100
+		},
101
+	}
102
+
103
+	storage := ImageRepositoryStorage{
104
+		registry: mockRepositoryRegistry,
105
+	}
106
+
107
+	list, err := storage.List(labels.Everything())
108
+	if err != nil {
109
+		t.Errorf("Unexpected non-nil error: %#v", err)
110
+	}
111
+
112
+	imageRepositories := list.(*api.ImageRepositoryList)
113
+
114
+	if e, a := 2, len(imageRepositories.Items); e != a {
115
+		t.Errorf("Expected %v, got %v", e, a)
116
+	}
117
+}
118
+
119
+func TestCreateImageRepositoryBadObject(t *testing.T) {
120
+	storage := ImageRepositoryStorage{}
121
+
122
+	channel, err := storage.Create("hello")
123
+	if channel != nil {
124
+		t.Errorf("Expected nil, got %v", channel)
125
+	}
126
+	if strings.Index(err.Error(), "not an image repository:") == -1 {
127
+		t.Errorf("Expected 'not an image repository' error, got %v", err)
128
+	}
129
+}
130
+
131
+func TestCreateImageRepositoryOK(t *testing.T) {
132
+	mockRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
133
+	storage := ImageRepositoryStorage{registry: mockRepositoryRegistry}
134
+
135
+	channel, err := storage.Create(&api.ImageRepository{})
136
+	if err != nil {
137
+		t.Errorf("Unexpected non-nil error: %#v", err)
138
+	}
139
+
140
+	result := <-channel
141
+	repo, ok := result.(*api.ImageRepository)
142
+	if !ok {
143
+		t.Errorf("Unexpected result: %#v", result)
144
+	}
145
+	if len(repo.ID) == 0 {
146
+		t.Errorf("Expected repo's ID to be set: %#v", repo)
147
+	}
148
+	if repo.CreationTimestamp.IsZero() {
149
+		t.Error("Unexpected zero CreationTimestamp")
150
+	}
151
+}
152
+
153
+func TestCreateImageRepositoryRegistryErrorSaving(t *testing.T) {
154
+	mockRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
155
+	mockRepositoryRegistry.Err = fmt.Errorf("foo")
156
+	storage := ImageRepositoryStorage{registry: mockRepositoryRegistry}
157
+
158
+	channel, err := storage.Create(&api.ImageRepository{})
159
+	if err != nil {
160
+		t.Errorf("Unexpected non-nil error: %#v", err)
161
+	}
162
+	result := <-channel
163
+	status, ok := result.(*kubeapi.Status)
164
+	if !ok {
165
+		t.Errorf("Expected status, got %#v", result)
166
+	}
167
+	if status.Status != "failure" || status.Message != "foo" {
168
+		t.Errorf("Expected status=failure, message=foo, got %#v", status)
169
+	}
170
+}
171
+
172
+func TestUpdateImageRepositoryBadObject(t *testing.T) {
173
+	storage := ImageRepositoryStorage{}
174
+
175
+	channel, err := storage.Update("hello")
176
+	if channel != nil {
177
+		t.Errorf("Expected nil, got %v", channel)
178
+	}
179
+	if strings.Index(err.Error(), "not an image repository:") == -1 {
180
+		t.Errorf("Expected 'not an image repository' error, got %v", err)
181
+	}
182
+}
183
+
184
+func TestUpdateImageRepositoryMissingID(t *testing.T) {
185
+	storage := ImageRepositoryStorage{}
186
+
187
+	channel, err := storage.Update(&api.ImageRepository{})
188
+	if channel != nil {
189
+		t.Errorf("Expected nil, got %v", channel)
190
+	}
191
+	if strings.Index(err.Error(), "id is unspecified:") == -1 {
192
+		t.Errorf("Expected 'id is unspecified' error, got %v", err)
193
+	}
194
+}
195
+
196
+func TestUpdateImageRepositoryRegistryErrorSaving(t *testing.T) {
197
+	mockRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
198
+	mockRepositoryRegistry.Err = fmt.Errorf("foo")
199
+	storage := ImageRepositoryStorage{registry: mockRepositoryRegistry}
200
+
201
+	channel, err := storage.Update(&api.ImageRepository{
202
+		JSONBase: kubeapi.JSONBase{ID: "bar"},
203
+	})
204
+	if err != nil {
205
+		t.Errorf("Unexpected non-nil error: %#v", err)
206
+	}
207
+	result := <-channel
208
+	status, ok := result.(*kubeapi.Status)
209
+	if !ok {
210
+		t.Errorf("Expected status, got %#v", result)
211
+	}
212
+	if status.Status != "failure" || status.Message != "foo" {
213
+		t.Errorf("Expected status=failure, message=foo, got %#v", status)
214
+	}
215
+}
216
+
217
+func TestUpdateImageRepositoryOK(t *testing.T) {
218
+	mockRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
219
+	storage := ImageRepositoryStorage{registry: mockRepositoryRegistry}
220
+
221
+	channel, err := storage.Update(&api.ImageRepository{
222
+		JSONBase: kubeapi.JSONBase{ID: "bar"},
223
+	})
224
+	if err != nil {
225
+		t.Errorf("Unexpected non-nil error: %#v", err)
226
+	}
227
+	result := <-channel
228
+	repo, ok := result.(*api.ImageRepository)
229
+	if !ok {
230
+		t.Errorf("Expected image repository, got %#v", result)
231
+	}
232
+	if repo.ID != "bar" {
233
+		t.Errorf("Unexpected repo returned: %#v", repo)
234
+	}
235
+}
236
+
237
+func TestDeleteImageRepository(t *testing.T) {
238
+	mockRepositoryRegistry := imagetest.NewImageRepositoryRegistry()
239
+	storage := ImageRepositoryStorage{registry: mockRepositoryRegistry}
240
+
241
+	channel, err := storage.Delete("foo")
242
+	if err != nil {
243
+		t.Errorf("Unexpected non-nil error: %#v", err)
244
+	}
245
+	result := <-channel
246
+	status, ok := result.(*kubeapi.Status)
247
+	if !ok {
248
+		t.Errorf("Expected status, got %#v", result)
249
+	}
250
+	if status.Status != "success" {
251
+		t.Errorf("Expected status=success, got %#v", status)
252
+	}
253
+}
0 254
new file mode 100644
... ...
@@ -0,0 +1,80 @@
0
+package image
1
+
2
+import (
3
+	"errors"
4
+	"fmt"
5
+
6
+	kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
7
+	kubeerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
8
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
10
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
11
+	"github.com/openshift/origin/pkg/image/api"
12
+)
13
+
14
+// ImageStorage implements the RESTStorage interface in terms of an ImageRegistry.
15
+type ImageStorage struct {
16
+	registry ImageRegistry
17
+}
18
+
19
+// NewStorage returns a new ImageStorage.
20
+func NewImageStorage(registry ImageRegistry) apiserver.RESTStorage {
21
+	return &ImageStorage{registry}
22
+}
23
+
24
+// New returns a new Image for use with Create and Update.
25
+func (s *ImageStorage) New() interface{} {
26
+	return &api.Image{}
27
+}
28
+
29
+// Get retrieves an Image by id.
30
+func (s *ImageStorage) Get(id string) (interface{}, error) {
31
+	image, err := s.registry.GetImage(id)
32
+	if err != nil {
33
+		return nil, err
34
+	}
35
+	return image, nil
36
+}
37
+
38
+// List retrieves a list of Images that match selector.
39
+func (s *ImageStorage) List(selector labels.Selector) (interface{}, error) {
40
+	images, err := s.registry.ListImages(selector)
41
+	if err != nil {
42
+		return nil, err
43
+	}
44
+
45
+	return images, nil
46
+}
47
+
48
+// Create registers the given Image.
49
+func (s *ImageStorage) Create(obj interface{}) (<-chan interface{}, error) {
50
+	image, ok := obj.(*api.Image)
51
+	if !ok {
52
+		return nil, fmt.Errorf("not an image: %#v", obj)
53
+	}
54
+
55
+	image.CreationTimestamp = util.Now()
56
+
57
+	if errs := ValidateImage(image); len(errs) > 0 {
58
+		return nil, kubeerrors.NewInvalid("image", image.ID, errs)
59
+	}
60
+
61
+	return apiserver.MakeAsync(func() (interface{}, error) {
62
+		if err := s.registry.CreateImage(*image); err != nil {
63
+			return nil, err
64
+		}
65
+		return s.Get(image.ID)
66
+	}), nil
67
+}
68
+
69
+// Update is not supported for Images, as they are immutable.
70
+func (s *ImageStorage) Update(obj interface{}) (<-chan interface{}, error) {
71
+	return nil, errors.New("not supported")
72
+}
73
+
74
+// Delete asynchronously deletes an Image specified by its id.
75
+func (s *ImageStorage) Delete(id string) (<-chan interface{}, error) {
76
+	return apiserver.MakeAsync(func() (interface{}, error) {
77
+		return &kubeapi.Status{Status: kubeapi.StatusSuccess}, s.registry.DeleteImage(id)
78
+	}), nil
79
+}
0 80
new file mode 100644
... ...
@@ -0,0 +1,241 @@
0
+package image
1
+
2
+import (
3
+	"fmt"
4
+	"strings"
5
+	"testing"
6
+	"time"
7
+
8
+	kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
9
+	kubeerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
10
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
11
+	"github.com/openshift/origin/pkg/image/api"
12
+	"github.com/openshift/origin/pkg/image/imagetest"
13
+)
14
+
15
+func TestListImagesError(t *testing.T) {
16
+	mockRegistry := imagetest.NewImageRegistry()
17
+	mockRegistry.Err = fmt.Errorf("test error")
18
+
19
+	storage := ImageStorage{
20
+		registry: mockRegistry,
21
+	}
22
+
23
+	images, err := storage.List(nil)
24
+	if err != mockRegistry.Err {
25
+		t.Errorf("Expected %#v, Got %#v", mockRegistry.Err, err)
26
+	}
27
+
28
+	if images != nil {
29
+		t.Errorf("Unexpected non-nil images list: %#v", images)
30
+	}
31
+}
32
+
33
+func TestListImagesEmptyList(t *testing.T) {
34
+	mockRegistry := imagetest.NewImageRegistry()
35
+	mockRegistry.Images = &api.ImageList{
36
+		Items: []api.Image{},
37
+	}
38
+
39
+	storage := ImageStorage{
40
+		registry: mockRegistry,
41
+	}
42
+
43
+	images, err := storage.List(labels.Everything())
44
+	if err != nil {
45
+		t.Errorf("Unexpected non-nil error: %#v", err)
46
+	}
47
+
48
+	if len(images.(*api.ImageList).Items) != 0 {
49
+		t.Errorf("Unexpected non-zero images list: %#v", images)
50
+	}
51
+}
52
+
53
+func TestListImagesPopulatedList(t *testing.T) {
54
+	mockRegistry := imagetest.NewImageRegistry()
55
+	mockRegistry.Images = &api.ImageList{
56
+		Items: []api.Image{
57
+			{
58
+				JSONBase: kubeapi.JSONBase{
59
+					ID: "foo",
60
+				},
61
+			},
62
+			{
63
+				JSONBase: kubeapi.JSONBase{
64
+					ID: "bar",
65
+				},
66
+			},
67
+		},
68
+	}
69
+
70
+	storage := ImageStorage{
71
+		registry: mockRegistry,
72
+	}
73
+
74
+	list, err := storage.List(labels.Everything())
75
+	if err != nil {
76
+		t.Errorf("Unexpected non-nil error: %#v", err)
77
+	}
78
+
79
+	images := list.(*api.ImageList)
80
+
81
+	if e, a := 2, len(images.Items); e != a {
82
+		t.Errorf("Expected %v, got %v", e, a)
83
+	}
84
+}
85
+
86
+func TestCreateImageBadObject(t *testing.T) {
87
+	storage := ImageStorage{}
88
+
89
+	channel, err := storage.Create("hello")
90
+	if channel != nil {
91
+		t.Errorf("Expected nil, got %v", channel)
92
+	}
93
+	if strings.Index(err.Error(), "not an image:") == -1 {
94
+		t.Errorf("Expected 'not an image' error, got %v", err)
95
+	}
96
+}
97
+
98
+func TestCreateImageMissingID(t *testing.T) {
99
+	storage := ImageStorage{}
100
+
101
+	channel, err := storage.Create(&api.Image{})
102
+	if channel != nil {
103
+		t.Errorf("Expected nil channel, got %v", channel)
104
+	}
105
+	if !kubeerrors.IsInvalid(err) {
106
+		t.Errorf("Expected 'invalid' error, got %v", err)
107
+	}
108
+}
109
+
110
+func TestCreateImageRegistrySaveError(t *testing.T) {
111
+	mockRegistry := imagetest.NewImageRegistry()
112
+	mockRegistry.Err = fmt.Errorf("test error")
113
+	storage := ImageStorage{registry: mockRegistry}
114
+
115
+	channel, err := storage.Create(&api.Image{
116
+		JSONBase:             kubeapi.JSONBase{ID: "foo"},
117
+		DockerImageReference: "openshift/ruby-19-centos",
118
+	})
119
+	if channel == nil {
120
+		t.Errorf("Expected nil channel, got %v", channel)
121
+	}
122
+	if err != nil {
123
+		t.Errorf("Unexpected non-nil error: %#v", err)
124
+	}
125
+
126
+	select {
127
+	case result := <-channel:
128
+		status, ok := result.(*kubeapi.Status)
129
+		if !ok {
130
+			t.Errorf("Expected status type, got: %#v", result)
131
+		}
132
+		if status.Status != "failure" || status.Message != "foo" {
133
+			t.Errorf("Expected failure status, got %#V", status)
134
+		}
135
+	case <-time.After(50 * time.Millisecond):
136
+		t.Errorf("Timed out waiting for result")
137
+	default:
138
+	}
139
+}
140
+
141
+func TestCreateImageOK(t *testing.T) {
142
+	mockRegistry := imagetest.NewImageRegistry()
143
+	storage := ImageStorage{registry: mockRegistry}
144
+
145
+	channel, err := storage.Create(&api.Image{
146
+		JSONBase:             kubeapi.JSONBase{ID: "foo"},
147
+		DockerImageReference: "openshift/ruby-19-centos",
148
+	})
149
+	if channel == nil {
150
+		t.Errorf("Expected nil channel, got %v", channel)
151
+	}
152
+	if err != nil {
153
+		t.Errorf("Unexpected non-nil error: %#v", err)
154
+	}
155
+
156
+	select {
157
+	case result := <-channel:
158
+		image, ok := result.(*api.Image)
159
+		if !ok {
160
+			t.Errorf("Expected image type, got: %#v", result)
161
+		}
162
+		if image.ID != "foo" {
163
+			t.Errorf("Unexpected image: %#v", image)
164
+		}
165
+	case <-time.After(50 * time.Millisecond):
166
+		t.Errorf("Timed out waiting for result")
167
+	default:
168
+	}
169
+}
170
+
171
+func TestGetImageError(t *testing.T) {
172
+	mockRegistry := imagetest.NewImageRegistry()
173
+	mockRegistry.Err = fmt.Errorf("bad")
174
+	storage := ImageStorage{registry: mockRegistry}
175
+
176
+	image, err := storage.Get("foo")
177
+	if image != nil {
178
+		t.Errorf("Unexpected non-nil image: %#v", image)
179
+	}
180
+	if err != mockRegistry.Err {
181
+		t.Errorf("Expected %#v, got %#v", mockRegistry.Err, err)
182
+	}
183
+}
184
+
185
+func TestGetImageOK(t *testing.T) {
186
+	mockRegistry := imagetest.NewImageRegistry()
187
+	mockRegistry.Image = &api.Image{
188
+		JSONBase:             kubeapi.JSONBase{ID: "foo"},
189
+		DockerImageReference: "openshift/ruby-19-centos",
190
+	}
191
+	storage := ImageStorage{registry: mockRegistry}
192
+
193
+	image, err := storage.Get("foo")
194
+	if image == nil {
195
+		t.Error("Unexpected nil image")
196
+	}
197
+	if err != nil {
198
+		t.Errorf("Unexpected non-nil error", err)
199
+	}
200
+	if image.(*api.Image).ID != "foo" {
201
+		t.Errorf("Unexpected image: %#v", image)
202
+	}
203
+}
204
+
205
+func TestUpdateImage(t *testing.T) {
206
+	storage := ImageStorage{}
207
+	channel, err := storage.Update(&api.Image{})
208
+	if channel != nil {
209
+		t.Errorf("Unexpected non-nil channel: %#v", channel)
210
+	}
211
+	if err == nil || strings.Index(err.Error(), "not supported") == -1 {
212
+		t.Errorf("Expected 'not supported' error, got: %#v", err)
213
+	}
214
+}
215
+
216
+func TestDeleteImage(t *testing.T) {
217
+	mockRegistry := imagetest.NewImageRegistry()
218
+	storage := ImageStorage{registry: mockRegistry}
219
+	channel, err := storage.Delete("foo")
220
+	if channel == nil {
221
+		t.Error("Unexpected nil channel")
222
+	}
223
+	if err != nil {
224
+		t.Errorf("Unexpected non-nil error: %#v", err)
225
+	}
226
+
227
+	select {
228
+	case result := <-channel:
229
+		status, ok := result.(*kubeapi.Status)
230
+		if !ok {
231
+			t.Errorf("Expected status type, got: %#v", result)
232
+		}
233
+		if status.Status != "success" {
234
+			t.Errorf("Expected status=success, got: %#v", status)
235
+		}
236
+	case <-time.After(50 * time.Millisecond):
237
+		t.Errorf("Timed out waiting for result")
238
+	default:
239
+	}
240
+}
0 241
new file mode 100644
... ...
@@ -0,0 +1,56 @@
0
+package imagetest
1
+
2
+import (
3
+	"sync"
4
+
5
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
6
+	"github.com/openshift/origin/pkg/image/api"
7
+)
8
+
9
+type ImageRegistry struct {
10
+	Err    error
11
+	Image  *api.Image
12
+	Images *api.ImageList
13
+	sync.Mutex
14
+}
15
+
16
+func NewImageRegistry() *ImageRegistry {
17
+	return &ImageRegistry{}
18
+}
19
+
20
+func (r *ImageRegistry) ListImages(selector labels.Selector) (*api.ImageList, error) {
21
+	r.Lock()
22
+	defer r.Unlock()
23
+
24
+	return r.Images, r.Err
25
+}
26
+
27
+func (r *ImageRegistry) GetImage(id string) (*api.Image, error) {
28
+	r.Lock()
29
+	defer r.Unlock()
30
+
31
+	return r.Image, r.Err
32
+}
33
+
34
+func (r *ImageRegistry) CreateImage(image api.Image) error {
35
+	r.Lock()
36
+	defer r.Unlock()
37
+
38
+	r.Image = &image
39
+	return r.Err
40
+}
41
+
42
+func (r *ImageRegistry) UpdateImage(image api.Image) error {
43
+	r.Lock()
44
+	defer r.Unlock()
45
+
46
+	r.Image = &image
47
+	return r.Err
48
+}
49
+
50
+func (r *ImageRegistry) DeleteImage(id string) error {
51
+	r.Lock()
52
+	defer r.Unlock()
53
+
54
+	return r.Err
55
+}
0 56
new file mode 100644
... ...
@@ -0,0 +1,61 @@
0
+package imagetest
1
+
2
+import (
3
+	"sync"
4
+
5
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
6
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
7
+	"github.com/openshift/origin/pkg/image/api"
8
+)
9
+
10
+type ImageRepositoryRegistry struct {
11
+	Err               error
12
+	ImageRepository   *api.ImageRepository
13
+	ImageRepositories *api.ImageRepositoryList
14
+	sync.Mutex
15
+}
16
+
17
+func NewImageRepositoryRegistry() *ImageRepositoryRegistry {
18
+	return &ImageRepositoryRegistry{}
19
+}
20
+
21
+func (r *ImageRepositoryRegistry) ListImageRepositories(selector labels.Selector) (*api.ImageRepositoryList, error) {
22
+	r.Lock()
23
+	defer r.Unlock()
24
+
25
+	return r.ImageRepositories, r.Err
26
+}
27
+
28
+func (r *ImageRepositoryRegistry) GetImageRepository(id string) (*api.ImageRepository, error) {
29
+	r.Lock()
30
+	defer r.Unlock()
31
+
32
+	return r.ImageRepository, r.Err
33
+}
34
+
35
+func (r *ImageRepositoryRegistry) WatchImageRepositories(resourceVersion uint64, filter func(repo *api.ImageRepository) bool) (watch.Interface, error) {
36
+	return nil, r.Err
37
+}
38
+
39
+func (r *ImageRepositoryRegistry) CreateImageRepository(repo api.ImageRepository) error {
40
+	r.Lock()
41
+	defer r.Unlock()
42
+
43
+	r.ImageRepository = &repo
44
+	return r.Err
45
+}
46
+
47
+func (r *ImageRepositoryRegistry) UpdateImageRepository(repo api.ImageRepository) error {
48
+	r.Lock()
49
+	defer r.Unlock()
50
+
51
+	r.ImageRepository = &repo
52
+	return r.Err
53
+}
54
+
55
+func (r *ImageRepositoryRegistry) DeleteImageRepository(id string) error {
56
+	r.Lock()
57
+	defer r.Unlock()
58
+
59
+	return r.Err
60
+}
0 61
new file mode 100644
... ...
@@ -0,0 +1,37 @@
0
+package image
1
+
2
+import (
3
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
4
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
5
+)
6
+import "github.com/openshift/origin/pkg/image/api"
7
+
8
+// ImageRegistry is an interface for things that know how to store Image objects.
9
+type ImageRegistry interface {
10
+	// ListImages obtains a list of images that match a selector.
11
+	ListImages(selector labels.Selector) (*api.ImageList, error)
12
+	// GetImage retrieves a specific image.
13
+	GetImage(id string) (*api.Image, error)
14
+	// CreateImage creates a new image.
15
+	CreateImage(image api.Image) error
16
+	// UpdateImage updates an image.
17
+	UpdateImage(image api.Image) error
18
+	// DeleteImage deletes an image.
19
+	DeleteImage(id string) error
20
+}
21
+
22
+// ImageRepositoryRegistry is an interface for things that know how to store ImageRepository objects.
23
+type ImageRepositoryRegistry interface {
24
+	// ListImageRepositories obtains a list of image repositories that match a selector.
25
+	ListImageRepositories(selector labels.Selector) (*api.ImageRepositoryList, error)
26
+	// GetImageRepository retrieves a specific image repository.
27
+	GetImageRepository(id string) (*api.ImageRepository, error)
28
+	// WatchImageRepositories watches for new/changed/deleted image repositories.
29
+	WatchImageRepositories(resourceVersion uint64, filter func(repo *api.ImageRepository) bool) (watch.Interface, error)
30
+	// CreateImageRepository creates a new image repository.
31
+	CreateImageRepository(repo api.ImageRepository) error
32
+	// UpdateImageRepository updates an image repository.
33
+	UpdateImageRepository(repo api.ImageRepository) error
34
+	// DeleteImageRepository deletes an image repository.
35
+	DeleteImageRepository(id string) error
36
+}
0 37
new file mode 100644
... ...
@@ -0,0 +1,40 @@
0
+package image
1
+
2
+import (
3
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
4
+	"github.com/openshift/origin/pkg/image/api"
5
+)
6
+
7
+// ValidateImage tests required fields for an Image.
8
+func ValidateImage(image *api.Image) errors.ErrorList {
9
+	result := errors.ErrorList{}
10
+
11
+	if len(image.ID) == 0 {
12
+		result = append(result, errors.NewFieldRequired("ID", image.ID))
13
+	}
14
+
15
+	if len(image.DockerImageReference) == 0 {
16
+		result = append(result, errors.NewFieldRequired("DockerImageReference", image.DockerImageReference))
17
+	}
18
+
19
+	return result
20
+}
21
+
22
+// ValidateImageRepositoryMapping tests required fields for an ImageRepositoryMapping.
23
+func ValidateImageRepositoryMapping(mapping *api.ImageRepositoryMapping) errors.ErrorList {
24
+	result := errors.ErrorList{}
25
+
26
+	if len(mapping.DockerImageRepository) == 0 {
27
+		result = append(result, errors.NewFieldRequired("DockerImageRepository", mapping.DockerImageRepository))
28
+	}
29
+
30
+	if len(mapping.Tag) == 0 {
31
+		result = append(result, errors.NewFieldRequired("Tag", mapping.Tag))
32
+	}
33
+
34
+	for _, err := range ValidateImage(&mapping.Image).Prefix("image") {
35
+		result = append(result, err)
36
+	}
37
+
38
+	return result
39
+}
0 40
new file mode 100644
... ...
@@ -0,0 +1,108 @@
0
+package image
1
+
2
+import (
3
+	"testing"
4
+
5
+	kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
6
+	kubeerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
7
+	"github.com/openshift/origin/pkg/image/api"
8
+)
9
+
10
+func TestValidateImageOK(t *testing.T) {
11
+	errs := ValidateImage(&api.Image{
12
+		JSONBase:             kubeapi.JSONBase{ID: "foo"},
13
+		DockerImageReference: "openshift/ruby-19-centos",
14
+	})
15
+	if len(errs) > 0 {
16
+		t.Errorf("Unexpected non-empty error list: %#v", errs)
17
+	}
18
+}
19
+
20
+func TestValidateImageMissingFields(t *testing.T) {
21
+	errorCases := map[string]struct {
22
+		I api.Image
23
+		T kubeerrors.ValidationErrorType
24
+		F string
25
+	}{
26
+		"missing ID":                   {api.Image{DockerImageReference: "ref"}, kubeerrors.ValidationErrorTypeRequired, "ID"},
27
+		"missing DockerImageReference": {api.Image{JSONBase: kubeapi.JSONBase{ID: "foo"}}, kubeerrors.ValidationErrorTypeRequired, "DockerImageReference"},
28
+	}
29
+
30
+	for k, v := range errorCases {
31
+		errs := ValidateImage(&v.I)
32
+		if len(errs) == 0 {
33
+			t.Errorf("Expected failure for %s", k)
34
+			continue
35
+		}
36
+		for i := range errs {
37
+			if errs[i].(kubeerrors.ValidationError).Type != v.T {
38
+				t.Errorf("%s: expected errors to have type %s: %v", k, v.T, errs[i])
39
+			}
40
+			if errs[i].(kubeerrors.ValidationError).Field != v.F {
41
+				t.Errorf("%s: expected errors to have field %s: %v", k, v.F, errs[i])
42
+			}
43
+		}
44
+	}
45
+}
46
+
47
+func TestValidateImageRepositoryMappingNotOK(t *testing.T) {
48
+	errorCases := map[string]struct {
49
+		I api.ImageRepositoryMapping
50
+		T kubeerrors.ValidationErrorType
51
+		F string
52
+	}{
53
+		"missing DockerImageRepository": {
54
+			api.ImageRepositoryMapping{
55
+				Tag: "latest",
56
+				Image: api.Image{
57
+					JSONBase: kubeapi.JSONBase{
58
+						ID: "foo",
59
+					},
60
+					DockerImageReference: "openshift/ruby-19-centos",
61
+				},
62
+			},
63
+			kubeerrors.ValidationErrorTypeRequired,
64
+			"DockerImageRepository",
65
+		},
66
+		"missing Tag": {
67
+			api.ImageRepositoryMapping{
68
+				DockerImageRepository: "openshift/ruby-19-centos",
69
+				Image: api.Image{
70
+					JSONBase: kubeapi.JSONBase{
71
+						ID: "foo",
72
+					},
73
+					DockerImageReference: "openshift/ruby-19-centos",
74
+				},
75
+			},
76
+			kubeerrors.ValidationErrorTypeRequired,
77
+			"Tag",
78
+		},
79
+		"missing image attributes": {
80
+			api.ImageRepositoryMapping{
81
+				Tag: "latest",
82
+				DockerImageRepository: "openshift/ruby-19-centos",
83
+				Image: api.Image{
84
+					DockerImageReference: "openshift/ruby-19-centos",
85
+				},
86
+			},
87
+			kubeerrors.ValidationErrorTypeRequired,
88
+			"image.ID",
89
+		},
90
+	}
91
+
92
+	for k, v := range errorCases {
93
+		errs := ValidateImageRepositoryMapping(&v.I)
94
+		if len(errs) == 0 {
95
+			t.Errorf("Expected failure for %s", k)
96
+			continue
97
+		}
98
+		for i := range errs {
99
+			if errs[i].(kubeerrors.ValidationError).Type != v.T {
100
+				t.Errorf("%s: expected errors to have type %s: %v", k, v.T, errs[i])
101
+			}
102
+			if errs[i].(kubeerrors.ValidationError).Field != v.F {
103
+				t.Errorf("%s: expected errors to have field %s: %v", k, v.F, errs[i])
104
+			}
105
+		}
106
+	}
107
+}