@@ -18,9 +18,12 @@ package markers
18
18
19
19
import (
20
20
"fmt"
21
+ "path/filepath"
22
+ "regexp"
21
23
"strings"
22
24
23
25
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
26
+ "k8s.io/utils/ptr"
24
27
25
28
"sigs.k8s.io/controller-tools/pkg/markers"
26
29
)
@@ -58,6 +61,9 @@ var CRDMarkers = []*definitionWithHelp{
58
61
59
62
must (markers .MakeDefinition ("kubebuilder:selectablefield" , markers .DescribesType , SelectableField {})).
60
63
WithHelp (SelectableField {}.Help ()),
64
+
65
+ must (markers .MakeDefinition ("kubebuilder:schemaModifier" , markers .DescribesType , SchemaModifier {})).
66
+ WithHelp (SchemaModifier {}.Help ()),
61
67
}
62
68
63
69
// TODO: categories and singular used to be annotations types
@@ -420,3 +426,189 @@ func (s SelectableField) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, ve
420
426
421
427
return nil
422
428
}
429
+
430
+ // +controllertools:marker:generateHelp:category=CRD
431
+
432
+ // SchemaModifier allows modifying JSONSchemaProps for CRDs.
433
+ //
434
+ // The PathPattern field defines the rule for selecting target fields within the CRD structure.
435
+ // This rule is specified as a path in a JSONPath-like format and supports special wildcard characters:
436
+ // - `*`: matches any single field name (e.g., `/spec/*/field`).
437
+ // - `**`: matches fields at any depth, across multiple levels of nesting (e.g., `/spec/**/field`).
438
+ //
439
+ // Example:
440
+ // +kubebuilder:schemaModifier:pathPattern=/spec/exampleField/*,description=""
441
+ //
442
+ // In this example, all fields matching the path `/spec/exampleField/*` will have the empty description applied.
443
+ //
444
+ // Any specified values (e.g., Description, Format, Maximum, etc.) will be applied to all schemas matching the given path.
445
+ type SchemaModifier struct {
446
+ // PathPattern defines the path for selecting JSON schemas.
447
+ // Supports `*` and `**` for matching nested fields.
448
+ PathPattern string `marker:"pathPattern"`
449
+
450
+ // Description sets a new value for JSONSchemaProps.Description.
451
+ Description * string `marker:",optional"`
452
+ // Format sets a new value for JSONSchemaProps.Format.
453
+ Format * string `marker:",optional"`
454
+ // Maximum sets a new value for JSONSchemaProps.Maximum.
455
+ Maximum * float64 `marker:",optional"`
456
+ // ExclusiveMaximum sets a new value for JSONSchemaProps.ExclusiveMaximum.
457
+ ExclusiveMaximum * bool `marker:",optional"`
458
+ // Minimum sets a new value for JSONSchemaProps.Minimum.
459
+ Minimum * float64 `marker:",optional"`
460
+ // ExclusiveMinimum sets a new value for JSONSchemaProps.ExclusiveMinimum.
461
+ ExclusiveMinimum * bool `marker:",optional"`
462
+ // MaxLength sets a new value for JSONSchemaProps.MaxLength.
463
+ MaxLength * int `marker:",optional"`
464
+ // MinLength sets a new value for JSONSchemaProps.MinLength.
465
+ MinLength * int `marker:",optional"`
466
+ // Pattern sets a new value for JSONSchemaProps.Pattern.
467
+ Pattern * string `marker:",optional"`
468
+ // MaxItems sets a new value for JSONSchemaProps.MaxItems.
469
+ MaxItems * int `marker:",optional"`
470
+ // MinItems sets a new value for JSONSchemaProps.MinItems.
471
+ MinItems * int `marker:",optional"`
472
+ // UniqueItems sets a new value for JSONSchemaProps.UniqueItems.
473
+ UniqueItems * bool `marker:",optional"`
474
+ // MultipleOf sets a new value for JSONSchemaProps.MultipleOf.
475
+ MultipleOf * float64 `marker:",optional"`
476
+ // MaxProperties sets a new value for JSONSchemaProps.MaxProperties.
477
+ MaxProperties * int `marker:",optional"`
478
+ // MinProperties sets a new value for JSONSchemaProps.MinProperties.
479
+ MinProperties * int `marker:",optional"`
480
+ // Required sets a new value for JSONSchemaProps.Required.
481
+ Required * []string `marker:",optional"`
482
+ // Nullable sets a new value for JSONSchemaProps.Nullable.
483
+ Nullable * bool `marker:",optional"`
484
+ }
485
+
486
+ func (s SchemaModifier ) ApplyToCRD (crd * apiext.CustomResourceDefinitionSpec , _ string ) error {
487
+ ruleRegex , err := s .parsePattern ()
488
+ if err != nil {
489
+ return fmt .Errorf ("failed to parse rule: %w" , err )
490
+ }
491
+
492
+ for i := range crd .Versions {
493
+ ver := & crd .Versions [i ]
494
+ if err = s .applyRuleToSchema (ver .Schema .OpenAPIV3Schema , ruleRegex , "/" ); err != nil {
495
+ return err
496
+ }
497
+ }
498
+ return nil
499
+ }
500
+
501
+ func (s SchemaModifier ) applyRuleToSchema (schema * apiext.JSONSchemaProps , ruleRegex * regexp.Regexp , path string ) error {
502
+ if schema == nil {
503
+ return nil
504
+ }
505
+
506
+ if ruleRegex .MatchString (path ) {
507
+ s .applyToSchema (schema )
508
+ }
509
+
510
+ if schema .Properties != nil {
511
+ for key := range schema .Properties {
512
+ prop := schema .Properties [key ]
513
+
514
+ newPath := filepath .Join (path , key )
515
+
516
+ if err := s .applyRuleToSchema (& prop , ruleRegex , newPath ); err != nil {
517
+ return err
518
+ }
519
+ schema .Properties [key ] = prop
520
+ }
521
+ }
522
+
523
+ if schema .Items != nil {
524
+ if schema .Items .Schema != nil {
525
+ if err := s .applyRuleToSchema (schema .Items .Schema , ruleRegex , path + "/items" ); err != nil {
526
+ return err
527
+ }
528
+ } else if len (schema .Items .JSONSchemas ) > 0 {
529
+ for i , item := range schema .Items .JSONSchemas {
530
+ newPath := fmt .Sprintf ("%s/items[%d]" , path , i )
531
+ if err := s .applyRuleToSchema (& item , ruleRegex , newPath ); err != nil {
532
+ return err
533
+ }
534
+ }
535
+ }
536
+ }
537
+
538
+ return nil
539
+ }
540
+
541
+ func (s SchemaModifier ) applyToSchema (schema * apiext.JSONSchemaProps ) {
542
+ if schema == nil {
543
+ return
544
+ }
545
+ if s .Description != nil {
546
+ schema .Description = * s .Description
547
+ }
548
+ if s .Format != nil {
549
+ schema .Format = * s .Format
550
+ }
551
+ if s .Maximum != nil {
552
+ schema .Maximum = s .Maximum
553
+ }
554
+ if s .ExclusiveMaximum != nil {
555
+ schema .ExclusiveMaximum = * s .ExclusiveMaximum
556
+ }
557
+ if s .Minimum != nil {
558
+ schema .Minimum = s .Minimum
559
+ }
560
+ if s .ExclusiveMinimum != nil {
561
+ schema .ExclusiveMinimum = * s .ExclusiveMinimum
562
+ }
563
+ if s .MaxLength != nil {
564
+ schema .MaxLength = ptr .To (int64 (* s .MaxLength ))
565
+ }
566
+ if s .MinLength != nil {
567
+ schema .MinLength = ptr .To (int64 (* s .MinLength ))
568
+ }
569
+ if s .Pattern != nil {
570
+ schema .Pattern = * s .Pattern
571
+ }
572
+ if s .MaxItems != nil {
573
+ schema .MaxItems = ptr .To (int64 (* s .MaxItems ))
574
+ }
575
+ if s .MinItems != nil {
576
+ schema .MinItems = ptr .To (int64 (* s .MinItems ))
577
+ }
578
+ if s .UniqueItems != nil {
579
+ schema .UniqueItems = * s .UniqueItems
580
+ }
581
+ if s .MultipleOf != nil {
582
+ schema .MultipleOf = s .MultipleOf
583
+ }
584
+ if s .MaxProperties != nil {
585
+ schema .MaxProperties = ptr .To (int64 (* s .MaxProperties ))
586
+ }
587
+ if s .MinProperties != nil {
588
+ schema .MinProperties = ptr .To (int64 (* s .MinProperties ))
589
+ }
590
+ if s .Required != nil {
591
+ schema .Required = * s .Required
592
+ }
593
+ if s .Nullable != nil {
594
+ schema .Nullable = * s .Nullable
595
+ }
596
+ }
597
+
598
+ func (s SchemaModifier ) parsePattern () (* regexp.Regexp , error ) {
599
+ pattern := s .PathPattern
600
+ pattern = strings .ReplaceAll (pattern , "[" , "\\ [" )
601
+ pattern = strings .ReplaceAll (pattern , "]" , "\\ ]" )
602
+ pattern = strings .ReplaceAll (pattern , "**" , "!☸!" )
603
+ pattern = strings .ReplaceAll (pattern , "*" , "[^/]+" )
604
+ pattern = strings .ReplaceAll (pattern , "!☸!" , ".*" )
605
+
606
+ regexStr := "^" + pattern + "$"
607
+
608
+ compiledRegex , err := regexp .Compile (regexStr )
609
+ if err != nil {
610
+ return nil , fmt .Errorf ("invalid rule: %w" , err )
611
+ }
612
+
613
+ return compiledRegex , nil
614
+ }
0 commit comments