Browse code

Initial support for Config{} creation

Michal Fojtik authored on 2014/09/08 21:24:56
Showing 6 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,131 @@
0
+{
1
+  "id": "guestbook",
2
+  "kind": "Config",
3
+  "name": "guestbook",
4
+  "description": "A simple guestbook application configuration",
5
+  "items": [
6
+    {
7
+      "id": "frontend",
8
+      "kind": "Service",
9
+      "apiVersion": "v1beta1",
10
+      "port": 5432,
11
+      "selector": {
12
+        "name": "frontend"
13
+      }
14
+    },
15
+    {
16
+      "id": "redismaster",
17
+      "kind": "Service",
18
+      "apiVersion": "v1beta1",
19
+      "port": 10000,
20
+      "selector": {
21
+        "name": "redis-master"
22
+      }
23
+    },
24
+    {
25
+      "id": "redisslave",
26
+      "kind": "Service",
27
+      "apiVersion": "v1beta1",
28
+      "port": 10001,
29
+      "labels": {
30
+        "name": "redisslave"
31
+      },
32
+      "selector": {
33
+        "name": "redisslave"
34
+      }
35
+    },
36
+    {
37
+      "id": "redis-master-2",
38
+      "kind": "Pod",
39
+      "apiVersion": "v1beta1",
40
+      "desiredState": {
41
+        "manifest": {
42
+          "version": "v1beta1",
43
+          "id": "redis-master-2",
44
+          "containers": [{
45
+            "name": "master",
46
+            "image": "dockerfile/redis",
47
+            "env": [
48
+              {
49
+                "name": "REDIS_PASSWORD",
50
+                "value": "secret"
51
+              }
52
+            ],
53
+            "ports": [{
54
+              "containerPort": 6379
55
+            }]
56
+          }]
57
+        }
58
+      },
59
+      "labels": {
60
+        "name": "redis-master"
61
+      }
62
+    },
63
+    {
64
+      "id": "frontendController",
65
+      "kind": "ReplicationController",
66
+      "apiVersion": "v1beta1",
67
+      "desiredState": {
68
+        "replicas": 3,
69
+        "replicaSelector": {"name": "frontend"},
70
+        "podTemplate": {
71
+          "desiredState": {
72
+            "manifest": {
73
+              "version": "v1beta1",
74
+              "id": "frontendController",
75
+              "containers": [{
76
+                "name": "php-redis",
77
+                "image": "brendanburns/php-redis",
78
+                "env": [
79
+                  {
80
+                    "name": "ADMIN_USERNAME",
81
+                    "value": "admin"
82
+                  },
83
+                  {
84
+                    "name": "ADMIN_PASSWORD",
85
+                    "value": "secret"
86
+                  },
87
+                  {
88
+                    "name": "REDIS_PASSWORD",
89
+                    "value": "secret"
90
+                  }
91
+                ],
92
+                "ports": [{"containerPort": 80, "hostPort": 8000}]
93
+              }]
94
+            }
95
+          },
96
+          "labels": {"name": "frontend"}
97
+        }},
98
+        "labels": {"name": "frontend"}
99
+    },
100
+    {
101
+      "id": "redisSlaveController",
102
+      "kind": "ReplicationController",
103
+      "apiVersion": "v1beta1",
104
+      "desiredState": {
105
+        "replicas": 2,
106
+        "replicaSelector": {"name": "redisslave"},
107
+        "podTemplate": {
108
+          "desiredState": {
109
+            "manifest": {
110
+              "version": "v1beta1",
111
+              "id": "redisSlaveController",
112
+              "containers": [{
113
+                "name": "slave",
114
+                "image": "brendanburns/redis-slave",
115
+                "env": [
116
+                  {
117
+                    "name": "REDIS_PASSWORD",
118
+                    "value": "secret"
119
+                  }
120
+                ],
121
+                "ports": [{"containerPort": 6379, "hostPort": 6380}]
122
+              }]
123
+            }
124
+          },
125
+          "labels": {"name": "redisslave"}
126
+        }},
127
+        "labels": {"name": "redisslave"}
128
+    }
129
+  ]
130
+}
... ...
@@ -69,3 +69,6 @@ ${KUBE_CMD} -c examples/image/test-mapping.json create imageRepositoryMappings
69 69
 ${KUBE_CMD} list images
70 70
 ${KUBE_CMD} list imageRepositories
