Skip to content

Commit c174978

Browse files
fix: add recursive check of cel rules (#630)
Signed-off-by: Timur Tuktamyshev <timur.tuktamyshev@flant.com> Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com> Co-authored-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
1 parent 02cf552 commit c174978

File tree

5 files changed

+318
-39
lines changed

5 files changed

+318
-39
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ require (
99
github.com/dominikbraun/graph v0.23.0
1010
github.com/ettle/strcase v0.2.0
1111
github.com/flant/kube-client v1.3.1
12-
github.com/flant/shell-operator v1.9.0
12+
github.com/flant/shell-operator v1.9.1
1313
github.com/go-chi/chi/v5 v5.2.1
1414
github.com/go-openapi/loads v0.19.5
1515
github.com/go-openapi/spec v0.19.8

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ github.com/flant/go-openapi-validate v0.19.12-flant.1 h1:GuB9XEfiLHq3M7fafRLq1AW
144144
github.com/flant/go-openapi-validate v0.19.12-flant.1/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0waH08tGe6kAQ4=
145145
github.com/flant/kube-client v1.3.1 h1:1SdD799sujXNg2F6Z27le/+qkcKQaKf9Z492YGEhVhc=
146146
github.com/flant/kube-client v1.3.1/go.mod h1:mql6hsZMgBLAhdj3Emb8TrP5MVdXduFQ2NLjzn6IF0Y=
147-
github.com/flant/shell-operator v1.9.0 h1:XaNif0v0NK2S0AzSfujYO6d/ifHsw6nmPylGmuBlPaU=
148-
github.com/flant/shell-operator v1.9.0/go.mod h1:9gZmjxCuyLDz3hFsmRXYTlw7+hpw199CMYzSodWKk50=
147+
github.com/flant/shell-operator v1.9.1 h1:P4f8kyxSK+TRUS7EYLcY9+IrCvvF0e8MBRPA7hPdCnQ=
148+
github.com/flant/shell-operator v1.9.1/go.mod h1:9gZmjxCuyLDz3hFsmRXYTlw7+hpw199CMYzSodWKk50=
149149
github.com/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6mU=
150150
github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw=
151151
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=

pkg/values/validation/cel/cel.go

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,43 @@ type rule struct {
1818
}
1919

2020
// Validate validates config values against x-deckhouse-validation rules in schema
21-
func Validate(schema *spec.Schema, values map[string]interface{}) error {
22-
env, err := cel.NewEnv(cel.Variable("self", cel.MapType(cel.StringType, cel.DynType)))
23-
if err != nil {
24-
return fmt.Errorf("create CEL env: %w", err)
21+
func Validate(schema *spec.Schema, values any) ([]error, error) {
22+
var validationErrs []error
23+
// First we validate only object nested properties
24+
if m, ok := values.(map[string]any); ok {
25+
for propName, propSchema := range schema.Properties {
26+
if propValue, ok := m[propName]; ok {
27+
subErrs, err := Validate(&propSchema, propValue)
28+
if err != nil {
29+
return nil, err
30+
}
31+
validationErrs = append(validationErrs, subErrs...)
32+
}
33+
}
2534
}
2635

2736
raw, found := schema.Extensions[ruleKey]
2837
if !found {
29-
return nil
38+
if len(validationErrs) > 0 {
39+
return validationErrs, nil
40+
}
41+
return nil, nil
3042
}
3143

3244
var rules []rule
3345
switch v := raw.(type) {
34-
case []interface{}:
46+
case []any:
3547
for _, entry := range v {
36-
mapEntry, ok := entry.(map[string]interface{})
48+
mapEntry, ok := entry.(map[string]any)
3749
if !ok || len(mapEntry) == 0 {
38-
return fmt.Errorf("x-deckhouse-validations invalid")
50+
return nil, fmt.Errorf("x-deckhouse-validations invalid")
3951
}
4052

4153
if val, ok := mapEntry["expression"]; !ok || len(val.(string)) == 0 {
42-
return fmt.Errorf("x-deckhouse-validations invalid: missing expression")
54+
return nil, fmt.Errorf("x-deckhouse-validations invalid: missing expression")
4355
}
4456
if val, ok := mapEntry["message"]; !ok || len(val.(string)) == 0 {
45-
return fmt.Errorf("x-deckhouse-validations invalid: missing message")
57+
return nil, fmt.Errorf("x-deckhouse-validations invalid: missing message")
4658
}
4759

4860
rules = append(rules, rule{
@@ -51,41 +63,68 @@ func Validate(schema *spec.Schema, values map[string]interface{}) error {
5163
})
5264
}
5365
default:
54-
return fmt.Errorf("x-deckhouse-validations invalid")
66+
return nil, fmt.Errorf("x-deckhouse-validations invalid")
5567
}
5668

57-
obj, err := structpb.NewStruct(values)
69+
celSelfType, celSelfValue, err := buildCELValueAndType(values)
5870
if err != nil {
59-
return fmt.Errorf("convert values to struct: %w", err)
71+
return nil, err
6072
}
6173

74+
env, err := cel.NewEnv(cel.Variable("self", celSelfType))
75+
if err != nil {
76+
return nil, fmt.Errorf("create CEL env: %w", err)
77+
}
6278
for _, r := range rules {
6379
ast, issues := env.Compile(r.Expression)
6480
if issues.Err() != nil {
65-
return fmt.Errorf("compile the '%s' rule: %w", r.Expression, issues.Err())
81+
return nil, fmt.Errorf("compile the '%s' rule: %w", r.Expression, issues.Err())
6682
}
6783

6884
prg, err := env.Program(ast)
6985
if err != nil {
70-
return fmt.Errorf("create program for the '%s' rule: %w", r.Expression, err)
86+
return nil, fmt.Errorf("create program for the '%s' rule: %w", r.Expression, err)
7187
}
7288

73-
out, _, err := prg.Eval(map[string]interface{}{"self": obj})
89+
out, _, err := prg.Eval(map[string]any{"self": celSelfValue})
7490
if err != nil {
7591
if strings.Contains(err.Error(), "no such key:") {
7692
continue
7793
}
78-
return fmt.Errorf("evaluate the '%s' rule: %w", r.Expression, err)
94+
return nil, fmt.Errorf("evaluate the '%s' rule: %w", r.Expression, err)
7995
}
8096

8197
pass, ok := out.Value().(bool)
8298
if !ok {
83-
return errors.New("rule should return boolean")
99+
return nil, errors.New("rule should return boolean")
84100
}
85101
if !pass {
86-
return errors.New(r.Message)
102+
validationErrs = append(validationErrs, errors.New(r.Message))
87103
}
88104
}
89105

90-
return nil
106+
return validationErrs, nil
107+
}
108+
109+
func buildCELValueAndType(value any) (*cel.Type, any, error) {
110+
switch v := value.(type) {
111+
case map[string]any:
112+
obj, err := structpb.NewStruct(v)
113+
if err != nil {
114+
return nil, nil, fmt.Errorf("convert values to struct: %w", err)
115+
}
116+
return cel.MapType(cel.StringType, cel.DynType), obj, nil
117+
case []any:
118+
list, err := structpb.NewList(v)
119+
if err != nil {
120+
return nil, nil, fmt.Errorf("convert array to list: %w", err)
121+
}
122+
return cel.ListType(cel.DynType), list, nil
123+
default:
124+
val, err := structpb.NewValue(v)
125+
if err != nil {
126+
return nil, nil, fmt.Errorf("convert dyn to value: %w", err)
127+
}
128+
return cel.DynType, val, nil
129+
}
91130
}

pkg/values/validation/schemas.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package validation
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
67
"log/slog"
78
"reflect"
@@ -133,9 +134,13 @@ func validateObject(dataObj interface{}, s *spec.Schema, rootName string) error
133134

134135
// Validate values against x-deckhouse-validation rules.
135136
if values, ok := dataObj.(map[string]interface{}); ok {
136-
if err := cel.Validate(s, values); err != nil {
137+
validationErrs, err := cel.Validate(s, values)
138+
if err != nil {
137139
return err
138140
}
141+
if len(validationErrs) > 0 {
142+
return errors.Join(validationErrs...)
143+
}
139144
}
140145

141146
result := validator.Validate(dataObj)

0 commit comments

Comments
 (0)