mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-10 04:27:54 +00:00
ratcheting-cel: add optionalOldSelf field
This commit is contained in:
parent
18adc30933
commit
5edb27aa38
@ -210,6 +210,19 @@ type ValidationRule struct {
|
|||||||
// - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values
|
// - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values
|
||||||
// are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with
|
// are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with
|
||||||
// non-intersecting keys are appended, retaining their partial order.
|
// non-intersecting keys are appended, retaining their partial order.
|
||||||
|
//
|
||||||
|
// If `rule` makes use of the `oldSelf` variable it is implicitly a
|
||||||
|
// `transition rule`.
|
||||||
|
//
|
||||||
|
// By default, the `oldSelf` variable is the same type as `self`.
|
||||||
|
// When `optionalOldSelf` is true, the `oldSelf` variable is a CEL optional
|
||||||
|
// variable whose value() is the same type as `self`.
|
||||||
|
// See the documentation for the `optionalOldSelf` field for details.
|
||||||
|
//
|
||||||
|
// Transition rules by default are applied only on UPDATE requests and are
|
||||||
|
// skipped if an old value could not be found. You can opt a transition
|
||||||
|
// rule into unconditional evaluation by setting `optionalOldSelf` to true.
|
||||||
|
//
|
||||||
Rule string
|
Rule string
|
||||||
// Message represents the message displayed when validation fails. The message is required if the Rule contains
|
// Message represents the message displayed when validation fails. The message is required if the Rule contains
|
||||||
// line breaks. The message must not contain line breaks.
|
// line breaks. The message must not contain line breaks.
|
||||||
@ -246,6 +259,24 @@ type ValidationRule struct {
|
|||||||
// e.g. for attribute `foo.34$` appears in a list `testList`, the fieldPath could be set to `.testList['foo.34$']`
|
// e.g. for attribute `foo.34$` appears in a list `testList`, the fieldPath could be set to `.testList['foo.34$']`
|
||||||
// +optional
|
// +optional
|
||||||
FieldPath string
|
FieldPath string
|
||||||
|
|
||||||
|
// optionalOldSelf is used to opt a transition rule into evaluation
|
||||||
|
// even when the object is first created, or if the old object is
|
||||||
|
// missing the value.
|
||||||
|
//
|
||||||
|
// When enabled `oldSelf` will be a CEL optional whose value will be
|
||||||
|
// `None` if there is no old value, or when the object is initially created.
|
||||||
|
//
|
||||||
|
// You may check for presence of oldSelf using `oldSelf.hasValue()` and
|
||||||
|
// unwrap it after checking using `oldSelf.value()`. Check the CEL
|
||||||
|
// documentation for Optional types for more information:
|
||||||
|
// https://pkg.go.dev/github.com/google/cel-go/cel#OptionalTypes
|
||||||
|
//
|
||||||
|
// May not be set unless `oldSelf` is used in `rule`.
|
||||||
|
//
|
||||||
|
// +featureGate=CRDValidationRatcheting
|
||||||
|
// +optional
|
||||||
|
OptionalOldSelf *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON represents any valid JSON value.
|
// JSON represents any valid JSON value.
|
||||||
|
@ -249,6 +249,19 @@ type ValidationRule struct {
|
|||||||
// - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values
|
// - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values
|
||||||
// are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with
|
// are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with
|
||||||
// non-intersecting keys are appended, retaining their partial order.
|
// non-intersecting keys are appended, retaining their partial order.
|
||||||
|
//
|
||||||
|
// If `rule` makes use of the `oldSelf` variable it is implicitly a
|
||||||
|
// `transition rule`.
|
||||||
|
//
|
||||||
|
// By default, the `oldSelf` variable is the same type as `self`.
|
||||||
|
// When `optionalOldSelf` is true, the `oldSelf` variable is a CEL optional
|
||||||
|
// variable whose value() is the same type as `self`.
|
||||||
|
// See the documentation for the `optionalOldSelf` field for details.
|
||||||
|
//
|
||||||
|
// Transition rules by default are applied only on UPDATE requests and are
|
||||||
|
// skipped if an old value could not be found. You can opt a transition
|
||||||
|
// rule into unconditional evaluation by setting `optionalOldSelf` to true.
|
||||||
|
//
|
||||||
Rule string `json:"rule" protobuf:"bytes,1,opt,name=rule"`
|
Rule string `json:"rule" protobuf:"bytes,1,opt,name=rule"`
|
||||||
// Message represents the message displayed when validation fails. The message is required if the Rule contains
|
// Message represents the message displayed when validation fails. The message is required if the Rule contains
|
||||||
// line breaks. The message must not contain line breaks.
|
// line breaks. The message must not contain line breaks.
|
||||||
@ -285,6 +298,24 @@ type ValidationRule struct {
|
|||||||
// e.g. for attribute `foo.34$` appears in a list `testList`, the fieldPath could be set to `.testList['foo.34$']`
|
// e.g. for attribute `foo.34$` appears in a list `testList`, the fieldPath could be set to `.testList['foo.34$']`
|
||||||
// +optional
|
// +optional
|
||||||
FieldPath string `json:"fieldPath,omitempty" protobuf:"bytes,5,opt,name=fieldPath"`
|
FieldPath string `json:"fieldPath,omitempty" protobuf:"bytes,5,opt,name=fieldPath"`
|
||||||
|
|
||||||
|
// optionalOldSelf is used to opt a transition rule into evaluation
|
||||||
|
// even when the object is first created, or if the old object is
|
||||||
|
// missing the value.
|
||||||
|
//
|
||||||
|
// When enabled `oldSelf` will be a CEL optional whose value will be
|
||||||
|
// `None` if there is no old value, or when the object is initially created.
|
||||||
|
//
|
||||||
|
// You may check for presence of oldSelf using `oldSelf.hasValue()` and
|
||||||
|
// unwrap it after checking using `oldSelf.value()`. Check the CEL
|
||||||
|
// documentation for Optional types for more information:
|
||||||
|
// https://pkg.go.dev/github.com/google/cel-go/cel#OptionalTypes
|
||||||
|
//
|
||||||
|
// May not be set unless `oldSelf` is used in `rule`.
|
||||||
|
//
|
||||||
|
// +featureGate=CRDValidationRatcheting
|
||||||
|
// +optional
|
||||||
|
OptionalOldSelf *bool `json:"optionalOldSelf,omitempty" protobuf:"bytes,6,opt,name=optionalOldSelf"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON represents any valid JSON value.
|
// JSON represents any valid JSON value.
|
||||||
|
@ -249,6 +249,19 @@ type ValidationRule struct {
|
|||||||
// - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values
|
// - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values
|
||||||
// are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with
|
// are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with
|
||||||
// non-intersecting keys are appended, retaining their partial order.
|
// non-intersecting keys are appended, retaining their partial order.
|
||||||
|
//
|
||||||
|
// If `rule` makes use of the `oldSelf` variable it is implicitly a
|
||||||
|
// `transition rule`.
|
||||||
|
//
|
||||||
|
// By default, the `oldSelf` variable is the same type as `self`.
|
||||||
|
// When `optionalOldSelf` is true, the `oldSelf` variable is a CEL optional
|
||||||
|
// variable whose value() is the same type as `self`.
|
||||||
|
// See the documentation for the `optionalOldSelf` field for details.
|
||||||
|
//
|
||||||
|
// Transition rules by default are applied only on UPDATE requests and are
|
||||||
|
// skipped if an old value could not be found. You can opt a transition
|
||||||
|
// rule into unconditional evaluation by setting `optionalOldSelf` to true.
|
||||||
|
//
|
||||||
Rule string `json:"rule" protobuf:"bytes,1,opt,name=rule"`
|
Rule string `json:"rule" protobuf:"bytes,1,opt,name=rule"`
|
||||||
// Message represents the message displayed when validation fails. The message is required if the Rule contains
|
// Message represents the message displayed when validation fails. The message is required if the Rule contains
|
||||||
// line breaks. The message must not contain line breaks.
|
// line breaks. The message must not contain line breaks.
|
||||||
@ -285,6 +298,24 @@ type ValidationRule struct {
|
|||||||
// e.g. for attribute `foo.34$` appears in a list `testList`, the fieldPath could be set to `.testList['foo.34$']`
|
// e.g. for attribute `foo.34$` appears in a list `testList`, the fieldPath could be set to `.testList['foo.34$']`
|
||||||
// +optional
|
// +optional
|
||||||
FieldPath string `json:"fieldPath,omitempty" protobuf:"bytes,5,opt,name=fieldPath"`
|
FieldPath string `json:"fieldPath,omitempty" protobuf:"bytes,5,opt,name=fieldPath"`
|
||||||
|
|
||||||
|
// optionalOldSelf is used to opt a transition rule into evaluation
|
||||||
|
// even when the object is first created, or if the old object is
|
||||||
|
// missing the value.
|
||||||
|
//
|
||||||
|
// When enabled `oldSelf` will be a CEL optional whose value will be
|
||||||
|
// `None` if there is no old value, or when the object is initially created.
|
||||||
|
//
|
||||||
|
// You may check for presence of oldSelf using `oldSelf.hasValue()` and
|
||||||
|
// unwrap it after checking using `oldSelf.value()`. Check the CEL
|
||||||
|
// documentation for Optional types for more information:
|
||||||
|
// https://pkg.go.dev/github.com/google/cel-go/cel#OptionalTypes
|
||||||
|
//
|
||||||
|
// May not be set unless `oldSelf` is used in `rule`.
|
||||||
|
//
|
||||||
|
// +featureGate=CRDValidationRatcheting
|
||||||
|
// +optional
|
||||||
|
OptionalOldSelf *bool `json:"optionalOldSelf,omitempty" protobuf:"bytes,6,opt,name=optionalOldSelf"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON represents any valid JSON value.
|
// JSON represents any valid JSON value.
|
||||||
|
@ -1186,6 +1186,8 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
|
|||||||
if uncorrelatablePath := ssv.forbidOldSelfValidations(); uncorrelatablePath != nil {
|
if uncorrelatablePath := ssv.forbidOldSelfValidations(); uncorrelatablePath != nil {
|
||||||
allErrs.CELErrors = append(allErrs.CELErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), schema.XValidations[i].Rule, fmt.Sprintf("oldSelf cannot be used on the uncorrelatable portion of the schema within %v", uncorrelatablePath)))
|
allErrs.CELErrors = append(allErrs.CELErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), schema.XValidations[i].Rule, fmt.Sprintf("oldSelf cannot be used on the uncorrelatable portion of the schema within %v", uncorrelatablePath)))
|
||||||
}
|
}
|
||||||
|
} else if schema.XValidations[i].OptionalOldSelf != nil {
|
||||||
|
allErrs.CELErrors = append(allErrs.CELErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("optionalOldSelf"), *schema.XValidations[i].OptionalOldSelf, "may not be set if oldSelf is not used in rule"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/cel/environment"
|
"k8s.io/apiserver/pkg/cel/environment"
|
||||||
"k8s.io/apiserver/pkg/cel/library"
|
"k8s.io/apiserver/pkg/cel/library"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/pointer"
|
||||||
|
"k8s.io/utils/ptr"
|
||||||
)
|
)
|
||||||
|
|
||||||
type validationMatch struct {
|
type validationMatch struct {
|
||||||
@ -9650,6 +9651,157 @@ func TestValidateCustomResourceDefinitionValidation(t *testing.T) {
|
|||||||
required("spec.validation.openAPIV3Schema.properties[f].x-kubernetes-validations[0].messageExpression"),
|
required("spec.validation.openAPIV3Schema.properties[f].x-kubernetes-validations[0].messageExpression"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "forbid transition rule on element of list of type atomic when optionalOldSelf is set",
|
||||||
|
opts: validationOptions{requireStructuralSchema: true},
|
||||||
|
input: apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"value": {
|
||||||
|
Type: "array",
|
||||||
|
XListType: strPtr("atomic"),
|
||||||
|
Items: &apiextensions.JSONSchemaPropsOrArray{
|
||||||
|
Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "string",
|
||||||
|
MaxLength: int64ptr(10),
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{Rule: `self == oldSelf.orValue("")`, OptionalOldSelf: ptr.To(true)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErrors: []validationMatch{
|
||||||
|
invalid("spec.validation.openAPIV3Schema.properties[value].items.x-kubernetes-validations[0].rule"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forbid transition rule on element of list defaulting to type atomic when optionalOldSelf is set",
|
||||||
|
opts: validationOptions{requireStructuralSchema: true},
|
||||||
|
input: apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"value": {
|
||||||
|
Type: "array",
|
||||||
|
Items: &apiextensions.JSONSchemaPropsOrArray{
|
||||||
|
Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "string",
|
||||||
|
MaxLength: int64ptr(10),
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{Rule: `self == oldSelf.orValue("")`, OptionalOldSelf: ptr.To(true)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErrors: []validationMatch{
|
||||||
|
invalid("spec.validation.openAPIV3Schema.properties[value].items.x-kubernetes-validations[0].rule"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forbid transition rule on element of list of type set when optionalOldSelf is set",
|
||||||
|
opts: validationOptions{requireStructuralSchema: true},
|
||||||
|
input: apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"value": {
|
||||||
|
Type: "array",
|
||||||
|
MaxItems: int64ptr(10),
|
||||||
|
XListType: strPtr("set"),
|
||||||
|
Items: &apiextensions.JSONSchemaPropsOrArray{
|
||||||
|
Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "string",
|
||||||
|
MaxLength: int64ptr(10),
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{Rule: `self == oldSelf.orValue("")`, OptionalOldSelf: ptr.To(true)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErrors: []validationMatch{
|
||||||
|
invalid("spec.validation.openAPIV3Schema.properties[value].items.x-kubernetes-validations[0].rule"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forbid transition rule on element of map of unrecognized type when optionalOldSelf is set",
|
||||||
|
opts: validationOptions{requireStructuralSchema: true},
|
||||||
|
input: apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"value": {
|
||||||
|
Type: "object",
|
||||||
|
XMapType: strPtr("future"),
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"subfield": {
|
||||||
|
Type: "string",
|
||||||
|
MaxLength: int64ptr(10),
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{Rule: `self == oldSelf.orValue("")`, OptionalOldSelf: ptr.To(true)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErrors: []validationMatch{
|
||||||
|
invalid("spec.validation.openAPIV3Schema.properties[value].properties[subfield].x-kubernetes-validations[0].rule"),
|
||||||
|
unsupported("spec.validation.openAPIV3Schema.properties[value].x-kubernetes-map-type"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forbid setting optionalOldSelf to true if oldSelf is not used",
|
||||||
|
opts: validationOptions{requireStructuralSchema: true},
|
||||||
|
input: apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"value": {
|
||||||
|
Type: "string",
|
||||||
|
MaxLength: int64ptr(10),
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{Rule: `self == "foo"`, OptionalOldSelf: ptr.To(true)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErrors: []validationMatch{
|
||||||
|
invalid("spec.validation.openAPIV3Schema.properties[value].x-kubernetes-validations[0].optionalOldSelf"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forbid setting optionalOldSelf to false if oldSelf is not used",
|
||||||
|
opts: validationOptions{requireStructuralSchema: true},
|
||||||
|
input: apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"value": {
|
||||||
|
Type: "string",
|
||||||
|
MaxLength: int64ptr(10),
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{Rule: `self == "foo"`, OptionalOldSelf: ptr.To(false)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErrors: []validationMatch{
|
||||||
|
invalid("spec.validation.openAPIV3Schema.properties[value].x-kubernetes-validations[0].optionalOldSelf"),
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation"
|
||||||
|
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/fields"
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
@ -33,6 +34,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/registry/generic"
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
"k8s.io/apiserver/pkg/storage"
|
"k8s.io/apiserver/pkg/storage"
|
||||||
"k8s.io/apiserver/pkg/storage/names"
|
"k8s.io/apiserver/pkg/storage/names"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
)
|
)
|
||||||
|
|
||||||
// strategy implements behavior for CustomResources.
|
// strategy implements behavior for CustomResources.
|
||||||
@ -223,3 +225,60 @@ func MatchCustomResourceDefinition(label labels.Selector, field fields.Selector)
|
|||||||
func CustomResourceDefinitionToSelectableFields(obj *apiextensions.CustomResourceDefinition) fields.Set {
|
func CustomResourceDefinitionToSelectableFields(obj *apiextensions.CustomResourceDefinition) fields.Set {
|
||||||
return generic.ObjectMetaFieldsSet(&obj.ObjectMeta, true)
|
return generic.ObjectMetaFieldsSet(&obj.ObjectMeta, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dropDisabledFields drops disabled fields that are not used if their associated feature gates
|
||||||
|
// are not enabled.
|
||||||
|
func dropDisabledFields(newCRD *apiextensions.CustomResourceDefinition, oldCRD *apiextensions.CustomResourceDefinition) {
|
||||||
|
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CRDValidationRatcheting) && (oldCRD == nil || (oldCRD != nil && !specHasOptionalOldSelf(&oldCRD.Spec))) {
|
||||||
|
if newCRD.Spec.Validation != nil {
|
||||||
|
dropOptionalOldSelfField(newCRD.Spec.Validation.OpenAPIV3Schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range newCRD.Spec.Versions {
|
||||||
|
if v.Schema != nil {
|
||||||
|
dropOptionalOldSelfField(v.Schema.OpenAPIV3Schema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dropOptionalOldSelfField drops field optionalOldSelf from CRD schema
|
||||||
|
func dropOptionalOldSelfField(schema *apiextensions.JSONSchemaProps) {
|
||||||
|
if schema == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range schema.XValidations {
|
||||||
|
schema.XValidations[i].OptionalOldSelf = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.AdditionalProperties != nil {
|
||||||
|
dropOptionalOldSelfField(schema.AdditionalProperties.Schema)
|
||||||
|
}
|
||||||
|
for def, jsonSchema := range schema.Properties {
|
||||||
|
dropOptionalOldSelfField(&jsonSchema)
|
||||||
|
schema.Properties[def] = jsonSchema
|
||||||
|
}
|
||||||
|
if schema.Items != nil {
|
||||||
|
dropOptionalOldSelfField(schema.Items.Schema)
|
||||||
|
for i, jsonSchema := range schema.Items.JSONSchemas {
|
||||||
|
dropOptionalOldSelfField(&jsonSchema)
|
||||||
|
schema.Items.JSONSchemas[i] = jsonSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func specHasOptionalOldSelf(spec *apiextensions.CustomResourceDefinitionSpec) bool {
|
||||||
|
return validation.HasSchemaWith(spec, schemaHasOptionalOldSelf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func schemaHasOptionalOldSelf(s *apiextensions.JSONSchemaProps) bool {
|
||||||
|
return validation.SchemaHas(s, func(s *apiextensions.JSONSchemaProps) bool {
|
||||||
|
for _, v := range s.XValidations {
|
||||||
|
if v.OptionalOldSelf != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -20,12 +20,17 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation"
|
||||||
|
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/pointer"
|
||||||
|
"k8s.io/utils/ptr"
|
||||||
)
|
)
|
||||||
|
|
||||||
func strPtr(in string) *string {
|
func strPtr(in string) *string {
|
||||||
@ -189,3 +194,521 @@ func TestValidateAPIApproval(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDropDisabledFields tests if the drop functionality is working fine or not with feature gate switch
|
||||||
|
func TestDropDisabledFields(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
enableRatcheting bool
|
||||||
|
crd *apiextensions.CustomResourceDefinition
|
||||||
|
oldCRD *apiextensions.CustomResourceDefinition
|
||||||
|
expectedCRD *apiextensions.CustomResourceDefinition
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Ratcheting, For creation, FG disabled, no OptionalOldSelf, no field drop",
|
||||||
|
enableRatcheting: false,
|
||||||
|
crd: &apiextensions.CustomResourceDefinition{},
|
||||||
|
oldCRD: nil,
|
||||||
|
expectedCRD: &apiextensions.CustomResourceDefinition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ratcheting, For creation, FG disabled, set OptionalOldSelf, drop OptionalOldSelf",
|
||||||
|
enableRatcheting: false,
|
||||||
|
crd: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"subRule": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "isTest == true",
|
||||||
|
Message: "isTest should be true.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"isTest": {
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldCRD: nil,
|
||||||
|
expectedCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"subRule": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "isTest == true",
|
||||||
|
Message: "isTest should be true.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"isTest": {
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ratcheting, For creation, FG enabled, set OptionalOldSelf, update with OptionalOldSelf",
|
||||||
|
enableRatcheting: true,
|
||||||
|
crd: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"subRule": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "isTest == true",
|
||||||
|
Message: "isTest should be true.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"isTest": {
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldCRD: nil,
|
||||||
|
expectedCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"subRule": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "isTest == true",
|
||||||
|
Message: "isTest should be true.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"isTest": {
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ratcheting, For update, FG disabled, oldCRD OptionalOldSelf in use, don't drop OptionalOldSelfs",
|
||||||
|
enableRatcheting: false,
|
||||||
|
crd: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"subRule": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "isTest == true",
|
||||||
|
Message: "isTest should be true.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"isTest": {
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"otherRule": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "self.isTest == true",
|
||||||
|
Message: "isTest should be true.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"subRule": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "isTest == true",
|
||||||
|
Message: "isTest should be true.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"isTest": {
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ratcheting, For update, FG disabled, oldCRD OptionalOldSelf in use, but different from new, don't drop OptionalOldSelfs",
|
||||||
|
enableRatcheting: false,
|
||||||
|
crd: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"subRule": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "isTest == true",
|
||||||
|
Message: "isTest should be true.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"isTest": {
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"subRule": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "isTest == true",
|
||||||
|
Message: "isTest should be true.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"subRule": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "isTest == true",
|
||||||
|
Message: "isTest should be true.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"isTest": {
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ratcheting, For update, FG disabled, oldCRD has no OptionalOldSelf, drop OptionalOldSelf",
|
||||||
|
enableRatcheting: false,
|
||||||
|
crd: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ratcheting, For update, FG enabled, oldCRD has optionalOldSelf, updated to newCRD",
|
||||||
|
enableRatcheting: true,
|
||||||
|
crd: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "old data",
|
||||||
|
Message: "old data",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ratcheting, For update, FG enabled, oldCRD has no OptionalOldSelf, updated to newCRD",
|
||||||
|
enableRatcheting: true,
|
||||||
|
crd: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
OptionalOldSelf: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CRDValidationRatcheting, tc.enableRatcheting)()
|
||||||
|
old := tc.oldCRD.DeepCopy()
|
||||||
|
|
||||||
|
dropDisabledFields(tc.crd, tc.oldCRD)
|
||||||
|
|
||||||
|
// old crd should never be changed
|
||||||
|
if diff := cmp.Diff(tc.oldCRD, old); diff != "" {
|
||||||
|
t.Fatalf("old crd changed from %v to %v\n%v", tc.oldCRD, old, diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tc.expectedCRD, tc.crd); diff != "" {
|
||||||
|
t.Fatalf("unexpected crd: %v\n%v", tc.crd, diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user