71 71
 echo "kube(imageRepositoryMappings): ok"
72
+
73
+${KUBE_CMD} apply -c examples/guestbook/config.json
74
+echo "kube(config): ok"
72 75
new file mode 100644
... ...
@@ -0,0 +1,12 @@
0
+package api
1
+
2
+import "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
3
+
4
+type RESTClient interface {
5
+	Verb(verb string) *client.Request
6
+}
7
+
8
+type ClientMappings map[string]struct {
9
+	Kind   string
10
+	Client RESTClient
11
+}
... ...
@@ -38,15 +38,15 @@ import (
38 38
 	"github.com/golang/glog"
39 39
 	buildapi "github.com/openshift/origin/pkg/build/api"
40 40
 	osclient "github.com/openshift/origin/pkg/client"
41
+	. "github.com/openshift/origin/pkg/cmd/client/api"
41 42
 	"github.com/openshift/origin/pkg/cmd/client/build"
42 43
 	"github.com/openshift/origin/pkg/cmd/client/image"
44
+	"github.com/openshift/origin/pkg/config"
45
+	configapi "github.com/openshift/origin/pkg/config/api"
46
+	_ "github.com/openshift/origin/pkg/config/api/v1beta1"
43 47
 	imageapi "github.com/openshift/origin/pkg/image/api"
44 48
 )
45 49
 
