mirror of
https://github.com/outbackdingo/cozystack.git
synced 2026-01-27 10:18:39 +00:00
Fix: Sanitize v2 schema (#1353)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com> <!-- Thank you for making a contribution! Here are some tips for you: - Start the PR title with the [label] of Cozystack component: - For system components: [platform], [system], [linstor], [cilium], [kube-ovn], [dashboard], [cluster-api], etc. - For managed apps: [apps], [tenant], [kubernetes], [postgres], [virtual-machine] etc. - For development and maintenance: [tests], [ci], [docs], [maintenance]. - If it's a work in progress, consider creating this PR as a draft. - Don't hesistate to ask for opinion and review in the community chats, even if it's still a draft. - Add the label `backport` if it's a bugfix that needs to be backported to a previous version. --> ## What this PR does fixes https://github.com/cozystack/cozystack/issues/1352#issuecomment-3210026159 ### Release note <!-- Write a release note: - Explain what has changed internally and for users. - Start with the same [label] as in the PR title - Follow the guidelines at https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md. --> ```release-note [cozystack-api] Fix: Sanitize v2 schema ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - Bug Fixes - Improved OpenAPI/Swagger v2 compatibility by normalizing schemas (handle int-or-string patterns, remove unsupported oneOf/anyOf, and fix empty additionalProperties), producing more consistent v2-compliant definitions. - Refactor - Added internal v2 post-processing to sanitize schemas across all definitions without changing public APIs. - Tests - Added end-to-end OpenAPI tests validating v2, v3 and protobuf v2 endpoints and integrated them into the test suite. - CI - Added an OpenAPI test step to the pull-request workflow so OpenAPI tests run during CI. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
5
.github/workflows/pull-requests.yaml
vendored
5
.github/workflows/pull-requests.yaml
vendored
@@ -254,6 +254,11 @@ jobs:
|
||||
done
|
||||
echo "✅ The task completed successfully after $attempt attempts."
|
||||
|
||||
- name: Run OpenAPI tests
|
||||
run: |
|
||||
cd /tmp/$SANDBOX_NAME
|
||||
make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME test-openapi
|
||||
|
||||
detect_test_matrix:
|
||||
name: "Detect e2e test matrix"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
20
hack/e2e-test-openapi.bats
Normal file
20
hack/e2e-test-openapi.bats
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bats
|
||||
# -----------------------------------------------------------------------------
|
||||
# Test OpenAPI endpoints in a Kubernetes cluster
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@test "Test OpenAPI v2 endpoint" {
|
||||
kubectl get -v7 --raw '/openapi/v2?timeout=32s' > /dev/null
|
||||
}
|
||||
|
||||
@test "Test OpenAPI v3 endpoint" {
|
||||
kubectl get -v7 --raw '/openapi/v3/apis/apps.cozystack.io/v1alpha1' > /dev/null
|
||||
}
|
||||
|
||||
@test "Test OpenAPI v2 endpoint (protobuf)" {
|
||||
(
|
||||
kubectl proxy --port=21234 & sleep 0.5
|
||||
trap "kill $!" EXIT
|
||||
curl -sS --fail 'http://localhost:21234/openapi/v2?timeout=32s' -H 'Accept: application/com.github.proto-openapi.spec.v2@v1.0+protobuf' > /dev/null
|
||||
)
|
||||
}
|
||||
@@ -30,7 +30,7 @@ image-e2e-sandbox:
|
||||
yq -i '.e2e.image = strenv(IMAGE)' values.yaml
|
||||
rm -f images/e2e-sandbox.json
|
||||
|
||||
test: test-cluster test-apps ## Run the end-to-end tests in existing sandbox
|
||||
test: test-cluster test-openapi test-apps ## Run the end-to-end tests in existing sandbox
|
||||
|
||||
copy-nocloud-image:
|
||||
docker cp ../../../_out/assets/nocloud-amd64.raw.xz "${SANDBOX_NAME}":/workspace/_out/assets/nocloud-amd64.raw.xz
|
||||
@@ -47,6 +47,9 @@ install-cozystack: copy-installer-manifest
|
||||
test-cluster: copy-nocloud-image copy-installer-manifest ## Run the end-to-end for creating a cluster
|
||||
docker exec "${SANDBOX_NAME}" sh -c 'cd /workspace && hack/cozytest.sh hack/e2e-cluster.bats'
|
||||
|
||||
test-openapi:
|
||||
docker exec "${SANDBOX_NAME}" sh -c 'cd /workspace && hack/cozytest.sh hack/e2e-test-openapi.bats'
|
||||
|
||||
test-apps-%:
|
||||
docker exec "${SANDBOX_NAME}" sh -c 'cd /workspace && hack/cozytest.sh hack/e2e-apps/$*.bats'
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ func patchSpec(target *spec.Schema, raw string) error {
|
||||
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}
|
||||
target.Properties["spec"] = prop
|
||||
return nil
|
||||
}
|
||||
@@ -75,7 +75,7 @@ func patchSpec(target *spec.Schema, raw string) error {
|
||||
return err
|
||||
}
|
||||
if custom.AdditionalProperties == nil {
|
||||
custom.AdditionalProperties = &spec.SchemaOrBool{Allows: true, Schema: &spec.Schema{}}
|
||||
custom.AdditionalProperties = &spec.SchemaOrBool{Allows: true}
|
||||
}
|
||||
if target.Properties == nil {
|
||||
target.Properties = map[string]spec.Schema{}
|
||||
@@ -271,13 +271,70 @@ func buildPostProcessV3(kindSchemas map[string]string) func(*spec3.OpenAPI) (*sp
|
||||
}
|
||||
}
|
||||
|
||||
// hasIntAndStringAnyOf returns true if anyOf is exactly a combination of string and integer.
|
||||
func hasIntAndStringAnyOf(anyOf []spec.Schema) bool {
|
||||
seen := map[string]bool{}
|
||||
for i := range anyOf {
|
||||
for _, t := range anyOf[i].Type {
|
||||
seen[t] = true
|
||||
}
|
||||
}
|
||||
return seen["string"] && seen["integer"] && len(seen) <= 2
|
||||
}
|
||||
|
||||
// sanitizeForV2 removes unsupported constructs for Swagger v2 and normalizes common patterns.
|
||||
func sanitizeForV2(s *spec.Schema) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(s.AnyOf) > 0 {
|
||||
if hasIntAndStringAnyOf(s.AnyOf) {
|
||||
s.Type = spec.StringOrArray{"string"}
|
||||
if s.Extensions == nil {
|
||||
s.Extensions = map[string]interface{}{}
|
||||
}
|
||||
s.Extensions["x-kubernetes-int-or-string"] = true
|
||||
}
|
||||
s.AnyOf = nil
|
||||
}
|
||||
|
||||
if len(s.OneOf) > 0 {
|
||||
s.OneOf = nil
|
||||
}
|
||||
|
||||
if s.AdditionalProperties != nil {
|
||||
ap := s.AdditionalProperties
|
||||
if ap.Schema != nil {
|
||||
sanitizeForV2(ap.Schema)
|
||||
}
|
||||
}
|
||||
|
||||
for k := range s.Properties {
|
||||
prop := s.Properties[k]
|
||||
sanitizeForV2(&prop)
|
||||
s.Properties[k] = prop
|
||||
}
|
||||
|
||||
if s.Items != nil {
|
||||
if s.Items.Schema != nil {
|
||||
sanitizeForV2(s.Items.Schema)
|
||||
}
|
||||
for i := range s.Items.Schemas {
|
||||
sanitizeForV2(&s.Items.Schemas[i])
|
||||
}
|
||||
}
|
||||
|
||||
for i := range s.AllOf {
|
||||
sanitizeForV2(&s.AllOf[i])
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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) {
|
||||
|
||||
// Get base schemas
|
||||
defs := sw.Definitions
|
||||
base, ok1 := defs[baseRef]
|
||||
list, ok2 := defs[baseListRef]
|
||||
@@ -286,28 +343,26 @@ func buildPostProcessV2(kindSchemas map[string]string) func(*spec.Swagger) (*spe
|
||||
return sw, fmt.Errorf("base Application* schemas not found")
|
||||
}
|
||||
|
||||
// Clone base schemas for each kind
|
||||
for kind, raw := range kindSchemas {
|
||||
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
|
||||
obj, status, l := cloneKindSchemas(kind, &base, &stat, &list, false)
|
||||
|
||||
if err := patchSpec(obj, raw); err != nil {
|
||||
return nil, fmt.Errorf("kind %s: %w", kind, err)
|
||||
}
|
||||
|
||||
defs[ref] = *obj
|
||||
defs[statusRef] = *status
|
||||
defs[listRef] = *l
|
||||
}
|
||||
|
||||
// Delete base schemas
|
||||
delete(defs, baseRef)
|
||||
delete(defs, baseListRef)
|
||||
delete(defs, baseStatusRef)
|
||||
|
||||
// Disable strategic-merge-patch+json
|
||||
for p, op := range sw.Paths.Paths {
|
||||
if op.Patch != nil && len(op.Patch.Consumes) > 0 {
|
||||
var out []string
|
||||
@@ -321,11 +376,20 @@ func buildPostProcessV2(kindSchemas map[string]string) func(*spec.Swagger) (*spe
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite all $ref in the document
|
||||
out, err := rewriteDocRefs(sw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sw, json.Unmarshal(out, sw)
|
||||
if err := json.Unmarshal(out, sw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for name := range sw.Definitions {
|
||||
s := sw.Definitions[name]
|
||||
sanitizeForV2(&s)
|
||||
sw.Definitions[name] = s
|
||||
}
|
||||
|
||||
return sw, nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user