diff --git a/pkg/api/validation/schema.go b/pkg/api/validation/schema.go index f6a4d0798bb..f694baeaeb2 100644 --- a/pkg/api/validation/schema.go +++ b/pkg/api/validation/schema.go @@ -17,6 +17,7 @@ limitations under the License. package validation import ( + "bytes" "encoding/json" "fmt" "reflect" @@ -24,6 +25,7 @@ import ( "strings" "github.com/emicklei/go-restful/swagger" + ejson "github.com/exponent-io/jsonpath" "github.com/golang/glog" apiutil "k8s.io/kubernetes/pkg/api/util" "k8s.io/kubernetes/pkg/runtime" @@ -62,6 +64,69 @@ type NullSchema struct{} func (NullSchema) ValidateBytes(data []byte) error { return nil } +type NoDoubleKeySchema struct{} + +func (NoDoubleKeySchema) ValidateBytes(data []byte) error { + var list []error = nil + if err := validateNoDuplicateKeys(data, "metadata", "labels"); err != nil { + list = append(list, err) + } + if err := validateNoDuplicateKeys(data, "metadata", "annotations"); err != nil { + list = append(list, err) + } + return utilerrors.NewAggregate(list) +} + +func validateNoDuplicateKeys(data []byte, path ...string) error { + r := ejson.NewDecoder(bytes.NewReader(data)) + // This is Go being unfriendly. The 'path ...string' comes in as a + // []string, and SeekTo takes ...interface{}, so we can't just pass + // the path straight in, we have to copy it. *sigh* + ifacePath := []interface{}{} + for ix := range path { + ifacePath = append(ifacePath, path[ix]) + } + found, err := r.SeekTo(ifacePath...) + if err != nil { + return err + } + if !found { + return nil + } + seen := map[string]bool{} + for { + tok, err := r.Token() + if err != nil { + return err + } + switch t := tok.(type) { + case json.Delim: + if t.String() == "}" { + return nil + } + case ejson.KeyString: + if seen[string(t)] { + return fmt.Errorf("duplicate key: %s", string(t)) + } else { + seen[string(t)] = true + } + } + } +} + +type ConjunctiveSchema []Schema + +func (c ConjunctiveSchema) ValidateBytes(data []byte) error { + var list []error = nil + schemas := []Schema(c) + for ix := range schemas { + if err := schemas[ix].ValidateBytes(data); err != nil { + list = append(list, err) + } + } + return utilerrors.NewAggregate(list) +} + type SwaggerSchema struct { api swagger.ApiDeclaration delegate Schema // For delegating to other api groups diff --git a/pkg/api/validation/schema_test.go b/pkg/api/validation/schema_test.go index 41eea8c0bc6..8caf0ef098d 100644 --- a/pkg/api/validation/schema_test.go +++ b/pkg/api/validation/schema_test.go @@ -309,3 +309,118 @@ func TestTypeAny(t *testing.T) { } } } + +func TestValidateDuplicateLabelsFailCases(t *testing.T) { + strs := []string{ + `{ + "metadata": { + "labels": { + "foo": "bar", + "foo": "baz" + } + } +}`, + `{ + "metadata": { + "annotations": { + "foo": "bar", + "foo": "baz" + } + } +}`, + `{ + "metadata": { + "labels": { + "foo": "blah" + }, + "annotations": { + "foo": "bar", + "foo": "baz" + } + } +}`, + } + schema := NoDoubleKeySchema{} + for _, str := range strs { + err := schema.ValidateBytes([]byte(str)) + if err == nil { + t.Errorf("Unexpected non-error %s", str) + } + } +} + +func TestValidateDuplicateLabelsPassCases(t *testing.T) { + strs := []string{ + `{ + "metadata": { + "labels": { + "foo": "bar" + }, + "annotations": { + "foo": "baz" + } + } +}`, + `{ + "metadata": {} +}`, + `{ + "metadata": { + "labels": {} + } +}`, + } + schema := NoDoubleKeySchema{} + for _, str := range strs { + err := schema.ValidateBytes([]byte(str)) + if err != nil { + t.Errorf("Unexpected error: %v %s", err, str) + } + } +} + +type AlwaysInvalidSchema struct{} + +func (AlwaysInvalidSchema) ValidateBytes([]byte) error { + return fmt.Errorf("Always invalid!") +} + +func TestConjunctiveSchema(t *testing.T) { + tests := []struct { + schemas []Schema + shouldPass bool + name string + }{ + { + schemas: []Schema{NullSchema{}, NullSchema{}}, + shouldPass: true, + name: "all pass", + }, + { + schemas: []Schema{NullSchema{}, AlwaysInvalidSchema{}}, + shouldPass: false, + name: "one fail", + }, + { + schemas: []Schema{AlwaysInvalidSchema{}, AlwaysInvalidSchema{}}, + shouldPass: false, + name: "all fail", + }, + { + schemas: []Schema{}, + shouldPass: true, + name: "empty", + }, + } + + for _, test := range tests { + schema := ConjunctiveSchema(test.schemas) + err := schema.ValidateBytes([]byte{}) + if err != nil && test.shouldPass { + t.Errorf("Unexpected error: %v in %s", err, test.name) + } + if err == nil && !test.shouldPass { + t.Errorf("Unexpected non-error: %s", test.name) + } + } +} diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index 78ae76cda2d..ddbe4f5ddc8 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -762,10 +762,14 @@ func (f *factory) Validator(validate bool, cacheDir string) (validation.Schema, if err != nil { return nil, err } - return &clientSwaggerSchema{ + swaggerSchema := &clientSwaggerSchema{ c: restclient, fedc: fedClient, cacheDir: dir, + } + return validation.ConjunctiveSchema{ + swaggerSchema, + validation.NoDoubleKeySchema{}, }, nil } return validation.NullSchema{}, nil