Merge pull request #123475 from alexzielenski/apiserver/apiextensions/cel-error-fieldpath

bugfix: incorrect fieldpath when using multiple crd validation rules
This commit is contained in:
Kubernetes Prow Robot
2024-02-27 18:45:57 -08:00
committed by GitHub
2 changed files with 87 additions and 40 deletions

View File

@@ -369,8 +369,9 @@ func (s *Validator) validateExpressions(ctx context.Context, fldPath *field.Path
continue continue
} }
if evalResult != types.True { if evalResult != types.True {
currentFldPath := fldPath
if len(compiled.NormalizedRuleFieldPath) > 0 { if len(compiled.NormalizedRuleFieldPath) > 0 {
fldPath = fldPath.Child(compiled.NormalizedRuleFieldPath) currentFldPath = currentFldPath.Child(compiled.NormalizedRuleFieldPath)
} }
addErr := func(e *field.Error) { addErr := func(e *field.Error) {
@@ -385,22 +386,22 @@ func (s *Validator) validateExpressions(ctx context.Context, fldPath *field.Path
messageExpression, newRemainingBudget, msgErr := evalMessageExpression(ctx, compiled.MessageExpression, rule.MessageExpression, activation, remainingBudget) messageExpression, newRemainingBudget, msgErr := evalMessageExpression(ctx, compiled.MessageExpression, rule.MessageExpression, activation, remainingBudget)
if msgErr != nil { if msgErr != nil {
if msgErr.Type == cel.ErrorTypeInternal { if msgErr.Type == cel.ErrorTypeInternal {
addErr(field.InternalError(fldPath, msgErr)) addErr(field.InternalError(currentFldPath, msgErr))
return errs, -1 return errs, -1
} else if msgErr.Type == cel.ErrorTypeInvalid { } else if msgErr.Type == cel.ErrorTypeInvalid {
addErr(field.Invalid(fldPath, sts.Type, msgErr.Error())) addErr(field.Invalid(currentFldPath, sts.Type, msgErr.Error()))
return errs, -1 return errs, -1
} else { } else {
klog.V(2).ErrorS(msgErr, "messageExpression evaluation failed") klog.V(2).ErrorS(msgErr, "messageExpression evaluation failed")
addErr(fieldErrorForReason(fldPath, sts.Type, ruleMessageOrDefault(rule), rule.Reason)) addErr(fieldErrorForReason(currentFldPath, sts.Type, ruleMessageOrDefault(rule), rule.Reason))
remainingBudget = newRemainingBudget remainingBudget = newRemainingBudget
} }
} else { } else {
addErr(fieldErrorForReason(fldPath, sts.Type, messageExpression, rule.Reason)) addErr(fieldErrorForReason(currentFldPath, sts.Type, messageExpression, rule.Reason))
remainingBudget = newRemainingBudget remainingBudget = newRemainingBudget
} }
} else { } else {
addErr(fieldErrorForReason(fldPath, sts.Type, ruleMessageOrDefault(rule), rule.Reason)) addErr(fieldErrorForReason(currentFldPath, sts.Type, ruleMessageOrDefault(rule), rule.Reason))
} }
} }
} }

View File

