Skip to content

Commit 1d84211

Browse files
ipaqsaldmonster
andauthored
[feature] add cel validation for module values (#592)
Signed-off-by: Stepan Paksashvili <stepan.paksashvili@flant.com> Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com> Co-authored-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
1 parent 1c098b3 commit 1d84211

File tree

5 files changed

+141
-2
lines changed

5 files changed

+141
-2
lines changed

go.mod

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ require (
1818
github.com/go-openapi/validate v0.19.12
1919
github.com/goccy/go-graphviz v0.2.9
2020
github.com/gofrs/uuid/v5 v5.3.2
21+
github.com/google/cel-go v0.17.8
2122
github.com/hashicorp/go-multierror v1.1.1
2223
github.com/kennygrant/sanitize v1.2.4
2324
github.com/onsi/gomega v1.37.0
2425
github.com/pkg/errors v0.9.1
2526
github.com/stretchr/testify v1.10.0
2627
github.com/tidwall/gjson v1.14.4
28+
google.golang.org/protobuf v1.36.5
2729
gopkg.in/alecthomas/kingpin.v2 v2.2.6
2830
gopkg.in/yaml.v3 v3.0.1
2931
helm.sh/helm/v3 v3.15.4
@@ -50,6 +52,7 @@ require (
5052
github.com/Masterminds/squirrel v1.5.4 // indirect
5153
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
5254
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
55+
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
5356
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
5457
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
5558
github.com/beorn7/perks v1.0.1 // indirect
@@ -153,6 +156,7 @@ require (
153156
github.com/spf13/cast v1.5.0 // indirect
154157
github.com/spf13/cobra v1.8.1 // indirect
155158
github.com/spf13/pflag v1.0.5 // indirect
159+
github.com/stoewer/go-strcase v1.2.0 // indirect
156160
github.com/tetratelabs/wazero v1.8.1 // indirect
157161
github.com/tidwall/match v1.1.1 // indirect
158162
github.com/tidwall/pretty v1.2.0 // indirect
@@ -176,9 +180,9 @@ require (
176180
golang.org/x/term v0.30.0 // indirect
177181
golang.org/x/text v0.23.0 // indirect
178182
golang.org/x/time v0.11.0 // indirect
183+
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect
179184
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
180185
google.golang.org/grpc v1.59.0 // indirect
181-
google.golang.org/protobuf v1.36.5 // indirect
182186
gopkg.in/inf.v0 v0.9.1 // indirect
183187
gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5 // indirect
184188
gopkg.in/yaml.v2 v2.4.0 // indirect

go.sum

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
3636
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
3737
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
3838
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
39+
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18=
40+
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
3941
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
4042
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
4143
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
@@ -276,6 +278,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
276278
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
277279
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
278280
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
281+
github.com/google/cel-go v0.17.8 h1:j9m730pMZt1Fc4oKhCLUHfjj6527LuhYcYw0Rl8gqto=
282+
github.com/google/cel-go v0.17.8/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY=
279283
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
280284
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
281285
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -541,6 +545,8 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k
541545
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
542546
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
543547
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
548+
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
549+
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
544550
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
545551
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
546552
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
@@ -761,7 +767,6 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
761767
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
762768
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
763769
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
764-
google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg=
765770
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY=
766771
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI=
767772
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=

pkg/values/validation/cel/cel.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package cel
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/go-openapi/spec"
9+
"github.com/google/cel-go/cel"
10+
"google.golang.org/protobuf/types/known/structpb"
11+
)
12+
13+
const ruleKey = "x-deckhouse-validations"
14+
15+
type rule struct {
16+
Expression string `json:"expression" yaml:"expression"`
17+
Message string `json:"message" yaml:"message"`
18+
}
19+
20+
// 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)
25+
}
26+
27+
raw, found := schema.Extensions[ruleKey]
28+
if !found {
29+
return nil
30+
}
31+
32+
var rules []rule
33+
switch v := raw.(type) {
34+
case []interface{}:
35+
for _, entry := range v {
36+
mapEntry, ok := entry.(map[string]interface{})
37+
if !ok || len(mapEntry) == 0 {
38+
return fmt.Errorf("x-deckhouse-validations invalid")
39+
}
40+
41+
if val, ok := mapEntry["expression"]; !ok || len(val.(string)) == 0 {
42+
return fmt.Errorf("x-deckhouse-validations invalid: missing expression")
43+
}
44+
if val, ok := mapEntry["message"]; !ok || len(val.(string)) == 0 {
45+
return fmt.Errorf("x-deckhouse-validations invalid: missing message")
46+
}
47+
48+
rules = append(rules, rule{
49+
Expression: mapEntry["expression"].(string),
50+
Message: mapEntry["message"].(string),
51+
})
52+
}
53+
default:
54+
return fmt.Errorf("x-deckhouse-validations invalid")
55+
}
56+
57+
obj, err := structpb.NewStruct(values)
58+
if err != nil {
59+
return fmt.Errorf("convert values to struct: %w", err)
60+
}
61+
62+
for _, r := range rules {
63+
ast, issues := env.Compile(r.Expression)
64+
if issues.Err() != nil {
65+
return fmt.Errorf("compile the '%s' rule: %w", r.Expression, issues.Err())
66+
}
67+
68+
prg, err := env.Program(ast)
69+
if err != nil {
70+
return fmt.Errorf("create program for the '%s' rule: %w", r.Expression, err)
71+
}
72+
73+
out, _, err := prg.Eval(map[string]interface{}{"self": obj})
74+
if err != nil {
75+
if strings.Contains(err.Error(), "no such key:") {
76+
continue
77+
}
78+
return fmt.Errorf("evaluate the '%s' rule: %w", r.Expression, err)
79+
}
80+
81+
pass, ok := out.Value().(bool)
82+
if !ok {
83+
return errors.New("rule should return boolean")
84+
}
85+
if !pass {
86+
return errors.New(r.Message)
87+
}
88+
}
89+
90+
return nil
91+
}

pkg/values/validation/schemas.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"sigs.k8s.io/yaml"
1818

1919
"github.com/flant/addon-operator/pkg/utils"
20+
"github.com/flant/addon-operator/pkg/values/validation/cel"
2021
"github.com/flant/addon-operator/pkg/values/validation/schema"
2122
)
2223

@@ -130,6 +131,13 @@ func validateObject(dataObj interface{}, s *spec.Schema, rootName string) error
130131
return fmt.Errorf("validated data object have to be utils.Values or map[string]interface{}, got %v instead", reflect.TypeOf(v))
131132
}
132133

