Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add validation back in #2621

Merged
merged 2 commits into from
Dec 9, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 14 additions & 5 deletions cmd/kubecfg/kubecfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ var (
json = flag.Bool("json", false, "If true, print raw JSON for responses")
yaml = flag.Bool("yaml", false, "If true, print raw YAML for responses")
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")
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")
Expand Down Expand Up @@ -152,12 +153,20 @@ 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()
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 {
glog.Fatalf("Error parsing %v as an object for %v: %v\n", *config, storage, err)
Expand Down Expand Up @@ -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())
}
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)
Expand All @@ -403,7 +412,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)
Expand All @@ -419,7 +428,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()
Expand Down
39 changes: 26 additions & 13 deletions pkg/api/validation/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

"github.com/emicklei/go-restful/swagger"
"github.com/golang/glog"
"gopkg.in/v2/yaml"
)

type InvalidTypeError struct {
Expand All @@ -40,32 +41,41 @@ 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
}
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)
err := yaml.Unmarshal(data, &obj)
if err != nil {
return err
}
fields := obj.(map[string]interface{})
fields := obj.(map[interface{}]interface{})
apiVersion := fields["apiVersion"].(string)
kind := fields["kind"].(string)
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]
Expand All @@ -74,12 +84,12 @@ func (s *Schema) ValidateObject(obj interface{}, apiVersion, fieldName, typeName
return nil
}
properties := model.Properties
fields := obj.(map[string]interface{})
fields := obj.(map[interface{}]interface{})
if len(fieldName) > 0 {
fieldName = fieldName + "."
}
for key, value := range fields {
details, ok := properties[key]
details, ok := properties[key.(string)]
if !ok {
glog.V(2).Infof("couldn't find properties for %s, skipping", key)
continue
Expand All @@ -89,7 +99,7 @@ func (s *Schema) ValidateObject(obj interface{}, apiVersion, fieldName, typeName
glog.V(2).Infof("Skipping nil field: %s", key)
continue
}
err := s.validateField(value, apiVersion, fieldName+key, fieldType, &details)
err := s.validateField(value, apiVersion, fieldName+key.(string), fieldType, &details)
if err != nil {
glog.Errorf("Validation failed for: %s, %v", key, value)
return err
Expand All @@ -98,7 +108,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)
}
Expand All @@ -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
_, isString := value.(string)
_, isNumber := value.(float64)
if !isString && !isNumber {
_, isInteger := value.(int)
if !isString && !isNumber && !isInteger {
return NewInvalidTypeError(reflect.String, reflect.TypeOf(value).Kind(), fieldName)
}
case "array":
Expand All @@ -124,7 +135,9 @@ func (s *Schema) validateField(value interface{}, apiVersion, fieldName, fieldTy
}
case "uint64":
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)
}
case "float64":
Expand Down
57 changes: 54 additions & 3 deletions pkg/api/validation/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
schema, err := LoadSchemaForTest("v1beta1-swagger.json")
if err != nil {
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 {
err = schema.ValidateBytes([]byte(test))
if err == nil {
Expand Down
51 changes: 51 additions & 0 deletions pkg/kubecfg/validate.go
Original file line number Diff line number Diff line change
@@ -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)
}
40 changes: 40 additions & 0 deletions pkg/kubectl/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ 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/client/clientcmd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
Expand All @@ -41,6 +43,7 @@ type Factory struct {
Client func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, 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)
Validator func(*cobra.Command) (validation.Schema, error)
}

// NewFactory creates a factory with the default Kubernetes resources defined
Expand All @@ -49,6 +52,17 @@ func NewFactory(clientBuilder clientcmd.Builder) *Factory {
ClientBuilder: clientBuilder,
Mapper: latest.RESTMapper,
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) {
return clientBuilder.Client()
},
Expand Down Expand Up @@ -88,6 +102,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`,
// 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().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.NewCmdProxy(out))
Expand Down Expand Up @@ -154,3 +169,28 @@ func GetExplicitKubeNamespace(cmd *cobra.Command) (string, bool) {
// value and return its value and true.
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)
}
4 changes: 3 additions & 1 deletion pkg/kubectl/cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 3 additions & 1 deletion pkg/kubectl/cmd/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ Examples:
<delete a pod with ID 1234-56-7890-234234-456456>`,
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)

Expand Down