ratcheting-cel: use Optional[T] for oldSelf when optionalOldSelf is true
This commit is contained in:
		@@ -23,6 +23,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	"github.com/google/cel-go/cel"
 | 
						"github.com/google/cel-go/cel"
 | 
				
			||||||
	"github.com/google/cel-go/checker"
 | 
						"github.com/google/cel-go/checker"
 | 
				
			||||||
 | 
						"github.com/google/cel-go/common/types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 | 
						apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 | 
				
			||||||
	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
 | 
						"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
 | 
				
			||||||
@@ -126,7 +127,7 @@ func Compile(s *schema.Structural, declType *apiservercel.DeclType, perCallLimit
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	celRules := s.Extensions.XValidations
 | 
						celRules := s.Extensions.XValidations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	envSet, err := prepareEnvSet(baseEnvSet, declType)
 | 
						oldSelfEnvSet, optionalOldSelfEnvSet, err := prepareEnvSet(baseEnvSet, declType)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -135,15 +136,20 @@ func Compile(s *schema.Structural, declType *apiservercel.DeclType, perCallLimit
 | 
				
			|||||||
	compResults := make([]CompilationResult, len(celRules))
 | 
						compResults := make([]CompilationResult, len(celRules))
 | 
				
			||||||
	maxCardinality := maxCardinality(declType.MinSerializedSize)
 | 
						maxCardinality := maxCardinality(declType.MinSerializedSize)
 | 
				
			||||||
	for i, rule := range celRules {
 | 
						for i, rule := range celRules {
 | 
				
			||||||
		compResults[i] = compileRule(s, rule, envSet, envLoader, estimator, maxCardinality, perCallLimit)
 | 
							ruleEnvSet := oldSelfEnvSet
 | 
				
			||||||
 | 
							if rule.OptionalOldSelf != nil && *rule.OptionalOldSelf {
 | 
				
			||||||
 | 
								ruleEnvSet = optionalOldSelfEnvSet
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							compResults[i] = compileRule(s, rule, ruleEnvSet, envLoader, estimator, maxCardinality, perCallLimit)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return compResults, nil
 | 
						return compResults, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func prepareEnvSet(baseEnvSet *environment.EnvSet, declType *apiservercel.DeclType) (*environment.EnvSet, error) {
 | 
					func prepareEnvSet(baseEnvSet *environment.EnvSet, declType *apiservercel.DeclType) (oldSelfEnvSet *environment.EnvSet, optionalOldSelfEnvSet *environment.EnvSet, err error) {
 | 
				
			||||||
	scopedType := declType.MaybeAssignTypeName(generateUniqueSelfTypeName())
 | 
						scopedType := declType.MaybeAssignTypeName(generateUniqueSelfTypeName())
 | 
				
			||||||
	return baseEnvSet.Extend(
 | 
					
 | 
				
			||||||
 | 
						oldSelfEnvSet, err = baseEnvSet.Extend(
 | 
				
			||||||
		environment.VersionedOptions{
 | 
							environment.VersionedOptions{
 | 
				
			||||||
			// Feature epoch was actually 1.23, but we artificially set it to 1.0 because these
 | 
								// Feature epoch was actually 1.23, but we artificially set it to 1.0 because these
 | 
				
			||||||
			// options should always be present.
 | 
								// options should always be present.
 | 
				
			||||||
@@ -162,6 +168,34 @@ func prepareEnvSet(baseEnvSet *environment.EnvSet, declType *apiservercel.DeclTy
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						optionalOldSelfEnvSet, err = baseEnvSet.Extend(
 | 
				
			||||||
 | 
							environment.VersionedOptions{
 | 
				
			||||||
 | 
								// Feature epoch was actually 1.23, but we artificially set it to 1.0 because these
 | 
				
			||||||
 | 
								// options should always be present.
 | 
				
			||||||
 | 
								IntroducedVersion: version.MajorMinor(1, 0),
 | 
				
			||||||
 | 
								EnvOptions: []cel.EnvOption{
 | 
				
			||||||
 | 
									cel.Variable(ScopedVarName, scopedType.CelType()),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								DeclTypes: []*apiservercel.DeclType{
 | 
				
			||||||
 | 
									scopedType,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							environment.VersionedOptions{
 | 
				
			||||||
 | 
								IntroducedVersion: version.MajorMinor(1, 24),
 | 
				
			||||||
 | 
								EnvOptions: []cel.EnvOption{
 | 
				
			||||||
 | 
									cel.Variable(OldScopedVarName, types.NewOptionalType(scopedType.CelType())),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return oldSelfEnvSet, optionalOldSelfEnvSet, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func compileRule(s *schema.Structural, rule apiextensions.ValidationRule, envSet *environment.EnvSet, envLoader EnvLoader, estimator *library.CostEstimator, maxCardinality uint64, perCallLimit uint64) (compilationResult CompilationResult) {
 | 
					func compileRule(s *schema.Structural, rule apiextensions.ValidationRule, envSet *environment.EnvSet, envLoader EnvLoader, estimator *library.CostEstimator, maxCardinality uint64, perCallLimit uint64) (compilationResult CompilationResult) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,10 +29,14 @@ import (
 | 
				
			|||||||
	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 | 
						apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 | 
				
			||||||
	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
 | 
						"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
 | 
				
			||||||
	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
 | 
						"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
 | 
				
			||||||
 | 
						apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/util/version"
 | 
						"k8s.io/apimachinery/pkg/util/version"
 | 
				
			||||||
	celconfig "k8s.io/apiserver/pkg/apis/cel"
 | 
						celconfig "k8s.io/apiserver/pkg/apis/cel"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/cel"
 | 
						"k8s.io/apiserver/pkg/cel"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/cel/environment"
 | 
						"k8s.io/apiserver/pkg/cel/environment"
 | 
				
			||||||
 | 
						utilfeature "k8s.io/apiserver/pkg/util/feature"
 | 
				
			||||||
 | 
						featuregatetesting "k8s.io/component-base/featuregate/testing"
 | 
				
			||||||
 | 
						"k8s.io/utils/ptr"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
@@ -151,12 +155,99 @@ func (v transitionRuleMatcher) String() string {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestCelCompilation(t *testing.T) {
 | 
					func TestCelCompilation(t *testing.T) {
 | 
				
			||||||
 | 
						defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CRDValidationRatcheting, true)()
 | 
				
			||||||
	cases := []struct {
 | 
						cases := []struct {
 | 
				
			||||||
		name            string
 | 
							name            string
 | 
				
			||||||
		input           schema.Structural
 | 
							input           schema.Structural
 | 
				
			||||||
		expectedResults []validationMatcher
 | 
							expectedResults []validationMatcher
 | 
				
			||||||
		unmodified      bool
 | 
							unmodified      bool
 | 
				
			||||||
	}{
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "optional primitive transition rule type checking",
 | 
				
			||||||
 | 
								input: schema.Structural{
 | 
				
			||||||
 | 
									Generic: schema.Generic{
 | 
				
			||||||
 | 
										Type: "integer",
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Extensions: schema.Extensions{
 | 
				
			||||||
 | 
										XValidations: apiextensions.ValidationRules{
 | 
				
			||||||
 | 
											{Rule: "self >= oldSelf.value()", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
											{Rule: "self >= oldSelf.orValue(1)", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
											{Rule: "oldSelf.hasValue() ? self >= oldSelf.value() : true", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
											{Rule: "self >= oldSelf", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
											{Rule: "self >= oldSelf.orValue('')", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectedResults: []validationMatcher{
 | 
				
			||||||
 | 
									matchesAll(noError(), transitionRule(true)),
 | 
				
			||||||
 | 
									matchesAll(noError(), transitionRule(true)),
 | 
				
			||||||
 | 
									matchesAll(noError(), transitionRule(true)),
 | 
				
			||||||
 | 
									matchesAll(invalidError("optional")),
 | 
				
			||||||
 | 
									matchesAll(invalidError("orValue")),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "optional complex transition rule type checking",
 | 
				
			||||||
 | 
								input: schema.Structural{
 | 
				
			||||||
 | 
									Generic: schema.Generic{
 | 
				
			||||||
 | 
										Type: "object",
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Properties: map[string]schema.Structural{
 | 
				
			||||||
 | 
										"i": {Generic: schema.Generic{Type: "integer"}},
 | 
				
			||||||
 | 
										"b": {Generic: schema.Generic{Type: "boolean"}},
 | 
				
			||||||
 | 
										"s": {Generic: schema.Generic{Type: "string"}},
 | 
				
			||||||
 | 
										"a": {
 | 
				
			||||||
 | 
											Generic: schema.Generic{Type: "array"},
 | 
				
			||||||
 | 
											Items:   &schema.Structural{Generic: schema.Generic{Type: "integer"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										"o": {
 | 
				
			||||||
 | 
											Generic: schema.Generic{Type: "object"},
 | 
				
			||||||
 | 
											Properties: map[string]schema.Structural{
 | 
				
			||||||
 | 
												"i": {Generic: schema.Generic{Type: "integer"}},
 | 
				
			||||||
 | 
												"b": {Generic: schema.Generic{Type: "boolean"}},
 | 
				
			||||||
 | 
												"s": {Generic: schema.Generic{Type: "string"}},
 | 
				
			||||||
 | 
												"a": {
 | 
				
			||||||
 | 
													Generic: schema.Generic{Type: "array"},
 | 
				
			||||||
 | 
													Items:   &schema.Structural{Generic: schema.Generic{Type: "integer"}},
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
												"o": {
 | 
				
			||||||
 | 
													Generic: schema.Generic{Type: "object"},
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Extensions: schema.Extensions{
 | 
				
			||||||
 | 
										XValidations: apiextensions.ValidationRules{
 | 
				
			||||||
 | 
											{Rule: "self.i >= oldSelf.i.value()", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
											{Rule: "self.s == oldSelf.s.value()", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
											{Rule: "self.b == oldSelf.b.value()", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
											{Rule: "self.o == oldSelf.o.value()", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
											{Rule: "self.o.i >= oldSelf.o.i.value()", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
											{Rule: "self.o.s == oldSelf.o.s.value()", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
											{Rule: "self.o.b == oldSelf.o.b.value()", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
											{Rule: "self.o.o == oldSelf.o.o.value()", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
											{Rule: "self.o.i >= oldSelf.o.i.orValue(1)", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
											{Rule: "oldSelf.hasValue() ? self.o.i >= oldSelf.o.i.value() : true", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
											{Rule: "self.o.i >= oldSelf.o.i", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
											{Rule: "self.o.i >= oldSelf.o.s.orValue(0)", OptionalOldSelf: ptr.To(true)},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectedResults: []validationMatcher{
 | 
				
			||||||
 | 
									matchesAll(noError(), transitionRule(true)),
 | 
				
			||||||
 | 
									matchesAll(noError(), transitionRule(true)),
 | 
				
			||||||
 | 
									matchesAll(noError(), transitionRule(true)),
 | 
				
			||||||
 | 
									matchesAll(noError(), transitionRule(true)),
 | 
				
			||||||
 | 
									matchesAll(noError(), transitionRule(true)),
 | 
				
			||||||
 | 
									matchesAll(noError(), transitionRule(true)),
 | 
				
			||||||
 | 
									matchesAll(noError(), transitionRule(true)),
 | 
				
			||||||
 | 
									matchesAll(noError(), transitionRule(true)),
 | 
				
			||||||
 | 
									matchesAll(noError(), transitionRule(true)),
 | 
				
			||||||
 | 
									matchesAll(noError(), transitionRule(true)),
 | 
				
			||||||
 | 
									matchesAll(invalidError("optional")),
 | 
				
			||||||
 | 
									matchesAll(invalidError("orValue")),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			name: "valid object",
 | 
								name: "valid object",
 | 
				
			||||||
			input: schema.Structural{
 | 
								input: schema.Structural{
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,15 +32,18 @@ import (
 | 
				
			|||||||
	"github.com/google/cel-go/interpreter"
 | 
						"github.com/google/cel-go/interpreter"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"k8s.io/klog/v2"
 | 
						"k8s.io/klog/v2"
 | 
				
			||||||
 | 
						"k8s.io/utils/ptr"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 | 
						apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 | 
				
			||||||
	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
 | 
						"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
 | 
				
			||||||
	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
 | 
						"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
 | 
				
			||||||
 | 
						"k8s.io/apiextensions-apiserver/pkg/features"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/util/validation/field"
 | 
						"k8s.io/apimachinery/pkg/util/validation/field"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/cel"
 | 
						"k8s.io/apiserver/pkg/cel"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/cel/common"
 | 
						"k8s.io/apiserver/pkg/cel/common"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/cel/environment"
 | 
						"k8s.io/apiserver/pkg/cel/environment"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/cel/metrics"
 | 
						"k8s.io/apiserver/pkg/cel/metrics"
 | 
				
			||||||
 | 
						utilfeature "k8s.io/apiserver/pkg/util/feature"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/warning"
 | 
						"k8s.io/apiserver/pkg/warning"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	celconfig "k8s.io/apiserver/pkg/apis/cel"
 | 
						celconfig "k8s.io/apiserver/pkg/apis/cel"
 | 
				
			||||||
@@ -65,9 +68,10 @@ type Validator struct {
 | 
				
			|||||||
	// custom resource being validated, or the root of an XEmbeddedResource object.
 | 
						// custom resource being validated, or the root of an XEmbeddedResource object.
 | 
				
			||||||
	isResourceRoot bool
 | 
						isResourceRoot bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// celActivationFactory produces an Activation, which resolves identifiers (e.g. self and
 | 
						// celActivationFactory produces a Activations, which resolve identifiers
 | 
				
			||||||
	// oldSelf) to CEL values.
 | 
						// (e.g. self and oldSelf) to CEL values. One activation must be produced
 | 
				
			||||||
	celActivationFactory func(sts *schema.Structural, obj, oldObj interface{}) interpreter.Activation
 | 
						// for each of the cases when oldSelf is optional and non-optional.
 | 
				
			||||||
 | 
						celActivationFactory func(sts *schema.Structural, obj, oldObj interface{}) (activation interpreter.Activation, optionalOldSelfActivation interpreter.Activation)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewValidator returns compiles all the CEL programs defined in x-kubernetes-validations extensions
 | 
					// NewValidator returns compiles all the CEL programs defined in x-kubernetes-validations extensions
 | 
				
			||||||
@@ -122,7 +126,7 @@ func validator(s *schema.Structural, isResourceRoot bool, declType *cel.DeclType
 | 
				
			|||||||
		additionalPropertiesValidator = validator(s.AdditionalProperties.Structural, s.AdditionalProperties.Structural.XEmbeddedResource, declType.ElemType, perCallLimit)
 | 
							additionalPropertiesValidator = validator(s.AdditionalProperties.Structural, s.AdditionalProperties.Structural.XEmbeddedResource, declType.ElemType, perCallLimit)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if len(compiledRules) > 0 || err != nil || itemsValidator != nil || additionalPropertiesValidator != nil || len(propertiesValidators) > 0 {
 | 
						if len(compiledRules) > 0 || err != nil || itemsValidator != nil || additionalPropertiesValidator != nil || len(propertiesValidators) > 0 {
 | 
				
			||||||
		var activationFactory func(*schema.Structural, interface{}, interface{}) interpreter.Activation = validationActivationWithoutOldSelf
 | 
							activationFactory := validationActivationWithoutOldSelf
 | 
				
			||||||
		for _, rule := range compiledRules {
 | 
							for _, rule := range compiledRules {
 | 
				
			||||||
			if rule.UsesOldSelf {
 | 
								if rule.UsesOldSelf {
 | 
				
			||||||
				activationFactory = validationActivationWithOldSelf
 | 
									activationFactory = validationActivationWithOldSelf
 | 
				
			||||||
@@ -289,7 +293,7 @@ func (s *Validator) validateExpressions(ctx context.Context, fldPath *field.Path
 | 
				
			|||||||
	if s.isResourceRoot {
 | 
						if s.isResourceRoot {
 | 
				
			||||||
		sts = model.WithTypeAndObjectMeta(sts)
 | 
							sts = model.WithTypeAndObjectMeta(sts)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	activation := s.celActivationFactory(sts, obj, oldObj)
 | 
						activation, optionalOldSelfActivation := s.celActivationFactory(sts, obj, oldObj)
 | 
				
			||||||
	for i, compiled := range s.compiledRules {
 | 
						for i, compiled := range s.compiledRules {
 | 
				
			||||||
		rule := sts.XValidations[i]
 | 
							rule := sts.XValidations[i]
 | 
				
			||||||
		if compiled.Error != nil {
 | 
							if compiled.Error != nil {
 | 
				
			||||||
@@ -300,11 +304,29 @@ func (s *Validator) validateExpressions(ctx context.Context, fldPath *field.Path
 | 
				
			|||||||
			// rule is empty
 | 
								// rule is empty
 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// If ratcheting is enabled, allow rule with oldSelf to evaluate
 | 
				
			||||||
 | 
							// when `optionalOldSelf` is set to true
 | 
				
			||||||
 | 
							optionalOldSelfRule := ptr.Deref(rule.OptionalOldSelf, false)
 | 
				
			||||||
		if compiled.UsesOldSelf && oldObj == nil {
 | 
							if compiled.UsesOldSelf && oldObj == nil {
 | 
				
			||||||
			// transition rules are evaluated only if there is a comparable existing value
 | 
								// transition rules are evaluated only if there is a comparable existing value
 | 
				
			||||||
			continue
 | 
								// But if the rule uses optional oldSelf and gate is enabled we allow
 | 
				
			||||||
 | 
								// the rule to be evaluated
 | 
				
			||||||
 | 
								if !utilfeature.DefaultFeatureGate.Enabled(features.CRDValidationRatcheting) {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if !optionalOldSelfRule {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		evalResult, evalDetails, err := compiled.Program.ContextEval(ctx, activation)
 | 
					
 | 
				
			||||||
 | 
							ruleActivation := activation
 | 
				
			||||||
 | 
							if optionalOldSelfRule {
 | 
				
			||||||
 | 
								ruleActivation = optionalOldSelfActivation
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							evalResult, evalDetails, err := compiled.Program.ContextEval(ctx, ruleActivation)
 | 
				
			||||||
		if evalDetails == nil {
 | 
							if evalDetails == nil {
 | 
				
			||||||
			errs = append(errs, field.InternalError(fldPath, fmt.Errorf("runtime cost could not be calculated for validation rule: %v, no further validation rules will be run", ruleErrorString(rule))))
 | 
								errs = append(errs, field.InternalError(fldPath, fmt.Errorf("runtime cost could not be calculated for validation rule: %v, no further validation rules will be run", ruleErrorString(rule))))
 | 
				
			||||||
			return errs, -1
 | 
								return errs, -1
 | 
				
			||||||
@@ -622,21 +644,31 @@ type validationActivation struct {
 | 
				
			|||||||
	hasOldSelf    bool
 | 
						hasOldSelf    bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func validationActivationWithOldSelf(sts *schema.Structural, obj, oldObj interface{}) interpreter.Activation {
 | 
					func validationActivationWithOldSelf(sts *schema.Structural, obj, oldObj interface{}) (activation interpreter.Activation, optionalOldSelfActivation interpreter.Activation) {
 | 
				
			||||||
	va := &validationActivation{
 | 
						va := &validationActivation{
 | 
				
			||||||
		self: UnstructuredToVal(obj, sts),
 | 
							self: UnstructuredToVal(obj, sts),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						optionalVA := &validationActivation{
 | 
				
			||||||
 | 
							self:       va.self,
 | 
				
			||||||
 | 
							hasOldSelf: true, // this means the oldSelf variable is defined for CEL to reference, not that it has a value
 | 
				
			||||||
 | 
							oldSelf:    types.OptionalNone,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if oldObj != nil {
 | 
						if oldObj != nil {
 | 
				
			||||||
		va.oldSelf = UnstructuredToVal(oldObj, sts) // +k8s:verify-mutation:reason=clone
 | 
							va.oldSelf = UnstructuredToVal(oldObj, sts) // +k8s:verify-mutation:reason=clone
 | 
				
			||||||
		va.hasOldSelf = true                        // +k8s:verify-mutation:reason=clone
 | 
							va.hasOldSelf = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							optionalVA.oldSelf = types.OptionalOf(va.oldSelf) // +k8s:verify-mutation:reason=clone
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return va
 | 
					
 | 
				
			||||||
 | 
						return va, optionalVA
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func validationActivationWithoutOldSelf(sts *schema.Structural, obj, _ interface{}) interpreter.Activation {
 | 
					func validationActivationWithoutOldSelf(sts *schema.Structural, obj, _ interface{}) (interpreter.Activation, interpreter.Activation) {
 | 
				
			||||||
	return &validationActivation{
 | 
						res := &validationActivation{
 | 
				
			||||||
		self: UnstructuredToVal(obj, sts),
 | 
							self: UnstructuredToVal(obj, sts),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						return res, res
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (a *validationActivation) ResolveName(name string) (interface{}, bool) {
 | 
					func (a *validationActivation) ResolveName(name string) (interface{}, bool) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,16 +31,20 @@ import (
 | 
				
			|||||||
	"github.com/stretchr/testify/require"
 | 
						"github.com/stretchr/testify/require"
 | 
				
			||||||
	"k8s.io/klog/v2"
 | 
						"k8s.io/klog/v2"
 | 
				
			||||||
	"k8s.io/kube-openapi/pkg/validation/strfmt"
 | 
						"k8s.io/kube-openapi/pkg/validation/strfmt"
 | 
				
			||||||
 | 
						"k8s.io/utils/ptr"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	apiextensionsinternal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
 | 
						apiextensionsinternal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
 | 
				
			||||||
	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 | 
						apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 | 
				
			||||||
	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
 | 
						"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
 | 
				
			||||||
	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
 | 
						"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
 | 
				
			||||||
 | 
						apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/util/validation/field"
 | 
						"k8s.io/apimachinery/pkg/util/validation/field"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/util/yaml"
 | 
						"k8s.io/apimachinery/pkg/util/yaml"
 | 
				
			||||||
	celconfig "k8s.io/apiserver/pkg/apis/cel"
 | 
						celconfig "k8s.io/apiserver/pkg/apis/cel"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/cel/common"
 | 
						"k8s.io/apiserver/pkg/cel/common"
 | 
				
			||||||
 | 
						utilfeature "k8s.io/apiserver/pkg/util/feature"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/warning"
 | 
						"k8s.io/apiserver/pkg/warning"
 | 
				
			||||||
 | 
						featuregatetesting "k8s.io/component-base/featuregate/testing"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TestValidationExpressions tests CEL integration with custom resource values and OpenAPIv3.
 | 
					// TestValidationExpressions tests CEL integration with custom resource values and OpenAPIv3.
 | 
				
			||||||
@@ -3618,6 +3622,401 @@ func TestRatcheting(t *testing.T) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Runs transition rule cases with OptionalOldSelf set to true on the schema
 | 
				
			||||||
 | 
					func TestOptionalOldSelf(t *testing.T) {
 | 
				
			||||||
 | 
						defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CRDValidationRatcheting, true)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							name   string
 | 
				
			||||||
 | 
							schema *schema.Structural
 | 
				
			||||||
 | 
							obj    interface{}
 | 
				
			||||||
 | 
							oldObj interface{}
 | 
				
			||||||
 | 
							errors []string // strings that error message must contain
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "allow new value if old value is null",
 | 
				
			||||||
 | 
								obj: map[string]interface{}{
 | 
				
			||||||
 | 
									"foo": "bar",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
 | 
				
			||||||
 | 
									"foo": stringType,
 | 
				
			||||||
 | 
								}), "self.foo == 'not bar' || !oldSelf.hasValue()"),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "block new value if old value is not null",
 | 
				
			||||||
 | 
								obj: map[string]interface{}{
 | 
				
			||||||
 | 
									"foo": "invalid",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								oldObj: map[string]interface{}{
 | 
				
			||||||
 | 
									"foo": "bar",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
 | 
				
			||||||
 | 
									"foo": stringType,
 | 
				
			||||||
 | 
								}), "self.foo == 'valid' || !oldSelf.hasValue()"),
 | 
				
			||||||
 | 
								errors: []string{"failed rule"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "allow invalid new value if old value is also invalid",
 | 
				
			||||||
 | 
								obj: map[string]interface{}{
 | 
				
			||||||
 | 
									"foo": "invalid again",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								oldObj: map[string]interface{}{
 | 
				
			||||||
 | 
									"foo": "invalid",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
 | 
				
			||||||
 | 
									"foo": stringType,
 | 
				
			||||||
 | 
								}), "self.foo == 'valid' || (oldSelf.hasValue() && oldSelf.value().foo != 'valid')"),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "allow invalid new value if old value is also invalid with chained optionals",
 | 
				
			||||||
 | 
								obj: map[string]interface{}{
 | 
				
			||||||
 | 
									"foo": "invalid again",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								oldObj: map[string]interface{}{
 | 
				
			||||||
 | 
									"foo": "invalid",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
 | 
				
			||||||
 | 
									"foo": stringType,
 | 
				
			||||||
 | 
								}), "self.foo == 'valid' || oldSelf.foo.orValue('') != 'valid'"),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "block invalid new value if old value is valid",
 | 
				
			||||||
 | 
								obj: map[string]interface{}{
 | 
				
			||||||
 | 
									"foo": "invalid",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								oldObj: map[string]interface{}{
 | 
				
			||||||
 | 
									"foo": "valid",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
 | 
				
			||||||
 | 
									"foo": stringType,
 | 
				
			||||||
 | 
								}), "self.foo == 'valid' || (oldSelf.hasValue() && oldSelf.value().foo != 'valid')"),
 | 
				
			||||||
 | 
								errors: []string{"failed rule"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:   "create: new min or allow higher than oldValue",
 | 
				
			||||||
 | 
								obj:    10,
 | 
				
			||||||
 | 
								schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "block create: new min or allow higher than oldValue",
 | 
				
			||||||
 | 
								obj:  9,
 | 
				
			||||||
 | 
								// Can't use != null because type is integer and no overload
 | 
				
			||||||
 | 
								// workaround by comparing type, but kinda hacky
 | 
				
			||||||
 | 
								schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
 | 
				
			||||||
 | 
								errors: []string{"failed rule"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:   "update: new min or allow higher than oldValue",
 | 
				
			||||||
 | 
								obj:    10,
 | 
				
			||||||
 | 
								oldObj: 5,
 | 
				
			||||||
 | 
								schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:   "ratchet update: new min or allow higher than oldValue",
 | 
				
			||||||
 | 
								obj:    9,
 | 
				
			||||||
 | 
								oldObj: 5,
 | 
				
			||||||
 | 
								schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:   "ratchet noop update: new min or allow higher than oldValue",
 | 
				
			||||||
 | 
								obj:    5,
 | 
				
			||||||
 | 
								oldObj: 5,
 | 
				
			||||||
 | 
								schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:   "block update: new min or allow higher than oldValue",
 | 
				
			||||||
 | 
								obj:    4,
 | 
				
			||||||
 | 
								oldObj: 5,
 | 
				
			||||||
 | 
								schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
 | 
				
			||||||
 | 
								errors: []string{"failed rule"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tt := range tests {
 | 
				
			||||||
 | 
							tt := tt
 | 
				
			||||||
 | 
							tp := true
 | 
				
			||||||
 | 
							for i := range tt.schema.XValidations {
 | 
				
			||||||
 | 
								tt.schema.XValidations[i].OptionalOldSelf = &tp
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run(tt.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								// t.Parallel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								ctx := context.TODO()
 | 
				
			||||||
 | 
								celValidator := validator(tt.schema, true, model.SchemaDeclType(tt.schema, false), celconfig.PerCallLimit)
 | 
				
			||||||
 | 
								if celValidator == nil {
 | 
				
			||||||
 | 
									t.Fatal("expected non nil validator")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								errs, _ := celValidator.Validate(ctx, field.NewPath("root"), tt.schema, tt.obj, tt.oldObj, math.MaxInt)
 | 
				
			||||||
 | 
								unmatched := map[string]struct{}{}
 | 
				
			||||||
 | 
								for _, e := range tt.errors {
 | 
				
			||||||
 | 
									unmatched[e] = struct{}{}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								for _, err := range errs {
 | 
				
			||||||
 | 
									if err.Type != field.ErrorTypeInvalid {
 | 
				
			||||||
 | 
										t.Errorf("expected only ErrorTypeInvalid errors, but got: %v", err)
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									matched := false
 | 
				
			||||||
 | 
									for expected := range unmatched {
 | 
				
			||||||
 | 
										if strings.Contains(err.Error(), expected) {
 | 
				
			||||||
 | 
											delete(unmatched, expected)
 | 
				
			||||||
 | 
											matched = true
 | 
				
			||||||
 | 
											break
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if !matched {
 | 
				
			||||||
 | 
										t.Errorf("expected error to contain one of %v, but got: %v", unmatched, err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if len(unmatched) > 0 {
 | 
				
			||||||
 | 
									t.Errorf("expected errors %v", unmatched)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Shows that type(oldSelf) == null_type works for all supported OpenAPI types
 | 
				
			||||||
 | 
					// both when oldSelf is null and when it is not null
 | 
				
			||||||
 | 
					func TestOptionalOldSelfCheckForNull(t *testing.T) {
 | 
				
			||||||
 | 
						defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CRDValidationRatcheting, true)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							name   string
 | 
				
			||||||
 | 
							schema schema.Structural
 | 
				
			||||||
 | 
							obj    interface{}
 | 
				
			||||||
 | 
							oldObj interface{}
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "object",
 | 
				
			||||||
 | 
								obj: map[string]interface{}{
 | 
				
			||||||
 | 
									"foo": "bar",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								oldObj: map[string]interface{}{
 | 
				
			||||||
 | 
									"foo": "baz",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								schema: withRule(objectType(map[string]schema.Structural{
 | 
				
			||||||
 | 
									"foo": stringType,
 | 
				
			||||||
 | 
								}), `!oldSelf.hasValue() || self.foo == "bar"`),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "object - conditional field",
 | 
				
			||||||
 | 
								obj: map[string]interface{}{
 | 
				
			||||||
 | 
									"foo": "bar",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								oldObj: map[string]interface{}{
 | 
				
			||||||
 | 
									"foo": "baz",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								schema: withRule(objectType(map[string]schema.Structural{
 | 
				
			||||||
 | 
									"foo": stringType,
 | 
				
			||||||
 | 
								}), `self.foo != "bar" || oldSelf.?foo.orValue("baz") == "baz"`),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:   "string",
 | 
				
			||||||
 | 
								obj:    "bar",
 | 
				
			||||||
 | 
								oldObj: "baz",
 | 
				
			||||||
 | 
								schema: withRule(stringType, `
 | 
				
			||||||
 | 
									!oldSelf.hasValue() || self == "bar"
 | 
				
			||||||
 | 
								`),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:   "integer",
 | 
				
			||||||
 | 
								obj:    1,
 | 
				
			||||||
 | 
								oldObj: 2,
 | 
				
			||||||
 | 
								schema: withRule(integerType, `
 | 
				
			||||||
 | 
									!oldSelf.hasValue() || self == 1
 | 
				
			||||||
 | 
								`),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:   "number",
 | 
				
			||||||
 | 
								obj:    1.1,
 | 
				
			||||||
 | 
								oldObj: 2.2,
 | 
				
			||||||
 | 
								schema: withRule(numberType, `
 | 
				
			||||||
 | 
									!oldSelf.hasValue() || self == 1.1
 | 
				
			||||||
 | 
								`),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:   "boolean",
 | 
				
			||||||
 | 
								obj:    true,
 | 
				
			||||||
 | 
								oldObj: false,
 | 
				
			||||||
 | 
								schema: withRule(booleanType, `
 | 
				
			||||||
 | 
									!oldSelf.hasValue() || self == true
 | 
				
			||||||
 | 
								`),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:   "array",
 | 
				
			||||||
 | 
								obj:    []interface{}{"bar"},
 | 
				
			||||||
 | 
								oldObj: []interface{}{"baz"},
 | 
				
			||||||
 | 
								schema: withRule(arrayType("", nil, &stringSchema), `
 | 
				
			||||||
 | 
									!oldSelf.hasValue() || self[0] == "bar"
 | 
				
			||||||
 | 
								`),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "array - conditional index",
 | 
				
			||||||
 | 
								obj:  []interface{}{},
 | 
				
			||||||
 | 
								oldObj: []interface{}{
 | 
				
			||||||
 | 
									"baz",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								schema: withRule(arrayType("", nil, &stringSchema), `
 | 
				
			||||||
 | 
									self.size() > 0 || oldSelf[?0].orValue("baz") == "baz"
 | 
				
			||||||
 | 
								`),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:   "set-array",
 | 
				
			||||||
 | 
								obj:    []interface{}{"bar"},
 | 
				
			||||||
 | 
								oldObj: []interface{}{"baz"},
 | 
				
			||||||
 | 
								schema: withRule(arrayType("set", nil, &stringSchema), `
 | 
				
			||||||
 | 
									!oldSelf.hasValue() || self[0] == "bar"
 | 
				
			||||||
 | 
								`),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "map-array",
 | 
				
			||||||
 | 
								obj: []interface{}{map[string]interface{}{
 | 
				
			||||||
 | 
									"key":   "foo",
 | 
				
			||||||
 | 
									"value": "bar",
 | 
				
			||||||
 | 
								}},
 | 
				
			||||||
 | 
								oldObj: []interface{}{map[string]interface{}{
 | 
				
			||||||
 | 
									"key":   "foo",
 | 
				
			||||||
 | 
									"value": "baz",
 | 
				
			||||||
 | 
								}},
 | 
				
			||||||
 | 
								schema: withRule(arrayType("map", []string{"key"}, objectTypePtr(map[string]schema.Structural{
 | 
				
			||||||
 | 
									"key":   stringType,
 | 
				
			||||||
 | 
									"value": stringType,
 | 
				
			||||||
 | 
								})), `
 | 
				
			||||||
 | 
									!oldSelf.hasValue() || self[0].value == "bar"
 | 
				
			||||||
 | 
								`),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tt := range tests {
 | 
				
			||||||
 | 
							tt := tt
 | 
				
			||||||
 | 
							tp := true
 | 
				
			||||||
 | 
							for i := range tt.schema.XValidations {
 | 
				
			||||||
 | 
								tt.schema.XValidations[i].OptionalOldSelf = &tp
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run(tt.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								ctx := context.TODO()
 | 
				
			||||||
 | 
								celValidator := validator(&tt.schema, false, model.SchemaDeclType(&tt.schema, false), celconfig.PerCallLimit)
 | 
				
			||||||
 | 
								if celValidator == nil {
 | 
				
			||||||
 | 
									t.Fatal("expected non nil validator")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								t.Run("null old", func(t *testing.T) {
 | 
				
			||||||
 | 
									errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &tt.schema, tt.obj, nil, math.MaxInt)
 | 
				
			||||||
 | 
									if len(errs) != 0 {
 | 
				
			||||||
 | 
										t.Errorf("expected no errors, but got: %v", errs)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								t.Run("non-null old", func(t *testing.T) {
 | 
				
			||||||
 | 
									errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &tt.schema, tt.obj, tt.oldObj, math.MaxInt)
 | 
				
			||||||
 | 
									if len(errs) != 0 {
 | 
				
			||||||
 | 
										t.Errorf("expected no errors, but got: %v", errs)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Show that we cant just use oldSelf as if it was unwrapped
 | 
				
			||||||
 | 
					func TestOptionalOldSelfIsOptionalType(t *testing.T) {
 | 
				
			||||||
 | 
						defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CRDValidationRatcheting, true)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cases := []struct {
 | 
				
			||||||
 | 
							name   string
 | 
				
			||||||
 | 
							schema schema.Structural
 | 
				
			||||||
 | 
							obj    interface{}
 | 
				
			||||||
 | 
							errors []string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "forbid direct usage of optional integer",
 | 
				
			||||||
 | 
								schema: withRule(integerType, `
 | 
				
			||||||
 | 
									oldSelf + self > 5
 | 
				
			||||||
 | 
								`),
 | 
				
			||||||
 | 
								obj:    5,
 | 
				
			||||||
 | 
								errors: []string{"no matching overload for '_+_' applied to '(optional(int), int)"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "forbid direct usage of optional string",
 | 
				
			||||||
 | 
								schema: withRule(stringType, `
 | 
				
			||||||
 | 
									oldSelf == "foo"
 | 
				
			||||||
 | 
								`),
 | 
				
			||||||
 | 
								obj:    "bar",
 | 
				
			||||||
 | 
								errors: []string{"no matching overload for '_==_' applied to '(optional(string), string)"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "forbid direct usage of optional array",
 | 
				
			||||||
 | 
								schema: withRule(arrayType("", nil, &stringSchema), `
 | 
				
			||||||
 | 
									oldSelf.all(x, x == x)
 | 
				
			||||||
 | 
								`),
 | 
				
			||||||
 | 
								obj:    []interface{}{"bar"},
 | 
				
			||||||
 | 
								errors: []string{"expression of type 'optional(list(string))' cannot be range of a comprehension"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "forbid direct usage of optional array element",
 | 
				
			||||||
 | 
								schema: withRule(arrayType("", nil, &stringSchema), `
 | 
				
			||||||
 | 
									oldSelf[0] == "foo"
 | 
				
			||||||
 | 
								`),
 | 
				
			||||||
 | 
								obj:    []interface{}{"bar"},
 | 
				
			||||||
 | 
								errors: []string{"found no matching overload for '_==_' applied to '(optional(string), string)"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "forbid direct usage of optional struct",
 | 
				
			||||||
 | 
								schema: withRule(arrayType("map", []string{"key"}, objectTypePtr(map[string]schema.Structural{
 | 
				
			||||||
 | 
									"key":   stringType,
 | 
				
			||||||
 | 
									"value": stringType,
 | 
				
			||||||
 | 
								})), `oldSelf.key == "foo"`),
 | 
				
			||||||
 | 
								obj: []interface{}{map[string]interface{}{
 | 
				
			||||||
 | 
									"key":   "bar",
 | 
				
			||||||
 | 
									"value": "baz",
 | 
				
			||||||
 | 
								}},
 | 
				
			||||||
 | 
								errors: []string{"does not support field selection"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tt := range cases {
 | 
				
			||||||
 | 
							t.Run(tt.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								ctx := context.TODO()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for i := range tt.schema.XValidations {
 | 
				
			||||||
 | 
									tt.schema.XValidations[i].OptionalOldSelf = ptr.To(true)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								celValidator := validator(&tt.schema, false, model.SchemaDeclType(&tt.schema, false), celconfig.PerCallLimit)
 | 
				
			||||||
 | 
								if celValidator == nil {
 | 
				
			||||||
 | 
									t.Fatal("expected non nil validator")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &tt.schema, tt.obj, tt.obj, math.MaxInt)
 | 
				
			||||||
 | 
								unmatched := map[string]struct{}{}
 | 
				
			||||||
 | 
								for _, e := range tt.errors {
 | 
				
			||||||
 | 
									unmatched[e] = struct{}{}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								for _, err := range errs {
 | 
				
			||||||
 | 
									if err.Type != field.ErrorTypeInvalid {
 | 
				
			||||||
 | 
										t.Errorf("expected only ErrorTypeInvalid errors, but got: %v", err)
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									matched := false
 | 
				
			||||||
 | 
									for expected := range unmatched {
 | 
				
			||||||
 | 
										if strings.Contains(err.Error(), expected) {
 | 
				
			||||||
 | 
											delete(unmatched, expected)
 | 
				
			||||||
 | 
											matched = true
 | 
				
			||||||
 | 
											break
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if !matched {
 | 
				
			||||||
 | 
										t.Errorf("expected error to contain one of %v, but got: %v", unmatched, err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if len(unmatched) > 0 {
 | 
				
			||||||
 | 
									t.Errorf("expected errors %v", unmatched)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func genString(n int, c rune) string {
 | 
					func genString(n int, c rune) string {
 | 
				
			||||||
	b := strings.Builder{}
 | 
						b := strings.Builder{}
 | 
				
			||||||
	for i := 0; i < n; i++ {
 | 
						for i := 0; i < n; i++ {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1340,6 +1340,50 @@ func TestRatchetingFunctionality(t *testing.T) {
 | 
				
			|||||||
						}}},
 | 
											}}},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								Name: "CEL Optional OldSelf",
 | 
				
			||||||
 | 
								Operations: []ratchetingTestOperation{
 | 
				
			||||||
 | 
									updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
 | 
				
			||||||
 | 
										Type: "object",
 | 
				
			||||||
 | 
										Properties: map[string]apiextensionsv1.JSONSchemaProps{
 | 
				
			||||||
 | 
											"field": {
 | 
				
			||||||
 | 
												Type: "string",
 | 
				
			||||||
 | 
												XValidations: []apiextensionsv1.ValidationRule{
 | 
				
			||||||
 | 
													{
 | 
				
			||||||
 | 
														Rule:            "!oldSelf.hasValue()",
 | 
				
			||||||
 | 
														Message:         "oldSelf must be null",
 | 
				
			||||||
 | 
														OptionalOldSelf: ptr(true),
 | 
				
			||||||
 | 
													},
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									}},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									applyPatchOperation{
 | 
				
			||||||
 | 
										"create instance passes since oldself is null",
 | 
				
			||||||
 | 
										myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
 | 
				
			||||||
 | 
											"field": "value",
 | 
				
			||||||
 | 
										}},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									expectError{
 | 
				
			||||||
 | 
										applyPatchOperation{
 | 
				
			||||||
 | 
											"update field fails, since oldself is not null",
 | 
				
			||||||
 | 
											myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
 | 
				
			||||||
 | 
												"field": "value2",
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									expectError{
 | 
				
			||||||
 | 
										applyPatchOperation{
 | 
				
			||||||
 | 
											"noop update field fails, since oldself is not null and transition rules are not ratcheted",
 | 
				
			||||||
 | 
											myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
 | 
				
			||||||
 | 
												"field": "value",
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		// Features that should not ratchet
 | 
							// Features that should not ratchet
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Name: "AllOf_should_not_ratchet",
 | 
								Name: "AllOf_should_not_ratchet",
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user