4
4
package openapi
5
5
6
6
import (
7
+ "context"
7
8
"encoding/json"
8
9
"fmt"
9
10
"strings"
10
11
11
12
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
12
13
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
14
+ "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
13
15
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
14
16
structuralpruning "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning"
15
17
"k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
16
18
"k8s.io/apimachinery/pkg/util/validation/field"
19
+ celconfig "k8s.io/apiserver/pkg/apis/cel"
17
20
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
18
21
)
19
22
@@ -26,62 +29,133 @@ func ValidateClusterVariable(
26
29
definition * clusterv1.ClusterClassVariable ,
27
30
fldPath * field.Path ,
28
31
) field.ErrorList {
32
+ validator , apiExtensionsSchema , structuralSchema , err := validatorAndSchemas (fldPath , definition )
33
+ if err != nil {
34
+ return field.ErrorList {err }
35
+ }
36
+
37
+ variableValue , err := unmarshalAndDefaultVariableValue (fldPath , value , structuralSchema )
38
+ if err != nil {
39
+ return field.ErrorList {err }
40
+ }
41
+
42
+ // Validate variable against the schema.
43
+ // NOTE: We're reusing a library func used in CRD validation.
44
+ if err := validation .ValidateCustomResource (fldPath , variableValue , validator ); err != nil {
45
+ return err
46
+ }
47
+
48
+ // Validate variable against the schema using CEL.
49
+ if err := validateCEL (fldPath , variableValue , nil , structuralSchema ); err != nil {
50
+ return err
51
+ }
52
+
53
+ return validateUnknownFields (fldPath , value , variableValue , apiExtensionsSchema )
54
+ }
55
+
56
+ func unmarshalAndDefaultVariableValue (
57
+ fldPath * field.Path ,
58
+ value * clusterv1.ClusterVariable ,
59
+ s * structuralschema.Structural ,
60
+ ) (any , * field.Error ) {
29
61
// Parse JSON value.
30
- var variableValue interface {}
62
+ var variableValue any
31
63
// Only try to unmarshal the clusterVariable if it is not nil, otherwise the variableValue is nil.
32
64
// Note: A clusterVariable with a nil value is the result of setting the variable value to "null" via YAML.
33
65
if value .Value .Raw != nil {
34
66
if err := json .Unmarshal (value .Value .Raw , & variableValue ); err != nil {
35
- return field.ErrorList {field .Invalid (fldPath .Child ("value" ), string (value .Value .Raw ),
36
- fmt .Sprintf ("variable %q could not be parsed: %v" , value .Name , err ))}
67
+ return nil , field .Invalid (
68
+ fldPath .Child ("value" ), string (value .Value .Raw ),
69
+ fmt .Sprintf ("variable %q could not be parsed: %v" , value .Name , err ),
70
+ )
37
71
}
38
72
}
39
73
74
+ defaulting .Default (variableValue , s )
75
+
76
+ return variableValue , nil
77
+ }
78
+
79
+ func validatorAndSchemas (
80
+ fldPath * field.Path , definition * clusterv1.ClusterClassVariable ,
81
+ ) (validation.SchemaValidator , * apiextensions.JSONSchemaProps , * structuralschema.Structural , * field.Error ) {
40
82
// Convert schema to Kubernetes APIExtensions Schema.
41
83
apiExtensionsSchema , allErrs := ConvertJSONSchemaPropsToAPIExtensions (
42
84
& definition .Schema .OpenAPIV3Schema , field .NewPath ("schema" ),
43
85
)
44
86
if len (allErrs ) > 0 {
45
- return field.ErrorList {field .InternalError (fldPath ,
87
+ return nil , nil , nil , field .InternalError (
88
+ fldPath ,
46
89
fmt .Errorf (
47
90
"failed to convert schema definition for variable %q; ClusterClass should be checked: %v" ,
48
91
definition .Name ,
49
92
allErrs ,
50
93
),
51
- )}
94
+ )
52
95
}
53
96
54
97
// Create validator for schema.
55
98
validator , _ , err := validation .NewSchemaValidator (apiExtensionsSchema )
56
99
if err != nil {
57
- return field.ErrorList {field .InternalError (fldPath ,
100
+ return nil , nil , nil , field .InternalError (
101
+ fldPath ,
58
102
fmt .Errorf (
59
103
"failed to create schema validator for variable %q; ClusterClass should be checked: %v" ,
60
- value .Name ,
104
+ definition .Name ,
61
105
err ,
62
106
),
63
- )}
107
+ )
64
108
}
65
109
66
110
s , err := structuralschema .NewStructural (apiExtensionsSchema )
67
111
if err != nil {
68
- return field.ErrorList {field .InternalError (fldPath ,
112
+ return nil , nil , nil , field .InternalError (
113
+ fldPath ,
69
114
fmt .Errorf (
70
115
"failed to create structural schema for variable %q; ClusterClass should be checked: %v" ,
71
- value .Name ,
116
+ definition .Name ,
72
117
err ,
73
118
),
74
- )}
119
+ )
75
120
}
76
- defaulting .Default (variableValue , s )
77
121
78
- // Validate variable against the schema.
79
- // NOTE: We're reusing a library func used in CRD validation.
80
- if err := validation .ValidateCustomResource (fldPath , variableValue , validator ); err != nil {
81
- return err
122
+ return validator , apiExtensionsSchema , s , nil
123
+ }
124
+
125
+ func validateCEL (
126
+ fldPath * field.Path ,
127
+ variableValue , oldVariableValue any ,
128
+ structuralSchema * structuralschema.Structural ,
129
+ ) field.ErrorList {
130
+ // Note: k/k CR validation also uses celconfig.PerCallLimit when creating the validator for a custom resource.
131
+ // The current PerCallLimit gives roughly 0.1 second for each expression validation call.
132
+ celValidator := cel .NewValidator (structuralSchema , false , celconfig .PerCallLimit )
133
+ // celValidation will be nil if there are no CEL validations specified in the schema
134
+ // under `x-kubernetes-validations`.
135
+ if celValidator == nil {
136
+ return nil
82
137
}
83
138
84
- return validateUnknownFields (fldPath , value , variableValue , apiExtensionsSchema )
139
+ // Note: k/k CRD validation also uses celconfig.RuntimeCELCostBudget for the Validate call.
140
+ // The current RuntimeCELCostBudget gives roughly 1 second for the validation of a variable value.
141
+ if validationErrors , _ := celValidator .Validate (
142
+ context .Background (),
143
+ fldPath .Child ("value" ),
144
+ structuralSchema ,
145
+ variableValue ,
146
+ oldVariableValue ,
147
+ celconfig .RuntimeCELCostBudget ,
148
+ ); len (validationErrors ) > 0 {
149
+ var allErrs field.ErrorList
150
+ for _ , validationError := range validationErrors {
151
+ // Set correct value in the field error. ValidateCustomResource sets the type instead of the value.
152
+ validationError .BadValue = variableValue
153
+ allErrs = append (allErrs , validationError )
154
+ }
155
+ return allErrs
156
+ }
157
+
158
+ return nil
85
159
}
86
160
87
161
// validateUnknownFields validates the given variableValue for unknown fields.
@@ -140,3 +214,38 @@ func validateUnknownFields(
140
214
141
215
return nil
142
216
}
217
+
218
+ // ValidateClusterVariable validates an update to a clusterVariable.
219
+ func ValidateClusterVariableUpdate (
220
+ value , oldValue * clusterv1.ClusterVariable ,
221
+ definition * clusterv1.ClusterClassVariable ,
222
+ fldPath * field.Path ,
223
+ ) field.ErrorList {
224
+ validator , apiExtensionsSchema , structuralSchema , err := validatorAndSchemas (fldPath , definition )
225
+ if err != nil {
226
+ return field.ErrorList {err }
227
+ }
228
+
229
+ variableValue , err := unmarshalAndDefaultVariableValue (fldPath , value , structuralSchema )
230
+ if err != nil {
231
+ return field.ErrorList {err }
232
+ }
233
+
234
+ oldVariableValue , err := unmarshalAndDefaultVariableValue (fldPath , oldValue , structuralSchema )
235
+ if err != nil {
236
+ return field.ErrorList {err }
237
+ }
238
+
239
+ // Validate variable against the schema.
240
+ // NOTE: We're reusing a library func used in CRD validation.
241
+ if err := validation .ValidateCustomResourceUpdate (fldPath , variableValue , oldVariableValue , validator ); err != nil {
242
+ return err
243
+ }
244
+
245
+ // Validate variable against the schema using CEL.
246
+ if err := validateCEL (fldPath , variableValue , oldVariableValue , structuralSchema ); err != nil {
247
+ return err
248
+ }
249
+
250
+ return validateUnknownFields (fldPath , value , variableValue , apiExtensionsSchema )
251
+ }
0 commit comments