46
-type RESTClient interface {
47
-	Verb(verb string) *kubeclient.Request
48
-}
49
-
50 50
 type KubeConfig struct {
51 51
 	ServerVersion bool
52 52
 	PreventSkew   bool
... ...
@@ -84,6 +84,9 @@ func usage(name string) string {
84 84
   %[1]s [OPTIONS] stop|rm|rollingupdate <controller>
85 85
   %[1]s [OPTIONS] run <image> <replicas> <controller>
86 86
   %[1]s [OPTIONS] resize <controller> <replicas>
87
+
88
+	Perform bulk operations on groups of Kubernetes resources:
89
+  %[1]s [OPTIONS] apply -c config.json
87 90
 `, name, prettyWireStorage())
88 91
 }
89 92
 
... ...
@@ -97,6 +100,7 @@ var parser = kubecfg.NewParser(map[string]interface{}{
97 97
 	"images":                  imageapi.Image{},
98 98
 	"imageRepositories":       imageapi.ImageRepository{},
99 99
 	"imageRepositoryMappings": imageapi.ImageRepositoryMapping{},
100
+	"config":                  configapi.Config{},
100 101
 })
101 102
 
102 103
 func prettyWireStorage() string {
... ...
@@ -213,19 +217,19 @@ func (c *KubeConfig) Run() {
213 213
 	}
214 214
 
215 215
 	method := c.Arg(0)
216
-	clients := map[string]RESTClient{
217
-		"minions":                 kubeClient.RESTClient,
218
-		"pods":                    kubeClient.RESTClient,
219
-		"services":                kubeClient.RESTClient,
220
-		"replicationControllers":  kubeClient.RESTClient,
221
-		"builds":                  client.RESTClient,
222
-		"buildConfigs":            client.RESTClient,
223
-		"images":                  client.RESTClient,
224
-		"imageRepositories":       client.RESTClient,
225
-		"imageRepositoryMappings": client.RESTClient,
216
+	clients := ClientMappings{
217
+		"minions":                 {"Minion", kubeClient.RESTClient},
218
+		"pods":                    {"Pod", kubeClient.RESTClient},
219
+		"services":                {"Service", kubeClient.RESTClient},
220
+		"replicationControllers":  {"ReplicationController", kubeClient.RESTClient},
221
+		"builds":                  {"Build", client.RESTClient},
222
+		"buildConfigs":            {"BuildConfig", client.RESTClient},
223
+		"images":                  {"Image", client.RESTClient},
224
+		"imageRepositories":       {"ImageRepository", client.RESTClient},
225
+		"imageRepositoryMappings": {"ImageRepositoryMapping", client.RESTClient},
226 226
 	}
227 227
 
228
-	matchFound := c.executeAPIRequest(method, clients) || c.executeControllerRequest(method, kubeClient)
228
+	matchFound := c.executeConfigRequest(method, clients) || c.executeAPIRequest(method, clients) || c.executeControllerRequest(method, kubeClient)
229 229
 	if matchFound == false {
230 230
 		glog.Fatalf("Unknown command %s", method)
231 231
 	}
... ...
@@ -252,7 +256,7 @@ func checkStorage(storage string) bool {
252 252
 	return false
253 253
 }
254 254
 
255
-func (c *KubeConfig) executeAPIRequest(method string, clients map[string]RESTClient) bool {
255
+func (c *KubeConfig) executeAPIRequest(method string, clients ClientMappings) bool {
256 256
 	storage, path, hasSuffix := storagePathFromArg(c.Arg(1))
257 257
 	validStorage := checkStorage(storage)
258 258
 	client, ok := clients[storage]
... ...
@@ -286,7 +290,7 @@ func (c *KubeConfig) executeAPIRequest(method string, clients map[string]RESTCli
286 286
 			glog.Fatalf("usage: kubecfg [OPTIONS] %s <%s>", method, prettyWireStorage())
287 287
 		}
288 288
 	case "update":
289
-		obj, err := client.Verb("GET").Path(path).Do().Get()
289
+		obj, err := client.Client.Verb("GET").Path(path).Do().Get()
290 290
 		if err != nil {
291 291
 			glog.Fatalf("error obtaining resource version for update: %v", err)
292 292
 		}
... ...
@@ -304,7 +308,7 @@ func (c *KubeConfig) executeAPIRequest(method string, clients map[string]RESTCli
304 304
 		return false
305 305
 	}
306 306
 
307
-	r := client.Verb(verb).
307
+	r := client.Client.Verb(verb).
308 308
 		Path(path).
309 309
 		ParseSelectorParam("labels", c.Selector)
310 310
 	if setBody {
... ...
@@ -421,6 +425,20 @@ func (c *KubeConfig) executeControllerRequest(method string, client *kubeclient.
421 421
 	return true
422 422
 }
423 423
 
424
+func (c *KubeConfig) executeConfigRequest(method string, clients ClientMappings) bool {
425
+	if method != "apply" {
426
+		return false
427
+	}
428
+	if len(c.Config) == 0 {
429
+		glog.Fatal("Need to pass valid configuration file (-c config.json)")
430
+	}
431
+	errs := config.Apply(c.readConfig("config"), clients)
432
+	for _, err := range errs {
433
+		fmt.Println(err.Error())
434
+	}
435
+	return true
436
+}
437
+
424 438
 func humanReadablePrinter() *kubecfg.HumanReadablePrinter {
425 439
 	printer := kubecfg.NewHumanReadablePrinter()
426 440
 
427 441
new file mode 100644
... ...
@@ -0,0 +1,88 @@
0
+package config
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+
6
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
7
+	clientapi "github.com/openshift/origin/pkg/cmd/client/api"
8
+)
9
+
10
+// configJSON stores the raw Config JSON representation
11
+// TODO: Replace this with configapi.Config when it handles the unregistred types.
12
+type configJSON struct {
13
+	Items []interface{} `json:"items" yaml:"items"`
14
+}
15
+
16
+// Apply creates and manages resources defined in the Config. It wont stop on
17
+// error, but it will finish the job and then return list of errors.
18
+func Apply(data []byte, storage clientapi.ClientMappings) (errs errors.ErrorList) {
19
+
20
+	// Unmarshal the Config JSON using default json package instead of
21
+	// api.Decode()
22
+	conf := configJSON{}
23
+	if err := json.Unmarshal(data, &conf); err != nil {
24
+		return append(errs, fmt.Errorf("Unable to parse Config: %v", err))
25
+	}
26
+
27
+	for _, item := range conf.Items {
28
+		kind, itemId, parseErrs := parseKindAndId(item)
29
+		if len(parseErrs) != 0 {
30
+			errs = append(errs, parseErrs...)
31
+			continue
32
+		}
33
+
34
+		client, path := getClientAndPath(kind, storage)
35
+		if client == nil {
36
+			errs = append(errs, fmt.Errorf("The resource %s is not a known type - unable to create %s", kind, itemId))
37
+			continue
38
+		}
39
+
40
+		// Serialize the single Config item back into JSON
41
+		itemJson, _ := json.Marshal(item)
42
+
43
+		request := client.Verb("POST").Path(path).Body(itemJson)
44
+		_, err := request.Do().Get()
45
+		if err != nil {
46
+			errs = append(errs, fmt.Errorf("[%s#%s] Failed to create: %v", kind, itemId, err))
47
+		}
48
+	}
49
+
50
+	return
51
+}
52
+
53
+// getClientAndPath returns the RESTClient and path defined for given resource
54
+// kind.
55
+func getClientAndPath(kind string, mappings clientapi.ClientMappings) (client clientapi.RESTClient, path string) {
56
+	for k, m := range mappings {
57
+		if k == kind {
58
+			return m.Client, k
59
+		}
60
+	}
61
+	return
62
+}
63
+
64
+// parseKindAndId extracts the 'kind' and 'id' fields from the Config item JSON
65
+// and report errors if these fields are missing.
66
+func parseKindAndId(item interface{}) (kind, id string, errs errors.ErrorList) {
67
+	itemMap := item.(map[string]interface{})
68
+
69
+	kind, ok := itemMap["kind"].(string)
70
+	if !ok {
71
+		errs = append(errs, reportError(item, "Missing 'kind' field for Config item"))
72
+	}
73
+
74
+	id, ok = itemMap["id"].(string)
75
+	if !ok {
76
+		errs = append(errs, reportError(item, "Missing 'id' field for Config item"))
77
+	}
78
+
79
+	return
80
+}
81
+
82
+// reportError provides a human-readable error message that include the Config
83
+// item JSON representation.
84
+func reportError(item interface{}, message string) error {
85
+	itemJson, _ := json.Marshal(item)
86
+	return fmt.Errorf(message+": %s", string(itemJson))
87
+}
0 88
new file mode 100644
... ...
@@ -0,0 +1,65 @@
0
+package config
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+	"io/ioutil"
6
+	"testing"
7
+
8
+	kubeclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
9
+	clientapi "github.com/openshift/origin/pkg/cmd/client/api"
10
+)
11
+
12
+func TestParseKindAndItem(t *testing.T) {
13
+	data, _ := ioutil.ReadFile("../../examples/guestbook/config.json")
14
+	conf := configJSON{}
15
+	if err := json.Unmarshal(data, &conf); err != nil {
16
+		t.Errorf("Failed to parse Config: %v", err)
17
+	}
18
+
19
+	kind, itemId, err := parseKindAndId(conf.Items[0])
20
+	if len(err) != 0 {
21
+		t.Errorf("Failed to parse kind and id from the Config item: %v", err)
22
+	}
23
+
24
+	if kind != "Service" && itemId != "frontend" {
25
+		t.Errorf("Invalid kind and id, should be Service and frontend: %s, %s", kind, itemId)
26
+	}
27
+}
28
+
29
+func TestApply(t *testing.T) {
30
+	invalidData := []byte(`{"items": [ { "foo": "bar" } ]}`)
31
+	invalidConf := configJSON{}
32
+	if err := json.Unmarshal(invalidData, &invalidConf); err != nil {
33
+		t.Errorf("Failed to parse Config: %v", err)
34
+	}
35
+	clients := clientapi.ClientMappings{}
36
+	errs := Apply(invalidData, clients)
37
+	if len(errs) == 0 {
38
+		t.Errorf("Expected missing kind field for Config item, got %v", errs)
39
+	}
40
+	uErrs := Apply([]byte(`{ "foo": }`), clients)
41
+	if len(uErrs) == 0 {
42
+		t.Errorf("Expected unmarshal error, got nothing")
43
+	}
44
+}
45
+
46
+func ExampleApply() {
47
+	kubeClient, _ := kubeclient.New("127.0.0.1", nil)
48
+	clients := clientapi.ClientMappings{
49
+		"pods": {
50
+			Kind:   "Pod",
51
+			Client: kubeClient.RESTClient,
52
+		},
53
+		"services": {
54
+			Kind:   "Service",
55
+			Client: kubeClient.RESTClient,
56
+		},
57
+	}
58
+	data, _ := ioutil.ReadFile("../../examples/guestbook/config.json")
59
+	errs := Apply(data, clients)
60
+	fmt.Println(errs)
61
+	// Output:
62
+	// [The resource Service is not a known type - unable to create frontend The resource Service is not a known type - unable to create redismaster The resource Service is not a known type - unable to create redisslave The resource Pod is not a known type - unable to create redis-master-2 The resource ReplicationController is not a known type - unable to create frontendController The resource ReplicationController is not a known type - unable to create redisSlaveController]
63
+	//
64
+}