Merge pull request #2621 from brendandburns/kubecfg

Add validation back in
This commit is contained in:
bgrant0607 2014-12-09 15:10:51 -08:00
commit 4a9afe61da
9 changed files with 201 additions and 27 deletions

View File

@ -54,6 +54,7 @@ var (
json = flag.Bool("json", false, "If true, print raw JSON for responses") json = flag.Bool("json", false, "If true, print raw JSON for responses")
yaml = flag.Bool("yaml", false, "If true, print raw YAML for responses") yaml = flag.Bool("yaml", false, "If true, print raw YAML for responses")
verbose = flag.Bool("verbose", false, "If true, print extra information") verbose = flag.Bool("verbose", false, "If true, print extra information")
validate = flag.Bool("validate", false, "If true, try to validate the passed in object using a swagger schema on the api server")
proxy = flag.Bool("proxy", false, "If true, run a proxy to the api server") proxy = flag.Bool("proxy", false, "If true, run a proxy to the api server")
www = flag.String("www", "", "If -proxy is true, use this directory to serve static files") www = flag.String("www", "", "If -proxy is true, use this directory to serve static files")
templateFile = flag.String("template_file", "", "If present, load this file as a golang template and use it for output printing") templateFile = flag.String("template_file", "", "If present, load this file as a golang template and use it for output printing")
@ -152,12 +153,20 @@ func readConfigData() []byte {
// readConfig reads and parses pod, replicationController, and service // readConfig reads and parses pod, replicationController, and service
// configuration files. If any errors log and exit non-zero. // 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 { if len(*config) == 0 {
glog.Fatal("Need config file (-c)") glog.Fatal("Need config file (-c)")
} }
data, err := parser.ToWireFormat(readConfigData(), storage, latest.Codec, serverCodec) dataInput := readConfigData()
if *validate {
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 { if err != nil {
glog.Fatalf("Error parsing %v as an object for %v: %v\n", *config, storage, err) glog.Fatalf("Error parsing %v as an object for %v: %v\n", *config, storage, err)
@ -383,7 +392,7 @@ func executeAPIRequest(ctx api.Context, method string, c *client.Client) bool {
glog.Fatalf("usage: kubecfg [OPTIONS] %s <%s>/<id>", method, prettyWireStorage()) glog.Fatalf("usage: kubecfg [OPTIONS] %s <%s>/<id>", method, prettyWireStorage())
} }
case "print": case "print":
data := readConfig(storage, c.RESTClient.Codec) data := readConfig(storage, c)
obj, err := latest.Codec.Decode(data) obj, err := latest.Codec.Decode(data)
if err != nil { if err != nil {
glog.Fatalf("error setting resource version: %v", err) glog.Fatalf("error setting resource version: %v", err)
@ -403,7 +412,7 @@ func executeAPIRequest(ctx api.Context, method string, c *client.Client) bool {
} }
if setBody { if setBody {
if len(version) > 0 { if len(version) > 0 {
data := readConfig(storage, c.RESTClient.Codec) data := readConfig(storage, c)
obj, err := latest.Codec.Decode(data) obj, err := latest.Codec.Decode(data)
if err != nil { if err != nil {
glog.Fatalf("error setting resource version: %v", err) glog.Fatalf("error setting resource version: %v", err)
@ -419,7 +428,7 @@ func executeAPIRequest(ctx api.Context, method string, c *client.Client) bool {
} }
r.Body(data) r.Body(data)
} else { } else {
r.Body(readConfig(storage, c.RESTClient.Codec)) r.Body(readConfig(storage, c))
} }
} }
result := r.Do() result := r.Do()

View File

@ -24,6 +24,7 @@ import (
"github.com/emicklei/go-restful/swagger" "github.com/emicklei/go-restful/swagger"
"github.com/golang/glog" "github.com/golang/glog"
"gopkg.in/v2/yaml"
) )
type InvalidTypeError struct { type InvalidTypeError struct {
@ -40,12 +41,21 @@ func NewInvalidTypeError(expected reflect.Kind, observed reflect.Kind, fieldName
return &InvalidTypeError{expected, observed, 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 api swagger.ApiDeclaration
} }
func NewSchemaFromBytes(data []byte) (*Schema, error) { func NewSwaggerSchemaFromBytes(data []byte) (Schema, error) {
schema := &Schema{} schema := &SwaggerSchema{}
err := json.Unmarshal(data, &schema.api) err := json.Unmarshal(data, &schema.api)
if err != nil { if err != nil {
return nil, err return nil, err
@ -53,19 +63,19 @@ func NewSchemaFromBytes(data []byte) (*Schema, error) {
return schema, nil return schema, nil
} }
func (s *Schema) ValidateBytes(data []byte) error { func (s *SwaggerSchema) ValidateBytes(data []byte) error {
var obj interface{} var obj interface{}
err := json.Unmarshal(data, &obj) err := yaml.Unmarshal(data, &obj)
if err != nil { if err != nil {
return err return err
} }
fields := obj.(map[string]interface{}) fields := obj.(map[interface{}]interface{})
apiVersion := fields["apiVersion"].(string) apiVersion := fields["apiVersion"].(string)
kind := fields["kind"].(string) kind := fields["kind"].(string)
return s.ValidateObject(obj, apiVersion, "", apiVersion+"."+kind) 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 models := s.api.Models
// TODO: handle required fields here too. // TODO: handle required fields here too.
model, ok := models[typeName] model, ok := models[typeName]
@ -74,12 +84,12 @@ func (s *Schema) ValidateObject(obj interface{}, apiVersion, fieldName, typeName
return nil return nil
} }
properties := model.Properties properties := model.Properties
fields := obj.(map[string]interface{}) fields := obj.(map[interface{}]interface{})
if len(fieldName) > 0 { if len(fieldName) > 0 {
fieldName = fieldName + "." fieldName = fieldName + "."
} }
for key, value := range fields { for key, value := range fields {
details, ok := properties[key] details, ok := properties[key.(string)]
if !ok { if !ok {
glog.V(2).Infof("couldn't find properties for %s, skipping", key) glog.V(2).Infof("couldn't find properties for %s, skipping", key)
continue continue
@ -89,7 +99,7 @@ func (s *Schema) ValidateObject(obj interface{}, apiVersion, fieldName, typeName
glog.V(2).Infof("Skipping nil field: %s", key) glog.V(2).Infof("Skipping nil field: %s", key)
continue continue
} }
err := s.validateField(value, apiVersion, fieldName+key, fieldType, &details) err := s.validateField(value, apiVersion, fieldName+key.(string), fieldType, &details)
if err != nil { if err != nil {
glog.Errorf("Validation failed for: %s, %v", key, value) glog.Errorf("Validation failed for: %s, %v", key, value)
return err return err
@ -98,7 +108,7 @@ func (s *Schema) ValidateObject(obj interface{}, apiVersion, fieldName, typeName
return nil 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) { if strings.HasPrefix(fieldType, apiVersion) {
return s.ValidateObject(value, apiVersion, fieldName, fieldType) return s.ValidateObject(value, apiVersion, fieldName, fieldType)
} }
@ -107,7 +117,8 @@ func (s *Schema) validateField(value interface{}, apiVersion, fieldName, fieldTy
// Be loose about what we accept for 'string' since we use IntOrString in a couple of places // Be loose about what we accept for 'string' since we use IntOrString in a couple of places
_, isString := value.(string) _, isString := value.(string)
_, isNumber := value.(float64) _, isNumber := value.(float64)
if !isString && !isNumber { _, isInteger := value.(int)
if !isString && !isNumber && !isInteger {
return NewInvalidTypeError(reflect.String, reflect.TypeOf(value).Kind(), fieldName) return NewInvalidTypeError(reflect.String, reflect.TypeOf(value).Kind(), fieldName)
} }
case "array": case "array":
@ -124,7 +135,9 @@ func (s *Schema) validateField(value interface{}, apiVersion, fieldName, fieldTy
} }
case "uint64": case "uint64":
case "integer": case "integer":
if _, ok := value.(float64); !ok { _, isNumber := value.(float64)
_, isInteger := value.(int)
if !isNumber && !isInteger {
return NewInvalidTypeError(reflect.Int, reflect.TypeOf(value).Kind(), fieldName) return NewInvalidTypeError(reflect.Int, reflect.TypeOf(value).Kind(), fieldName)
} }
case "float64": case "float64":

View File

@ -30,12 +30,12 @@ import (
fuzz "github.com/google/gofuzz" fuzz "github.com/google/gofuzz"
) )
func LoadSchemaForTest(file string) (*Schema, error) { func LoadSchemaForTest(file string) (Schema, error) {
data, err := ioutil.ReadFile(file) data, err := ioutil.ReadFile(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewSchemaFromBytes(data) return NewSwaggerSchemaFromBytes(data)
} }
// TODO: this is cloned from serialization_test.go, refactor to somewhere common like util // TODO: this is cloned from serialization_test.go, refactor to somewhere common like util
@ -251,12 +251,63 @@ var invalidPod3 = `{
} }
` `
var invalidYaml = `
id: name
kind: Pod
apiVersion: v1beta1
desiredState:
manifest:
version: v1beta1
id: redis-master
containers:
- name: "master"
image: "dockerfile/redis"
command: "this is a bad command"
labels:
name: "redis-master"
`
func TestInvalid(t *testing.T) { func TestInvalid(t *testing.T) {
schema, err := LoadSchemaForTest("v1beta1-swagger.json") schema, err := LoadSchemaForTest("v1beta1-swagger.json")
if err != nil { if err != nil {
t.Errorf("Failed to load: %v", err) t.Errorf("Failed to load: %v", err)
} }
tests := []string{invalidPod, invalidPod2} tests := []string{invalidPod, invalidPod2, invalidPod3, invalidYaml}
for _, test := range tests {
err = schema.ValidateBytes([]byte(test))
if err == nil {
t.Errorf("unexpected non-error\n%s", test)
}
}
}
var validYaml = `
id: name
kind: Pod
apiVersion: v1beta1
desiredState:
manifest:
version: v1beta1
id: redis-master
containers:
- name: "master"
image: "dockerfile/redis"
command:
- this
- is
- an
- ok
- command
labels:
name: "redis-master"
`
func TestValid(t *testing.T) {
schema, err := LoadSchemaForTest("v1beta1-swagger.json")
if err != nil {
t.Errorf("Failed to load: %v", err)
}
tests := []string{validYaml}
for _, test := range tests { for _, test := range tests {
err = schema.ValidateBytes([]byte(test)) err = schema.ValidateBytes([]byte(test))
if err == nil { if err == nil {

51
pkg/kubecfg/validate.go Normal file
View File

@ -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)
}

View File

@ -24,6 +24,8 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "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/client/clientcmd" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
@ -41,6 +43,7 @@ type Factory struct {
Client func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) Client func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error)
Describer func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) Describer func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error)
Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error)
Validator func(*cobra.Command) (validation.Schema, error)
} }
// NewFactory creates a factory with the default Kubernetes resources defined // NewFactory creates a factory with the default Kubernetes resources defined
@ -49,6 +52,17 @@ func NewFactory(clientBuilder clientcmd.Builder) *Factory {
ClientBuilder: clientBuilder, ClientBuilder: clientBuilder,
Mapper: latest.RESTMapper, Mapper: latest.RESTMapper,
Typer: api.Scheme, Typer: api.Scheme,
Validator: func(cmd *cobra.Command) (validation.Schema, error) {
if GetFlagBool(cmd, "validate") {
client, err := clientBuilder.Client()
if err != nil {
return nil, err
}
return &clientSwaggerSchema{client, api.Scheme}, nil
} else {
return validation.NullSchema{}, nil
}
},
Client: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) { Client: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) {
return clientBuilder.Client() return clientBuilder.Client()
}, },
@ -88,6 +102,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`,
// to do that automatically for every subcommand. // to do that automatically for every subcommand.
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().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().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(f.NewCmdVersion(out)) cmds.AddCommand(f.NewCmdVersion(out))
cmds.AddCommand(f.NewCmdProxy(out)) cmds.AddCommand(f.NewCmdProxy(out))
@ -154,3 +169,28 @@ func GetExplicitKubeNamespace(cmd *cobra.Command) (string, bool) {
// value and return its value and true. // value and return its value and true.
return "", false return "", false
} }
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)
}

View File

@ -43,7 +43,9 @@ Examples:
if len(filename) == 0 { if len(filename) == 0 {
usageError(cmd, "Must specify filename to create") 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) client, err := f.Client(cmd, mapping)
checkErr(err) checkErr(err)

View File

@ -50,7 +50,9 @@ Examples:
<delete a pod with ID 1234-56-7890-234234-456456>`, <delete a pod with ID 1234-56-7890-234234-456456>`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
filename := GetFlagString(cmd, "filename") 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) client, err := f.Client(cmd, mapping)
checkErr(err) checkErr(err)

View File

@ -20,6 +20,7 @@ import (
"fmt" "fmt"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "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/kubectl"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "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 // 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 // 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. // 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 command line args are passed in, use those preferentially.
if len(args) > 0 && len(args) != 2 { if len(args) > 0 && len(args) != 2 {
usageError(cmd, "If passing in command line parameters, must be resource and name") 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") 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 { if len(name) == 0 {
checkErr(fmt.Errorf("the resource in the provided file has no name (or ID) defined")) 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 // 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 // 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. // 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) configData, err := ReadConfigData(filename)
checkErr(err) checkErr(err)
data = configData 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")) 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) mapping, err = mapper.RESTMapping(version, kind)
checkErr(err) checkErr(err)

View File

@ -43,7 +43,9 @@ Examples:
if len(filename) == 0 { if len(filename) == 0 {
usageError(cmd, "Must specify filename to update") 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) client, err := f.Client(cmd, mapping)
checkErr(err) checkErr(err)