Skip to content

Commit f4a38e9

Browse files
committed
fix: address ConvertToNative failures when dereferencing optional.none()
Signed-off-by: Kevin Conner <kev.conner@gmail.com>
1 parent 600f749 commit f4a38e9

File tree

7 files changed

+225
-14
lines changed

7 files changed

+225
-14
lines changed

eval/eval.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,13 @@ package eval
1717
import (
1818
"encoding/json"
1919
"fmt"
20-
"reflect"
2120

2221
"github.com/google/cel-go/cel"
2322
"github.com/google/cel-go/checker"
2423
"github.com/google/cel-go/common/types/ref"
2524
"github.com/google/cel-go/ext"
2625
"github.com/google/cel-go/interpreter"
27-
"google.golang.org/protobuf/types/known/structpb"
26+
"github.com/undistro/cel-playground/utils"
2827
"gopkg.in/yaml.v2"
2928
k8s "k8s.io/apiserver/pkg/cel/library"
3029
)
@@ -120,16 +119,16 @@ func Eval(exp string, input map[string]any) (string, error) {
120119
return string(out), nil
121120
}
122121

123-
func getResults(val *ref.Val) (any, error) {
124-
if value, err := (*val).ConvertToNative(reflect.TypeOf(&structpb.Value{})); err != nil {
122+
func getResults(val ref.Val) (any, error) {
123+
if value, err := utils.ConvertValToNative(val); err != nil {
125124
return nil, err
126125
} else {
127126
return value, nil
128127
}
129128
}
130129

131130
func generateResponse(val ref.Val, costTracker *cel.EvalDetails) (*EvalResponse, error) {
132-
result, evalError := getResults(&val)
131+
result, evalError := getResults(val)
133132
if evalError != nil {
134133
return nil, evalError
135134
}

eval/eval_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ var input = map[string]any{
3232
"abc": []string{"a", "b", "c"},
3333
"memory": "1.3G",
3434
},
35+
"nested": []any{
36+
map[string]any{
37+
"name": "test",
38+
},
39+
},
3540
}
3641

3742
func TestEval(t *testing.T) {
@@ -154,6 +159,20 @@ func TestEval(t *testing.T) {
154159
exp: `sets.intersects([[1], [2, 3]], [[1, 2], [2, 3]])`,
155160
want: true,
156161
},
162+
{
163+
name: "optional list",
164+
exp: `nested.map(m, m.?optional)`,
165+
want: []any{nil},
166+
},
167+
{
168+
name: "optional map",
169+
exp: `nested.map(m, {m.name: m.?optional})`,
170+
want: []any{
171+
map[string]any{
172+
"test": nil,
173+
},
174+
},
175+
},
157176
}
158177
for _, tt := range tests {
159178
t.Run(tt.name, func(t *testing.T) {

k8s/evals.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@ package k8s
1616

1717
import (
1818
"fmt"
19-
"reflect"
2019

2120
"github.com/google/cel-go/cel"
2221
"github.com/google/cel-go/common/types"
2322
"github.com/google/cel-go/common/types/ref"
2423
"github.com/google/cel-go/interpreter"
25-
"google.golang.org/protobuf/types/known/structpb"
24+
"github.com/undistro/cel-playground/utils"
2625
)
2726

2827
type evalResponseError struct {
@@ -124,16 +123,17 @@ type EvalResponse struct {
124123
Cost *uint64 `json:"cost,omitempty"`
125124
}
126125

127-
func getResults(val *ref.Val) (any, *string) {
128-
if val == nil || *val == nil {
126+
func getResults(val ref.Val) (any, *string) {
127+
if val == nil {
129128
return nil, nil
130129
}
131-
value := (*val).Value()
130+
value := val.Value()
132131
if err, ok := value.(error); ok {
133132
errResponse := err.Error()
134133
return nil, &errResponse
135134
}
136-
if value, err := (*val).ConvertToNative(reflect.TypeOf(&structpb.Value{})); err != nil {
135+
136+
if value, err := utils.ConvertValToNative(val); err != nil {
137137
errResponse := err.Error()
138138
return nil, &errResponse
139139
} else {
@@ -152,7 +152,7 @@ func generateEvalVariables(names []string, lazyEvals lazyEvalMap) []*EvalVariabl
152152
variables := []*EvalVariable{}
153153
for _, name := range names {
154154
if varLazyEval, ok := lazyEvals[name]; ok && varLazyEval.val != nil {
155-
value, err := getResults(&varLazyEval.val.val)
155+
value, err := getResults(varLazyEval.val.val)
156156
variables = append(variables, &EvalVariable{
157157
Name: varLazyEval.name,
158158
Value: value,
@@ -168,10 +168,10 @@ func generateEvalVariables(names []string, lazyEvals lazyEvalMap) []*EvalVariabl
168168
func generateEvalResults(responses evalResponses) []*EvalResult {
169169
evals := []*EvalResult{}
170170
for _, eval := range responses {
171-
value, err := getResults(&eval.val)
171+
value, err := getResults(eval.val)
172172
var message any
173173
if eval.messageVal != nil {
174-
message, _ = getResults(&eval.messageVal)
174+
message, _ = getResults(eval.messageVal)
175175
} else if eval.message != "" {
176176
message = eval.message
177177
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
apiVersion: admissionregistration.k8s.io/v1
2+
kind: ValidatingAdmissionPolicy
3+
metadata:
4+
name: "pod-security.policy.example.com"
5+
spec:
6+
failurePolicy: Fail
7+
matchConstraints:
8+
resourceRules:
9+
- apiGroups: ["apps"]
10+
apiVersions: ["v1"]
11+
operations: ["CREATE", "UPDATE"]
12+
resources: ["deployments"]
13+
variables:
14+
- name: containers
15+
expression: object.spec.template.spec.containers
16+
- name: securityContexts
17+
expression: 'variables.containers.map(c, c.?securityContext)'
18+
- name: namedSecurityContexts
19+
expression: 'variables.containers.map(c, {c.name: c.?securityContext})'
20+
validations:
21+
- expression: variables.securityContexts.all(c, c.?runAsNonRoot == optional.of(true))
22+
message: 'all containers must set runAsNonRoot to true'
23+
- expression: variables.securityContexts.all(c, c.?readOnlyRootFilesystem == optional.of(true))
24+
message: 'all containers must set readOnlyRootFilesystem to true'
25+
- expression: variables.securityContexts.all(c, c.?allowPrivilegeEscalation != optional.of(true))
26+
message: 'all containers must NOT set allowPrivilegeEscalation to true'
27+
- expression: variables.securityContexts.all(c, c.?privileged != optional.of(true))
28+
message: 'all containers must NOT set privileged to true'
29+
- expression: variables.namedSecurityContexts.all(c, c.?securityContext.privileged != optional.of(true))
30+
message: 'all named containers must NOT set privileged to true'
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
labels:
5+
app: kubernetes-bootcamp
6+
name: kubernetes-bootcamp
7+
namespace: default
8+
spec:
9+
progressDeadlineSeconds: 600
10+
replicas: 3
11+
revisionHistoryLimit: 10
12+
selector:
13+
matchLabels:
14+
app: kubernetes-bootcamp
15+
strategy:
16+
rollingUpdate:
17+
maxSurge: 25%
18+
maxUnavailable: 25%
19+
type: RollingUpdate
20+
template:
21+
metadata:
22+
creationTimestamp: null
23+
labels:
24+
app: kubernetes-bootcamp
25+
spec:
26+
containers:
27+
- image: gcr.io/google-samples/kubernetes-bootcamp:v1
28+
imagePullPolicy: IfNotPresent
29+
name: kubernetes-bootcamp
30+
resources: {}
31+
terminationMessagePath: /dev/termination-log
32+
terminationMessagePolicy: File
33+
dnsPolicy: ClusterFirst
34+
restartPolicy: Always
35+
schedulerName: default-scheduler
36+
securityContext: {}
37+
terminationGracePeriodSeconds: 30

k8s/validatingadmissionpolicy_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,59 @@ func TestValidationEval(t *testing.T) {
326326
}},
327327
Cost: uint64ptr(8),
328328
},
329+
}, {
330+
name: "test optional.none() dereference",
331+
policy: "optional_none_dereference policy.yaml",
332+
orig: "",
333+
updated: "optional_none_dereference updated.yaml",
334+
335+
expected: k8s.EvalResponse{
336+
ValidationVariables: []*k8s.EvalVariable{{
337+
Name: "containers",
338+
Value: []any{
339+
map[string]any{
340+
"image": "gcr.io/google-samples/kubernetes-bootcamp:v1",
341+
"imagePullPolicy": "IfNotPresent",
342+
"name": "kubernetes-bootcamp",
343+
"resources": map[string]any{},
344+
"terminationMessagePath": "/dev/termination-log",
345+
"terminationMessagePolicy": "File",
346+
},
347+
},
348+
Cost: uint64ptr(5),
349+
}, {
350+
Name: "securityContexts",
351+
Value: []any{nil},
352+
Cost: uint64ptr(15),
353+
}, {
354+
Name: "namedSecurityContexts",
355+
Value: []any{
356+
map[string]any{
357+
"kubernetes-bootcamp": nil,
358+
},
359+
},
360+
Cost: uint64ptr(47),
361+
}},
362+
Validations: []*k8s.EvalResult{{
363+
Result: false,
364+
Message: "all containers must set runAsNonRoot to true",
365+
Cost: uint64ptr(8),
366+
}, {
367+
Result: false,
368+
Message: "all containers must set readOnlyRootFilesystem to true",
369+
Cost: uint64ptr(8),
370+
}, {
371+
Result: true,
372+
Cost: uint64ptr(8),
373+
}, {
374+
Result: true,
375+
Cost: uint64ptr(8),
376+
}, {
377+
Result: true,
378+
Cost: uint64ptr(8),
379+
}},
380+
Cost: uint64ptr(107),
381+
},
329382
}}
330383
for _, tt := range tests {
331384
t.Run(tt.name, func(t *testing.T) {

utils/conversion.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package utils
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"reflect"
7+
8+
"github.com/google/cel-go/common/types"
9+
"github.com/google/cel-go/common/types/ref"
10+
"github.com/google/cel-go/common/types/traits"
11+
"google.golang.org/protobuf/types/known/structpb"
12+
)
13+
14+
type conversionTraits interface {
15+
traits.Iterable
16+
traits.Indexer
17+
}
18+
19+
var stringType = reflect.TypeOf("")
20+
21+
func ConvertValToNative(val ref.Val) (any, error) {
22+
valType := val.Type()
23+
switch valType {
24+
case types.ListType:
25+
if iterable, ok := val.(conversionTraits); !ok {
26+
return nil, errors.New("type conversion error from list to iterable")
27+
} else {
28+
values := []any{}
29+
iter := iterable.Iterator()
30+
for iter.HasNext() == types.True {
31+
if value, err := ConvertValToNative(iter.Next()); err != nil {
32+
return nil, err
33+
} else {
34+
values = append(values, value)
35+
}
36+
}
37+
return values, nil
38+
}
39+
case types.MapType:
40+
if iterable, ok := val.(conversionTraits); !ok {
41+
return nil, errors.New("type conversion error from map to iterable")
42+
} else {
43+
values := map[string]any{}
44+
iter := iterable.Iterator()
45+
for iter.HasNext() == types.True {
46+
keyVal := iter.Next()
47+
if key, err := keyVal.ConvertToNative(stringType); err != nil {
48+
return nil, fmt.Errorf("unexpected map key type: %v", keyVal.Type())
49+
} else if value, err := ConvertValToNative(iterable.Get(keyVal)); err != nil {
50+
return nil, err
51+
} else {
52+
values[key.(string)] = value
53+
}
54+
}
55+
return values, nil
56+
}
57+
case types.OptionalType:
58+
opt, ok := val.(*types.Optional)
59+
if !ok {
60+
return nil, errors.New("type conversion error for optional")
61+
} else if !opt.HasValue() {
62+
return nil, nil
63+
}
64+
val = opt.GetValue()
65+
fallthrough
66+
default:
67+
if value, err := val.ConvertToNative(reflect.TypeOf(&structpb.Value{})); err != nil {
68+
return nil, err
69+
} else {
70+
return value, nil
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)