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.
... | ... |
@@ -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 | 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 |
+} |