mirror of
https://github.com/outbackdingo/cozystack.git
synced 2026-01-27 10:18:39 +00:00
[cozystack-api] Fix non-existing OpenAPI refs
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
This commit is contained in:
@@ -14,33 +14,36 @@ import (
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const (
|
||||
baseRef = "com.github.cozystack.cozystack.pkg.apis.apps.v1alpha1.Application"
|
||||
baseListRef = baseRef + "List"
|
||||
smp = "application/strategic-merge-patch+json"
|
||||
apiPrefix = "com.github.cozystack.cozystack.pkg.apis.apps.v1alpha1"
|
||||
baseRef = apiPrefix + ".Application"
|
||||
baseListRef = apiPrefix + ".ApplicationList"
|
||||
baseStatusRef = apiPrefix + ".ApplicationStatus"
|
||||
smp = "application/strategic-merge-patch+json"
|
||||
)
|
||||
|
||||
// deepCopySchema clones *spec.Schema via JSON-marshal/unmarshal.
|
||||
func deepCopySchema(in *spec.Schema) *spec.Schema {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
b, err := json.Marshal(in)
|
||||
raw, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
// Log error or panic since this is unexpected
|
||||
panic(fmt.Sprintf("failed to marshal schema: %v", err))
|
||||
panic(fmt.Errorf("failed to marshal schema: %w", err))
|
||||
}
|
||||
var out spec.Schema
|
||||
if err := json.Unmarshal(b, &out); err != nil {
|
||||
panic(fmt.Sprintf("failed to unmarshal schema: %v", err))
|
||||
err = json.Unmarshal(raw, &out)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to unmarshal schema: %w", err))
|
||||
}
|
||||
return &out
|
||||
}
|
||||
|
||||
// find the object that already owns ".spec"
|
||||
// findSpecContainer returns first object owning ".spec".
|
||||
func findSpecContainer(s *spec.Schema) *spec.Schema {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
if len(s.Type) > 0 && s.Type.Contains("object") && s.Properties != nil {
|
||||
if len(s.Type) > 0 && s.Type.Contains("object") {
|
||||
if _, ok := s.Properties["spec"]; ok {
|
||||
return s
|
||||
}
|
||||
@@ -55,40 +58,25 @@ func findSpecContainer(s *spec.Schema) *spec.Schema {
|
||||
return nil
|
||||
}
|
||||
|
||||
// apply user-supplied schema; when raw == "" turn the field into a schemaless object
|
||||
// patchSpec injects/overrides ".spec" with user JSON (or schemaless object).
|
||||
func patchSpec(target *spec.Schema, raw string) error {
|
||||
// ------------------------------------------------------------------
|
||||
// 1) schema not provided → make ".spec" a fully open object
|
||||
// ------------------------------------------------------------------
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
if target.Properties == nil {
|
||||
target.Properties = map[string]spec.Schema{}
|
||||
}
|
||||
prop := target.Properties["spec"]
|
||||
prop.AdditionalProperties = &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{},
|
||||
}
|
||||
prop.AdditionalProperties = &spec.SchemaOrBool{Allows: true, Schema: &spec.Schema{}}
|
||||
target.Properties["spec"] = prop
|
||||
return nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 2) custom schema provided → keep / inject additionalProperties
|
||||
// ------------------------------------------------------------------
|
||||
var custom spec.Schema
|
||||
if err := json.Unmarshal([]byte(raw), &custom); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if user didn't specify additionalProperties, add a permissive one
|
||||
if custom.AdditionalProperties == nil {
|
||||
custom.AdditionalProperties = &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{},
|
||||
}
|
||||
custom.AdditionalProperties = &spec.SchemaOrBool{Allows: true, Schema: &spec.Schema{}}
|
||||
}
|
||||
|
||||
if target.Properties == nil {
|
||||
target.Properties = map[string]spec.Schema{}
|
||||
}
|
||||
@@ -96,60 +84,190 @@ func patchSpec(target *spec.Schema, raw string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────── */
|
||||
/* DRY helpers */
|
||||
/* ────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
// cloneKindSchemas: from base schemas, create new schemas for a specific kind.
|
||||
func cloneKindSchemas(kind string, base, baseStatus, baseList *spec.Schema, v3 bool) (obj, status, list *spec.Schema) {
|
||||
obj = deepCopySchema(base)
|
||||
status = deepCopySchema(baseStatus)
|
||||
list = deepCopySchema(baseList)
|
||||
|
||||
// Ensure we have valid clones
|
||||
if obj == nil || status == nil || list == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// GVK-extensions
|
||||
setGVK := func(s *spec.Schema, k string) {
|
||||
s.Extensions = map[string]interface{}{
|
||||
"x-kubernetes-group-version-kind": []interface{}{
|
||||
map[string]interface{}{"group": "apps.cozystack.io", "version": "v1alpha1", "kind": k},
|
||||
},
|
||||
}
|
||||
}
|
||||
setGVK(obj, kind)
|
||||
setGVK(list, kind+"List")
|
||||
|
||||
// fix refs
|
||||
refPrefix := "#/components/schemas/" // v3
|
||||
if !v3 {
|
||||
refPrefix = "#/definitions/"
|
||||
}
|
||||
statusRef := refPrefix + apiPrefix + "." + kind + "Status"
|
||||
itemRef := refPrefix + apiPrefix + "." + kind
|
||||
|
||||
if prop, ok := obj.Properties["status"]; ok {
|
||||
prop.Ref = spec.MustCreateRef(statusRef)
|
||||
obj.Properties["status"] = prop
|
||||
}
|
||||
if list.Properties != nil {
|
||||
if items := list.Properties["items"]; items.Items != nil && items.Items.Schema != nil {
|
||||
items.Items.Schema.Ref = spec.MustCreateRef(itemRef)
|
||||
list.Properties["items"] = items
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// rewriteDocRefs rewrites all $ref in the OpenAPI document
|
||||
func rewriteDocRefs(doc interface{}) ([]byte, error) {
|
||||
raw, err := json.Marshal(doc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal OpenAPI document: %w", err)
|
||||
}
|
||||
var any interface{}
|
||||
if err := json.Unmarshal(raw, &any); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
walkAndRewriteRefs(any, "")
|
||||
return json.Marshal(any)
|
||||
}
|
||||
|
||||
// walkAndRewriteRefs walks arbitrary JSON (map/array) and
|
||||
// - when encountering x-kubernetes-group-version-kind, extracts kind,
|
||||
// updating the currentKind context;
|
||||
// - rewrites all $ref inside the current context from Application* → kind*.
|
||||
func walkAndRewriteRefs(node interface{}, currentKind string) {
|
||||
switch n := node.(type) {
|
||||
case map[string]interface{}:
|
||||
if gvk, ok := n["x-kubernetes-group-version-kind"]; ok {
|
||||
switch g := gvk.(type) {
|
||||
case map[string]interface{}:
|
||||
if k, ok := g["kind"].(string); ok {
|
||||
currentKind = k
|
||||
}
|
||||
case []interface{}:
|
||||
if len(g) > 0 {
|
||||
if mm, ok := g[0].(map[string]interface{}); ok {
|
||||
if k, ok := mm["kind"].(string); ok {
|
||||
currentKind = k
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, v := range n {
|
||||
if k == "$ref" && currentKind != "" {
|
||||
if s, ok := v.(string); ok {
|
||||
n[k] = rewriteRefForKind(s, currentKind)
|
||||
continue
|
||||
}
|
||||
}
|
||||
walkAndRewriteRefs(v, currentKind)
|
||||
}
|
||||
case []interface{}:
|
||||
for _, v := range n {
|
||||
walkAndRewriteRefs(v, currentKind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rewriteRefForKind rewrites a reference to a specific kind.
|
||||
func rewriteRefForKind(old, kind string) string {
|
||||
var base string
|
||||
switch {
|
||||
case strings.HasPrefix(old, "#/components/schemas/"):
|
||||
base = "#/components/schemas/"
|
||||
case strings.HasPrefix(old, "#/definitions/"):
|
||||
base = "#/definitions/"
|
||||
default:
|
||||
return old
|
||||
}
|
||||
switch {
|
||||
case strings.HasSuffix(old, ".Application"):
|
||||
return base + apiPrefix + "." + kind
|
||||
case strings.HasSuffix(old, ".ApplicationList"):
|
||||
return base + apiPrefix + "." + kind + "List"
|
||||
case strings.HasSuffix(old, ".ApplicationStatus"):
|
||||
return base + apiPrefix + "." + kind + "Status"
|
||||
default:
|
||||
return old
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// OpenAPI **v3** post-processor
|
||||
// -----------------------------------------------------------------------------
|
||||
func buildPostProcessV3(kindSchemas map[string]string) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) {
|
||||
|
||||
return func(doc *spec3.OpenAPI) (*spec3.OpenAPI, error) {
|
||||
|
||||
// Replace the basic "Application" schema with the user-supplied kinds.
|
||||
if doc.Components == nil {
|
||||
doc.Components = &spec3.Components{}
|
||||
}
|
||||
if doc.Components.Schemas == nil {
|
||||
doc.Components.Schemas = map[string]*spec.Schema{}
|
||||
}
|
||||
base, ok := doc.Components.Schemas[baseRef]
|
||||
if !ok {
|
||||
return doc, fmt.Errorf("base schema %q not found", baseRef)
|
||||
|
||||
// Get base schemas
|
||||
base, ok1 := doc.Components.Schemas[baseRef]
|
||||
list, ok2 := doc.Components.Schemas[baseListRef]
|
||||
stat, ok3 := doc.Components.Schemas[baseStatusRef]
|
||||
if !(ok1 && ok2 && ok3) {
|
||||
return doc, fmt.Errorf("base Application* schemas not found")
|
||||
}
|
||||
|
||||
// Clone base schemas for each kind
|
||||
for kind, raw := range kindSchemas {
|
||||
ref := fmt.Sprintf("%s.%s", "com.github.cozystack.cozystack.pkg.apis.apps.v1alpha1", kind)
|
||||
s := doc.Components.Schemas[ref]
|
||||
if s == nil { // first time – clone "Application"
|
||||
s = deepCopySchema(base)
|
||||
s.Extensions = map[string]interface{}{
|
||||
"x-kubernetes-group-version-kind": []interface{}{
|
||||
map[string]interface{}{
|
||||
"group": "apps.cozystack.io", "version": "v1alpha1", "kind": kind,
|
||||
},
|
||||
},
|
||||
}
|
||||
doc.Components.Schemas[ref] = s
|
||||
}
|
||||
container := findSpecContainer(s)
|
||||
if container == nil { // fallback: use the root
|
||||
container = s
|
||||
ref := apiPrefix + "." + kind
|
||||
statusRef := ref + "Status"
|
||||
listRef := ref + "List"
|
||||
|
||||
obj, status, l := cloneKindSchemas(kind, base, stat, list /*v3=*/, true)
|
||||
doc.Components.Schemas[ref] = obj
|
||||
doc.Components.Schemas[statusRef] = status
|
||||
doc.Components.Schemas[listRef] = l
|
||||
|
||||
// patch .spec
|
||||
container := findSpecContainer(obj)
|
||||
if container == nil {
|
||||
container = obj
|
||||
}
|
||||
if err := patchSpec(container, raw); err != nil {
|
||||
return nil, fmt.Errorf("kind %s: %w", kind, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete base schemas
|
||||
delete(doc.Components.Schemas, baseRef)
|
||||
delete(doc.Components.Schemas, baseListRef)
|
||||
delete(doc.Components.Schemas, baseStatusRef)
|
||||
|
||||
// Disable strategic-merge-patch+json support in all PATCH operations
|
||||
// Disable strategic-merge-patch+json
|
||||
for p, pi := range doc.Paths.Paths {
|
||||
if pi == nil || pi.Patch == nil || pi.Patch.RequestBody == nil {
|
||||
continue
|
||||
if pi != nil && pi.Patch != nil && pi.Patch.RequestBody != nil {
|
||||
delete(pi.Patch.RequestBody.Content, smp)
|
||||
doc.Paths.Paths[p] = pi
|
||||
}
|
||||
delete(pi.Patch.RequestBody.Content, smp)
|
||||
|
||||
doc.Paths.Paths[p] = pi
|
||||
}
|
||||
|
||||
return doc, nil
|
||||
// Rewrite all $ref in the document
|
||||
out, err := rewriteDocRefs(doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return doc, json.Unmarshal(out, doc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,52 +275,39 @@ func buildPostProcessV3(kindSchemas map[string]string) func(*spec3.OpenAPI) (*sp
|
||||
// OpenAPI **v2** (swagger) post-processor
|
||||
// -----------------------------------------------------------------------------
|
||||
func buildPostProcessV2(kindSchemas map[string]string) func(*spec.Swagger) (*spec.Swagger, error) {
|
||||
|
||||
return func(sw *spec.Swagger) (*spec.Swagger, error) {
|
||||
|
||||
// Replace the basic "Application" schema with the user-supplied kinds.
|
||||
// Get base schemas
|
||||
defs := sw.Definitions
|
||||
base, ok := defs[baseRef]
|
||||
if !ok {
|
||||
return sw, fmt.Errorf("base schema %q not found", baseRef)
|
||||
base, ok1 := defs[baseRef]
|
||||
list, ok2 := defs[baseListRef]
|
||||
stat, ok3 := defs[baseStatusRef]
|
||||
if !(ok1 && ok2 && ok3) {
|
||||
return sw, fmt.Errorf("base Application* schemas not found")
|
||||
}
|
||||
|
||||
// Clone base schemas for each kind
|
||||
for kind, raw := range kindSchemas {
|
||||
ref := fmt.Sprintf("%s.%s", "com.github.cozystack.cozystack.pkg.apis.apps.v1alpha1", kind)
|
||||
s := deepCopySchema(&base)
|
||||
s.Extensions = map[string]interface{}{
|
||||
"x-kubernetes-group-version-kind": []interface{}{
|
||||
map[string]interface{}{
|
||||
"group": "apps.cozystack.io", "version": "v1alpha1", "kind": kind,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := patchSpec(s, raw); err != nil {
|
||||
ref := apiPrefix + "." + kind
|
||||
statusRef := ref + "Status"
|
||||
listRef := ref + "List"
|
||||
|
||||
obj, status, l := cloneKindSchemas(kind, &base, &stat, &list /*v3=*/, false)
|
||||
defs[ref] = *obj
|
||||
defs[statusRef] = *status
|
||||
defs[listRef] = *l
|
||||
|
||||
if err := patchSpec(obj, raw); err != nil {
|
||||
return nil, fmt.Errorf("kind %s: %w", kind, err)
|
||||
}
|
||||
defs[ref] = *s
|
||||
// clone the List variant
|
||||
listName := ref + "List"
|
||||
listSrc := defs[baseListRef]
|
||||
listCopy := deepCopySchema(&listSrc)
|
||||
listCopy.Extensions = map[string]interface{}{
|
||||
"x-kubernetes-group-version-kind": []interface{}{
|
||||
map[string]interface{}{
|
||||
"group": "apps.cozystack.io",
|
||||
"version": "v1alpha1",
|
||||
"kind": kind + "List",
|
||||
},
|
||||
},
|
||||
}
|
||||
if items := listCopy.Properties["items"]; items.Items != nil && items.Items.Schema != nil {
|
||||
items.Items.Schema.Ref = spec.MustCreateRef("#/definitions/" + ref)
|
||||
listCopy.Properties["items"] = items
|
||||
}
|
||||
defs[listName] = *listCopy
|
||||
}
|
||||
|
||||
// Delete base schemas
|
||||
delete(defs, baseRef)
|
||||
delete(defs, baseListRef)
|
||||
delete(defs, baseStatusRef)
|
||||
|
||||
// Disable strategic-merge-patch+json support in all PATCH operations
|
||||
// Disable strategic-merge-patch+json
|
||||
for p, op := range sw.Paths.Paths {
|
||||
if op.Patch != nil && len(op.Patch.Consumes) > 0 {
|
||||
var out []string
|
||||
@@ -216,6 +321,11 @@ func buildPostProcessV2(kindSchemas map[string]string) func(*spec.Swagger) (*spe
|
||||
}
|
||||
}
|
||||
|
||||
return sw, nil
|
||||
// Rewrite all $ref in the document
|
||||
out, err := rewriteDocRefs(sw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sw, json.Unmarshal(out, sw)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user