134+
// Validate values against x-deckhouse-validation rules.
135+
if values, ok := dataObj.(map[string]interface{}); ok {
136+
if err := cel.Validate(s, values); err != nil {
137+
return err
138+
}
139+
}
140+
133141
result := validator.Validate(dataObj)
134142
if result.IsValid() {
135143
return nil

pkg/values/validation/validator_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,34 @@ properties:
196196
mErr := valuesStorage.GetSchemaStorage().ValidateConfigValues("moduleName", moduleValues)
197197
g.Expect(mErr).ShouldNot(HaveOccurred())
198198
}
199+
200+
func Test_ValidateConfigValues_CEL(t *testing.T) {
201+
g := NewWithT(t)
202+
203+
// Prepare module values that violate the CEL rule
204+
values, err := utils.NewValuesFromBytes([]byte(`
205+
moduleName:
206+
replicas: 1
207+
`))
208+
g.Expect(err).ShouldNot(HaveOccurred())
209+
210+
// Schema with a CEL validation: replicas must be > 0
211+
schema := `
212+
type: object
213+
properties:
214+
replicas:
215+
type: integer
216+
x-deckhouse-validations:
217+
- expression: "self.ignoredField == 'Ignored'"
218+
message: "ignore not existing field"
219+
- expression: "self.replicas < 1"
220+
message: "replicas must be greater than 1"
221+
`
222+
223+
vs, err := modules.NewValuesStorage("moduleName", nil, []byte(schema), nil)
224+
g.Expect(err).ShouldNot(HaveOccurred())
225+
226+
err = vs.GetSchemaStorage().ValidateConfigValues("moduleName", values)
227+
g.Expect(err).Should(HaveOccurred(), "expected CEL validation error")
228+
g.Expect(err.Error()).Should(ContainSubstring("replicas must be greater than 1"))
229+
}

0 commit comments

Comments
 (0)