1
16
17 package customresource
18
19 import (
20 "context"
21 "fmt"
22 "strings"
23
24 "sigs.k8s.io/structured-merge-diff/v4/fieldpath"
25
26 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
27 v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
28 structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
29 "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
30 "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
31 structurallisttype "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/listtype"
32 schemaobjectmeta "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta"
33 "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
34 apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
35 apiequality "k8s.io/apimachinery/pkg/api/equality"
36 "k8s.io/apimachinery/pkg/api/meta"
37 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
38 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
39 "k8s.io/apimachinery/pkg/fields"
40 "k8s.io/apimachinery/pkg/labels"
41 "k8s.io/apimachinery/pkg/runtime"
42 "k8s.io/apimachinery/pkg/runtime/schema"
43 "k8s.io/apimachinery/pkg/util/sets"
44 "k8s.io/apimachinery/pkg/util/validation/field"
45 celconfig "k8s.io/apiserver/pkg/apis/cel"
46 "k8s.io/apiserver/pkg/cel/common"
47 "k8s.io/apiserver/pkg/features"
48 "k8s.io/apiserver/pkg/registry/generic"
49 apiserverstorage "k8s.io/apiserver/pkg/storage"
50 "k8s.io/apiserver/pkg/storage/names"
51 utilfeature "k8s.io/apiserver/pkg/util/feature"
52 "k8s.io/client-go/util/jsonpath"
53 )
54
55
56
57 type customResourceStrategy struct {
58 runtime.ObjectTyper
59 names.NameGenerator
60
61 namespaceScoped bool
62 validator customResourceValidator
63 structuralSchema *structuralschema.Structural
64 celValidator *cel.Validator
65 status *apiextensions.CustomResourceSubresourceStatus
66 scale *apiextensions.CustomResourceSubresourceScale
67 kind schema.GroupVersionKind
68 selectableFieldSet []selectableField
69 }
70
71 type selectableField struct {
72 name string
73 fieldPath *jsonpath.JSONPath
74 err error
75 }
76
77 func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind, schemaValidator, statusSchemaValidator validation.SchemaValidator, structuralSchema *structuralschema.Structural, status *apiextensions.CustomResourceSubresourceStatus, scale *apiextensions.CustomResourceSubresourceScale, selectableFields []v1.SelectableField) customResourceStrategy {
78 var celValidator *cel.Validator
79 if utilfeature.DefaultFeatureGate.Enabled(features.CustomResourceValidationExpressions) {
80 celValidator = cel.NewValidator(structuralSchema, true, celconfig.PerCallLimit)
81 }
82
83 strategy := customResourceStrategy{
84 ObjectTyper: typer,
85 NameGenerator: names.SimpleNameGenerator,
86 namespaceScoped: namespaceScoped,
87 status: status,
88 scale: scale,
89 validator: customResourceValidator{
90 namespaceScoped: namespaceScoped,
91 kind: kind,
92 schemaValidator: schemaValidator,
93 statusSchemaValidator: statusSchemaValidator,
94 },
95 structuralSchema: structuralSchema,
96 celValidator: celValidator,
97 kind: kind,
98 }
99 if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceFieldSelectors) {
100 strategy.selectableFieldSet = prepareSelectableFields(selectableFields)
101 }
102 return strategy
103 }
104
105 func prepareSelectableFields(selectableFields []v1.SelectableField) []selectableField {
106 result := make([]selectableField, len(selectableFields))
107 for i, sf := range selectableFields {
108 name := strings.TrimPrefix(sf.JSONPath, ".")
109
110 parser := jsonpath.New("selectableField")
111 parser.AllowMissingKeys(true)
112 err := parser.Parse("{" + sf.JSONPath + "}")
113 if err == nil {
114 result[i] = selectableField{
115 name: name,
116 fieldPath: parser,
117 }
118 } else {
119 result[i] = selectableField{
120 name: name,
121 err: err,
122 }
123 }
124 }
125
126 return result
127 }
128
129 func (a customResourceStrategy) NamespaceScoped() bool {
130 return a.namespaceScoped
131 }
132
133
134
135 func (a customResourceStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
136 fields := map[fieldpath.APIVersion]*fieldpath.Set{}
137
138 if a.status != nil {
139 fields[fieldpath.APIVersion(a.kind.GroupVersion().String())] = fieldpath.NewSet(
140 fieldpath.MakePathOrDie("status"),
141 )
142 }
143
144 return fields
145 }
146
147
148 func (a customResourceStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
149 if a.status != nil {
150 customResourceObject := obj.(*unstructured.Unstructured)
151 customResource := customResourceObject.UnstructuredContent()
152
153
154 delete(customResource, "status")
155 }
156
157 accessor, _ := meta.Accessor(obj)
158 accessor.SetGeneration(1)
159 }
160
161
162 func (a customResourceStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
163 newCustomResourceObject := obj.(*unstructured.Unstructured)
164 oldCustomResourceObject := old.(*unstructured.Unstructured)
165
166 newCustomResource := newCustomResourceObject.UnstructuredContent()
167 oldCustomResource := oldCustomResourceObject.UnstructuredContent()
168
169
170 if a.status != nil {
171 _, ok1 := newCustomResource["status"]
172 _, ok2 := oldCustomResource["status"]
173 switch {
174 case ok2:
175 newCustomResource["status"] = oldCustomResource["status"]
176 case ok1:
177 delete(newCustomResource, "status")
178 }
179 }
180
181
182
183 newCopyContent := copyNonMetadata(newCustomResource)
184 oldCopyContent := copyNonMetadata(oldCustomResource)
185 if !apiequality.Semantic.DeepEqual(newCopyContent, oldCopyContent) {
186 oldAccessor, _ := meta.Accessor(oldCustomResourceObject)
187 newAccessor, _ := meta.Accessor(newCustomResourceObject)
188 newAccessor.SetGeneration(oldAccessor.GetGeneration() + 1)
189 }
190 }
191
192 func copyNonMetadata(original map[string]interface{}) map[string]interface{} {
193 ret := make(map[string]interface{})
194 for key, val := range original {
195 if key == "metadata" {
196 continue
197 }
198 ret[key] = val
199 }
200 return ret
201 }
202
203
204 func (a customResourceStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
205 u, ok := obj.(*unstructured.Unstructured)
206 if !ok {
207 return field.ErrorList{field.Invalid(field.NewPath(""), u, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", obj))}
208 }
209
210 var errs field.ErrorList
211 errs = append(errs, a.validator.Validate(ctx, u, a.scale)...)
212
213
214 errs = append(errs, schemaobjectmeta.Validate(nil, u.Object, a.structuralSchema, false)...)
215
216
217 errs = append(errs, structurallisttype.ValidateListSetsAndMaps(nil, a.structuralSchema, u.Object)...)
218
219
220 if celValidator := a.celValidator; celValidator != nil {
221 if has, err := hasBlockingErr(errs); has {
222 errs = append(errs, err)
223 } else {
224 err, _ := celValidator.Validate(ctx, nil, a.structuralSchema, u.Object, nil, celconfig.RuntimeCELCostBudget)
225 errs = append(errs, err...)
226 }
227 }
228
229 return errs
230 }
231
232
233 func (a customResourceStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
234 return generateWarningsFromObj(obj, nil)
235 }
236
237 func generateWarningsFromObj(obj, old runtime.Object) []string {
238 var allWarnings []string
239 fldPath := field.NewPath("metadata", "finalizers")
240 newObjAccessor, err := meta.Accessor(obj)
241 if err != nil {
242 return allWarnings
243 }
244
245 newAdded := sets.NewString(newObjAccessor.GetFinalizers()...)
246 if old != nil {
247 oldObjAccessor, err := meta.Accessor(old)
248 if err != nil {
249 return allWarnings
250 }
251 newAdded = newAdded.Difference(sets.NewString(oldObjAccessor.GetFinalizers()...))
252 }
253
254 for _, finalizer := range newAdded.List() {
255 allWarnings = append(allWarnings, validateKubeFinalizerName(finalizer, fldPath)...)
256 }
257
258 return allWarnings
259 }
260
261
262 func (customResourceStrategy) Canonicalize(obj runtime.Object) {
263 }
264
265
266
267 func (customResourceStrategy) AllowCreateOnUpdate() bool {
268 return false
269 }
270
271
272 func (customResourceStrategy) AllowUnconditionalUpdate() bool {
273 return false
274 }
275
276
277 func (a customResourceStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
278 uNew, ok := obj.(*unstructured.Unstructured)
279 if !ok {
280 return field.ErrorList{field.Invalid(field.NewPath(""), obj, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", obj))}
281 }
282 uOld, ok := old.(*unstructured.Unstructured)
283 if !ok {
284 return field.ErrorList{field.Invalid(field.NewPath(""), old, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", old))}
285 }
286
287 var options []validation.ValidationOption
288 var celOptions []cel.Option
289 var correlatedObject *common.CorrelatedObject
290 if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CRDValidationRatcheting) {
291 correlatedObject = common.NewCorrelatedObject(uNew.Object, uOld.Object, &model.Structural{Structural: a.structuralSchema})
292 options = append(options, validation.WithRatcheting(correlatedObject))
293 celOptions = append(celOptions, cel.WithRatcheting(correlatedObject))
294 }
295
296 var errs field.ErrorList
297 errs = append(errs, a.validator.ValidateUpdate(ctx, uNew, uOld, a.scale, options...)...)
298
299
300 errs = append(errs, schemaobjectmeta.Validate(nil, uNew.Object, a.structuralSchema, false)...)
301
302
303 if oldErrs := structurallisttype.ValidateListSetsAndMaps(nil, a.structuralSchema, uOld.Object); len(oldErrs) == 0 {
304 errs = append(errs, structurallisttype.ValidateListSetsAndMaps(nil, a.structuralSchema, uNew.Object)...)
305 }
306
307
308 if celValidator := a.celValidator; celValidator != nil {
309 if has, err := hasBlockingErr(errs); has {
310 errs = append(errs, err)
311 } else {
312 err, _ := celValidator.Validate(ctx, nil, a.structuralSchema, uNew.Object, uOld.Object, celconfig.RuntimeCELCostBudget, celOptions...)
313 errs = append(errs, err...)
314 }
315 }
316
317
318 if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CRDValidationRatcheting) {
319 validation.Metrics.ObserveRatchetingTime(*correlatedObject.Duration)
320 }
321 return errs
322 }
323
324
325 func (a customResourceStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
326 return generateWarningsFromObj(obj, old)
327 }
328
329
330 func (a customResourceStrategy) GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
331 accessor, err := meta.Accessor(obj)
332 if err != nil {
333 return nil, nil, err
334 }
335 sFields, err := a.selectableFields(obj, accessor)
336 if err != nil {
337 return nil, nil, err
338 }
339 return accessor.GetLabels(), sFields, nil
340 }
341
342
343
344 func (a customResourceStrategy) selectableFields(obj runtime.Object, objectMeta metav1.Object) (fields.Set, error) {
345 objectMetaFields := objectMetaFieldsSet(objectMeta, a.namespaceScoped)
346 var selectableFieldsSet fields.Set
347
348 if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceFieldSelectors) && len(a.selectableFieldSet) > 0 {
349 us, ok := obj.(runtime.Unstructured)
350 if !ok {
351 return nil, fmt.Errorf("unexpected error casting a custom resource to unstructured")
352 }
353 uc := us.UnstructuredContent()
354
355 selectableFieldsSet = fields.Set{}
356 for _, sf := range a.selectableFieldSet {
357 if sf.err != nil {
358 return nil, fmt.Errorf("unexpected error parsing jsonPath: %w", sf.err)
359 }
360 results, err := sf.fieldPath.FindResults(uc)
361 if err != nil {
362 return nil, fmt.Errorf("unexpected error finding value with jsonPath: %w", err)
363 }
364 var value any
365
366 if len(results) > 0 && len(results[0]) > 0 {
367 if len(results) > 1 || len(results[0]) > 1 {
368 return nil, fmt.Errorf("unexpectedly received more than one JSON path result")
369 }
370 value = results[0][0].Interface()
371 }
372
373 if value != nil {
374 selectableFieldsSet[sf.name] = fmt.Sprint(value)
375 } else {
376 selectableFieldsSet[sf.name] = ""
377 }
378 }
379 }
380 return generic.MergeFieldsSets(objectMetaFields, selectableFieldsSet), nil
381 }
382
383
384 func objectMetaFieldsSet(objectMeta metav1.Object, namespaceScoped bool) fields.Set {
385 if namespaceScoped {
386 return fields.Set{
387 "metadata.name": objectMeta.GetName(),
388 "metadata.namespace": objectMeta.GetNamespace(),
389 }
390 }
391 return fields.Set{
392 "metadata.name": objectMeta.GetName(),
393 }
394 }
395
396
397
398
399 func (a customResourceStrategy) MatchCustomResourceDefinitionStorage(label labels.Selector, field fields.Selector) apiserverstorage.SelectionPredicate {
400 return apiserverstorage.SelectionPredicate{
401 Label: label,
402 Field: field,
403 GetAttrs: a.GetAttrs,
404 }
405 }
406
407
408 func hasBlockingErr(errs field.ErrorList) (bool, *field.Error) {
409 for _, err := range errs {
410 if err.Type == field.ErrorTypeNotSupported || err.Type == field.ErrorTypeRequired || err.Type == field.ErrorTypeTooLong || err.Type == field.ErrorTypeTooMany || err.Type == field.ErrorTypeTypeInvalid {
411 return true, field.Invalid(nil, nil, "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation")
412 }
413 }
414 return false, nil
415 }
416
View as plain text