Skip to content

Commit 50fc400

Browse files
authored
Merge pull request kubernetes#129028 from sttts/sttts-cel-test
apiextensions: add pkg/test with CEL unit test helpers
2 parents 0caa36c + 0c1b1e0 commit 50fc400

File tree

4 files changed

+838
-0
lines changed

4 files changed

+838
-0
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package test
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"os"
23+
"testing"
24+
25+
"github.com/stretchr/testify/require"
26+
27+
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
28+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
29+
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
30+
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
31+
"k8s.io/apimachinery/pkg/util/validation/field"
32+
"k8s.io/apimachinery/pkg/util/yaml"
33+
celconfig "k8s.io/apiserver/pkg/apis/cel"
34+
)
35+
36+
// MustLoadManifest loads a CRD from a file and panics on error.
37+
func MustLoadManifest[T any](t *testing.T, pth string) *T {
38+
data, err := os.ReadFile(pth)
39+
require.NoError(t, err)
40+
41+
var crd T
42+
err = yaml.Unmarshal(data, &crd)
43+
require.NoError(t, err)
44+
45+
return &crd
46+
}
47+
48+
// FieldValidators extracts the CEL validators by version and JSONPath from a CRD and returns
49+
// a validator func for testing against samples.
50+
func FieldValidators(t *testing.T, crd *apiextensionsv1.CustomResourceDefinition) (validatorsByVersionByJSONPath map[string]map[string]CELValidateFunc) {
51+
ret := map[string]map[string]CELValidateFunc{}
52+
for _, v := range crd.Spec.Versions {
53+
var internalSchema apiextensions.JSONSchemaProps
54+
err := apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(v.Schema.OpenAPIV3Schema, &internalSchema, nil)
55+
require.NoError(t, err, "failed to convert JSONSchemaProps for version %s: %v", v.Name, err)
56+
structuralSchema, err := schema.NewStructural(&internalSchema)
57+
require.NoError(t, err, "failed to create StructuralSchema for version %s: %v", v.Name, err)
58+
59+
versionVals, err := findCEL(t, structuralSchema, true, field.NewPath("openAPIV3Schema"))
60+
require.NoError(t, err, "failed to find CEL for version %s: %v", v.Name, err)
61+
ret[v.Name] = versionVals
62+
}
63+
64+
return ret
65+
}
66+
67+
// VersionValidatorsFromFile extracts the CEL validators by version from a CRD file and returns
68+
// a validator func for testing against samples.
69+
func VersionValidatorsFromFile(t *testing.T, crdFilePath string) map[string]CELValidateFunc {
70+
data, err := os.ReadFile(crdFilePath)
71+
require.NoError(t, err)
72+
73+
var crd apiextensionsv1.CustomResourceDefinition
74+
err = yaml.Unmarshal(data, &crd)
75+
require.NoError(t, err)
76+
77+
ret := map[string]CELValidateFunc{}
78+
for _, v := range crd.Spec.Versions {
79+
var internalSchema apiextensions.JSONSchemaProps
80+
err := apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(v.Schema.OpenAPIV3Schema, &internalSchema, nil)
81+
require.NoError(t, err, "failed to convert JSONSchemaProps for version %s: %v", v.Name, err)
82+
structuralSchema, err := schema.NewStructural(&internalSchema)
83+
require.NoError(t, err, "failed to create StructuralSchema for version %s: %v", v.Name, err)
84+
ret[v.Name] = func(obj, old interface{}) field.ErrorList {
85+
errs, _ := cel.NewValidator(structuralSchema, true, celconfig.RuntimeCELCostBudget).Validate(context.TODO(), nil, structuralSchema, obj, old, celconfig.PerCallLimit)
86+
return errs
87+
}
88+
}
89+
90+
return ret
91+
}
92+
93+
// VersionValidatorFromFile extracts the CEL validators for a given version from a CRD file and returns
94+
// a validator func for testing against samples.
95+
func VersionValidatorFromFile(t *testing.T, crdFilePath string, version string) (CELValidateFunc, error) {
96+
vals := VersionValidatorsFromFile(t, crdFilePath)
97+
if val, ok := vals[version]; ok {
98+
return val, nil
99+
}
100+
return nil, fmt.Errorf("version %s not found", version)
101+
}
102+
103+
// CELValidateFunc tests a sample object against a CEL validator.
104+
type CELValidateFunc func(obj, old interface{}) field.ErrorList
105+
106+
func findCEL(t *testing.T, s *schema.Structural, root bool, pth *field.Path) (map[string]CELValidateFunc, error) {
107+
ret := map[string]CELValidateFunc{}
108+
109+
if len(s.XValidations) > 0 {
110+
s := *s
111+
pth := *pth
112+
ret[pth.String()] = func(obj, old interface{}) field.ErrorList {
113+
errs, _ := cel.NewValidator(&s, root, celconfig.RuntimeCELCostBudget).Validate(context.TODO(), &pth, &s, obj, old, celconfig.PerCallLimit)
114+
return errs
115+
}
116+
}
117+
118+
for k, v := range s.Properties {
119+
v := v
120+
sub, err := findCEL(t, &v, false, pth.Child("properties").Child(k))
121+
if err != nil {
122+
return nil, err
123+
}
124+
125+
for pth, val := range sub {
126+
ret[pth] = val
127+
}
128+
}
129+
if s.Items != nil {
130+
sub, err := findCEL(t, s.Items, false, pth.Child("items"))
131+
if err != nil {
132+
return nil, err
133+
}
134+
for pth, val := range sub {
135+
ret[pth] = val
136+
}
137+
}
138+
if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil {
139+
sub, err := findCEL(t, s.AdditionalProperties.Structural, false, pth.Child("additionalProperties"))
140+
if err != nil {
141+
return nil, err
142+
}
143+
for pth, val := range sub {
144+
ret[pth] = val
145+
}
146+
}
147+
148+
return ret, nil
149+
}

0 commit comments

Comments
 (0)