44package openapi
55
66import (
7+ "context"
78 "encoding/json"
89 "fmt"
910 "strings"
1011
1112 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
1213 structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
14+ "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
1315 "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
1416 structuralpruning "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning"
1517 "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
1618 "k8s.io/apimachinery/pkg/util/validation/field"
19+ celconfig "k8s.io/apiserver/pkg/apis/cel"
1720 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
1821)
1922
@@ -26,62 +29,133 @@ func ValidateClusterVariable(
2629 definition * clusterv1.ClusterClassVariable ,
2730 fldPath * field.Path ,
2831) 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 ) {
2961 // Parse JSON value.
30- var variableValue interface {}
62+ var variableValue any
3163 // Only try to unmarshal the clusterVariable if it is not nil, otherwise the variableValue is nil.
3264 // Note: A clusterVariable with a nil value is the result of setting the variable value to "null" via YAML.
3365 if value .Value .Raw != nil {
3466 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+ )
3771 }
3872 }
3973
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 ) {
4082 // Convert schema to Kubernetes APIExtensions Schema.
4183 apiExtensionsSchema , allErrs := ConvertJSONSchemaPropsToAPIExtensions (
4284 & definition .Schema .OpenAPIV3Schema , field .NewPath ("schema" ),
4385 )
4486 if len (allErrs ) > 0 {
45- return field.ErrorList {field .InternalError (fldPath ,
87+ return nil , nil , nil , field .InternalError (
88+ fldPath ,
4689 fmt .Errorf (
4790 "failed to convert schema definition for variable %q; ClusterClass should be checked: %v" ,
4891 definition .Name ,
4992 allErrs ,
5093 ),
51- )}
94+ )
5295 }
5396
5497 // Create validator for schema.
5598 validator , _ , err := validation .NewSchemaValidator (apiExtensionsSchema )
5699 if err != nil {
57- return field.ErrorList {field .InternalError (fldPath ,
100+ return nil , nil , nil , field .InternalError (
101+ fldPath ,
58102 fmt .Errorf (
59103 "failed to create schema validator for variable %q; ClusterClass should be checked: %v" ,
60- value .Name ,
104+ definition .Name ,
61105 err ,
62106 ),
63- )}
107+ )
64108 }
65109
66110 s , err := structuralschema .NewStructural (apiExtensionsSchema )
67111 if err != nil {
68- return field.ErrorList {field .InternalError (fldPath ,
112+ return nil , nil , nil , field .InternalError (
113+ fldPath ,
69114 fmt .Errorf (
70115 "failed to create structural schema for variable %q; ClusterClass should be checked: %v" ,
71- value .Name ,
116+ definition .Name ,
72117 err ,
73118 ),
74- )}
119+ )
75120 }
76- defaulting .Default (variableValue , s )
77121
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
82137 }
83138
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
85159}
86160
87161// validateUnknownFields validates the given variableValue for unknown fields.
@@ -140,3 +214,38 @@ func validateUnknownFields(
140214
141215 return nil
142216}
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