Skip to content

Commit 65b71d1

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

File tree

8 files changed

+239
-14
lines changed

8 files changed

+239
-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: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2025 Undistro Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package utils
16+
17+
import (
18+
"errors"
19+
"fmt"
20+
"reflect"
21+
22+
"github.com/google/cel-go/common/types"
23+
"github.com/google/cel-go/common/types/ref"
24+
"github.com/google/cel-go/common/types/traits"
25+
"google.golang.org/protobuf/types/known/structpb"
26+
)
27+
28+
type conversionTraits interface {
29+
traits.Iterable
30+
traits.Indexer
31+
}
32+
33+
var stringType = reflect.TypeOf("")
34+
35+
func ConvertValToNative(val ref.Val) (any, error) {
36+
valType := val.Type()
37+
switch valType {
38+
case types.ListType:
39+
if iterable, ok := val.(conversionTraits); !ok {
40+
return nil, errors.New("type conversion error from list to iterable")
41+
} else {
42+
values := []any{}
43+
iter := iterable.Iterator()
44+
for iter.HasNext() == types.True {
45+
if value, err := ConvertValToNative(iter.Next()); err != nil {
46+
return nil, err
47+
} else {
48+
values = append(values, value)
49+
}
50+
}
51+
return values, nil
52+
}
53+
case types.MapType:
54+
if iterable, ok := val.(conversionTraits); !ok {
55+
return nil, errors.New("type conversion error from map to iterable")
56+
} else {
57+
values := map[string]any{}
58+
iter := iterable.Iterator()
59+
for iter.HasNext() == types.True {
60+
keyVal := iter.Next()
61+
if key, err := keyVal.ConvertToNative(stringType); err != nil {
62+
return nil, fmt.Errorf("unexpected map key type: %v", keyVal.Type())
63+
} else if value, err := ConvertValToNative(iterable.Get(keyVal)); err != nil {
64+
return nil, err
65+
} else {
66+
values[key.(string)] = value
67+
}
68+
}
69+
return values, nil
70+
}
71+
case types.OptionalType:
72+
opt, ok := val.(*types.Optional)
73+
if !ok {
74+
return nil, errors.New("type conversion error for optional")
75+
} else if !opt.HasValue() {
76+
return nil, nil
77+
}
78+
val = opt.GetValue()
79+
fallthrough
80+
default:
81+
if value, err := val.ConvertToNative(reflect.TypeOf(&structpb.Value{})); err != nil {
82+
return nil, err
83+
} else {
84+
return value, nil
85+
}
86+
}
87+
}

web/assets/main.wasm.gz

263 KB
Binary file not shown.

0 commit comments

Comments
 (0)