mirror of https://github.com/knative/pkg.git
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:
parent
3c8c4a9354
commit
70ab9cc77d
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue