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

Kubectl support for validating nested objects with different ApiGroups #25172

Merged
merged 1 commit into from
May 11, 2016
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
15 changes: 15 additions & 0 deletions hack/test-cmd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1727,6 +1727,21 @@ __EOF__
kubectl delete rs frontend "${kube_flags[@]}"


######################
# Lists #
######################

kube::log::status "Testing kubectl(${version}:lists)"

### Create a List with objects from multiple versions
# Command
kubectl create -f hack/testdata/list.yaml "${kube_flags[@]}"

### Delete the List with objects from multiple versions
# Command
kubectl delete service/list-service-test deployment/list-deployment-test


######################
# Multiple Resources #
######################
Expand Down
29 changes: 29 additions & 0 deletions hack/testdata/list.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
apiVersion: v1
kind: List
items:
- apiVersion: v1
kind: Service
metadata:
name: list-service-test
spec:
ports:
- protocol: TCP
port: 80
selector:
app: list-deployment-test
- apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: list-deployment-test
labels:
app: list-deployment-test
spec:
replicas: 1
template:
metadata:
labels:
app: list-deployment-test
spec:
containers:
- name: nginx
image: nginx
31 changes: 31 additions & 0 deletions pkg/api/testing/fuzzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,37 @@ func FuzzerFor(t *testing.T, version unversioned.GroupVersion, src rand.Source)
}
}
},
func(r *runtime.RawExtension, c fuzz.Continue) {
// Pick an arbitrary type and fuzz it
types := []runtime.Object{&api.Pod{}, &extensions.Deployment{}, &api.Service{}}
obj := types[c.Rand.Intn(len(types))]
c.Fuzz(obj)

// Find a codec for converting the object to raw bytes. This is necessary for the
// api version and kind to be correctly set be serialization.
var codec runtime.Codec
switch obj.(type) {
case *api.Pod:
codec = testapi.Default.Codec()
case *extensions.Deployment:
codec = testapi.Extensions.Codec()
case *api.Service:
codec = testapi.Default.Codec()
default:
t.Errorf("Failed to find codec for object type: %T", obj)
return
}

// Convert the object to raw bytes
bytes, err := runtime.Encode(codec, obj)
if err != nil {
t.Errorf("Failed to encode object: %v", err)
return
}

// Set the bytes field on the RawExtension
r.Raw = bytes
},
)
return f
}
Expand Down
82 changes: 78 additions & 4 deletions pkg/api/validation/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/emicklei/go-restful/swagger"
"github.com/golang/glog"
apiutil "k8s.io/kubernetes/pkg/api/util"
"k8s.io/kubernetes/pkg/runtime"
utilerrors "k8s.io/kubernetes/pkg/util/errors"
"k8s.io/kubernetes/pkg/util/yaml"
)
Expand Down Expand Up @@ -62,27 +63,33 @@ type NullSchema struct{}
func (NullSchema) ValidateBytes(data []byte) error { return nil }

type SwaggerSchema struct {
api swagger.ApiDeclaration
api swagger.ApiDeclaration
delegate Schema // For delegating to other api groups
}

