Files
cozystack/pkg/cmd/server/openapi.go
Andrei Kvapil 23a7281fbf [cozystack-api] Disable startegic-json-patch support
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-07-09 18:31:14 +02:00

222 lines
6.5 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"
smp = "application/strategic-merge-patch+json"
)
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) {
// 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)
}
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)
// Disable strategic-merge-patch+json support in all PATCH operations
for p, pi := range doc.Paths.Paths {
if pi == nil || pi.Patch == nil || pi.Patch.RequestBody == nil {
continue
}
delete(pi.Patch.RequestBody.Content, smp)
doc.Paths.Paths[p] = pi
}
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) {
// Replace the basic "Application" schema with the user-supplied kinds.
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)
// Disable strategic-merge-patch+json support in all PATCH operations
for p, op := range sw.Paths.Paths {
if op.Patch != nil && len(op.Patch.Consumes) > 0 {
var out []string
for _, c := range op.Patch.Consumes {
if c != smp {
out = append(out, c)
}
}
op.Patch.Consumes = out
sw.Paths.Paths[p] = op
}
}
return sw, nil
}
}