@@ -18,9 +18,12 @@ package markers
1818
1919import (
2020 "fmt"
21+ "path/filepath"
22+ "regexp"
2123 "strings"
2224
2325 apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
26+ "k8s.io/utils/ptr"
2427
2528 "sigs.k8s.io/controller-tools/pkg/markers"
2629)
@@ -58,6 +61,9 @@ var CRDMarkers = []*definitionWithHelp{
5861
5962 must (markers .MakeDefinition ("kubebuilder:selectablefield" , markers .DescribesType , SelectableField {})).
6063 WithHelp (SelectableField {}.Help ()),
64+
65+ must (markers .MakeDefinition ("kubebuilder:schemaModifier" , markers .DescribesType , SchemaModifier {})).
66+ WithHelp (SchemaModifier {}.Help ()),
6167}
6268
6369// TODO: categories and singular used to be annotations types
@@ -420,3 +426,189 @@ func (s SelectableField) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, ve
420426
421427 return nil
422428}
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