func NewSwaggerSchemaFromBytes(data []byte) (Schema, error) {
func NewSwaggerSchemaFromBytes(data []byte, factory Schema) (Schema, error) {
schema := &SwaggerSchema{}
err := json.Unmarshal(data, &schema.api)
if err != nil {
return nil, err
}
schema.delegate = factory
return schema, nil
}

// validateList unpacks a list and validate every item in the list.
// It return nil if every item is ok.
// Otherwise it return an error list contain errors of every item.
func (s *SwaggerSchema) validateList(obj map[string]interface{}) []error {
allErrs := []error{}
items, exists := obj["items"]
if !exists {
return append(allErrs, fmt.Errorf("no items field in %#v", obj))
return []error{fmt.Errorf("no items field in %#v", obj)}
}
return s.validateItems(items)
}

func (s *SwaggerSchema) validateItems(items interface{}) []error {
allErrs := []error{}
itemList, ok := items.([]interface{})
if !ok {
return append(allErrs, fmt.Errorf("items isn't a slice"))
Expand Down Expand Up @@ -125,6 +132,7 @@ func (s *SwaggerSchema) validateList(obj map[string]interface{}) []error {
allErrs = append(allErrs, errs...)
}
}

return allErrs
}

Expand Down Expand Up @@ -171,6 +179,25 @@ func (s *SwaggerSchema) ValidateObject(obj interface{}, fieldName, typeName stri
allErrs := []error{}
models := s.api.Models
model, ok := models.At(typeName)

// Verify the api version matches. This is required for nested types with differing api versions because
// s.api only has schema for 1 api version (the parent object type's version).
// e.g. an extensions/v1beta1 Template embedding a /v1 Service requires the schema for the extensions/v1beta1
// api to delegate to the schema for the /v1 api.
// Only do this for !ok objects so that cross ApiVersion vendored types take precedence.
if !ok && s.delegate != nil {
fields, mapOk := obj.(map[string]interface{})
if !mapOk {
return append(allErrs, fmt.Errorf("field %s: expected object of type map[string]interface{}, but the actual type is %T", fieldName, obj))
}
if delegated, err := s.delegateIfDifferentApiVersion(runtime.Unstructured{Object: fields}); delegated {
if err != nil {
allErrs = append(allErrs, err)
}
return allErrs
}
}

if !ok {
return append(allErrs, TypeNotFoundError(typeName))
}
Expand All @@ -194,6 +221,17 @@ func (s *SwaggerSchema) ValidateObject(obj interface{}, fieldName, typeName stri
}
for key, value := range fields {
details, ok := properties.At(key)

// Special case for runtime.RawExtension and runtime.Objects because they always fail to validate
// This is because the actual values will be of some sub-type (e.g. Deployment) not the expected
// super-type (RawExtention)
if s.isGenericArray(details) {
errs := s.validateItems(value)
if len(errs) > 0 {
allErrs = append(allErrs, errs...)
}
continue
}
if !ok {
allErrs = append(allErrs, fmt.Errorf("found invalid field %s for %s", key, typeName))
continue
Expand All @@ -219,6 +257,42 @@ func (s *SwaggerSchema) ValidateObject(obj interface{}, fieldName, typeName stri
return allErrs
}

// delegateIfDifferentApiVersion delegates the validation of an object if its ApiGroup does not match the
// current SwaggerSchema.
// First return value is true if the validation was delegated (by a different ApiGroup SwaggerSchema)
// Second return value is the result of the delegated validation if performed.
func (s *SwaggerSchema) delegateIfDifferentApiVersion(obj runtime.Unstructured) (bool, error) {
// Never delegate objects in the same ApiVersion or we will get infinite recursion
if !s.isDifferentApiVersion(obj) {
return false, nil
}

// Convert the object back into bytes so that we can pass it to the ValidateBytes function
m, err := json.Marshal(obj.Object)
if err != nil {
return true, err
}

// Delegate validation of this object to the correct SwaggerSchema for its ApiGroup
return true, s.delegate.ValidateBytes(m)
}

// isDifferentApiVersion Returns true if obj lives in a different ApiVersion than the SwaggerSchema does.
// The SwaggerSchema will not be able to process objects in different ApiVersions unless they are vendored.
func (s *SwaggerSchema) isDifferentApiVersion(obj runtime.Unstructured) bool {
groupVersion := obj.GetAPIVersion()
return len(groupVersion) > 0 && s.api.ApiVersion != groupVersion
}

// isGenericArray Returns true if p is an array of generic Objects - either RawExtension or Object.
func (s *SwaggerSchema) isGenericArray(p swagger.ModelProperty) bool {
return p.DataTypeFields.Type != nil &&
*p.DataTypeFields.Type == "array" &&
p.Items != nil &&
p.Items.Ref != nil &&
(*p.Items.Ref == "runtime.RawExtension" || *p.Items.Ref == "runtime.Object")
}

// This matches type name in the swagger spec, such as "v1.Binding".
var versionRegexp = regexp.MustCompile(`^v.+\..*`)

Expand Down
117 changes: 113 additions & 4 deletions pkg/api/validation/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package validation

import (
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"strings"
Expand All @@ -25,7 +27,9 @@ import (
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/testapi"
apitesting "k8s.io/kubernetes/pkg/api/testing"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/runtime"
k8syaml "k8s.io/kubernetes/pkg/util/yaml"

"github.com/ghodss/yaml"
)
Expand All @@ -39,17 +43,86 @@ func readPod(filename string) ([]byte, error) {
}

func readSwaggerFile() ([]byte, error) {
// TODO this path is broken
pathToSwaggerSpec := "../../../api/swagger-spec/" + testapi.Default.GroupVersion().Version + ".json"
return readSwaggerApiFile(testapi.Default)
}

func readSwaggerApiFile(group testapi.TestGroup) ([]byte, error) {
// TODO: Figure out a better way of finding these files
var pathToSwaggerSpec string
if group.GroupVersion().Group == "" {
pathToSwaggerSpec = "../../../api/swagger-spec/" + group.GroupVersion().Version + ".json"
} else {
pathToSwaggerSpec = "../../../api/swagger-spec/" + group.GroupVersion().Group + "_" + group.GroupVersion().Version + ".json"
}

return ioutil.ReadFile(pathToSwaggerSpec)
}

// Mock delegating Schema. Not a full fake impl.
type Factory struct {
defaultSchema Schema
extensionsSchema Schema
}

var _ Schema = &Factory{}

// TODO: Consider using a mocking library instead or fully fleshing this out into a fake impl and putting it in some
// generally available location
func (f *Factory) ValidateBytes(data []byte) error {
var obj interface{}
out, err := k8syaml.ToJSON(data)
if err != nil {
return err
}
data = out
if err := json.Unmarshal(data, &obj); err != nil {
return err
}
fields, ok := obj.(map[string]interface{})
if !ok {
return fmt.Errorf("error in unmarshaling data %s", string(data))
}
// Note: This only supports the 2 api versions we expect from the test it is currently supporting.
groupVersion := fields["apiVersion"]
switch groupVersion {
case "v1":
return f.defaultSchema.ValidateBytes(data)
case "extensions/v1beta1":
return f.extensionsSchema.ValidateBytes(data)
default:
return fmt.Errorf("Unsupported API version %s", groupVersion)
}
}

func loadSchemaForTest() (Schema, error) {
data, err := readSwaggerFile()
if err != nil {
return nil, err
}
return NewSwaggerSchemaFromBytes(data)
return NewSwaggerSchemaFromBytes(data, nil)
}

func loadSchemaForTestWithFactory(group testapi.TestGroup, factory Schema) (Schema, error) {
data, err := readSwaggerApiFile(group)
if err != nil {
return nil, err
}
return NewSwaggerSchemaFromBytes(data, factory)
}

func NewFactory() (*Factory, error) {
f := &Factory{}
defaultSchema, err := loadSchemaForTestWithFactory(testapi.Default, f)
if err != nil {
return nil, err
}
f.defaultSchema = defaultSchema
extensionSchema, err := loadSchemaForTestWithFactory(testapi.Extensions, f)
if err != nil {
return nil, err
}
f.extensionsSchema = extensionSchema
return f, nil
}

func TestLoad(t *testing.T) {
Expand Down Expand Up @@ -91,6 +164,42 @@ func TestValidateOk(t *testing.T) {
}
}

func TestValidateDifferentApiVersions(t *testing.T) {
schema, err := loadSchemaForTest()
if err != nil {
t.Errorf("Failed to load: %v", err)
}

pod := &api.Pod{}
pod.APIVersion = "v1"
pod.Kind = "Pod"

deployment := &extensions.Deployment{}
deployment.APIVersion = "extensions/v1beta1"
deployment.Kind = "Deployment"

list := &api.List{}
list.APIVersion = "v1"
list.Kind = "List"
list.Items = []runtime.Object{pod, deployment}
bytes, err := json.Marshal(list)
if err != nil {
t.Error(err)
}
err = schema.ValidateBytes(bytes)
if err == nil {
t.Error(fmt.Errorf("Expected error when validating different api version and no delegate exists."))
}
f, err := NewFactory()
if err != nil {
t.Error(fmt.Errorf("Failed to create Schema factory %v.", err))
}
err = f.ValidateBytes(bytes)
if err != nil {
t.Error(fmt.Errorf("Failed to validate object with multiple ApiGroups: %v.", err))
}
}

func TestInvalid(t *testing.T) {
schema, err := loadSchemaForTest()
if err != nil {
Expand Down Expand Up @@ -171,7 +280,7 @@ func TestTypeOAny(t *testing.T) {
}
// Replace type: "any" in the spec by type: "object" and verify that the validation still passes.
newData := strings.Replace(string(data), `"type": "object"`, `"type": "any"`, -1)
schema, err := NewSwaggerSchemaFromBytes([]byte(newData))
schema, err := NewSwaggerSchemaFromBytes([]byte(newData), nil)
if err != nil {
t.Errorf("Failed to load: %v", err)
}
Expand Down