Skip to content

Commit 5f682e2

Browse files
joelanfordclaude
andcommitted
Add crd-generator support for Required/Optional and standard descriptions
This commit enhances the CRD generator with the following capabilities: - Support for <opcon:validation:Required> and <opcon:validation:Optional> tags to control field requirements in the generated CRDs - Support for <opcon:standard:description> tags to provide channel-specific descriptions (complementing experimental descriptions) - Refactored opconTweaks functions to accept parent schema, enabling modification of the required fields list These changes provide more flexibility in defining CRD schemas with channel-specific behavior and field requirements. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent c06f27f commit 5f682e2

File tree

1 file changed

+49
-16
lines changed

1 file changed

+49
-16
lines changed

hack/tools/crd-generator/main.go

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"log"
2424
"os"
2525
"regexp"
26+
"slices"
2627
"strings"
2728

2829
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -136,7 +137,7 @@ func runGenerator(args ...string) {
136137
if channel == StandardChannel && strings.Contains(version.Name, "alpha") {
137138
channelCrd.Spec.Versions[i].Served = false
138139
}
139-
version.Schema.OpenAPIV3Schema.Properties = opconTweaksMap(channel, version.Schema.OpenAPIV3Schema.Properties)
140+
version.Schema.OpenAPIV3Schema.Properties = opconTweaksMap(channel, version.Schema.OpenAPIV3Schema)
140141
}
141142

142143
conv, err := crd.AsVersion(*channelCrd, apiextensionsv1.SchemeGroupVersion)
@@ -179,10 +180,11 @@ func runGenerator(args ...string) {
179180
}
180181
}
181182

