mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-09-14 21:53:52 +00:00
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:
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user