@@ -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 , version . Schema . OpenAPIV3Schema . Required = opconTweaksMap (channel , version .Schema .OpenAPIV3Schema .Properties , version . Schema . OpenAPIV3Schema . Required )
140141 }
141142
142143 conv , err := crd .AsVersion (* channelCrd , apiextensionsv1 .SchemeGroupVersion )
@@ -179,25 +180,51 @@ func runGenerator(args ...string) {
179180 }
180181}
181182
182- func opconTweaksMap (channel string , props map [string ]apiextensionsv1.JSONSchemaProps ) map [string ]apiextensionsv1.JSONSchemaProps {
183+ // Apply Opcon specific tweaks to all properties in a map, and return a list of required fields according to opcon tags.
184+ // For opcon validation optional/required tags, the required list is updated accordingly.
185+ func opconTweaksMap (channel string , props map [string ]apiextensionsv1.JSONSchemaProps , existingRequired []string ) (map [string ]apiextensionsv1.JSONSchemaProps , []string ) {
186+ newRequired := slices .Clone (existingRequired )
187+
183188 for name := range props {
184189 jsonProps := props [name ]
185- p := opconTweaks (channel , name , jsonProps )
190+ p , reqStatus := opconTweaks (channel , name , jsonProps )
186191 if p == nil {
187192 delete (props , name )
188193 } else {
189194 props [name ] = * p
195+ // Update required list based on tag
196+ switch reqStatus {
197+ case statusRequired :
198+ if ! slices .Contains (newRequired , name ) {
199+ newRequired = append (newRequired , name )
200+ }
201+ case statusOptional :
202+ newRequired = slices .DeleteFunc (newRequired , func (s string ) bool { return s == name })
203+ default :
204+ // "" (unspecified) means keep existing status
205+ }
190206 }
191207 }
192- return props
208+ return props , newRequired
193209}
194210
195211// Custom Opcon API Tweaks for tags prefixed with `<opcon:` that get past
196212// the limitations of Kubebuilder annotations.
197- func opconTweaks (channel string , name string , jsonProps apiextensionsv1.JSONSchemaProps ) * apiextensionsv1.JSONSchemaProps {
213+ // Returns the modified schema and a string indicating required status where indicated by opcon tags:
214+ // "required", "optional", or "" (no decision -- preserve any non-opcon required status).
215+
216+ const (
217+ statusRequired = "required"
218+ statusOptional = "optional"
219+ statusNoOpinion = ""
220+ )
221+
222+ func opconTweaks (channel string , name string , jsonProps apiextensionsv1.JSONSchemaProps ) (* apiextensionsv1.JSONSchemaProps , string ) {
223+ requiredStatus := statusNoOpinion
224+
198225 if channel == StandardChannel {
199226 if strings .Contains (jsonProps .Description , "<opcon:experimental>" ) {
200- return nil
227+ return nil , statusNoOpinion
201228 }
202229 }
203230
@@ -219,7 +246,7 @@ func opconTweaks(channel string, name string, jsonProps apiextensionsv1.JSONSche
219246
220247 numValid ++
221248 jsonProps .Enum = []apiextensionsv1.JSON {}
222- for _ , val := range strings .Split (enumMatch [1 ], ";" ) {
249+ for val := range strings .SplitSeq (enumMatch [1 ], ";" ) {
223250 jsonProps .Enum = append (jsonProps .Enum , apiextensionsv1.JSON {Raw : []byte ("\" " + val + "\" " )})
224251 }
225252 }
@@ -237,6 +264,21 @@ func opconTweaks(channel string, name string, jsonProps apiextensionsv1.JSONSche
237264 Rule : celMatch [2 ],
238265 })
239266 }
267+ optReqRe := regexp .MustCompile (validationPrefix + "(Optional|Required)>" )
268+ optReqMatches := optReqRe .FindAllStringSubmatch (jsonProps .Description , 64 )
269+ for _ , optReqMatch := range optReqMatches {
270+ if len (optReqMatch ) != 2 {
271+ log .Fatalf ("Invalid %s Optional/Required tag for %s" , validationPrefix , name )
272+ }
273+
274+ numValid ++
275+ switch optReqMatch [1 ] {
276+ case "Optional" :
277+ requiredStatus = statusOptional
278+ case "Required" :
279+ requiredStatus = statusRequired
280+ }
281+ }
240282 }
241283
242284 if numValid < numExpressions {
@@ -246,34 +288,42 @@ func opconTweaks(channel string, name string, jsonProps apiextensionsv1.JSONSche
246288 jsonProps .Description = formatDescription (jsonProps .Description , channel , name )
247289
248290 if len (jsonProps .Properties ) > 0 {
249- jsonProps .Properties = opconTweaksMap (channel , jsonProps .Properties )
291+ jsonProps .Properties , jsonProps . Required = opconTweaksMap (channel , jsonProps .Properties , jsonProps . Required )
250292 } else if jsonProps .Items != nil && jsonProps .Items .Schema != nil {
251- jsonProps .Items .Schema = opconTweaks (channel , name , * jsonProps .Items .Schema )
293+ jsonProps .Items .Schema , _ = opconTweaks (channel , name , * jsonProps .Items .Schema )
252294 }
253295
254- return & jsonProps
296+ return & jsonProps , requiredStatus
255297}
256298
257299func 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*`
262- re := regexp .MustCompile (regexPattern )
263- match := re .FindStringSubmatch (description )
264- if len (match ) != 2 {
265- log .Fatalf ("Invalid <opcon:experimental:description> tag for %s" , name )
300+ tagset := []struct {
301+ channel string
302+ start string
303+ end string
304+ }{
305+ {channel : ExperimentalChannel , start : "<opcon:standard:description>" , end : "</opcon:standard:description>" },
306+ {channel : StandardChannel , start : "<opcon:experimental:description>" , end : "</opcon:experimental:description>" },
307+ }
308+ for _ , ts := range tagset {
309+ if channel == ts .channel && strings .Contains (description , ts .start ) {
310+ regexPattern := `\n*` + regexp .QuoteMeta (ts .start ) + `(?s:(.*?))` + regexp .QuoteMeta (ts .end ) + `\n*`
311+ re := regexp .MustCompile (regexPattern )
312+ match := re .FindStringSubmatch (description )
313+ if len (match ) != 2 {
314+ log .Fatalf ("Invalid <opcon:experimental:description> tag for %s" , name )
315+ }
316+ description = re .ReplaceAllString (description , "\n \n " )
317+ } else {
318+ description = strings .ReplaceAll (description , ts .start , "" )
319+ description = strings .ReplaceAll (description , ts .end , "" )
266320 }
267- description = re .ReplaceAllString (description , "\n \n " )
268- } else {
269- description = strings .ReplaceAll (description , startTag , "" )
270- description = strings .ReplaceAll (description , endTag , "" )
271321 }
272322
273323 // Comments within "opcon:util:excludeFromCRD" tag are not included in the generated CRD and all trailing \n operators before
274324 // and after the tags are removed and replaced with three \n operators.
275- startTag = "<opcon:util:excludeFromCRD>"
276- endTag = "</opcon:util:excludeFromCRD>"
325+ startTag : = "<opcon:util:excludeFromCRD>"
326+ endTag : = "</opcon:util:excludeFromCRD>"
277327 if strings .Contains (description , startTag ) {
278328 regexPattern := `\n*` + regexp .QuoteMeta (startTag ) + `(?s:(.*?))` + regexp .QuoteMeta (endTag ) + `\n*`
279329 re := regexp .MustCompile (regexPattern )
0 commit comments