Adding Strict Validation based on struct.*Deprecated*Foo (#339)

* have simple tests. working on impl.

* strict setting, reflection based.

* ran codegen.

* adding license.

* update based on feedback and merge better.

* getting closer to something  simpler assuming shallow reflect.

* adding validation test.

* use the json tag.

* Golang things nil typed pointers are not nil.

* Use real value of reflect invalid.

* add a missing test.

* two methods, one for update, one for single check.

* checkdep is now in apis.

* fix pkg.

* Update apis/deprecated_test.go

Co-Authored-By: n3wscott <32305648+n3wscott@users.noreply.github.com>

* add code clarity.

* include inlined struct objects recursively.

* Update commnets and add a flatten error test for inlined.
This commit is contained in:
Scott Nichols 2019-04-19 09:19:27 -07:00 committed by Knative Prow Robot
parent 3c8c4a9354
commit 70ab9cc77d
9 changed files with 1516 additions and 4 deletions

180
apis/deprecated.go Normal file
View File

@ -0,0 +1,180 @@
/*
Copyright 2019 The Knative Authors
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 apis
import (
"context"
"reflect"
"strings"
)
const (
deprecatedPrefix = "Deprecated"
)
// CheckDeprecated checks whether the provided named deprecated fields
// are set in a context where deprecation is disallowed.
// This is a shallow check.
func CheckDeprecated(ctx context.Context, obj interface{}) *FieldError {
return CheckDeprecatedUpdate(ctx, obj, nil)
}
// CheckDeprecated checks whether the provided named deprecated fields
// are set in a context where deprecation is disallowed.
// This is a json shallow check. We will recursively check inlined structs.
func CheckDeprecatedUpdate(ctx context.Context, obj interface{}, original interface{}) *FieldError {
if IsDeprecatedAllowed(ctx) {
return nil
}
var errs *FieldError
objFields, objInlined := getPrefixedNamedFieldValues(deprecatedPrefix, obj)
if nonZero(reflect.ValueOf(original)) {
originalFields, originalInlined := getPrefixedNamedFieldValues(deprecatedPrefix, original)
// We only have to walk obj Fields because the assumption is that obj
// and original are of the same type.
for name, value := range objFields {
if nonZero(value) {
if differ(originalFields[name], value) {
// Not allowed to update the value.
errs = errs.Also(ErrDisallowedUpdateDeprecatedFields(name))
}
}
}
// Look for deprecated inlined updates.
if len(objInlined) > 0 {
for name, value := range objInlined {
errs = errs.Also(CheckDeprecatedUpdate(ctx, value, originalInlined[name]))
}
}
} else {
for name, value := range objFields {
if nonZero(value) {
// Not allowed to set the value.
errs = errs.Also(ErrDisallowedFields(name))
}
}
// Look for deprecated inlined creates.
if len(objInlined) > 0 {
for _, value := range objInlined {
errs = errs.Also(CheckDeprecated(ctx, value))
}
}
}
return errs
}
func getPrefixedNamedFieldValues(prefix string, obj interface{}) (map[string]reflect.Value, map[string]interface{}) {
fields := make(map[string]reflect.Value, 0)
inlined := make(map[string]interface{}, 0)
objValue := reflect.Indirect(reflect.ValueOf(obj))
// If res is not valid or a struct, don't even try to use it.
if !objValue.IsValid() || objValue.Kind() != reflect.Struct {
return fields, inlined
}
for i := 0; i < objValue.NumField(); i++ {
tf := objValue.Type().Field(i)
if v := objValue.Field(i); v.IsValid() {
jTag := tf.Tag.Get("json")
if strings.HasPrefix(tf.Name, prefix) {
name := strings.Split(jTag, ",")[0]
if name == "" {
// Default to field name in go struct if no json name.
name = tf.Name
}
fields[name] = v
} else if jTag == ",inline" {
inlined[tf.Name] = getInterface(v)
}
}
}
return fields, inlined
}
// getInterface returns the interface value of the reflected object.
func getInterface(a reflect.Value) interface{} {
switch a.Kind() {
case reflect.Ptr:
if a.IsNil() {
return nil
}
return a.Elem().Interface()
case reflect.Map, reflect.Slice, reflect.Array:
return a.Elem().Interface()
// This is a nil interface{} type.
case reflect.Invalid:
return nil
default:
return a.Interface()
}
}
// nonZero returns true if a is nil or reflect.Zero.
func nonZero(a reflect.Value) bool {
switch a.Kind() {
case reflect.Ptr:
if a.IsNil() {
return false
}
return nonZero(a.Elem())
case reflect.Map, reflect.Slice, reflect.Array:
if a.IsNil() {
return false
}
return true
// This is a nil interface{} type.
case reflect.Invalid:
return false
default:
if reflect.DeepEqual(a.Interface(), reflect.Zero(a.Type()).Interface()) {
return false
}
return true
}
}
// differ returns true if a != b
func differ(a, b reflect.Value) bool {
if a.Kind() != b.Kind() {
return true
}
switch a.Kind() {
case reflect.Ptr:
if a.IsNil() || b.IsNil() {
return a.IsNil() != b.IsNil()
}
return differ(a.Elem(), b.Elem())
default:
if reflect.DeepEqual(a.Interface(), b.Interface()) {
return false
}
return true
}
}

513
apis/deprecated_test.go Normal file
View File

@ -0,0 +1,513 @@
/*
Copyright 2019 The Knative Authors
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 apis_test
import (
"context"
"github.com/knative/pkg/apis"
"github.com/knative/pkg/ptr"
. "github.com/knative/pkg/testing"
"strings"
"testing"
)
func TestCheckDeprecated(t *testing.T) {
testCases := map[string]struct {
strict bool
obj interface{}
wantErrs []string
}{
"create strict, string": {
strict: true,
obj: &InnerDefaultSubSpec{
DeprecatedString: "an error",
},
wantErrs: []string{
"must not set",
"string",
},
},
"create strict, stringptr": {
strict: true,
obj: &InnerDefaultSubSpec{
DeprecatedStringPtr: ptr.String("test string"),
},
wantErrs: []string{
"must not set",
"stringPtr",
},
},
"create strict, int": {
strict: true,
obj: &InnerDefaultSubSpec{
DeprecatedInt: 42,
},
wantErrs: []string{
"must not set",
"int",
},
},
"create strict, intptr": {
strict: true,
obj: &InnerDefaultSubSpec{
DeprecatedIntPtr: ptr.Int64(42),
},
wantErrs: []string{
"must not set",
"intPtr",
},
},
"create strict, map": {
strict: true,
obj: &InnerDefaultSubSpec{
DeprecatedMap: map[string]string{"hello": "failure"},
},
wantErrs: []string{
"must not set",
"map",
},
},
"create strict, slice": {
strict: true,
obj: &InnerDefaultSubSpec{
DeprecatedSlice: []string{"hello", "failure"},
},
wantErrs: []string{
"must not set",
"slice",
},
},
"create strict, struct": {
strict: true,
obj: &InnerDefaultSubSpec{
DeprecatedStruct: InnerDefaultStruct{FieldAsString: "not ok"},
},
wantErrs: []string{
"must not set",
"struct",
},
},
"create strict, structptr": {
strict: true,
obj: &InnerDefaultSubSpec{
DeprecatedStructPtr: &InnerDefaultStruct{
FieldAsString: "fail",
},
},
wantErrs: []string{
"must not set",
"structPtr",
},
},
"create strict, not json": {
strict: true,
obj: &InnerDefaultSubSpec{
DeprecatedNotJson: "fail",
},
wantErrs: []string{
"must not set",
"DeprecatedNotJson",
},
},
"create strict, inlined": {
strict: true,
obj: &InnerDefaultSubSpec{
InlinedStruct: InlinedStruct{
DeprecatedField: "fail",
},
},
wantErrs: []string{
"must not set",
"fieldA",
},
},
"create strict, inlined ptr": {
strict: true,
obj: &InnerDefaultSubSpec{
InlinedPtrStruct: &InlinedPtrStruct{
DeprecatedField: "fail",
},
},
wantErrs: []string{
"must not set",
"fieldB",
},
},
"create strict, inlined nested": {
strict: true,
obj: &InnerDefaultSubSpec{
InlinedStruct: InlinedStruct{
InlinedPtrStruct: &InlinedPtrStruct{
DeprecatedField: "fail",
},
},
},
wantErrs: []string{
"must not set",
"fieldB",
},
},
"create strict, all errors": {
strict: true,
obj: &InnerDefaultSubSpec{
DeprecatedString: "an error",
DeprecatedStringPtr: ptr.String("test string"),
DeprecatedInt: 42,
DeprecatedIntPtr: ptr.Int64(42),
DeprecatedMap: map[string]string{"hello": "failure"},
DeprecatedSlice: []string{"hello", "failure"},
DeprecatedStruct: InnerDefaultStruct{FieldAsString: "not ok"},
DeprecatedStructPtr: &InnerDefaultStruct{
FieldAsString: "fail",
},
InlinedStruct: InlinedStruct{
DeprecatedField: "fail",
InlinedPtrStruct: &InlinedPtrStruct{
DeprecatedField: "fail",
},
},
},
wantErrs: []string{
"string",
"stringPtr",
"int",
"intPtr",
"map",
"slice",
"struct",
"structPtr",
"fieldA",
"fieldB",
},
},
}
for n, tc := range testCases {
t.Run(n, func(t *testing.T) {
ctx := context.Background()
if tc.strict {
ctx = apis.DisallowDeprecated(ctx)
}
resp := apis.CheckDeprecated(ctx, tc.obj)
if len(tc.wantErrs) > 0 {
for _, err := range tc.wantErrs {
var gotErr string
if resp != nil {
gotErr = resp.Error()
}
if !strings.Contains(gotErr, err) {
t.Errorf("Expected failure containing %q got %q", err, gotErr)
}
}
} else if resp != nil {
t.Errorf("Expected no error, got %q", resp.Error())
}
})
}
}
// This test makes sure that errors will flatten the duped error for fieldB.
// It comes in on obj.InlinedStruct.InlinedPtrStruct.DeprecatedField and
// obj.InlinedPtrStruct.DeprecatedField.
func TestCheckDeprecated_Dedupe(t *testing.T) {
obj := &InnerDefaultSubSpec{
InlinedStruct: InlinedStruct{
DeprecatedField: "fail",
InlinedPtrStruct: &InlinedPtrStruct{
DeprecatedField: "fail",
},
},
InlinedPtrStruct: &InlinedPtrStruct{
DeprecatedField: "fail",
},
}
wantErr := "must not set the field(s): fieldA, fieldB"
ctx := apis.DisallowDeprecated(context.Background())
resp := apis.CheckDeprecated(ctx, obj)
gotErr := resp.Error()
if gotErr != wantErr {
t.Errorf("Expected failure %q got %q", wantErr, gotErr)
}
}
func TestCheckDeprecatedUpdate(t *testing.T) {
testCases := map[string]struct {
strict bool
obj interface{}
org interface{}
wantErrs []string
}{
"update strict, intptr": {
strict: true,
org: &InnerDefaultSubSpec{},
obj: &InnerDefaultSubSpec{
DeprecatedIntPtr: ptr.Int64(42),
},
wantErrs: []string{
"must not set",
"intPtr",
},
},
"update strict, map": {
strict: true,
org: &InnerDefaultSubSpec{},
obj: &InnerDefaultSubSpec{
DeprecatedMap: map[string]string{"hello": "failure"},
},
wantErrs: []string{
"must not set",
"map",
},
},
"update strict, slice": {
strict: true,
org: &InnerDefaultSubSpec{},
obj: &InnerDefaultSubSpec{
DeprecatedSlice: []string{"hello", "failure"},
},
wantErrs: []string{
"must not set",
"slice",
},
},
"update strict, struct": {
strict: true,
org: &InnerDefaultSubSpec{},
obj: &InnerDefaultSubSpec{
DeprecatedStruct: InnerDefaultStruct{
FieldAsString: "fail",
},
},
wantErrs: []string{
"must not set",
"struct",
},
},
"update strict, structptr": {
strict: true,
org: &InnerDefaultSubSpec{},
obj: &InnerDefaultSubSpec{
DeprecatedStructPtr: &InnerDefaultStruct{
FieldAsString: "fail",
},
},
wantErrs: []string{
"must not set",
"structPtr",
},
},
"update strict, all errors": {
strict: true,
org: &InnerDefaultSubSpec{},
obj: &InnerDefaultSubSpec{
DeprecatedString: "an error",
DeprecatedStringPtr: ptr.String("test string"),
DeprecatedInt: 42,
DeprecatedIntPtr: ptr.Int64(42),
DeprecatedMap: map[string]string{"hello": "failure"},
DeprecatedSlice: []string{"hello", "failure"},
DeprecatedStruct: InnerDefaultStruct{FieldAsString: "not ok"},
DeprecatedStructPtr: &InnerDefaultStruct{
FieldAsString: "fail",
},
InlinedStruct: InlinedStruct{
DeprecatedField: "fail",
InlinedPtrStruct: &InlinedPtrStruct{
DeprecatedField: "fail",
},
},
},
wantErrs: []string{
"string",
"stringPtr",
"int",
"intPtr",
"map",
"slice",
"struct",
"structPtr",
"fieldA",
"fieldB",
},
},
"overwrite strict, string": {
strict: true,
org: &InnerDefaultSubSpec{
DeprecatedString: "original setting.",
},
obj: &InnerDefaultSubSpec{
DeprecatedString: "fail setting.",
},
wantErrs: []string{
"must not update",
"string",
},
},
"overwrite strict, stringptr": {
strict: true,
org: &InnerDefaultSubSpec{
DeprecatedStringPtr: ptr.String("original string"),
},
obj: &InnerDefaultSubSpec{
DeprecatedStringPtr: ptr.String("fail string"),
},
wantErrs: []string{
"must not update",
"stringPtr",
},
},
"overwrite strict, int": {
strict: true,
org: &InnerDefaultSubSpec{
DeprecatedInt: 10,
},
obj: &InnerDefaultSubSpec{
DeprecatedInt: 42,
},
wantErrs: []string{
"must not update",
"int",
},
},
"overwrite strict, intptr": {
strict: true,
org: &InnerDefaultSubSpec{
DeprecatedIntPtr: ptr.Int64(10),
},
obj: &InnerDefaultSubSpec{
DeprecatedIntPtr: ptr.Int64(42),
},
wantErrs: []string{
"must not update",
"intPtr",
},
},
"overwrite strict, map": {
strict: true,
org: &InnerDefaultSubSpec{
DeprecatedMap: map[string]string{"goodbye": "existing"},
},
obj: &InnerDefaultSubSpec{
DeprecatedMap: map[string]string{"hello": "failure"},
},
wantErrs: []string{
"must not update",
"map",
},
},
"overwrite strict, slice": {
strict: true,
org: &InnerDefaultSubSpec{
DeprecatedSlice: []string{"hello", "existing"},
},
obj: &InnerDefaultSubSpec{
DeprecatedSlice: []string{"hello", "failure"},
},
wantErrs: []string{
"must not update",
"slice",
},
},
"overwrite strict, struct": {
strict: true,
org: &InnerDefaultSubSpec{
DeprecatedStruct: InnerDefaultStruct{
FieldAsString: "original",
},
},
obj: &InnerDefaultSubSpec{
DeprecatedStruct: InnerDefaultStruct{
FieldAsString: "fail",
},
},
wantErrs: []string{
"must not update",
"struct",
},
},
"overwrite strict, structptr": {
strict: true,
org: &InnerDefaultSubSpec{
DeprecatedStructPtr: &InnerDefaultStruct{
FieldAsString: "original",
},
},
obj: &InnerDefaultSubSpec{
DeprecatedStructPtr: &InnerDefaultStruct{
FieldAsString: "fail",
},
},
wantErrs: []string{
"must not update",
"structPtr",
},
},
"create, not strict": {
strict: false,
obj: &InnerDefaultSubSpec{
DeprecatedString: "fail setting.",
},
},
"update, not strict": {
strict: false,
org: &InnerDefaultSubSpec{},
obj: &InnerDefaultSubSpec{
DeprecatedString: "it's k",
},
},
"overwrite, not strict": {
strict: false,
org: &InnerDefaultSubSpec{
DeprecatedString: "org",
},
obj: &InnerDefaultSubSpec{
DeprecatedString: "it's k",
},
},
}
for n, tc := range testCases {
t.Run(n, func(t *testing.T) {
ctx := context.TODO()
if tc.strict {
ctx = apis.DisallowDeprecated(ctx)
}
resp := apis.CheckDeprecatedUpdate(ctx, tc.obj, tc.org)
if len(tc.wantErrs) > 0 {
for _, err := range tc.wantErrs {
var gotErr string
if resp != nil {
gotErr = resp.Error()
}
if !strings.Contains(gotErr, err) {
t.Errorf("Expected failure containing %q got %q", err, gotErr)
}
}
} else if resp != nil {
t.Errorf("Expected no error, got %q", resp.Error())
}
})
}
}

View File

@ -302,6 +302,15 @@ func ErrDisallowedFields(fieldPaths ...string) *FieldError {
}
}
// ErrDisallowedUpdateDeprecatedFields is a variadic helper method for
// constructing a FieldError for updating of deprecated fields.
func ErrDisallowedUpdateDeprecatedFields(fieldPaths ...string) *FieldError {
return &FieldError{
Message: "must not update deprecated field(s)",
Paths: fieldPaths,
}
}
// ErrInvalidArrayValue constructs a FieldError for a repetetive `field`
// at `index` that has received an invalid string value.
func ErrInvalidArrayValue(value interface{}, field string, index int) *FieldError {

View File

@ -182,6 +182,11 @@ can not use @, do not try`,
prefixes: [][]string{{"baz"}},
want: `invalid key name "b@r": baz.foo[0].name
can not use @, do not try`,
}, {
name: "disallowed update deprecated fields",
err: ErrDisallowedUpdateDeprecatedFields("foo", "bar"),
prefixes: [][]string{{"baz"}},
want: `must not update deprecated field(s): baz.bar, baz.foo`,
}, {
name: "very complex to simple",
err: func() *FieldError {

View File

@ -18,7 +18,6 @@ package testing
import (
"context"
"github.com/knative/pkg/apis"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -35,6 +34,9 @@ type InnerDefaultResource struct {
// Note that this does _not_ have omitempty. So when JSON is round tripped through the Golang
// type, `spec: {}` will automatically be injected.
Spec InnerDefaultSpec `json:"spec"`
// Status is a simple status.
Status InnerDefaultStatus `json:"status,omitempty"`
}
// InnerDefaultSpec is the spec for InnerDefaultResource.
@ -42,6 +44,68 @@ type InnerDefaultSpec struct {
Generation int64 `json:"generation,omitempty"`
FieldWithDefault string `json:"fieldWithDefault,omitempty"`
// Deprecated: This field is deprecated.
DeprecatedField string `json:"field,omitempty"`
SubFields *InnerDefaultSubSpec `json:"subfields,omitempty"`
}
// InnerDefaultSubSpec is a helper to test strict deprecated validation.
type InnerDefaultSubSpec struct {
// Deprecated: This field is deprecated.
DeprecatedString string `json:"string,omitempty"`
// Deprecated: This field is deprecated.
DeprecatedStringPtr *string `json:"stringPtr,omitempty"`
// Deprecated: This field is deprecated.
DeprecatedInt int64 `json:"int,omitempty"`
// Deprecated: This field is deprecated.
DeprecatedIntPtr *int64 `json:"intPtr,omitempty"`
// Deprecated: This field is deprecated.
DeprecatedMap map[string]string `json:"map,omitempty"`
// Deprecated: This field is deprecated.
DeprecatedSlice []string `json:"slice,omitempty"`
// Deprecated: This field is deprecated.
DeprecatedStruct InnerDefaultStruct `json:"struct,omitempty"`
// Deprecated: This field is deprecated.
DeprecatedStructPtr *InnerDefaultStruct `json:"structPtr,omitempty"`
InlinedStruct `json:",inline"`
*InlinedPtrStruct `json:",inline"`
// Deprecated: This field is deprecated.
DeprecatedNotJson string
}
// Adding complication helper.
type InnerDefaultStruct struct {
FieldAsString string `json:"fieldAsString,omitempty"`
// Deprecated: This field is deprecated.
DeprecatedField string `json:"field,omitempty"`
}
type InlinedStruct struct {
// Deprecated: This field is deprecated.
DeprecatedField string `json:"fieldA,omitempty"`
*InlinedPtrStruct `json:",inline"`
}
type InlinedPtrStruct struct {
// Deprecated: This field is deprecated.
DeprecatedField string `json:"fieldB,omitempty"`
}
// InnerDefaultStatus is the status for InnerDefaultResource.
type InnerDefaultStatus struct {
FieldAsString string `json:"fieldAsString,omitempty"`
}
// Check that ImmutableDefaultResource may be validated and defaulted.
@ -61,6 +125,32 @@ func (cs *InnerDefaultSpec) SetDefaults(ctx context.Context) {
}
// Validate validates the resource.
func (*InnerDefaultResource) Validate(ctx context.Context) *apis.FieldError {
return nil
func (i *InnerDefaultResource) Validate(ctx context.Context) *apis.FieldError {
var errs *apis.FieldError
if apis.IsInUpdate(ctx) {
org := apis.GetBaseline(ctx).(*InnerDefaultResource)
errs = apis.CheckDeprecatedUpdate(ctx, i.Spec, org.Spec).ViaField("spec")
if i.Spec.SubFields != nil {
var orgSubFields interface{}
if org != nil && org.Spec.SubFields != nil {
orgSubFields = org.Spec.SubFields
}
errs = errs.Also(apis.CheckDeprecatedUpdate(ctx, i.Spec.SubFields, orgSubFields).ViaField("spec", "subFields"))
var orgDepStruct interface{}
if orgSubFields != nil {
orgDepStruct = org.Spec.SubFields.DeprecatedStruct
}
errs = errs.Also(apis.CheckDeprecatedUpdate(ctx, i.Spec.SubFields.DeprecatedStruct, orgDepStruct).ViaField("spec", "subFields", "deprecatedStruct"))
}
} else {
errs = apis.CheckDeprecated(ctx, i.Spec).ViaField("spec")
if i.Spec.SubFields != nil {
errs = errs.Also(apis.CheckDeprecated(ctx, i.Spec.SubFields).ViaField("spec", "subFields").
Also(apis.CheckDeprecated(ctx, i.Spec.SubFields.DeprecatedStruct).ViaField("deprecatedStruct")))
}
}
return errs
}

View File

@ -24,12 +24,50 @@ import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InlinedPtrStruct) DeepCopyInto(out *InlinedPtrStruct) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InlinedPtrStruct.
func (in *InlinedPtrStruct) DeepCopy() *InlinedPtrStruct {
if in == nil {
return nil
}
out := new(InlinedPtrStruct)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InlinedStruct) DeepCopyInto(out *InlinedStruct) {
*out = *in
if in.InlinedPtrStruct != nil {
in, out := &in.InlinedPtrStruct, &out.InlinedPtrStruct
*out = new(InlinedPtrStruct)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InlinedStruct.
func (in *InlinedStruct) DeepCopy() *InlinedStruct {
if in == nil {
return nil
}
out := new(InlinedStruct)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InnerDefaultResource) DeepCopyInto(out *InnerDefaultResource) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
in.Spec.DeepCopyInto(&out.Spec)
out.Status = in.Status
return
}
@ -54,6 +92,11 @@ func (in *InnerDefaultResource) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InnerDefaultSpec) DeepCopyInto(out *InnerDefaultSpec) {
*out = *in
if in.SubFields != nil {
in, out := &in.SubFields, &out.SubFields
*out = new(InnerDefaultSubSpec)
(*in).DeepCopyInto(*out)
}
return
}
@ -67,6 +110,88 @@ func (in *InnerDefaultSpec) DeepCopy() *InnerDefaultSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InnerDefaultStatus) DeepCopyInto(out *InnerDefaultStatus) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InnerDefaultStatus.
func (in *InnerDefaultStatus) DeepCopy() *InnerDefaultStatus {
if in == nil {
return nil
}
out := new(InnerDefaultStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InnerDefaultStruct) DeepCopyInto(out *InnerDefaultStruct) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InnerDefaultStruct.
func (in *InnerDefaultStruct) DeepCopy() *InnerDefaultStruct {
if in == nil {
return nil
}
out := new(InnerDefaultStruct)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InnerDefaultSubSpec) DeepCopyInto(out *InnerDefaultSubSpec) {
*out = *in
if in.DeprecatedStringPtr != nil {
in, out := &in.DeprecatedStringPtr, &out.DeprecatedStringPtr
*out = new(string)
**out = **in
}
if in.DeprecatedIntPtr != nil {
in, out := &in.DeprecatedIntPtr, &out.DeprecatedIntPtr
*out = new(int64)
**out = **in
}
if in.DeprecatedMap != nil {
in, out := &in.DeprecatedMap, &out.DeprecatedMap
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.DeprecatedSlice != nil {
in, out := &in.DeprecatedSlice, &out.DeprecatedSlice
*out = make([]string, len(*in))
copy(*out, *in)
}
out.DeprecatedStruct = in.DeprecatedStruct
if in.DeprecatedStructPtr != nil {
in, out := &in.DeprecatedStructPtr, &out.DeprecatedStructPtr
*out = new(InnerDefaultStruct)
**out = **in
}
in.InlinedStruct.DeepCopyInto(&out.InlinedStruct)
if in.InlinedPtrStruct != nil {
in, out := &in.InlinedPtrStruct, &out.InlinedPtrStruct
*out = new(InlinedPtrStruct)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InnerDefaultSubSpec.
func (in *InnerDefaultSubSpec) DeepCopy() *InnerDefaultSubSpec {
if in == nil {
return nil
}
out := new(InnerDefaultSubSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OnContext) DeepCopyInto(out *OnContext) {
*out = *in

View File

@ -227,6 +227,7 @@ func validate(ctx context.Context, new GenericCRD) error {
if err := new.Validate(ctx); err != nil {
return err
}
return nil
}
@ -573,6 +574,7 @@ func (ac *AdmissionController) mutate(ctx context.Context, req *admissionv1beta1
// discretion over (our portion of) the message that the user sees.
return nil, err
}
return json.Marshal(patches)
}

View File

@ -0,0 +1,566 @@
/*
Copyright 2019 The Knative Authors
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 webhook
import (
"github.com/knative/pkg/apis"
. "github.com/knative/pkg/logging/testing"
"github.com/knative/pkg/ptr"
. "github.com/knative/pkg/testing"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"testing"
)
// In strict mode, you are not allowed to set a deprecated filed when doing a Create.
func TestStrictValidation(t *testing.T) {
newCreateReq := func(new []byte) *admissionv1beta1.AdmissionRequest {
req := &admissionv1beta1.AdmissionRequest{
Operation: admissionv1beta1.Create,
Kind: metav1.GroupVersionKind{
Group: "pkg.knative.dev",
Version: "v1alpha1",
Kind: "InnerDefaultResource",
},
}
req.Object.Raw = new
return req
}
newUpdateReq := func(old, new []byte) *admissionv1beta1.AdmissionRequest {
req := &admissionv1beta1.AdmissionRequest{
Operation: admissionv1beta1.Create,
Kind: metav1.GroupVersionKind{
Group: "pkg.knative.dev",
Version: "v1alpha1",
Kind: "InnerDefaultResource",
},
}
req.OldObject.Raw = old
req.Object.Raw = new
return req
}
testCases := map[string]struct {
strict bool
req *admissionv1beta1.AdmissionRequest
wantErrs []string
}{
"create, strict": {
strict: true,
req: newCreateReq(createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
DeprecatedField: "fail setting.",
}, nil)),
wantErrs: []string{
"must not set",
"spec.field",
},
},
"create strict, spec.sub.string": {
strict: true,
req: newCreateReq(createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedString: "an error",
},
}, nil)),
wantErrs: []string{
"must not set",
"spec.subFields.string",
},
},
"create strict, spec.sub.stringptr": {
strict: true,
req: newCreateReq(createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedStringPtr: func() *string {
s := "test string"
return &s
}(),
},
}, nil)),
wantErrs: []string{
"must not set",
"spec.subFields.stringPtr",
},
},
"create strict, spec.sub.int": {
strict: true,
req: newCreateReq(createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedInt: 42,
},
}, nil)),
wantErrs: []string{
"must not set",
"spec.subFields.int",
},
},
"create strict, spec.sub.intptr": {
strict: true,
req: newCreateReq(createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedIntPtr: ptr.Int64(42),
},
}, nil)),
wantErrs: []string{
"must not set",
"spec.subFields.intPtr",
},
},
"create strict, spec.sub.map": {
strict: true,
req: newCreateReq(createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedMap: map[string]string{"hello": "failure"},
},
}, nil)),
wantErrs: []string{
"must not set",
"spec.subFields.map",
},
},
"create strict, spec.sub.slice": {
strict: true,
req: newCreateReq(createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedSlice: []string{"hello", "failure"},
},
}, nil)),
wantErrs: []string{
"must not set",
"spec.subFields.slice",
},
},
"create strict, spec.sub.struct": {
strict: true,
req: newCreateReq(createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedStruct: InnerDefaultStruct{
FieldAsString: "fail",
},
},
}, nil)),
wantErrs: []string{
"must not set",
"spec.subFields.struct",
},
},
"create strict, spec.sub.structptr": {
strict: true,
req: newCreateReq(createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedStructPtr: &InnerDefaultStruct{
FieldAsString: "fail",
},
},
}, nil)),
wantErrs: []string{
"must not set",
"spec.subFields.structPtr",
},
},
"update, strict": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithoutSpec(t),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
DeprecatedField: "fail setting.",
}, nil)),
wantErrs: []string{
"must not update",
"spec.field",
},
},
"update strict, spec.sub.string": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithoutSpec(t),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedString: "an error",
},
}, nil)),
wantErrs: []string{
"must not set",
"spec.subFields.string",
},
},
"update strict, spec.sub.stringptr": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithoutSpec(t),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedStringPtr: func() *string {
s := "test string"
return &s
}(),
},
}, nil)),
wantErrs: []string{
"must not set",
"spec.subFields.stringPtr",
},
},
"update strict, spec.sub.int": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithoutSpec(t),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedInt: 42,
},
}, nil)),
wantErrs: []string{
"must not set",
"spec.subFields.int",
},
},
"update strict, spec.sub.intptr": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithoutSpec(t),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedIntPtr: ptr.Int64(42),
},
}, nil)),
wantErrs: []string{
"must not set",
"spec.subFields.intPtr",
},
},
"update strict, spec.sub.map": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithoutSpec(t),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedMap: map[string]string{"hello": "failure"},
},
}, nil)),
wantErrs: []string{
"must not set",
"spec.subFields.map",
},
},
"update strict, spec.sub.slice": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithoutSpec(t),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedSlice: []string{"hello", "failure"},
},
}, nil)),
wantErrs: []string{
"must not set",
"spec.subFields.slice",
},
},
"update strict, spec.sub.struct": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithoutSpec(t),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedStruct: InnerDefaultStruct{
FieldAsString: "fail",
},
},
}, nil)),
wantErrs: []string{
"must not set",
"spec.subFields.struct",
},
},
"update strict, spec.sub.structptr": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithoutSpec(t),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedStructPtr: &InnerDefaultStruct{
FieldAsString: "fail",
},
},
}, nil)),
wantErrs: []string{
"must not set",
"spec.subFields.structPtr",
},
},
"overwrite, strict": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
DeprecatedField: "original setting.",
}, nil),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
DeprecatedField: "fail setting.",
}, nil)),
wantErrs: []string{
"must not update",
"spec.field",
},
},
"overwrite strict, spec.sub.string": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedString: "original string",
},
}, nil),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedString: "an error",
},
}, nil)),
wantErrs: []string{
"must not update",
"spec.subFields.string",
},
},
"overwrite strict, spec.sub.stringptr": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedStringPtr: func() *string {
s := "original string"
return &s
}(),
},
}, nil),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedStringPtr: func() *string {
s := "test string"
return &s
}(),
},
}, nil)),
wantErrs: []string{
"must not update",
"spec.subFields.stringPtr",
},
},
"overwrite strict, spec.sub.int": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedInt: 10,
},
}, nil),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedInt: 42,
},
}, nil)),
wantErrs: []string{
"must not update",
"spec.subFields.int",
},
},
"overwrite strict, spec.sub.intptr": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedIntPtr: ptr.Int64(10),
},
}, nil),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedIntPtr: ptr.Int64(42),
},
}, nil)),
wantErrs: []string{
"must not update",
"spec.subFields.intPtr",
},
},
"overwrite strict, spec.sub.map": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedMap: map[string]string{"goodbye": "existing"},
},
}, nil),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedMap: map[string]string{"hello": "failure"},
},
}, nil)),
wantErrs: []string{
"must not update",
"spec.subFields.map",
},
},
"overwrite strict, spec.sub.slice": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedSlice: []string{"hello", "existing"},
},
}, nil),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedSlice: []string{"hello", "failure"},
},
}, nil)),
wantErrs: []string{
"must not update",
"spec.subFields.slice",
},
},
"overwrite strict, spec.sub.struct": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedStruct: InnerDefaultStruct{
FieldAsString: "original",
},
},
}, nil),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedStruct: InnerDefaultStruct{
FieldAsString: "fail",
},
},
}, nil)),
wantErrs: []string{
"must not update",
"spec.subFields.struct",
},
},
"overwrite strict, spec.sub.structptr": {
strict: true,
req: newUpdateReq(
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedStructPtr: &InnerDefaultStruct{
FieldAsString: "original",
},
},
}, nil),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
SubFields: &InnerDefaultSubSpec{
DeprecatedStructPtr: &InnerDefaultStruct{
FieldAsString: "fail",
},
},
}, nil)),
wantErrs: []string{
"must not update",
"spec.subFields.structPtr",
},
},
"create, not strict": {
strict: false,
req: newCreateReq(createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
DeprecatedField: "fail setting.",
}, nil)),
},
"update, not strict": {
strict: false,
req: newUpdateReq(
createInnerDefaultResourceWithoutSpec(t),
createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
DeprecatedField: "fail setting.",
}, nil)),
},
}
for n, tc := range testCases {
t.Run(n, func(t *testing.T) {
ctx := TestContextWithLogger(t)
if tc.strict {
ctx = apis.DisallowDeprecated(ctx)
}
_, ac := newNonRunningTestAdmissionController(t, newDefaultOptions())
resp := ac.admit(ctx, tc.req)
if len(tc.wantErrs) > 0 {
for _, err := range tc.wantErrs {
expectFailsWith(t, resp, err)
}
} else {
expectAllowed(t, resp)
}
})
}
}
// In strict mode, you are not allowed to set a deprecated filed when doing a Create.
func TestStrictValidation_Spec_Create(t *testing.T) {
req := &admissionv1beta1.AdmissionRequest{
Operation: admissionv1beta1.Create,
Kind: metav1.GroupVersionKind{
Group: "pkg.knative.dev",
Version: "v1alpha1",
Kind: "InnerDefaultResource",
},
}
req.Object.Raw = createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
DeprecatedField: "fail setting.",
}, nil)
ctx := apis.DisallowDeprecated(TestContextWithLogger(t))
_, ac := newNonRunningTestAdmissionController(t, newDefaultOptions())
resp := ac.admit(ctx, req)
expectFailsWith(t, resp, "must not set")
expectFailsWith(t, resp, "spec.field")
}
// In strict mode, you are not allowed to update a deprecated filed when doing a Update.
func TestStrictValidation_Spec_Update(t *testing.T) {
req := &admissionv1beta1.AdmissionRequest{
Operation: admissionv1beta1.Update,
Kind: metav1.GroupVersionKind{
Group: "pkg.knative.dev",
Version: "v1alpha1",
Kind: "InnerDefaultResource",
},
}
req.OldObject.Raw = createInnerDefaultResourceWithoutSpec(t)
req.Object.Raw = createInnerDefaultResourceWithSpecAndStatus(t, &InnerDefaultSpec{
DeprecatedField: "fail setting.",
}, nil)
ctx := apis.DisallowDeprecated(TestContextWithLogger(t))
_, ac := newNonRunningTestAdmissionController(t, newDefaultOptions())
resp := ac.admit(ctx, req)
expectFailsWith(t, resp, "must not update")
expectFailsWith(t, resp, "spec.field")
}

View File

@ -490,6 +490,28 @@ func createInnerDefaultResourceWithoutSpec(t *testing.T) []byte {
return b
}
func createInnerDefaultResourceWithSpecAndStatus(t *testing.T, spec *InnerDefaultSpec, status *InnerDefaultStatus) []byte {
t.Helper()
r := InnerDefaultResource{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "a name",
},
}
if spec != nil {
r.Spec = *spec
}
if status != nil {
r.Status = *status
}
b, err := json.Marshal(r)
if err != nil {
t.Fatalf("Error marshaling bytes: %v", err)
}
return b
}
func TestValidWebhook(t *testing.T) {
_, ac := newNonRunningTestAdmissionController(t, newDefaultOptions())
createDeployment(ac)