diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 00000000000..41a93c7dc4b --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,92 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + errs "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +// ClientFunc returns the RESTClient defined for given resource +type ClientFunc func(mapping *meta.RESTMapping) (*client.RESTClient, error) + +// ApplyItems creates bulk of resources provided by items list. Each item must +// be valid API type. It requires ObjectTyper to parse the Version and Kind and +// RESTMapper to get the resource URI and REST client that knows how to create +// given type +func CreateObjects(typer runtime.ObjectTyper, mapper meta.RESTMapper, clientFor ClientFunc, objects []runtime.Object) errs.ValidationErrorList { + allErrors := errs.ValidationErrorList{} + for i, obj := range objects { + version, kind, err := typer.ObjectVersionAndKind(obj) + if err != nil { + reportError(&allErrors, i, errs.NewFieldInvalid("kind", obj)) + continue + } + + mapping, err := mapper.RESTMapping(version, kind) + if err != nil { + reportError(&allErrors, i, errs.NewFieldNotSupported("mapping", err)) + continue + } + + client, err := clientFor(mapping) + if err != nil { + reportError(&allErrors, i, errs.NewFieldNotSupported("client", obj)) + continue + } + + if err := CreateObject(client, mapping, obj); err != nil { + reportError(&allErrors, i, *err) + } + } + + return allErrors.Prefix("Config") +} + +// Apply creates the obj using the provided clients and the resource URI +// mapping. It reports ValidationError when the object is missing the Metadata +// or the Name and it will report any error occured during create REST call +func CreateObject(client *client.RESTClient, mapping *meta.RESTMapping, obj runtime.Object) *errs.ValidationError { + name, err := mapping.MetadataAccessor.Name(obj) + if err != nil || name == "" { + e := errs.NewFieldRequired("name", err) + return &e + } + + namespace, err := mapping.Namespace(obj) + if err != nil { + e := errs.NewFieldRequired("namespace", err) + return &e + } + + // TODO: This should be using RESTHelper + err = client.Post().Path(mapping.Resource).Namespace(namespace).Body(obj).Do().Error() + if err != nil { + return &errs.ValidationError{errs.ValidationErrorTypeInvalid, name, err} + } + + return nil +} + +// reportError reports the single item validation error and properly set the +// prefix and index to match the Config item JSON index +func reportError(allErrs *errs.ValidationErrorList, index int, err errs.ValidationError) { + i := errs.ValidationErrorList{} + *allErrs = append(*allErrs, append(i, err).PrefixIndex(index).Prefix("item")...) +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 00000000000..0c318f4e212 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,164 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +func getTyperAndMapper() (runtime.ObjectTyper, meta.RESTMapper) { + return api.Scheme, latest.RESTMapper +} + +func getFakeClient(t *testing.T, validURLs []string) (ClientFunc, *httptest.Server) { + handlerFunc := func(w http.ResponseWriter, r *http.Request) { + for _, u := range validURLs { + if u == r.RequestURI { + return + } + } + t.Errorf("Unexpected HTTP request: %s, expected %v", r.RequestURI, validURLs) + } + server := httptest.NewServer(http.HandlerFunc(handlerFunc)) + return func(mapping *meta.RESTMapping) (*client.RESTClient, error) { + fakeCodec := runtime.CodecFor(api.Scheme, "v1beta1") + fakeUri, _ := url.Parse(server.URL + "/api/v1beta1") + return client.NewRESTClient(fakeUri, fakeCodec), nil + }, server +} + +func TestCreateObjects(t *testing.T) { + items := []runtime.Object{} + + items = append(items, &api.Pod{ + TypeMeta: api.TypeMeta{APIVersion: "v1beta1", Kind: "Pod"}, + ObjectMeta: api.ObjectMeta{Name: "test-pod"}, + }) + + items = append(items, &api.Service{ + TypeMeta: api.TypeMeta{APIVersion: "v1beta1", Kind: "Service"}, + ObjectMeta: api.ObjectMeta{Name: "test-service"}, + }) + + typer, mapper := getTyperAndMapper() + client, s := getFakeClient(t, []string{"/api/v1beta1/pods", "/api/v1beta1/services"}) + + errs := CreateObjects(typer, mapper, client, items) + s.Close() + if len(errs) != 0 { + t.Errorf("Unexpected errors during config.Create(): %v", errs) + } +} + +func TestCreateNoNameItem(t *testing.T) { + items := []runtime.Object{} + + items = append(items, &api.Service{ + TypeMeta: api.TypeMeta{APIVersion: "v1beta1", Kind: "Service"}, + }) + + typer, mapper := getTyperAndMapper() + client, s := getFakeClient(t, []string{"/api/v1beta1/services"}) + + errs := CreateObjects(typer, mapper, client, items) + s.Close() + + if len(errs) == 0 { + t.Errorf("Expected required value error for missing name") + } + + e := errs[0].(errors.ValidationError) + if errors.ValueOf(e.Type) != "required value" { + t.Errorf("Expected ValidationErrorTypeRequired error, got %#v", e) + } + + if e.Field != "Config.item[0].name" { + t.Errorf("Expected 'Config.item[0].name' as error field, got '%#v'", e.Field) + } +} + +type InvalidItem struct{} + +func (*InvalidItem) IsAnAPIObject() {} + +func TestCreateInvalidItem(t *testing.T) { + items := []runtime.Object{ + &InvalidItem{}, + } + + typer, mapper := getTyperAndMapper() + client, s := getFakeClient(t, []string{}) + + errs := CreateObjects(typer, mapper, client, items) + s.Close() + + if len(errs) == 0 { + t.Errorf("Expected invalid value error for kind") + } + + e := errs[0].(errors.ValidationError) + if errors.ValueOf(e.Type) != "invalid value" { + t.Errorf("Expected ValidationErrorTypeInvalid error, got %#v", e) + } + + if e.Field != "Config.item[0].kind" { + t.Errorf("Expected 'Config.item[0].kind' as error field, got '%#v'", e.Field) + } +} + +func TestCreateNoClientItems(t *testing.T) { + items := []runtime.Object{} + + items = append(items, &api.Pod{ + TypeMeta: api.TypeMeta{APIVersion: "v1beta1", Kind: "Pod"}, + ObjectMeta: api.ObjectMeta{Name: "test-pod"}, + }) + + typer, mapper := getTyperAndMapper() + _, s := getFakeClient(t, []string{"/api/v1beta1/pods", "/api/v1beta1/services"}) + + noClientFunc := func(mapping *meta.RESTMapping) (*client.RESTClient, error) { + return nil, fmt.Errorf("no client") + } + + errs := CreateObjects(typer, mapper, noClientFunc, items) + s.Close() + + if len(errs) == 0 { + t.Errorf("Expected invalid value error for client") + } + + e := errs[0].(errors.ValidationError) + if errors.ValueOf(e.Type) != "unsupported value" { + t.Errorf("Expected ValidationErrorTypeUnsupported error, got %#v", e) + } + + if e.Field != "Config.item[0].client" { + t.Errorf("Expected 'Config.item[0].client' as error field, got '%#v'", e.Field) + } +} diff --git a/pkg/config/config_test.json b/pkg/config/config_test.json new file mode 100644 index 00000000000..df694a0bdad --- /dev/null +++ b/pkg/config/config_test.json @@ -0,0 +1,128 @@ +[ + { + "id": "frontend", + "name": "frontend", + "kind": "Service", + "apiVersion": "v1beta2", + "port": 5432, + "selector": { + "name": "frontend" + } + }, + { + "id": "redismaster", + "name": "redismaster", + "kind": "Service", + "apiVersion": "v1beta1", + "port": 10000, + "selector": { + "name": "redis-master" + } + }, + { + "id": "redisslave", + "name": "redisslave", + "kind": "Service", + "apiVersion": "v1beta1", + "port": 10001, + "labels": { + "name": "redisslave" + }, + "selector": { + "name": "redisslave" + } + }, + { + "id": "redis-master-2", + "name": "redis-master-2", + "kind": "Pod", + "apiVersion": "v1beta1", + "desiredState": { + "manifest": { + "version": "v1beta1", + "containers": [{ + "name": "master", + "image": "dockerfile/redis", + "env": [ + { + "name": "REDIS_PASSWORD", + "value": "secret" + } + ], + "ports": [{ + "containerPort": 6379 + }] + }] + } + }, + "labels": { + "name": "redis-master" + } + }, + { + "id": "frontendController", + "name": "frontendController", + "kind": "ReplicationController", + "apiVersion": "v1beta1", + "desiredState": { + "replicas": 3, + "replicaSelector": {"name": "frontend"}, + "podTemplate": { + "desiredState": { + "manifest": { + "version": "v1beta1", + "containers": [{ + "name": "php-redis", + "image": "brendanburns/php-redis", + "env": [ + { + "name": "ADMIN_USERNAME", + "value": "admin" + }, + { + "name": "ADMIN_PASSWORD", + "value": "secret" + }, + { + "name": "REDIS_PASSWORD", + "value": "secret" + } + ], + "ports": [{"containerPort": 80}] + }] + } + }, + "labels": {"name": "frontend"} + }}, + "labels": {"name": "frontend"} + }, + { + "id": "redisSlaveController", + "name": "redisSlaveController", + "kind": "ReplicationController", + "apiVersion": "v1beta1", + "desiredState": { + "replicas": 2, + "replicaSelector": {"name": "redisslave"}, + "podTemplate": { + "desiredState": { + "manifest": { + "version": "v1beta1", + "containers": [{ + "name": "slave", + "image": "brendanburns/redis-slave", + "env": [ + { + "name": "REDIS_PASSWORD", + "value": "secret" + } + ], + "ports": [{"containerPort": 6379}] + }] + } + }, + "labels": {"name": "redisslave"} + }}, + "labels": {"name": "redisslave"} + } +] diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index d86603032a9..575fccaccb0 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -68,6 +68,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, cmds.AddCommand(NewCmdDelete(out)) cmds.AddCommand(NewCmdNamespace(out)) cmds.AddCommand(NewCmdLog(out)) + cmds.AddCommand(NewCmdCreateAll(out)) if err := cmds.Execute(); err != nil { os.Exit(1) diff --git a/pkg/kubectl/cmd/createall.go b/pkg/kubectl/cmd/createall.go new file mode 100644 index 00000000000..55435484d1a --- /dev/null +++ b/pkg/kubectl/cmd/createall.go @@ -0,0 +1,116 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + errs "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/config" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/golang/glog" + "github.com/spf13/cobra" + "gopkg.in/v1/yaml" +) + +// DataToObjects converts the raw JSON data into API objects +func DataToObjects(m meta.RESTMapper, t runtime.ObjectTyper, data []byte) (result []runtime.Object, errors errs.ValidationErrorList) { + configObj := []runtime.RawExtension{} + + if err := yaml.Unmarshal(data, &configObj); err != nil { + errors = append(errors, errs.NewFieldInvalid("unmarshal", err)) + return result, errors.Prefix("Config") + } + + for i, in := range configObj { + version, kind, err := t.DataVersionAndKind(in.RawJSON) + if err != nil { + itemErrs := errs.ValidationErrorList{} + itemErrs = append(itemErrs, errs.NewFieldInvalid("kind", string(in.RawJSON))) + errors = append(errors, itemErrs.PrefixIndex(i).Prefix("item")...) + continue + } + + mapping, err := m.RESTMapping(version, kind) + if err != nil { + itemErrs := errs.ValidationErrorList{} + itemErrs = append(itemErrs, errs.NewFieldRequired("mapping", err)) + errors = append(errors, itemErrs.PrefixIndex(i).Prefix("item")...) + continue + } + + obj, err := mapping.Codec.Decode(in.RawJSON) + if err != nil { + itemErrs := errs.ValidationErrorList{} + itemErrs = append(itemErrs, errs.NewFieldInvalid("decode", err)) + errors = append(errors, itemErrs.PrefixIndex(i).Prefix("item")...) + continue + } + result = append(result, obj) + } + return +} + +func NewCmdCreateAll(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "createall -f filename", + Short: "Create all resources specified in filename or stdin", + Long: `Create all resources contained in JSON file specified in filename or stdin + +JSON and YAML formats are accepted. + +Examples: + $ kubectl createall -f config.json + + + $ cat config.json | kubectl apply -f - + `, + Run: func(cmd *cobra.Command, args []string) { + // TODO: Replace this with Factory.Typer + typer := api.Scheme + // TODO: Replace this with Factory.Mapper + mapper := latest.RESTMapper + // TODO: Replace this with Factory.Client + clientFunc := func(*meta.RESTMapping) (*client.RESTClient, error) { + return getKubeClient(cmd).RESTClient, nil + } + + filename := getFlagString(cmd, "filename") + if len(filename) == 0 { + usageError(cmd, "Must pass a filename to update") + } + + data, err := readConfigData(filename) + checkErr(err) + + items, errs := DataToObjects(mapper, typer, data) + applyErrs := config.CreateObjects(typer, mapper, clientFunc, items) + errs = append(errs, applyErrs...) + if len(errs) > 0 { + for _, e := range errs { + glog.Error(e) + } + } + }, + } + cmd.Flags().StringP("filename", "f", "", "Filename or URL to file to use to update the resource") + return cmd +}