diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go index 0710bead7f4..43b1cf71cf8 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go @@ -210,8 +210,16 @@ func (r ratchetingOptions) shouldRatchetError() bool { func (r ratchetingOptions) key(field string) ratchetingOptions { if r.currentCorrelation == nil { return r + } else if r.nearestParentCorrelation == nil && (field == "apiVersion" || field == "kind") { + // We cannot ratchet changes to the APIVersion and kind fields field since + // they aren't visible. (both old and new are converted to the same type) + // + return ratchetingOptions{} } + // nearestParentCorrelation is always non-nil except for the root node. + // The below line ensures that the next nearestParentCorrelation is set + // to a non-nil r.currentCorrelation return ratchetingOptions{currentCorrelation: r.currentCorrelation.Key(field), nearestParentCorrelation: r.currentCorrelation} } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation_test.go index 2170fb9eef6..3fc5ac2374c 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation_test.go @@ -3622,6 +3622,106 @@ func TestRatcheting(t *testing.T) { `rule compile error: compilation failed: ERROR: :1:1: undeclared reference to 'asdausidyhASDNJm'`, }, }, + { + name: "typemeta fields are not ratcheted", + schema: mustSchema(` + type: object + properties: + apiVersion: + type: string + x-kubernetes-validations: + - rule: self == "v1" + kind: + type: string + x-kubernetes-validations: + - rule: self == "Pod" + `), + oldObj: mustUnstructured(` + apiVersion: v2 + kind: Baz + `), + newObj: mustUnstructured(` + apiVersion: v2 + kind: Baz + `), + errors: []string{ + `root.apiVersion: Invalid value: "string": failed rule: self == "v1"`, + `root.kind: Invalid value: "string": failed rule: self == "Pod"`, + }, + }, + { + name: "nested typemeta fields may still be ratcheted", + schema: mustSchema(` + type: object + properties: + list: + type: array + x-kubernetes-list-type: map + x-kubernetes-list-map-keys: ["name"] + maxItems: 2 + items: + type: object + properties: + name: + type: string + apiVersion: + type: string + x-kubernetes-validations: + - rule: self == "v1" + kind: + type: string + x-kubernetes-validations: + - rule: self == "Pod" + subField: + type: object + properties: + apiVersion: + type: string + x-kubernetes-validations: + - rule: self == "v1" + kind: + type: string + x-kubernetes-validations: + - rule: self == "Pod" + otherField: + type: string + `), + oldObj: mustUnstructured(` + subField: + apiVersion: v2 + kind: Baz + list: + - name: entry1 + apiVersion: v2 + kind: Baz + - name: entry2 + apiVersion: v3 + kind: Bar + `), + newObj: mustUnstructured(` + subField: + apiVersion: v2 + kind: Baz + otherField: newValue + list: + - name: entry1 + apiVersion: v2 + kind: Baz + otherField: newValue2 + - name: entry2 + apiVersion: v3 + kind: Bar + otherField: newValue3 + `), + warnings: []string{ + `root.subField.apiVersion: Invalid value: "string": failed rule: self == "v1"`, + `root.subField.kind: Invalid value: "string": failed rule: self == "Pod"`, + `root.list[0].apiVersion: Invalid value: "string": failed rule: self == "v1"`, + `root.list[0].kind: Invalid value: "string": failed rule: self == "Pod"`, + `root.list[1].apiVersion: Invalid value: "string": failed rule: self == "v1"`, + `root.list[1].kind: Invalid value: "string": failed rule: self == "Pod"`, + }, + }, } for _, c := range cases { diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/ratcheting.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/ratcheting.go index 3cc653e7466..9cd01686863 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/ratcheting.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/ratcheting.go @@ -165,7 +165,13 @@ func (r *ratchetingValueValidator) Validate(new interface{}) *validate.Result { // If the old value cannot be correlated, then default validation is used. func (r *ratchetingValueValidator) SubPropertyValidator(field string, schema *spec.Schema, rootSchema interface{}, root string, formats strfmt.Registry, options ...validate.Option) validate.ValueValidator { childNode := r.correlation.Key(field) - if childNode == nil { + if childNode == nil || (r.path == "" && isTypeMetaField(field)) { + // Defer to default validation if we cannot correlate the old value + // or if we are validating the root object and the field is a metadata + // field. + // + // We cannot ratchet changes to the APIVersion field since they aren't visible. + // (both old and new are converted to the same type) return validate.NewSchemaValidator(schema, rootSchema, root, formats, options...) } @@ -210,3 +216,7 @@ func (r ratchetingValueValidator) SetPath(path string) { func (r ratchetingValueValidator) Applies(source interface{}, valueKind reflect.Kind) bool { return true } + +func isTypeMetaField(path string) bool { + return path == "kind" || path == "apiVersion" +}