@@ -18,9 +18,13 @@ 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" 
27+ 
2428	"sigs.k8s.io/controller-tools/pkg/markers" 
2529)
2630
@@ -57,6 +61,9 @@ var CRDMarkers = []*definitionWithHelp{
5761
5862	must (markers .MakeDefinition ("kubebuilder:selectablefield" , markers .DescribesType , SelectableField {})).
5963		WithHelp (SelectableField {}.Help ()),
64+ 
65+ 	must (markers .MakeDefinition ("kubebuilder:schemaModifier" , markers .DescribesType , SchemaModifier {})).
66+ 		WithHelp (SchemaModifier {}.Help ()),
6067}
6168
6269// TODO: categories and singular used to be annotations types 
@@ -419,3 +426,187 @@ func (s SelectableField) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, ve
419426
420427	return  nil 
421428}
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+ 
604+ 	regexStr  :=  "^"  +  pattern  +  "$" 
605+ 
606+ 	compiledRegex , err  :=  regexp .Compile (regexStr )
607+ 	if  err  !=  nil  {
608+ 		return  nil , fmt .Errorf ("invalid rule: %w" , err )
609+ 	}
610+ 
611+ 	return  compiledRegex , nil 
612+ }
0 commit comments