Skip to content

Commit 181e8dc

Browse files
committed
[cozystack-api] Fix non-existing OpenAPI refs
Signed-off-by: Andrei Kvapil <[email protected]>
1 parent bccf611 commit 181e8dc

1 file changed

Lines changed: 202 additions & 92 deletions

File tree

‎pkg/cmd/server/openapi.go‎

Lines changed: 202 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,36 @@ import (
1414
// -----------------------------------------------------------------------------
1515

1616
const (
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.
2225
func 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".
3942
func 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).
5962
func 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
// -----------------------------------------------------------------------------
102213
func 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
// -----------------------------------------------------------------------------
159277
func 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

Comments
 (0)