Skip to content

Commit bba0c9a

Browse files
author
Alexander Zielenski
committed
validate defaults across an update from nil to ensure create ratcheting rules work
1 parent eef1515 commit bba0c9a

File tree

2 files changed

+154
-4
lines changed

2 files changed

+154
-4
lines changed

staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting/validation.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ import (
2626
schemaobjectmeta "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta"
2727
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning"
2828
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
29+
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
2930
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3031
"k8s.io/apimachinery/pkg/runtime"
3132
"k8s.io/apimachinery/pkg/util/validation/field"
3233
celconfig "k8s.io/apiserver/pkg/apis/cel"
34+
utilfeature "k8s.io/apiserver/pkg/util/feature"
3335
)
3436

3537
// ValidateDefaults checks that default values validate and are properly pruned.
@@ -91,8 +93,27 @@ func validate(ctx context.Context, pth *field.Path, s *structuralschema.Structur
9193
allErrs = append(allErrs, errs...)
9294
} else if celValidator := cel.NewValidator(s, isResourceRoot, celconfig.PerCallLimit); celValidator != nil {
9395
celErrs, rmCost := celValidator.Validate(ctx, pth.Child("default"), s, s.Default.Object, s.Default.Object, remainingCost)
94-
remainingCost = rmCost
9596
allErrs = append(allErrs, celErrs...)
97+
98+
if len(celErrs) == 0 && utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CRDValidationRatcheting) {
99+
// If ratcheting is enabled some CEL rules may use optionalOldSelf
100+
// For such rules the above validation is not sufficient for
101+
// determining if the default value is a valid value to introduce
102+
// via create or uncorrelated update.
103+
//
104+
// Validate an update from nil to the default value to ensure
105+
// that the default value pass
106+
celErrs, rmCostWithoutOldObject := celValidator.Validate(ctx, pth.Child("default"), s, s.Default.Object, nil, remainingCost)
107+
allErrs = append(allErrs, celErrs...)
108+
109+
// capture the cost of both types of runs and take whichever
110+
// leaves less remaining cost
111+
if rmCostWithoutOldObject < rmCost {
112+
rmCost = rmCostWithoutOldObject
113+
}
114+
}
115+
116+
remainingCost = rmCost
96117
if remainingCost < 0 {
97118
return allErrs, nil, remainingCost
98119
}
@@ -116,8 +137,27 @@ func validate(ctx context.Context, pth *field.Path, s *structuralschema.Structur
116137
allErrs = append(allErrs, errs...)
117138
} else if celValidator := cel.NewValidator(s, isResourceRoot, celconfig.PerCallLimit); celValidator != nil {
118139
celErrs, rmCost := celValidator.Validate(ctx, pth.Child("default"), s, s.Default.Object, s.Default.Object, remainingCost)
119-
remainingCost = rmCost
120140
allErrs = append(allErrs, celErrs...)
141+
142+
if len(celErrs) == 0 && utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CRDValidationRatcheting) {
143+
// If ratcheting is enabled some CEL rules may use optionalOldSelf
144+
// For such rules the above validation is not sufficient for
145+
// determining if the default value is a valid value to introduce
146+
// via create or uncorrelated update.
147+
//
148+
// Validate an update from nil to the default value to ensure
149+
// that the default value pass
150+
celErrs, rmCostWithoutOldObject := celValidator.Validate(ctx, pth.Child("default"), s, s.Default.Object, nil, remainingCost)
151+
allErrs = append(allErrs, celErrs...)
152+
153+
// capture the cost of both types of runs and take whichever
154+
// leaves less remaining cost
155+
if rmCostWithoutOldObject < rmCost {
156+
rmCost = rmCostWithoutOldObject
157+
}
158+
}
159+
160+
remainingCost = rmCost
121161
if remainingCost < 0 {
122162
return allErrs, nil, remainingCost
123163
}

staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting/validation_test.go

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@ import (
2323

2424
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
2525
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
26+
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
2627
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2728
"k8s.io/apimachinery/pkg/util/validation/field"
29+
utilfeature "k8s.io/apiserver/pkg/util/feature"
30+
"k8s.io/component-base/featuregate"
31+
featuregatetesting "k8s.io/component-base/featuregate/testing"
32+
"k8s.io/utils/ptr"
2833
)
2934

