Skip to content

Commit 71a1237

Browse files
add marker
Signed-off-by: Yaroslav Borbat <[email protected]>
1 parent 2256668 commit 71a1237

File tree

3 files changed

+277
-0
lines changed

3 files changed

+277
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ hack/tools/bin
2323

2424
junit-report.xml
2525
/artifacts
26+
tmp

pkg/crd/markers/crd.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ package markers
1818

1919
import (
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+
}

pkg/crd/markers/zz_generated.markerhelp.go

Lines changed: 84 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)