@@ -14,33 +14,36 @@ import (
1414// -----------------------------------------------------------------------------
1515
1616const (
17- baseRef = "com.github.cozystack.cozystack.pkg.apis.apps.v1alpha1.Application"
18- baseListRef = baseRef + "List"
19- smp = "application/strategic-merge-patch+json"
17+ apiPrefix = "com.github.cozystack.cozystack.pkg.apis.apps.v1alpha1"
18+ baseRef = apiPrefix + ".Application"
19+ baseListRef = apiPrefix + ".ApplicationList"
20+ baseStatusRef = apiPrefix + ".ApplicationStatus"
21+ smp = "application/strategic-merge-patch+json"
2022)
2123
24+ // deepCopySchema clones *spec.Schema via JSON-marshal/unmarshal.
2225func deepCopySchema (in * spec.Schema ) * spec.Schema {
2326 if in == nil {
2427 return nil
2528 }
26- b , err := json .Marshal (in )
29+ raw , err := json .Marshal (in )
2730 if err != nil {
28- // Log error or panic since this is unexpected
29- panic (fmt .Sprintf ("failed to marshal schema: %v" , err ))
31+ panic (fmt .Errorf ("failed to marshal schema: %w" , err ))
3032 }
3133 var out spec.Schema
32- if err := json .Unmarshal (b , & out ); err != nil {
33- panic (fmt .Sprintf ("failed to unmarshal schema: %v" , err ))
34+ err = json .Unmarshal (raw , & out )
35+ if err != nil {
36+ panic (fmt .Errorf ("failed to unmarshal schema: %w" , err ))
3437 }
3538 return & out
3639}
3740
38- // find the object that already owns ".spec"
41+ // findSpecContainer returns first object owning ".spec".
3942func findSpecContainer (s * spec.Schema ) * spec.Schema {
4043 if s == nil {
4144 return nil
4245 }
43- if len (s .Type ) > 0 && s .Type .Contains ("object" ) && s . Properties != nil {
46+ if len (s .Type ) > 0 && s .Type .Contains ("object" ) {
4447 if _ , ok := s .Properties ["spec" ]; ok {
4548 return s
4649 }
@@ -55,154 +58,256 @@ func findSpecContainer(s *spec.Schema) *spec.Schema {
5558 return nil
5659}
5760
58- // apply user-supplied schema; when raw == "" turn the field into a schemaless object
61+ // patchSpec injects/overrides ".spec" with user JSON (or schemaless object).
5962func patchSpec (target * spec.Schema , raw string ) error {
60- // ------------------------------------------------------------------
61- // 1) schema not provided → make ".spec" a fully open object
62- // ------------------------------------------------------------------
6363 if strings .TrimSpace (raw ) == "" {
6464 if target .Properties == nil {
6565 target .Properties = map [string ]spec.Schema {}
6666 }
6767 prop := target .Properties ["spec" ]
68- prop .AdditionalProperties = & spec.SchemaOrBool {
69- Allows : true ,
70- Schema : & spec.Schema {},
71- }
68+ prop .AdditionalProperties = & spec.SchemaOrBool {Allows : true , Schema : & spec.Schema {}}
7269 target .Properties ["spec" ] = prop
7370 return nil
7471 }
7572
76- // ------------------------------------------------------------------
77- // 2) custom schema provided → keep / inject additionalProperties
78- // ------------------------------------------------------------------
7973 var custom spec.Schema
8074 if err := json .Unmarshal ([]byte (raw ), & custom ); err != nil {
8175 return err
8276 }
83-
84- // if user didn't specify additionalProperties, add a permissive one
8577 if custom .AdditionalProperties == nil {
86- custom .AdditionalProperties = & spec.SchemaOrBool {
87- Allows : true ,
88- Schema : & spec.Schema {},
89- }
78+ custom .AdditionalProperties = & spec.SchemaOrBool {Allows : true , Schema : & spec.Schema {}}
9079 }
91-
9280 if target .Properties == nil {
9381 target .Properties = map [string ]spec.Schema {}
9482 }
9583 target .Properties ["spec" ] = custom
9684 return nil
9785}
9886
87+ /* ────────────────────────────────────────────────────────────────────────── */
88+ /* DRY helpers */
89+ /* ────────────────────────────────────────────────────────────────────────── */
90+
91+ // cloneKindSchemas: from base schemas, create new schemas for a specific kind.
92+ func cloneKindSchemas (kind string , base , baseStatus , baseList * spec.Schema , v3 bool ) (obj , status , list * spec.Schema ) {
93+ obj = deepCopySchema (base )
94+ status = deepCopySchema (baseStatus )
95+ list = deepCopySchema (baseList )
96+
97+ // Ensure we have valid clones
98+ if obj == nil || status == nil || list == nil {
99+ return nil , nil , nil
100+ }
101+
102+ // GVK-extensions
103+ setGVK := func (s * spec.Schema , k string ) {
104+ s .Extensions = map [string ]interface {}{
105+ "x-kubernetes-group-version-kind" : []interface {}{
106+ map [string ]interface {}{"group" : "apps.cozystack.io" , "version" : "v1alpha1" , "kind" : k },
107+ },
108+ }
109+ }
110+ setGVK (obj , kind )
111+ setGVK (list , kind + "List" )
112+
113+ // fix refs
114+ refPrefix := "#/components/schemas/" // v3
115+ if ! v3 {
116+ refPrefix = "#/definitions/"
117+ }
118+ statusRef := refPrefix + apiPrefix + "." + kind + "Status"
119+ itemRef := refPrefix + apiPrefix + "." + kind
120+
121+ if prop , ok := obj .Properties ["status" ]; ok {
122+ prop .Ref = spec .MustCreateRef (statusRef )
123+ obj .Properties ["status" ] = prop
124+ }
125+ if list .Properties != nil {
126+ if items := list .Properties ["items" ]; items .Items != nil && items .Items .Schema != nil {
127+ items .Items .Schema .Ref = spec .MustCreateRef (itemRef )
128+ list .Properties ["items" ] = items
129+ }
130+ }
131+ return
132+ }
133+
134+ // rewriteDocRefs rewrites all $ref in the OpenAPI document
135+ func rewriteDocRefs (doc interface {}) ([]byte , error ) {
136+ raw , err := json .Marshal (doc )
137+ if err != nil {
138+ return nil , fmt .Errorf ("failed to marshal OpenAPI document: %w" , err )
139+ }
140+ var any interface {}
141+ if err := json .Unmarshal (raw , & any ); err != nil {
142+ return nil , err
143+ }
144+ walkAndRewriteRefs (any , "" )
145+ return json .Marshal (any )
146+ }
147+
148+ // walkAndRewriteRefs walks arbitrary JSON (map/array) and
149+ // - when encountering x-kubernetes-group-version-kind, extracts kind,
150+ // updating the currentKind context;
151+ // - rewrites all $ref inside the current context from Application* → kind*.
152+ func walkAndRewriteRefs (node interface {}, currentKind string ) {
153+ switch n := node .(type ) {
154+ case map [string ]interface {}:
155+ if gvk , ok := n ["x-kubernetes-group-version-kind" ]; ok {
156+ switch g := gvk .(type ) {
157+ case map [string ]interface {}:
158+ if k , ok := g ["kind" ].(string ); ok {
159+ currentKind = k
160+ }
161+ case []interface {}:
162+ if len (g ) > 0 {
163+ if mm , ok := g [0 ].(map [string ]interface {}); ok {
164+ if k , ok := mm ["kind" ].(string ); ok {
165+ currentKind = k
166+ }
167+ }
168+ }
169+ }
170+ }
171+ for k , v := range n {
172+ if k == "$ref" && currentKind != "" {
173+ if s , ok := v .(string ); ok {
174+ n [k ] = rewriteRefForKind (s , currentKind )
175+ continue
176+ }
177+ }
178+ walkAndRewriteRefs (v , currentKind )
179+ }
180+ case []interface {}:
181+ for _ , v := range n {
182+ walkAndRewriteRefs (v , currentKind )
183+ }
184+ }
185+ }
186+
187+ // rewriteRefForKind rewrites a reference to a specific kind.
188+ func rewriteRefForKind (old , kind string ) string {
189+ var base string
190+ switch {
191+ case strings .HasPrefix (old , "#/components/schemas/" ):
192+ base = "#/components/schemas/"
193+ case strings .HasPrefix (old , "#/definitions/" ):
194+ base = "#/definitions/"
195+ default :
196+ return old
197+ }
198+ switch {
199+ case strings .HasSuffix (old , ".Application" ):
200+ return base + apiPrefix + "." + kind
201+ case strings .HasSuffix (old , ".ApplicationList" ):
202+ return base + apiPrefix + "." + kind + "List"
203+ case strings .HasSuffix (old , ".ApplicationStatus" ):
204+ return base + apiPrefix + "." + kind + "Status"
205+ default :
206+ return old
207+ }
208+ }
209+
99210// -----------------------------------------------------------------------------
100211// OpenAPI **v3** post-processor
101212// -----------------------------------------------------------------------------
102213func buildPostProcessV3 (kindSchemas map [string ]string ) func (* spec3.OpenAPI ) (* spec3.OpenAPI , error ) {
103-
104214 return func (doc * spec3.OpenAPI ) (* spec3.OpenAPI , error ) {
105215
106- // Replace the basic "Application" schema with the user-supplied kinds.
107216 if doc .Components == nil {
108217 doc .Components = & spec3.Components {}
109218 }
110219 if doc .Components .Schemas == nil {
111220 doc .Components .Schemas = map [string ]* spec.Schema {}
112221 }
113- base , ok := doc .Components .Schemas [baseRef ]
114- if ! ok {
115- return doc , fmt .Errorf ("base schema %q not found" , baseRef )
222+
223+ // Get base schemas
224+ base , ok1 := doc .Components .Schemas [baseRef ]
225+ list , ok2 := doc .Components .Schemas [baseListRef ]
226+ stat , ok3 := doc .Components .Schemas [baseStatusRef ]
227+ if ! (ok1 && ok2 && ok3 ) {
228+ return doc , fmt .Errorf ("base Application* schemas not found" )
116229 }
230+
231+ // Clone base schemas for each kind
117232 for kind , raw := range kindSchemas {
118- ref := fmt .Sprintf ("%s.%s" , "com.github.cozystack.cozystack.pkg.apis.apps.v1alpha1" , kind )
119- s := doc .Components .Schemas [ref ]
120- if s == nil { // first time – clone "Application"
121- s = deepCopySchema (base )
122- s .Extensions = map [string ]interface {}{
123- "x-kubernetes-group-version-kind" : []interface {}{
124- map [string ]interface {}{
125- "group" : "apps.cozystack.io" , "version" : "v1alpha1" , "kind" : kind ,
126- },
127- },
128- }
129- doc .Components .Schemas [ref ] = s
130- }
131- container := findSpecContainer (s )
132- if container == nil { // fallback: use the root
133- container = s
233+ ref := apiPrefix + "." + kind
234+ statusRef := ref + "Status"
235+ listRef := ref + "List"
236+
237+ obj , status , l := cloneKindSchemas (kind , base , stat , list /*v3=*/ , true )
238+ doc .Components .Schemas [ref ] = obj
239+ doc .Components .Schemas [statusRef ] = status
240+ doc .Components .Schemas [listRef ] = l
241+
242+ // patch .spec
243+ container := findSpecContainer (obj )
244+ if container == nil {
245+ container = obj
134246 }
135247 if err := patchSpec (container , raw ); err != nil {
136248 return nil , fmt .Errorf ("kind %s: %w" , kind , err )
137249 }
138250 }
251+
252+ // Delete base schemas
139253 delete (doc .Components .Schemas , baseRef )
140254 delete (doc .Components .Schemas , baseListRef )
255+ delete (doc .Components .Schemas , baseStatusRef )
141256
142- // Disable strategic-merge-patch+json support in all PATCH operations
257+ // Disable strategic-merge-patch+json
143258 for p , pi := range doc .Paths .Paths {
144- if pi == nil || pi .Patch == nil || pi .Patch .RequestBody == nil {
145- continue
259+ if pi != nil && pi .Patch != nil && pi .Patch .RequestBody != nil {
260+ delete (pi .Patch .RequestBody .Content , smp )
261+ doc .Paths .Paths [p ] = pi
146262 }
147- delete (pi .Patch .RequestBody .Content , smp )
148-
149- doc .Paths .Paths [p ] = pi
150263 }
151264
152- return doc , nil
265+ // Rewrite all $ref in the document
266+ out , err := rewriteDocRefs (doc )
267+ if err != nil {
268+ return nil , err
269+ }
270+ return doc , json .Unmarshal (out , doc )
153271 }
154272}
155273
156274// -----------------------------------------------------------------------------
157275// OpenAPI **v2** (swagger) post-processor
158276// -----------------------------------------------------------------------------
159277func buildPostProcessV2 (kindSchemas map [string ]string ) func (* spec.Swagger ) (* spec.Swagger , error ) {
160-
161278 return func (sw * spec.Swagger ) (* spec.Swagger , error ) {
162279
163- // Replace the basic "Application" schema with the user-supplied kinds.
280+ // Get base schemas
164281 defs := sw .Definitions
165- base , ok := defs [baseRef ]
166- if ! ok {
167- return sw , fmt .Errorf ("base schema %q not found" , baseRef )
282+ base , ok1 := defs [baseRef ]
283+ list , ok2 := defs [baseListRef ]
284+ stat , ok3 := defs [baseStatusRef ]
285+ if ! (ok1 && ok2 && ok3 ) {
286+ return sw , fmt .Errorf ("base Application* schemas not found" )
168287 }
288+
289+ // Clone base schemas for each kind
169290 for kind , raw := range kindSchemas {
170- ref := fmt . Sprintf ( "%s.%s" , "com.github.cozystack.cozystack.pkg.apis.apps.v1alpha1" , kind )
171- s := deepCopySchema ( & base )
172- s . Extensions = map [ string ] interface {}{
173- "x-kubernetes-group-version-kind" : [] interface {}{
174- map [ string ] interface {}{
175- "group" : "apps.cozystack.io" , "version" : "v1alpha1" , "kind" : kind ,
176- },
177- },
178- }
179- if err := patchSpec (s , raw ); err != nil {
291+ ref := apiPrefix + "." + kind
292+ statusRef := ref + "Status"
293+ listRef := ref + "List"
294+
295+ obj , status , l := cloneKindSchemas ( kind , & base , & stat , & list /*v3=*/ , false )
296+ defs [ ref ] = * obj
297+ defs [ statusRef ] = * status
298+ defs [ listRef ] = * l
299+
300+ if err := patchSpec (obj , raw ); err != nil {
180301 return nil , fmt .Errorf ("kind %s: %w" , kind , err )
181302 }
182- defs [ref ] = * s
183- // clone the List variant
184- listName := ref + "List"
185- listSrc := defs [baseListRef ]
186- listCopy := deepCopySchema (& listSrc )
187- listCopy .Extensions = map [string ]interface {}{
188- "x-kubernetes-group-version-kind" : []interface {}{
189- map [string ]interface {}{
190- "group" : "apps.cozystack.io" ,
191- "version" : "v1alpha1" ,
192- "kind" : kind + "List" ,
193- },
194- },
195- }
196- if items := listCopy .Properties ["items" ]; items .Items != nil && items .Items .Schema != nil {
197- items .Items .Schema .Ref = spec .MustCreateRef ("#/definitions/" + ref )
198- listCopy .Properties ["items" ] = items
199- }
200- defs [listName ] = * listCopy
201303 }
304+
305+ // Delete base schemas
202306 delete (defs , baseRef )
203307 delete (defs , baseListRef )
308+ delete (defs , baseStatusRef )
204309
205- // Disable strategic-merge-patch+json support in all PATCH operations
310+ // Disable strategic-merge-patch+json
206311 for p , op := range sw .Paths .Paths {
207312 if op .Patch != nil && len (op .Patch .Consumes ) > 0 {
208313 var out []string
@@ -216,6 +321,11 @@ func buildPostProcessV2(kindSchemas map[string]string) func(*spec.Swagger) (*spe
216321 }
217322 }
218323
219- return sw , nil
324+ // Rewrite all $ref in the document
325+ out , err := rewriteDocRefs (sw )
326+ if err != nil {
327+ return nil , err
328+ }
329+ return sw , json .Unmarshal (out , sw )
220330 }
221331}
0 commit comments