Skip to content

Commit 34a51e5

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 1355ff7 commit 34a51e5

File tree

1 file changed

+51
-16
lines changed

1 file changed

+51
-16
lines changed

hack/tools/crd-generator/main.go

Lines changed: 51 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,24 @@ 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+
if !slices.Contains(parent.Required, name) {
220+
parent.Required = append(parent.Required, name)
221+
}
222+
slices.Sort(parent.Required)
223+
}
224+
225+
optionalRe := regexp.MustCompile(validationPrefix + "Optional>")
226+
optionalMatches := optionalRe.FindAllStringSubmatch(jsonProps.Description, -1)
227+
for _, _ = range optionalMatches {
228+
numValid += 1
229+
parent.Required = slices.DeleteFunc(parent.Required, func(s string) bool { return s == name })
230+
slices.Sort(parent.Required)
231+
}
232+
213233
enumRe := regexp.MustCompile(validationPrefix + "Enum=([A-Za-z;]*)>")
214234
enumMatches := enumRe.FindAllStringSubmatch(jsonProps.Description, 64)
215235
for _, enumMatch := range enumMatches {
@@ -246,36 +266,51 @@ func opconTweaks(channel string, name string, jsonProps apiextensionsv1.JSONSche
246266
jsonProps.Description = formatDescription(jsonProps.Description, channel, name)
247267

248268
if len(jsonProps.Properties) > 0 {
249-
jsonProps.Properties = opconTweaksMap(channel, jsonProps.Properties)
269+
jsonProps.Properties = opconTweaksMap(channel, &jsonProps)
250270
} else if jsonProps.Items != nil && jsonProps.Items.Schema != nil {
251-
jsonProps.Items.Schema = opconTweaks(channel, name, *jsonProps.Items.Schema)
271+
jsonProps.Items.Schema = opconTweaks(channel, name, &jsonProps, *jsonProps.Items.Schema)
252272
}
253273

254274
return &jsonProps
255275
}
256276

257277
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*`
278+
startTagStandard := "<opcon:standard:description>"
279+
endTagStandard := "</opcon:standard:description>"
280+
if channel == ExperimentalChannel && strings.Contains(description, startTagStandard) {
281+
regexPattern := `\n*` + regexp.QuoteMeta(startTagStandard) + `(?s:(.*?))` + regexp.QuoteMeta(endTagStandard) + `\n*`
282+
re := regexp.MustCompile(regexPattern)
283+
match := re.FindStringSubmatch(description)
284+
if len(match) != 2 {
285+
log.Fatalf("Invalid <opcon:standard:description> tag for %s", name)
286+
}
287+
description = re.ReplaceAllString(description, "\n\n")
288+
} else {
289+
description = strings.ReplaceAll(description, startTagStandard, "")
290+
description = strings.ReplaceAll(description, endTagStandard, "")
291+
}
292+
293+
startTagExperimental := "<opcon:experimental:description>"
294+
endTagExperimental := "</opcon:experimental:description>"
295+
if channel == StandardChannel && strings.Contains(description, startTagExperimental) {
296+
regexPattern := `\n*` + regexp.QuoteMeta(startTagExperimental) + `(?s:(.*?))` + regexp.QuoteMeta(endTagExperimental) + `\n*`
262297
re := regexp.MustCompile(regexPattern)
263298
match := re.FindStringSubmatch(description)
264299
if len(match) != 2 {
265300
log.Fatalf("Invalid <opcon:experimental:description> tag for %s", name)
266301
}
267302
description = re.ReplaceAllString(description, "\n\n")
268303
} else {
269-
description = strings.ReplaceAll(description, startTag, "")
270-
description = strings.ReplaceAll(description, endTag, "")
304+
description = strings.ReplaceAll(description, startTagExperimental, "")
305+
description = strings.ReplaceAll(description, endTagExperimental, "")
271306
}
272307

273308
// Comments within "opcon:util:excludeFromCRD" tag are not included in the generated CRD and all trailing \n operators before
274309
// 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*`
310+
startTagExclude := "<opcon:util:excludeFromCRD>"
311+
endTagExclude := "</opcon:util:excludeFromCRD>"
312+
if strings.Contains(description, startTagExclude) {
313+
regexPattern := `\n*` + regexp.QuoteMeta(startTagExclude) + `(?s:(.*?))` + regexp.QuoteMeta(endTagExclude) + `\n*`
279314
re := regexp.MustCompile(regexPattern)
280315
match := re.FindStringSubmatch(description)
281316
if len(match) != 2 {

0 commit comments

Comments
 (0)