Files
cozystack/pkg/cmd/server/openapi.go
Andrei Kvapil 91583a4e1a [cozystack-api] Refactor OpenAPI Schema
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-07-09 18:28:06 +02:00

202 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package server
import (
"encoding/json"
"fmt"
"strings"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
)
// -----------------------------------------------------------------------------
// shared helpers
// -----------------------------------------------------------------------------
const (
baseRef = "com.github.cozystack.cozystack.pkg.apis.apps.v1alpha1.Application"
baseListRef = baseRef + "List"
)
func deepCopySchema(in *spec.Schema) *spec.Schema {
if in == nil {
return nil
}
b, err := json.Marshal(in)
if err != nil {
// Log error or panic since this is unexpected
panic(fmt.Sprintf("failed to marshal schema: %v", err))
}
var out spec.Schema
if err := json.Unmarshal(b, &out); err != nil {
panic(fmt.Sprintf("failed to unmarshal schema: %v", err))
}
return &out
}
// find the object that already owns ".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 _, ok := s.Properties["spec"]; ok {
return s
}
}
for _, branch := range [][]spec.Schema{s.AllOf, s.OneOf, s.AnyOf} {
for i := range branch {
if res := findSpecContainer(&branch[i]); res != nil {
return res
}
}
}
return nil
}
// apply user-supplied schema; when raw == "" turn the field into a 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{},
}
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{},
}
}
if target.Properties == nil {
target.Properties = map[string]spec.Schema{}
}
target.Properties["spec"] = custom
return nil
}
// -----------------------------------------------------------------------------
// OpenAPI **v3** post-processor
// -----------------------------------------------------------------------------
func buildPostProcessV3(kindSchemas map[string]string) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) {
return func(doc *spec3.OpenAPI) (*spec3.OpenAPI, error) {
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)
}
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
}
if err := patchSpec(container, raw); err != nil {
return nil, fmt.Errorf("kind %s: %w", kind, err)
}
}
delete(doc.Components.Schemas, baseRef)
delete(doc.Components.Schemas, baseListRef)
return doc, nil
}
}
// -----------------------------------------------------------------------------
// 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) {
defs := sw.Definitions
base, ok := defs[baseRef]
if !ok {
return sw, fmt.Errorf("base schema %q not found", baseRef)
}
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 {
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(defs, baseRef)
delete(defs, baseListRef)
return sw, nil
}
}