3035
func jsonPtr(x interface{}) *apiextensions.JSON {
@@ -34,8 +39,9 @@ func jsonPtr(x interface{}) *apiextensions.JSON {
3439

3540
func TestDefaultValidationWithCostBudget(t *testing.T) {
3641
tests := []struct {
37-
name string
38-
input apiextensions.CustomResourceValidation
42+
name string
43+
input apiextensions.CustomResourceValidation
44+
features []featuregate.Feature
3945
}{
4046
{
4147
name: "default cel validation",
@@ -98,6 +104,10 @@ func TestDefaultValidationWithCostBudget(t *testing.T) {
98104
for _, tt := range tests {
99105
ctx := context.TODO()
100106
t.Run(tt.name, func(t *testing.T) {
107+
for _, f := range tt.features {
108+
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, f, true)()
109+
}
110+
101111
schema := tt.input.OpenAPIV3Schema
102112
ss, err := structuralschema.NewStructural(schema)
103113
if err != nil {
@@ -148,3 +158,103 @@ func TestDefaultValidationWithCostBudget(t *testing.T) {
148158
})
149159
}
150160
}
161+
162+
func TestDefaultValidationWithOptionalOldSelf(t *testing.T) {
163+
tests := []struct {
164+
name string
165+
input apiextensions.CustomResourceValidation
166+
errors []string
167+
}{
168+
{
169+
name: "invalid default",
170+
input: apiextensions.CustomResourceValidation{
171+
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
172+
Type: "object",
173+
Properties: map[string]apiextensions.JSONSchemaProps{
174+
"defaultFailsRatcheting": {
175+
Type: "string",
176+
Default: jsonPtr("default"),
177+
XValidations: apiextensions.ValidationRules{
178+
{
179+
Rule: "oldSelf.hasValue()",
180+
OptionalOldSelf: ptr.To(true),
181+
Message: "foobarErrorMessage",
182+
},
183+
},
184+
},
185+
},
186+
},
187+
},
188+
errors: []string{"foobarErrorMessage"},
189+
},
190+
{
191+
name: "valid default",
192+
input: apiextensions.CustomResourceValidation{
193+
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
194+
Type: "object",
195+
Properties: map[string]apiextensions.JSONSchemaProps{
196+
"defaultFailsRatcheting": {
197+
Type: "string",
198+
Default: jsonPtr("default"),
199+
XValidations: apiextensions.ValidationRules{
200+
{
201+
Rule: "oldSelf.orValue(self) == self",
202+
OptionalOldSelf: ptr.To(true),
203+
Message: "foobarErrorMessage",
204+
},
205+
},
206+
},
207+
},
208+
},
209+
},
210+
errors: []string{},
211+
},
212+
}
213+
214+
for _, tt := range tests {
215+
ctx := context.TODO()
216+
t.Run(tt.name, func(t *testing.T) {
217+
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CRDValidationRatcheting, true)()
218+
schema := tt.input.OpenAPIV3Schema
219+
ss, err := structuralschema.NewStructural(schema)
220+
if err != nil {
221+
t.Errorf("unexpected error: %v", err)
222+
}
223+
224+
f := NewRootObjectFunc().WithTypeMeta(metav1.TypeMeta{APIVersion: "validation/v1", Kind: "Validation"})
225+
226+
// cost budget is large enough to pass all validation rules
227+
allErrs, err, _ := validate(ctx, field.NewPath("test"), ss, ss, f, false, false, 10)
228+
if err != nil {
229+
t.Errorf("unexpected error: %v", err)
230+
}
231+
232+
for _, err := range allErrs {
233+
found := false
234+
for _, expected := range tt.errors {
235+
if strings.Contains(err.Error(), expected) {
236+
found = true
237+
break
238+
}
239+
}
240+
if !found {
241+
t.Errorf("unexpected error: %v", err)
242+
}
243+
}
244+
245+
for _, expected := range tt.errors {
246+
found := false
247+
for _, err := range allErrs {
248+
if strings.Contains(err.Error(), expected) {
249+
found = true
250+
break
251+
}
252+
}
253+
if !found {
254+
t.Errorf("expected error: %v", expected)
255+
}
256+
}
257+
258+
})
259+
}
260+
}

0 commit comments

Comments
 (0)