diff --git a/cmd/kubecfg/kubecfg.go b/cmd/kubecfg/kubecfg.go index 14f5a5c99b4..94c54c391bb 100644 --- a/cmd/kubecfg/kubecfg.go +++ b/cmd/kubecfg/kubecfg.go @@ -152,12 +152,18 @@ func readConfigData() []byte { // readConfig reads and parses pod, replicationController, and service // configuration files. If any errors log and exit non-zero. -func readConfig(storage string, serverCodec runtime.Codec) []byte { +func readConfig(storage string, c *client.Client) []byte { + serverCodec := c.RESTClient.Codec if len(*config) == 0 { glog.Fatal("Need config file (-c)") } - data, err := parser.ToWireFormat(readConfigData(), storage, latest.Codec, serverCodec) + dataInput := readConfigData() + err := kubecfg.ValidateObject(dataInput, c) + if err != nil { + glog.Fatalf("Error validating %v as an object for %v: %v\n", *config, storage, err) + } + data, err := parser.ToWireFormat(dataInput, storage, latest.Codec, serverCodec) if err != nil { glog.Fatalf("Error parsing %v as an object for %v: %v\n", *config, storage, err) @@ -383,7 +389,7 @@ func executeAPIRequest(ctx api.Context, method string, c *client.Client) bool { glog.Fatalf("usage: kubecfg [OPTIONS] %s <%s>/", method, prettyWireStorage()) } case "print": - data := readConfig(storage, c.RESTClient.Codec) + data := readConfig(storage, c) obj, err := latest.Codec.Decode(data) if err != nil { glog.Fatalf("error setting resource version: %v", err) @@ -403,7 +409,7 @@ func executeAPIRequest(ctx api.Context, method string, c *client.Client) bool { } if setBody { if len(version) > 0 { - data := readConfig(storage, c.RESTClient.Codec) + data := readConfig(storage, c) obj, err := latest.Codec.Decode(data) if err != nil { glog.Fatalf("error setting resource version: %v", err) @@ -419,7 +425,7 @@ func executeAPIRequest(ctx api.Context, method string, c *client.Client) bool { } r.Body(data) } else { - r.Body(readConfig(storage, c.RESTClient.Codec)) + r.Body(readConfig(storage, c)) } } result := r.Do() diff --git a/pkg/api/validation/schema.go b/pkg/api/validation/schema.go index 82457f9cff4..8b43ad456bc 100644 --- a/pkg/api/validation/schema.go +++ b/pkg/api/validation/schema.go @@ -40,12 +40,21 @@ func NewInvalidTypeError(expected reflect.Kind, observed reflect.Kind, fieldName return &InvalidTypeError{expected, observed, fieldName} } -type Schema struct { +// Schema is an interface that knows how to validate an API object serialized to a byte array. +type Schema interface { + ValidateBytes(data []byte) error +} + +type NullSchema struct{} + +func (NullSchema) ValidateBytes(data []byte) error { return nil } + +type SwaggerSchema struct { api swagger.ApiDeclaration } -func NewSchemaFromBytes(data []byte) (*Schema, error) { - schema := &Schema{} +func NewSwaggerSchemaFromBytes(data []byte) (Schema, error) { + schema := &SwaggerSchema{} err := json.Unmarshal(data, &schema.api) if err != nil { return nil, err @@ -53,7 +62,7 @@ func NewSchemaFromBytes(data []byte) (*Schema, error) { return schema, nil } -func (s *Schema) ValidateBytes(data []byte) error { +func (s *SwaggerSchema) ValidateBytes(data []byte) error { var obj interface{} err := json.Unmarshal(data, &obj) if err != nil { @@ -65,7 +74,7 @@ func (s *Schema) ValidateBytes(data []byte) error { return s.ValidateObject(obj, apiVersion, "", apiVersion+"."+kind) } -func (s *Schema) ValidateObject(obj interface{}, apiVersion, fieldName, typeName string) error { +func (s *SwaggerSchema) ValidateObject(obj interface{}, apiVersion, fieldName, typeName string) error { models := s.api.Models // TODO: handle required fields here too. model, ok := models[typeName] @@ -98,7 +107,7 @@ func (s *Schema) ValidateObject(obj interface{}, apiVersion, fieldName, typeName return nil } -func (s *Schema) validateField(value interface{}, apiVersion, fieldName, fieldType string, fieldDetails *swagger.ModelProperty) error { +func (s *SwaggerSchema) validateField(value interface{}, apiVersion, fieldName, fieldType string, fieldDetails *swagger.ModelProperty) error { if strings.HasPrefix(fieldType, apiVersion) { return s.ValidateObject(value, apiVersion, fieldName, fieldType) } diff --git a/pkg/api/validation/schema_test.go b/pkg/api/validation/schema_test.go index e44f1190e7d..acb33efc08c 100644 --- a/pkg/api/validation/schema_test.go +++ b/pkg/api/validation/schema_test.go @@ -30,12 +30,12 @@ import ( fuzz "github.com/google/gofuzz" ) -func LoadSchemaForTest(file string) (*Schema, error) { +func LoadSchemaForTest(file string) (Schema, error) { data, err := ioutil.ReadFile(file) if err != nil { return nil, err } - return NewSchemaFromBytes(data) + return NewSwaggerSchemaFromBytes(data) } // TODO: this is cloned from serialization_test.go, refactor to somewhere common like util diff --git a/pkg/kubecfg/validate.go b/pkg/kubecfg/validate.go new file mode 100644 index 00000000000..b13175f0339 --- /dev/null +++ b/pkg/kubecfg/validate.go @@ -0,0 +1,51 @@ +/* +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 kubecfg + +import ( + "encoding/json" + "fmt" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +func ValidateObject(data []byte, c *client.Client) error { + var obj interface{} + err := json.Unmarshal(data, &obj) + if err != nil { + return err + } + apiVersion, found := obj.(map[string]interface{})["apiVersion"] + if !found { + return fmt.Errorf("couldn't find apiVersion in object") + } + + schemaData, err := c.RESTClient.Get(). + AbsPath("/swaggerapi/api"). + Path(apiVersion.(string)). + Do(). + Raw() + if err != nil { + return err + } + schema, err := validation.NewSwaggerSchemaFromBytes(schemaData) + if err != nil { + return err + } + return schema.ValidateBytes(data) +} diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index fe1bc246346..b092611e78c 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -24,6 +24,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" @@ -36,6 +37,7 @@ import ( type Factory struct { Mapper meta.RESTMapper Typer runtime.ObjectTyper + Validator func(*cobra.Command) (validation.Schema, error) Client func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error) Describer func(*cobra.Command, *meta.RESTMapping) (kubectl.Describer, error) Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) @@ -46,6 +48,13 @@ func NewFactory() *Factory { return &Factory{ Mapper: latest.RESTMapper, Typer: api.Scheme, + Validator: func(cmd *cobra.Command) (validation.Schema, error) { + if GetFlagBool(cmd, "validate") { + return &clientSwaggerSchema{getKubeClient(cmd), api.Scheme}, nil + } else { + return validation.NullSchema{}, nil + } + }, Client: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) { return getKubeClient(cmd), nil }, @@ -87,6 +96,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, cmds.PersistentFlags().Bool("insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure.") cmds.PersistentFlags().String("ns-path", os.Getenv("HOME")+"/.kubernetes_ns", "Path to the namespace info file that holds the namespace context to use for CLI requests.") cmds.PersistentFlags().StringP("namespace", "n", "", "If present, the namespace scope for this CLI request.") + cmds.PersistentFlags().Bool("validate", false, "If true, use a schema to validate the input before sending it") cmds.AddCommand(NewCmdVersion(out)) cmds.AddCommand(NewCmdProxy(out)) @@ -216,3 +226,28 @@ func getKubeClient(cmd *cobra.Command) *client.Client { } return c } + +type clientSwaggerSchema struct { + c *client.Client + t runtime.ObjectTyper +} + +func (c *clientSwaggerSchema) ValidateBytes(data []byte) error { + version, _, err := c.t.DataVersionAndKind(data) + if err != nil { + return err + } + schemaData, err := c.c.RESTClient.Get(). + AbsPath("/swaggerapi/api"). + Path(version). + Do(). + Raw() + if err != nil { + return err + } + schema, err := validation.NewSwaggerSchemaFromBytes(schemaData) + if err != nil { + return err + } + return schema.ValidateBytes(data) +} diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index 2d2bbca7853..6c6f632eeaf 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -43,7 +43,9 @@ Examples: if len(filename) == 0 { usageError(cmd, "Must specify filename to create") } - mapping, namespace, name, data := ResourceFromFile(filename, f.Typer, f.Mapper) + schema, err := f.Validator(cmd) + checkErr(err) + mapping, namespace, name, data := ResourceFromFile(filename, f.Typer, f.Mapper, schema) client, err := f.Client(cmd, mapping) checkErr(err) diff --git a/pkg/kubectl/cmd/delete.go b/pkg/kubectl/cmd/delete.go index 4bb2f2b701e..4ee75a6d8cb 100644 --- a/pkg/kubectl/cmd/delete.go +++ b/pkg/kubectl/cmd/delete.go @@ -50,7 +50,9 @@ Examples: `, Run: func(cmd *cobra.Command, args []string) { filename := GetFlagString(cmd, "filename") - mapping, namespace, name := ResourceFromArgsOrFile(cmd, args, filename, f.Typer, f.Mapper) + schema, err := f.Validator(cmd) + checkErr(err) + mapping, namespace, name := ResourceFromArgsOrFile(cmd, args, filename, f.Typer, f.Mapper, schema) client, err := f.Client(cmd, mapping) checkErr(err) diff --git a/pkg/kubectl/cmd/resource.go b/pkg/kubectl/cmd/resource.go index 8072a47a362..396048c7f25 100644 --- a/pkg/kubectl/cmd/resource.go +++ b/pkg/kubectl/cmd/resource.go @@ -20,6 +20,7 @@ import ( "fmt" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" @@ -29,7 +30,7 @@ import ( // ResourceFromArgsOrFile expects two arguments or a valid file with a given type, and extracts // the fields necessary to uniquely locate a resource. Displays a usageError if that contract is // not satisfied, or a generic error if any other problems occur. -func ResourceFromArgsOrFile(cmd *cobra.Command, args []string, filename string, typer runtime.ObjectTyper, mapper meta.RESTMapper) (mapping *meta.RESTMapping, namespace, name string) { +func ResourceFromArgsOrFile(cmd *cobra.Command, args []string, filename string, typer runtime.ObjectTyper, mapper meta.RESTMapper, schema validation.Schema) (mapping *meta.RESTMapping, namespace, name string) { // If command line args are passed in, use those preferentially. if len(args) > 0 && len(args) != 2 { usageError(cmd, "If passing in command line parameters, must be resource and name") @@ -58,7 +59,7 @@ func ResourceFromArgsOrFile(cmd *cobra.Command, args []string, filename string, usageError(cmd, "Must specify filename or command line params") } - mapping, namespace, name, _ = ResourceFromFile(filename, typer, mapper) + mapping, namespace, name, _ = ResourceFromFile(filename, typer, mapper, schema) if len(name) == 0 { checkErr(fmt.Errorf("the resource in the provided file has no name (or ID) defined")) } @@ -122,7 +123,7 @@ func ResourceOrTypeFromArgs(cmd *cobra.Command, args []string, mapper meta.RESTM // ResourceFromFile retrieves the name and namespace from a valid file. If the file does not // resolve to a known type an error is returned. The returned mapping can be used to determine // the correct REST endpoint to modify this resource with. -func ResourceFromFile(filename string, typer runtime.ObjectTyper, mapper meta.RESTMapper) (mapping *meta.RESTMapping, namespace, name string, data []byte) { +func ResourceFromFile(filename string, typer runtime.ObjectTyper, mapper meta.RESTMapper, schema validation.Schema) (mapping *meta.RESTMapping, namespace, name string, data []byte) { configData, err := ReadConfigData(filename) checkErr(err) data = configData @@ -135,6 +136,9 @@ func ResourceFromFile(filename string, typer runtime.ObjectTyper, mapper meta.RE checkErr(fmt.Errorf("the resource in the provided file has no apiVersion defined")) } + err = schema.ValidateBytes(data) + checkErr(err) + mapping, err = mapper.RESTMapping(version, kind) checkErr(err) diff --git a/pkg/kubectl/cmd/update.go b/pkg/kubectl/cmd/update.go index c59b75c8131..2a35cddbd78 100644 --- a/pkg/kubectl/cmd/update.go +++ b/pkg/kubectl/cmd/update.go @@ -43,7 +43,9 @@ Examples: if len(filename) == 0 { usageError(cmd, "Must specify filename to update") } - mapping, namespace, name, data := ResourceFromFile(filename, f.Typer, f.Mapper) + schema, err := f.Validator(cmd) + checkErr(err) + mapping, namespace, name, data := ResourceFromFile(filename, f.Typer, f.Mapper, schema) client, err := f.Client(cmd, mapping) checkErr(err)