Skip to content
Open
1 change: 1 addition & 0 deletions apis/parameters/v1alpha1/parametersdefinition_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ type IniConfig struct {
type ParametersSchema struct {
// Specifies the top-level key in the 'configSchema.cue' that organizes the validation rules for parameters.
// This key must exist within the CUE script defined in 'configSchema.cue'.
// If not specified, the first schema found will be used.
//
// +optional
TopLevelKey string `json:"topLevelKey,omitempty"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ spec:
description: |-
Specifies the top-level key in the 'configSchema.cue' that organizes the validation rules for parameters.
This key must exist within the CUE script defined in 'configSchema.cue'.
If not specified, the first schema found will be used.
type: string
type: object
reloadAction:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ spec:
description: |-
Specifies the top-level key in the 'configSchema.cue' that organizes the validation rules for parameters.
This key must exist within the CUE script defined in 'configSchema.cue'.
If not specified, the first schema found will be used.
type: string
type: object
reloadAction:
Expand Down
3 changes: 2 additions & 1 deletion docs/developer_docs/api-reference/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -3241,7 +3241,8 @@ string
<td>
<em>(Optional)</em>
<p>Specifies the top-level key in the &lsquo;configSchema.cue&rsquo; that organizes the validation rules for parameters.
This key must exist within the CUE script defined in &lsquo;configSchema.cue&rsquo;.</p>
This key must exist within the CUE script defined in &lsquo;configSchema.cue&rsquo;.
If not specified, the first schema found will be used.</p>
</td>
</tr>
<tr>
Expand Down
62 changes: 62 additions & 0 deletions pkg/parameters/openapi/cue_gen_openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,15 @@ func deReferenceSchema(props *apiextv1.JSONSchemaProps, resolveFn func(path stri
return err
}
}
for i := range props.AllOf {
schemaProps := util.ToPointer(props.AllOf[i])
schemaProps, err = oneProps(schemaProps)
if err != nil {
return err
}
props.AllOf[i] = *schemaProps
}
expandAllOf(props)
for key := range props.Properties {
schemaProps := util.ToPointer(props.Properties[key])
schemaProps, err = oneProps(schemaProps)
Expand All @@ -174,3 +183,56 @@ func deReferenceSchema(props *apiextv1.JSONSchemaProps, resolveFn func(path stri
}
return nil
}

func expandAllOf(props *apiextv1.JSONSchemaProps) {
if len(props.AllOf) == 0 {
return
}
allOf := props.AllOf
props.AllOf = nil
for _, schemaProps := range allOf {
if schemaProps.Type != "" && schemaProps.Type != SchemaStructType {
props.AllOf = append(props.AllOf, schemaProps)
continue
}
mergeSchemaProps(props, schemaProps)
}
}

func mergeSchemaProps(dst *apiextv1.JSONSchemaProps, src apiextv1.JSONSchemaProps) {
if dst.Type == "" {
dst.Type = src.Type
}
if len(src.Properties) > 0 {
if dst.Properties == nil {
dst.Properties = map[string]apiextv1.JSONSchemaProps{}
}
for key, value := range src.Properties {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] This is a lossy approximation of allOf, not an equivalent expansion. allOf requires every branch to apply simultaneously, but duplicate properties are handled as first-wins here, so constraints from later branches are silently discarded. The same loss occurs for object-level keywords because mergeSchemaProps copies only type, properties, additionalProperties, and required; fields such as description, maxProperties, oneOf, and not disappear. A CUE field whose lower and upper bounds come from separate branches therefore keeps only one bound in the generated schema and can accept a value rejected by CUE. Since this result is exposed as SchemaInJSON, this changes the API semantics and is a blocker.

if _, ok := dst.Properties[key]; !ok {
dst.Properties[key] = value
}
}
}
if dst.AdditionalProperties == nil {
dst.AdditionalProperties = src.AdditionalProperties
}
dst.Required = mergeRequired(dst.Required, src.Required)
}

func mergeRequired(dst, src []string) []string {
if len(src) == 0 {
return dst
}
required := make(map[string]struct{}, len(dst)+len(src))
for _, item := range dst {
required[item] = struct{}{}
}
for _, item := range src {
if _, ok := required[item]; ok {
continue
}
required[item] = struct{}{}
dst = append(dst, item)
}
return dst
}
99 changes: 28 additions & 71 deletions pkg/parameters/openapi/cue_gen_openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,88 +23,45 @@ import (
"bytes"
"encoding/json"
"os"
"reflect"
"testing"

"github.com/google/go-cmp/cmp"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/apecloud/kubeblocks/pkg/parameters/core"
"github.com/apecloud/kubeblocks/test/testdata"
)

func TestGenerateOpenApiSchema(t *testing.T) {
type args struct {
cueFile string
schemaType string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{{
name: "normal_test",
args: args{
cueFile: "test_import_type.cue",
schemaType: "Exemplar",
},
want: "test_import_type.json",
wantErr: false,
}, {
name: "normal_test",
args: args{
cueFile: "mysql_openapi.cue",
schemaType: "MysqlParameter",
},
want: "mysql_openapi.json",
wantErr: false,
}, {
// name: "normal_test",
// args: args{
// cueFile: "mysql_openapi_v2.cue",
// schemaType: "MysqlSchema",
// },
// want: "mysql_openapi_v2.json",
// wantErr: false,
// }, {
name: "normal_with_not_empty",
args: args{
cueFile: "mysql_openapi.cue",
schemaType: "",
},
want: "mysql_openapi.json",
wantErr: false,
}, {
name: "pg14_openapi",
args: args{
cueFile: "pg14.cue",
schemaType: "PGPameter",
},
want: "pg14_openapi.json",
wantErr: false,
}, {
name: "failed_test",
args: args{
cueFile: "mysql.cue",
schemaType: "NotType",
},
want: "mysql_openapi_failed_not_exist",
wantErr: true,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := runOpenAPITest(tt.args.cueFile, tt.args.schemaType)
if (err != nil) != tt.wantErr {
t.Errorf("GenerateOpenAPISchema() error = %v, wantErr %v", err, tt.wantErr)
RegisterFailHandler(Fail)
RunSpecs(t, "OpenAPI Schema Suite")
}

var _ = Describe("GenerateOpenAPISchema", func() {
DescribeTable("generates schema from CUE definitions",
func(cueFile, schemaType, want string, wantErr bool) {
got, err := runOpenAPITest(cueFile, schemaType)
GinkgoWriter.Println(string(got))
Expect(err != nil).To(Equal(wantErr), "GenerateOpenAPISchema() error = %v, wantErr %v", err, wantErr)
if wantErr {
return
}
wantContent := getContentFromFile(tt.want)
if !reflect.DeepEqual(got, wantContent) {
t.Errorf("GenerateOpenAPISchema() diff: %s", cmp.Diff(wantContent, got))
}
})
}
}

wantContent := getContentFromFile(want)
Expect(got).To(Equal(wantContent), "GenerateOpenAPISchema() diff: %s", cmp.Diff(wantContent, got))
},
Entry("test_import_type", "test_import_type.cue", "Exemplar", "test_import_type.json", false),
Entry("normal_test with mysql", "mysql_openapi.cue", "MysqlParameter", "mysql_openapi.json", false),
Entry("normal_test with mysql2", "mysql_openapi_v2.cue", "MysqlSchema", "mysql_openapi_v2.json", false),
Entry("normal_with_not_empty", "mysql_openapi.cue", "", "mysql_openapi.json", false),
Entry("pg14_openapi", "pg14.cue", "PGPameter", "pg14_openapi.json", false),
Entry("multiple_schema_arch_a", "multiple_schema.cue", "archA", "multiple_schema_arch_a.json", false),
Entry("multiple_schema_combined", "multiple_schema.cue", "combined", "multiple_schema_combined.json", false),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] These new cases only combine disjoint optional string properties and compare the serialized golden shape, so they cannot detect the semantic loss introduced by expandAllOf. Please cover branches that constrain the same property and branches with object-level constraints, then assert accepted and rejected values against the generated schema (and the source CUE semantics), rather than testing only that allOf disappeared.

Entry("multiple_schema_embedded", "multiple_schema.cue", "embedded", "multiple_schema_combined.json", false),
Entry("failed_test", "mysql.cue", "NotType", "mysql_openapi_failed_not_exist", true),
)
})

func getContentFromFile(file string) []byte {
content, err := os.ReadFile(testdata.SubTestDataPath("./cue_testdata/" + file))
Expand Down
2 changes: 1 addition & 1 deletion pkg/parameters/openapi/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func DeReference(f *ast.File, rt *Runtime) func(path string) (*apiextv1.JSONSche

func fetchTypeDefine(value string, node ast.Node) ast.Node {
n := node
for _, field := range strings.Split(value, "/") {
for field := range strings.SplitSeq(value, "/") {
if field == "" {
continue
}
Expand Down
35 changes: 35 additions & 0 deletions test/testdata/cue_testdata/multiple_schema.cue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (C) 2022-2026 ApeCloud Co., Ltd
//
// This file is part of KubeBlocks project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

#shared: {
a?: string
}

#archA: #shared & {
b?: string
}

#archB: #shared & {
c?: string
}

#combined: #archA & #archB

#embedded: {
#archA
#archB
}
16 changes: 16 additions & 0 deletions test/testdata/cue_testdata/multiple_schema_arch_a.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"type": "object",
"properties": {
"spec": {
"type": "object",
"properties": {
"a": {
"type": "string"
},
"b": {
"type": "string"
}
}
}
}
}
19 changes: 19 additions & 0 deletions test/testdata/cue_testdata/multiple_schema_combined.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"type": "object",
"properties": {
"spec": {
"type": "object",
"properties": {
"a": {
"type": "string"
},
"b": {
"type": "string"
},
"c": {
"type": "string"
}
}
}
}
}
9 changes: 6 additions & 3 deletions test/testdata/cue_testdata/mysql_openapi_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
}
},
"mysqlld": {
"description": "mysql config validator",
"type": "object",
"required": [
"automatic_sp_privileges",
Expand Down Expand Up @@ -57,7 +56,9 @@
"minimum": 4096
},
"caching_sha2_password_private_key_path": {
"$ref": "#/components/schemas/MysqlSchema.mysqlld.KeyPath"
"description": "custom format, reference Regular expressions",
"type": "string",
"pattern": "^[a-z][a-zA-Z0-9]+.pem$"
},
"flush_time": {
"type": "integer",
Expand All @@ -67,7 +68,9 @@
},
"group_concat_max_len": {
"type": "integer",
"default": 1024
"default": 1024,
"minimum": 4,
"exclusiveMinimum": true
},
"gtid_mode": {
"type": "string",
Expand Down
Loading