182-
func opconTweaksMap(channel string, props map[string]apiextensionsv1.JSONSchemaProps) map[string]apiextensionsv1.JSONSchemaProps {
183+
func opconTweaksMap(channel string, obj *apiextensionsv1.JSONSchemaProps) map[string]apiextensionsv1.JSONSchemaProps {
184+
props := obj.Properties
183185
for name := range props {
184186
jsonProps := props[name]
185-
p := opconTweaks(channel, name, jsonProps)
187+
p := opconTweaks(channel, name, obj, jsonProps)
186188
if p == nil {
187189
delete(props, name)
188190
} else {
@@ -194,7 +196,7 @@ func opconTweaksMap(channel string, props map[string]apiextensionsv1.JSONSchemaP
194196

195197
// Custom Opcon API Tweaks for tags prefixed with `<opcon:` that get past
196198
// the limitations of Kubebuilder annotations.
197-
func opconTweaks(channel string, name string, jsonProps apiextensionsv1.JSONSchemaProps) *apiextensionsv1.JSONSchemaProps {
199+
func opconTweaks(channel string, name string, parent *apiextensionsv1.JSONSchemaProps, jsonProps apiextensionsv1.JSONSchemaProps) *apiextensionsv1.JSONSchemaProps {
198200
if channel == StandardChannel {
199201
if strings.Contains(jsonProps.Description, "<opcon:experimental>") {
200202
return nil
@@ -210,6 +212,22 @@ func opconTweaks(channel string, name string, jsonProps apiextensionsv1.JSONSche
210212
numExpressions := strings.Count(jsonProps.Description, validationPrefix)
211213
numValid := 0
212214
if numExpressions > 0 {
215+
requiredRe := regexp.MustCompile(validationPrefix + "Required>")
216+
requiredMatches := requiredRe.FindAllStringSubmatch(jsonProps.Description, -1)
217+
for _, _ = range requiredMatches {
218+
numValid += 1
219+
parent.Required = append(parent.Required, name)
220+
slices.Sort(parent.Required)
221+
}
222+
223+
optionalRe := regexp.MustCompile(validationPrefix + "Optional>")
224+
optionalMatches := optionalRe.FindAllStringSubmatch(jsonProps.Description, -1)
225+
for _, _ = range optionalMatches {
226+
numValid += 1
227+
parent.Required = slices.DeleteFunc(parent.Required, func(s string) bool { return s == name })
228+
slices.Sort(parent.Required)
229+
}
230+
213231
enumRe := regexp.MustCompile(validationPrefix + "Enum=([A-Za-z;]*)>")
214232
enumMatches := enumRe.FindAllStringSubmatch(jsonProps.Description, 64)
215233
for _, enumMatch := range enumMatches {
@@ -246,36 +264,51 @@ func opconTweaks(channel string, name string, jsonProps apiextensionsv1.JSONSche
246264
jsonProps.Description = formatDescription(jsonProps.Description, channel, name)
247265

248266
if len(jsonProps.Properties) > 0 {
249-
jsonProps.Properties = opconTweaksMap(channel, jsonProps.Properties)
267+
jsonProps.Properties = opconTweaksMap(channel, &jsonProps)
250268
} else if jsonProps.Items != nil && jsonProps.Items.Schema != nil {
251-
jsonProps.Items.Schema = opconTweaks(channel, name, *jsonProps.Items.Schema)
269+
jsonProps.Items.Schema = opconTweaks(channel, name, &jsonProps, *jsonProps.Items.Schema)
252270
}
253271

254272
return &jsonProps
255273
}
256274

257275
func formatDescription(description string, channel string, name string) string {
258-
startTag := "<opcon:experimental:description>"
259-
endTag := "</opcon:experimental:description>"
260-
if channel == StandardChannel && strings.Contains(description, startTag) {
261-
regexPattern := `\n*` + regexp.QuoteMeta(startTag) + `(?s:(.*?))` + regexp.QuoteMeta(endTag) + `\n*`
276+
startTagStandard := "<opcon:standard:description>"
277+
endTagStandard := "</opcon:standard:description>"
278+
if channel == ExperimentalChannel && strings.Contains(description, startTagStandard) {
279+
regexPattern := `\n*` + regexp.QuoteMeta(startTagStandard) + `(?s:(.*?))` + regexp.QuoteMeta(endTagStandard) + `\n*`
280+
re := regexp.MustCompile(regexPattern)
281+
match := re.FindStringSubmatch(description)
282+
if len(match) != 2 {
283+
log.Fatalf("Invalid <opcon:standard:description> tag for %s", name)
284+
}
285+
description = re.ReplaceAllString(description, "\n\n")
286+
} else {
287+
description = strings.ReplaceAll(description, startTagStandard, "")
288+
description = strings.ReplaceAll(description, endTagStandard, "")
289+
}
290+
291+
startTagExperimental := "<opcon:experimental:description>"
292+
endTagExperimental := "</opcon:experimental:description>"
293+
if channel == StandardChannel && strings.Contains(description, startTagExperimental) {
294+
regexPattern := `\n*` + regexp.QuoteMeta(startTagExperimental) + `(?s:(.*?))` + regexp.QuoteMeta(endTagExperimental) + `\n*`
262295
re := regexp.MustCompile(regexPattern)
263296
match := re.FindStringSubmatch(description)
264297
if len(match) != 2 {
265298
log.Fatalf("Invalid <opcon:experimental:description> tag for %s", name)
266299
}
267300
description = re.ReplaceAllString(description, "\n\n")
268301
} else {
269-
description = strings.ReplaceAll(description, startTag, "")
270-
description = strings.ReplaceAll(description, endTag, "")
302+
description = strings.ReplaceAll(description, startTagExperimental, "")
303+
description = strings.ReplaceAll(description, endTagExperimental, "")
271304
}
272305

273306
// Comments within "opcon:util:excludeFromCRD" tag are not included in the generated CRD and all trailing \n operators before
274307
// and after the tags are removed and replaced with three \n operators.
275-
startTag = "<opcon:util:excludeFromCRD>"
276-
endTag = "</opcon:util:excludeFromCRD>"
277-
if strings.Contains(description, startTag) {
278-
regexPattern := `\n*` + regexp.QuoteMeta(startTag) + `(?s:(.*?))` + regexp.QuoteMeta(endTag) + `\n*`
308+
startTagExclude := "<opcon:util:excludeFromCRD>"
309+
endTagExclude := "</opcon:util:excludeFromCRD>"
310+
if strings.Contains(description, startTagExclude) {
311+
regexPattern := `\n*` + regexp.QuoteMeta(startTagExclude) + `(?s:(.*?))` + regexp.QuoteMeta(endTagExclude) + `\n*`
279312
re := regexp.MustCompile(regexPattern)
280313
match := re.FindStringSubmatch(description)
281314
if len(match) != 2 {

0 commit comments

Comments
 (0)