@@ -2676,11 +2676,10 @@ func TestReasonAndFldPath(t *testing.T) {
return &r return &r
}() }()
tests := []struct { tests := []struct {
name string name string
schema *schema.Structural schema *schema.Structural
obj interface{} obj interface{}
errors []string errors field.ErrorList
errorType field.ErrorType
}{ }{
{name: "Return error based on input reason", {name: "Return error based on input reason",
obj: map[string]interface{}{ obj: map[string]interface{}{
@@ -2691,8 +2690,12 @@ func TestReasonAndFldPath(t *testing.T) {
schema: withRulePtr(objectTypePtr(map[string]schema.Structural{ schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
"f": withReasonAndFldPath(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2", "", forbiddenReason), "f": withReasonAndFldPath(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2", "", forbiddenReason),
}), "1 == 1"), }), "1 == 1"),
errorType: field.ErrorTypeForbidden, errors: field.ErrorList{
errors: []string{"root.f: Forbidden"}, {
Type: field.ErrorTypeForbidden,
Field: "root.f",
},
},
}, },
{name: "Return error default is invalid", {name: "Return error default is invalid",
obj: map[string]interface{}{ obj: map[string]interface{}{
@@ -2703,8 +2706,12 @@ func TestReasonAndFldPath(t *testing.T) {
schema: withRulePtr(objectTypePtr(map[string]schema.Structural{ schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
"f": withReasonAndFldPath(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2", "", nil), "f": withReasonAndFldPath(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2", "", nil),
}), "1 == 1"), }), "1 == 1"),
errorType: field.ErrorTypeInvalid, errors: field.ErrorList{
errors: []string{"root.f: Invalid"}, {
Type: field.ErrorTypeInvalid,
Field: "root.f",
},
},
}, },
{name: "Return error based on input fieldPath", {name: "Return error based on input fieldPath",
obj: map[string]interface{}{ obj: map[string]interface{}{
@@ -2715,8 +2722,63 @@ func TestReasonAndFldPath(t *testing.T) {
schema: withRulePtr(objectTypePtr(map[string]schema.Structural{ schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
"f": withReasonAndFldPath(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2", ".m", forbiddenReason), "f": withReasonAndFldPath(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2", ".m", forbiddenReason),
}), "1 == 1"), }), "1 == 1"),
errorType: field.ErrorTypeForbidden, errors: field.ErrorList{
errors: []string{"root.f.m: Forbidden"}, {
Type: field.ErrorTypeForbidden,
Field: "root.f.m",
},
},
},
{
name: "multiple rules with custom reason and field path",
obj: map[string]interface{}{
"field1": "value1",
"field2": "value2",
"field3": "value3",
},
schema: &schema.Structural{
Generic: schema.Generic{
Type: "object",
},
Properties: map[string]schema.Structural{
"field1": stringType,
"field2": stringType,
"field3": stringType,
},
Extensions: schema.Extensions{
XValidations: apiextensions.ValidationRules{
{
Rule: `self.field2 != "value2"`,
Reason: ptr.To(apiextensions.FieldValueDuplicate),
FieldPath: ".field2",
},
{
Rule: `self.field3 != "value3"`,
Reason: ptr.To(apiextensions.FieldValueRequired),
FieldPath: ".field3",
},
{
Rule: `self.field1 != "value1"`,
Reason: ptr.To(apiextensions.FieldValueForbidden),
FieldPath: ".field1",
},
},
},
},
errors: field.ErrorList{
{
Type: field.ErrorTypeDuplicate,
Field: "root.field2",
},
{
Type: field.ErrorTypeRequired,
Field: "root.field3",
},
{
Type: field.ErrorTypeForbidden,
Field: "root.field1",
},
},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
@@ -2727,30 +2789,14 @@ func TestReasonAndFldPath(t *testing.T) {
t.Fatal("expected non nil validator") t.Fatal("expected non nil validator")
} }
errs, _ := celValidator.Validate(ctx, field.NewPath("root"), tt.schema, tt.obj, nil, celconfig.RuntimeCELCostBudget) errs, _ := celValidator.Validate(ctx, field.NewPath("root"), tt.schema, tt.obj, nil, celconfig.RuntimeCELCostBudget)
unmatched := map[string]struct{}{}
for _, e := range tt.errors { for i := range errs {
unmatched[e] = struct{}{} // Ignore unchecked fields for this test
} errs[i].Detail = ""
for _, err := range errs { errs[i].BadValue = nil
if err.Type != tt.errorType {
t.Errorf("expected error type %v, but got: %v", tt.errorType, err.Type)
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)
} }
require.ElementsMatch(t, tt.errors, errs)
}) })
} }
} }