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