diff --git a/.mise.toml b/.mise.toml index a21d5107b4..73652c1716 100644 --- a/.mise.toml +++ b/.mise.toml @@ -110,7 +110,6 @@ run = "controller-gen object:headerFile='../hack/generators/boilerplate.go.txt' [tasks.test-crdsvalidation] description = "Run CRD validation tests" -dir = "./{{env.DIR}}" # Update test paths below if more API groups are added run = ''' export KUBEBUILDER_ASSETS=$(setup-envtest use -p path) diff --git a/api/x-konnect/v1alpha1/konnecteventcontrolplane_types.go b/api/x-konnect/v1alpha1/konnecteventcontrolplane_types.go index bb8a29e265..acc6aaea3f 100644 --- a/api/x-konnect/v1alpha1/konnecteventcontrolplane_types.go +++ b/api/x-konnect/v1alpha1/konnecteventcontrolplane_types.go @@ -62,6 +62,7 @@ type KonnectEventControlPlaneAPISpec struct { // // // +optional + // +kubebuilder:validation:MaxProperties=50 Labels Labels `json:"labels,omitempty"` // The minimum runtime version supported by the API. diff --git a/api/x-konnect/v1alpha1/portal_types.go b/api/x-konnect/v1alpha1/portal_types.go index cac116e92a..82762d1919 100644 --- a/api/x-konnect/v1alpha1/portal_types.go +++ b/api/x-konnect/v1alpha1/portal_types.go @@ -129,6 +129,7 @@ type PortalAPISpec struct { // // // +optional + // +kubebuilder:validation:MaxProperties=50 Labels LabelsUpdate `json:"labels,omitempty"` // The name of the portal, used to distinguish it from other portals. diff --git a/api/x-konnect/v1alpha1/schema_types.go b/api/x-konnect/v1alpha1/schema_types.go index 505809fabb..de09fd6e67 100644 --- a/api/x-konnect/v1alpha1/schema_types.go +++ b/api/x-konnect/v1alpha1/schema_types.go @@ -166,6 +166,7 @@ type CreateDcrProviderRequestAuth0 struct { // // // +optional + // +kubebuilder:validation:MaxProperties=50 Labels Labels `json:"labels,omitempty"` // The name of the DCR provider. // This is used to identify the DCR provider in the Konnect UI. @@ -213,6 +214,7 @@ type CreateDcrProviderRequestAzureAd struct { // // // +optional + // +kubebuilder:validation:MaxProperties=50 Labels Labels `json:"labels,omitempty"` // The name of the DCR provider. // This is used to identify the DCR provider in the Konnect UI. @@ -260,6 +262,7 @@ type CreateDcrProviderRequestCurity struct { // // // +optional + // +kubebuilder:validation:MaxProperties=50 Labels Labels `json:"labels,omitempty"` // The name of the DCR provider. // This is used to identify the DCR provider in the Konnect UI. @@ -306,6 +309,7 @@ type CreateDcrProviderRequestHTTP struct { // // // +optional + // +kubebuilder:validation:MaxProperties=50 Labels Labels `json:"labels,omitempty"` // The name of the DCR provider. // This is used to identify the DCR provider in the Konnect UI. @@ -352,6 +356,7 @@ type CreateDcrProviderRequestOkta struct { // // // +optional + // +kubebuilder:validation:MaxProperties=50 Labels Labels `json:"labels,omitempty"` // The name of the DCR provider. // This is used to identify the DCR provider in the Konnect UI. @@ -459,12 +464,26 @@ type GatewayDescription string // GatewayName The name of the Gateway. type GatewayName string +// LabelsValue is the value type for Labels. +// +// +kubebuilder:validation:MinLength=1 +// +kubebuilder:validation:MaxLength=63 +// +kubebuilder:validation:Pattern=`^[a-z0-9A-Z]{1}([a-z0-9A-Z-._]*[a-z0-9A-Z]+)?$` +type LabelsValue string + // Labels store metadata of an entity that can be used for filtering an entity // list or for searching across entity types. // // Keys must be of length 1-63 characters, and cannot start with "kong", // "konnect", "mesh", "kic", or "_". -type Labels map[string]string +type Labels map[string]LabelsValue + +// LabelsUpdateValue is the value type for LabelsUpdate. +// +// +kubebuilder:validation:MinLength=1 +// +kubebuilder:validation:MaxLength=63 +// +kubebuilder:validation:Pattern=`^[a-z0-9A-Z]{1}([a-z0-9A-Z-._]*[a-z0-9A-Z]+)?$` +type LabelsUpdateValue string // LabelsUpdate Labels store metadata of an entity that can be used for // filtering an entity list or for searching across entity types. @@ -473,7 +492,7 @@ type Labels map[string]string // // Keys must be of length 1-63 characters, and cannot start with "kong", // "konnect", "mesh", "kic", or "_". -type LabelsUpdate map[string]string +type LabelsUpdate map[string]LabelsUpdateValue // MinRuntimeVersion The minimum runtime version supported by the API. // This is the lowest version of the data plane diff --git a/config/crd/kong-operator/x-konnect.konghq.com_dcrproviders.yaml b/config/crd/kong-operator/x-konnect.konghq.com_dcrproviders.yaml index 9435fd9019..08838267d6 100644 --- a/config/crd/kong-operator/x-konnect.konghq.com_dcrproviders.yaml +++ b/config/crd/kong-operator/x-konnect.konghq.com_dcrproviders.yaml @@ -112,6 +112,10 @@ spec: type: string labels: additionalProperties: + description: LabelsValue is the value type for Labels. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9A-Z]{1}([a-z0-9A-Z-._]*[a-z0-9A-Z]+)?$ type: string description: |- Labels store metadata of an entity that can be used for filtering an entity @@ -178,6 +182,10 @@ spec: type: string labels: additionalProperties: + description: LabelsValue is the value type for Labels. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9A-Z]{1}([a-z0-9A-Z-._]*[a-z0-9A-Z]+)?$ type: string description: |- Labels store metadata of an entity that can be used for filtering an entity @@ -244,6 +252,10 @@ spec: type: string labels: additionalProperties: + description: LabelsValue is the value type for Labels. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9A-Z]{1}([a-z0-9A-Z-._]*[a-z0-9A-Z]+)?$ type: string description: |- Labels store metadata of an entity that can be used for filtering an entity @@ -339,6 +351,10 @@ spec: type: string labels: additionalProperties: + description: LabelsValue is the value type for Labels. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9A-Z]{1}([a-z0-9A-Z-._]*[a-z0-9A-Z]+)?$ type: string description: |- Labels store metadata of an entity that can be used for filtering an entity @@ -396,6 +412,10 @@ spec: type: string labels: additionalProperties: + description: LabelsValue is the value type for Labels. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9A-Z]{1}([a-z0-9A-Z-._]*[a-z0-9A-Z]+)?$ type: string description: |- Labels store metadata of an entity that can be used for filtering an entity diff --git a/config/crd/kong-operator/x-konnect.konghq.com_konnecteventcontrolplanes.yaml b/config/crd/kong-operator/x-konnect.konghq.com_konnecteventcontrolplanes.yaml index fb06c5151e..0609697f12 100644 --- a/config/crd/kong-operator/x-konnect.konghq.com_konnecteventcontrolplanes.yaml +++ b/config/crd/kong-operator/x-konnect.konghq.com_konnecteventcontrolplanes.yaml @@ -68,6 +68,10 @@ spec: type: string labels: additionalProperties: + description: LabelsValue is the value type for Labels. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9A-Z]{1}([a-z0-9A-Z-._]*[a-z0-9A-Z]+)?$ type: string description: |- Labels store metadata of an entity that can be used for filtering an entity diff --git a/config/crd/kong-operator/x-konnect.konghq.com_portals.yaml b/config/crd/kong-operator/x-konnect.konghq.com_portals.yaml index 9096870856..e053fda8a2 100644 --- a/config/crd/kong-operator/x-konnect.konghq.com_portals.yaml +++ b/config/crd/kong-operator/x-konnect.konghq.com_portals.yaml @@ -187,6 +187,10 @@ spec: type: string labels: additionalProperties: + description: LabelsUpdateValue is the value type for LabelsUpdate. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9A-Z]{1}([a-z0-9A-Z-._]*[a-z0-9A-Z]+)?$ type: string description: |- Labels store metadata of an entity that can be used for filtering an entity diff --git a/crd-from-oas/pkg/generator/generator.go b/crd-from-oas/pkg/generator/generator.go index 683dc9eb81..c5c3f164f7 100644 --- a/crd-from-oas/pkg/generator/generator.go +++ b/crd-from-oas/pkg/generator/generator.go @@ -228,6 +228,25 @@ func (g *Generator) generateSchemaTypes(refs map[string]bool, parsed *parser.Par fmt.Fprintf(&buf, "\t// %sDisabled sets %s as disabled.\n", goName, goName) fmt.Fprintf(&buf, "\t%sDisabled %s = \"Disabled\"\n", goName, goName) fmt.Fprintf(&buf, ")\n\n") + + case schema.AdditionalProperties != nil: + // Map type with value constraints: generate a dedicated value type + // with native kubebuilder markers, then define the map using it. + valueTypeName := refName + "Value" + valueBaseType := propertyToGoBaseType(schema.AdditionalProperties) + + fmt.Fprintf(&buf, "// %s is the value type for %s.\n", valueTypeName, refName) + if markers := valueTypeMarkers(schema.AdditionalProperties); len(markers) > 0 { + buf.WriteString("//\n") + for _, marker := range markers { + fmt.Fprintf(&buf, "// %s\n", marker) + } + } + fmt.Fprintf(&buf, "type %s %s\n\n", valueTypeName, valueBaseType) + + buf.WriteString(comment) + fmt.Fprintf(&buf, "type %s map[string]%s\n\n", refName, valueTypeName) + default: // Generate based on the schema's actual type buf.WriteString(comment) diff --git a/crd-from-oas/pkg/generator/generator_test.go b/crd-from-oas/pkg/generator/generator_test.go index d205d17192..46aabbc359 100644 --- a/crd-from-oas/pkg/generator/generator_test.go +++ b/crd-from-oas/pkg/generator/generator_test.go @@ -11,61 +11,6 @@ import ( "github.com/kong/kong-operator/v2/crd-from-oas/pkg/parser" ) -func TestObjectRefTypeName(t *testing.T) { - tests := []struct { - name string - commonTypes *config.CommonTypesConfig - want string - }{ - { - name: "nil commonTypes returns ObjectRef", - commonTypes: nil, - want: "ObjectRef", - }, - { - name: "generate true returns ObjectRef", - commonTypes: &config.CommonTypesConfig{ - ObjectRef: &config.ObjectRefConfig{ - Generate: new(true), - }, - }, - want: "ObjectRef", - }, - { - name: "import with alias returns qualified name", - commonTypes: &config.CommonTypesConfig{ - ObjectRef: &config.ObjectRefConfig{ - Import: &config.ImportConfig{ - Path: "github.com/kong/kong-operator/v2/api/common/v1alpha1", - Alias: "commonv1alpha1", - }, - }, - }, - want: "commonv1alpha1.ObjectRef", - }, - { - name: "import without alias uses last path segment", - commonTypes: &config.CommonTypesConfig{ - ObjectRef: &config.ObjectRefConfig{ - Import: &config.ImportConfig{ - Path: "github.com/kong/kong-operator/v2/api/common/v1alpha1", - }, - }, - }, - want: "v1alpha1.ObjectRef", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - g := NewGenerator(Config{ - CommonTypes: tc.commonTypes, - }) - assert.Equal(t, tc.want, g.objectRefTypeName()) - }) - } -} - func TestGoType_ObjectRef(t *testing.T) { t.Run("without import uses ObjectRef", func(t *testing.T) { g := NewGenerator(Config{}) @@ -809,35 +754,6 @@ func TestGenerateSDKOps_NormalizesBooleanFields(t *testing.T) { assert.NotContains(t, content, "if err := normalizeSDKOpsBoolFields(payload); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to normalize PortalAPISpec for CreatePortal: %w\", err)") } -func TestGenerateSDKOpsTest_AssertsNormalizedPayload(t *testing.T) { - g := NewGenerator(Config{APIVersion: "v1alpha1"}) - schema := &parser.Schema{ - Properties: []*parser.Property{ - { - Name: "name", - Type: "string", - }, - { - Name: "rbac_enabled", - Type: "boolean", - }, - }, - } - opsConfig := &config.EntityOpsConfig{ - Ops: map[string]*config.OpConfig{ - "create": { - Path: "github.com/Kong/sdk-konnect-go/models/components.CreatePortal", - }, - }, - } - - content, err := g.generateSDKOpsTest("Portal", schema, opsConfig) - require.NoError(t, err) - assert.Contains(t, content, `RBACEnabled: "Enabled"`) - assert.Contains(t, content, `require.Equal(t, true, payload["rbac_enabled"])`) - assert.Contains(t, content, `require.Equal(t, "test-value", payload["name"])`) -} - func TestParseSDKTypePath(t *testing.T) { tests := []struct { name string @@ -893,3 +809,86 @@ func TestParseSDKTypePath(t *testing.T) { }) } } + +func TestGenerateSchemaTypes_MapWithValueTypes(t *testing.T) { + g := NewGenerator(Config{ + APIVersion: "v1alpha1", + }) + + parsed := &parser.ParsedSpec{ + Schemas: map[string]*parser.Schema{ + "Labels": { + Name: "Labels", + Description: "Labels store metadata.", + Type: "object", + MaxProperties: func() *int64 { v := int64(50); return &v }(), + AdditionalProperties: &parser.Property{ + Type: "string", + MinLength: func() *int64 { v := int64(1); return &v }(), + MaxLength: func() *int64 { v := int64(63); return &v }(), + Pattern: `^[a-z0-9A-Z]+$`, + }, + }, + "LabelsUpdate": { + Name: "LabelsUpdate", + Description: "LabelsUpdate store metadata.", + Type: "object", + AdditionalProperties: &parser.Property{ + Type: "string", + MinLength: func() *int64 { v := int64(1); return &v }(), + MaxLength: func() *int64 { v := int64(63); return &v }(), + Pattern: `^[a-z0-9A-Z]+$`, + }, + }, + }, + } + + refs := map[string]bool{ + "Labels": true, + "LabelsUpdate": true, + } + + content := g.generateSchemaTypes(refs, parsed) + + // Labels should generate a value type with native markers, then a map type using it + assert.Contains(t, content, "type LabelsValue string") + assert.Contains(t, content, "type Labels map[string]LabelsValue") + assert.Contains(t, content, "+kubebuilder:validation:MinLength=1") + assert.Contains(t, content, "+kubebuilder:validation:MaxLength=63") + assert.Contains(t, content, "+kubebuilder:validation:Pattern=`^[a-z0-9A-Z]+$`") + + // LabelsUpdate should also generate a value type + assert.Contains(t, content, "type LabelsUpdateValue string") + assert.Contains(t, content, "type LabelsUpdate map[string]LabelsUpdateValue") + + // No CEL XValidation rules or MaxProperties on the type (goes on the field) + assert.NotContains(t, content, "XValidation") + assert.NotContains(t, content, "MaxProperties") +} + +func TestGenerateSchemaTypes_NoValueTypeForNonMapTypes(t *testing.T) { + g := NewGenerator(Config{ + APIVersion: "v1alpha1", + }) + + parsed := &parser.ParsedSpec{ + Schemas: map[string]*parser.Schema{ + "GatewayName": { + Name: "GatewayName", + Description: "The name of the Gateway.", + Type: "string", + }, + }, + } + + refs := map[string]bool{ + "GatewayName": true, + } + + content := g.generateSchemaTypes(refs, parsed) + + assert.Contains(t, content, "type GatewayName string") + assert.NotContains(t, content, "Value") + assert.NotContains(t, content, "XValidation") + assert.NotContains(t, content, "MaxProperties") +} diff --git a/crd-from-oas/pkg/generator/markers.go b/crd-from-oas/pkg/generator/markers.go index 8967356bb6..c6424f0285 100644 --- a/crd-from-oas/pkg/generator/markers.go +++ b/crd-from-oas/pkg/generator/markers.go @@ -6,12 +6,13 @@ const ( kbOptional = "+optional" kbRequired = "+required" - kbValidationMaxLengthFmt = "+kubebuilder:validation:MaxLength=%d" - kbValidationMinLengthFmt = "+kubebuilder:validation:MinLength=%d" - kbValidationPatternFmt = "+kubebuilder:validation:Pattern=`%s`" - kbValidationMinimumFmt = "+kubebuilder:validation:Minimum=%v" - kbValidationMaximumFmt = "+kubebuilder:validation:Maximum=%v" - kbValidationEnumFmt = "+kubebuilder:validation:Enum=%s" + kbValidationMaxLengthFmt = "+kubebuilder:validation:MaxLength=%d" + kbValidationMinLengthFmt = "+kubebuilder:validation:MinLength=%d" + kbValidationPatternFmt = "+kubebuilder:validation:Pattern=`%s`" + kbValidationMinimumFmt = "+kubebuilder:validation:Minimum=%v" + kbValidationMaximumFmt = "+kubebuilder:validation:Maximum=%v" + kbValidationEnumFmt = "+kubebuilder:validation:Enum=%s" + kbValidationMaxPropertiesFmt = "+kubebuilder:validation:MaxProperties=%d" kbDefaultStringFmt = "+kubebuilder:default=%s" ) @@ -19,11 +20,12 @@ const ( func markerOptional() string { return kbOptional } func markerRequired() string { return kbRequired } -func markerValidationMaxLength(v int) string { return fmt.Sprintf(kbValidationMaxLengthFmt, v) } -func markerValidationMinLength(v int) string { return fmt.Sprintf(kbValidationMinLengthFmt, v) } -func markerValidationPattern(v string) string { return fmt.Sprintf(kbValidationPatternFmt, v) } -func markerValidationMinimum(v any) string { return fmt.Sprintf(kbValidationMinimumFmt, v) } -func markerValidationMaximum(v any) string { return fmt.Sprintf(kbValidationMaximumFmt, v) } -func markerValidationEnum(v string) string { return fmt.Sprintf(kbValidationEnumFmt, v) } +func markerValidationMaxLength(v int) string { return fmt.Sprintf(kbValidationMaxLengthFmt, v) } +func markerValidationMinLength(v int) string { return fmt.Sprintf(kbValidationMinLengthFmt, v) } +func markerValidationPattern(v string) string { return fmt.Sprintf(kbValidationPatternFmt, v) } +func markerValidationMinimum(v any) string { return fmt.Sprintf(kbValidationMinimumFmt, v) } +func markerValidationMaximum(v any) string { return fmt.Sprintf(kbValidationMaximumFmt, v) } +func markerValidationEnum(v string) string { return fmt.Sprintf(kbValidationEnumFmt, v) } +func markerValidationMaxProperties(v int) string { return fmt.Sprintf(kbValidationMaxPropertiesFmt, v) } func markerDefaultString(v string) string { return fmt.Sprintf(kbDefaultStringFmt, v) } diff --git a/crd-from-oas/pkg/generator/tags.go b/crd-from-oas/pkg/generator/tags.go index 536e81cd99..13d89b22fa 100644 --- a/crd-from-oas/pkg/generator/tags.go +++ b/crd-from-oas/pkg/generator/tags.go @@ -87,6 +87,11 @@ func KubebuilderTags(prop *parser.Property, entityName string, fieldConfig *conf } } + // Map MaxProperties constraint (applies to both ref and inline map types) + if prop.MaxProperties != nil { + tags = append(tags, markerValidationMaxProperties(int(*prop.MaxProperties))) + } + // Add custom validations from config if fieldConfig != nil { customValidations := fieldConfig.GetFieldValidations(entityName, prop.Name) @@ -95,3 +100,37 @@ func KubebuilderTags(prop *parser.Property, entityName string, fieldConfig *conf return tags } + +// valueTypeMarkers generates kubebuilder validation markers for a map value type +// based on the additionalProperties constraints from the OpenAPI spec. +func valueTypeMarkers(ap *parser.Property) []string { + var markers []string + if ap.Type == "string" { + if ap.MinLength != nil { + markers = append(markers, markerValidationMinLength(int(*ap.MinLength))) + } + if ap.MaxLength != nil { + markers = append(markers, markerValidationMaxLength(int(*ap.MaxLength))) + } + if ap.Pattern != "" { + markers = append(markers, markerValidationPattern(ap.Pattern)) + } + } + return markers +} + +// propertyToGoBaseType returns the Go base type for a simple property. +func propertyToGoBaseType(prop *parser.Property) string { + switch prop.Type { + case "string": + return "string" + case "integer": + return "int" + case "boolean": + return "bool" + case "number": + return "float64" + default: + return "string" + } +} diff --git a/crd-from-oas/pkg/generator/tags_test.go b/crd-from-oas/pkg/generator/tags_test.go index 0008d62ffd..95c21ca570 100644 --- a/crd-from-oas/pkg/generator/tags_test.go +++ b/crd-from-oas/pkg/generator/tags_test.go @@ -240,6 +240,147 @@ func TestKubebuilderTags(t *testing.T) { } } +func TestKubebuilderTags_MapType(t *testing.T) { + tests := []struct { + name string + prop *parser.Property + expected []string + }{ + { + name: "map field with MaxProperties", + prop: &parser.Property{ + Name: "labels", + Type: "object", + Required: false, + MaxProperties: new(int64(50)), + AdditionalProperties: &parser.Property{ + Name: "value", + Type: "string", + }, + }, + expected: []string{ + "+optional", + "+kubebuilder:validation:MaxProperties=50", + }, + }, + { + name: "map ref field with MaxProperties", + prop: &parser.Property{ + Name: "labels", + Type: "object", + Required: false, + RefName: "LabelsUpdate", + MaxProperties: new(int64(50)), + AdditionalProperties: &parser.Property{ + Name: "value", + Type: "string", + MinLength: new(int64(1)), + MaxLength: new(int64(63)), + }, + }, + expected: []string{ + "+optional", + "+kubebuilder:validation:MaxProperties=50", + }, + }, + { + name: "map field without MaxProperties", + prop: &parser.Property{ + Name: "tags", + Type: "object", + Required: false, + AdditionalProperties: &parser.Property{ + Name: "value", + Type: "string", + }, + }, + expected: []string{ + "+optional", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := KubebuilderTags(tt.prop, "TestEntity", nil) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestValueTypeMarkers(t *testing.T) { + tests := []struct { + name string + prop *parser.Property + expected []string + }{ + { + name: "string with all constraints", + prop: &parser.Property{ + Type: "string", + MinLength: new(int64(1)), + MaxLength: new(int64(63)), + Pattern: `^[a-z0-9A-Z]+$`, + }, + expected: []string{ + "+kubebuilder:validation:MinLength=1", + "+kubebuilder:validation:MaxLength=63", + "+kubebuilder:validation:Pattern=`^[a-z0-9A-Z]+$`", + }, + }, + { + name: "string with only maxLength", + prop: &parser.Property{ + Type: "string", + MaxLength: new(int64(256)), + }, + expected: []string{ + "+kubebuilder:validation:MaxLength=256", + }, + }, + { + name: "string with no constraints", + prop: &parser.Property{Type: "string"}, + expected: nil, + }, + { + name: "non-string type returns nil", + prop: &parser.Property{ + Type: "integer", + MinLength: new(int64(1)), + }, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := valueTypeMarkers(tt.prop) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestPropertyToGoBaseType(t *testing.T) { + tests := []struct { + propType string + expected string + }{ + {"string", "string"}, + {"integer", "int"}, + {"boolean", "bool"}, + {"number", "float64"}, + {"unknown", "string"}, + } + + for _, tt := range tests { + t.Run(tt.propType, func(t *testing.T) { + result := propertyToGoBaseType(&parser.Property{Type: tt.propType}) + assert.Equal(t, tt.expected, result) + }) + } +} + func TestKubebuilderTags_WithFieldConfig(t *testing.T) { tests := []struct { name string diff --git a/crd-from-oas/pkg/parser/parser.go b/crd-from-oas/pkg/parser/parser.go index cf59462ed0..054d8aa91d 100644 --- a/crd-from-oas/pkg/parser/parser.go +++ b/crd-from-oas/pkg/parser/parser.go @@ -33,13 +33,14 @@ type Property struct { ReadOnly bool // Validations - MinLength *int64 - MaxLength *int64 - Minimum *float64 - Maximum *float64 - Pattern string - Enum []any - Default any + MinLength *int64 + MaxLength *int64 + MaxProperties *int64 + Minimum *float64 + Maximum *float64 + Pattern string + Enum []any + Default any // Reference info RefName string // If this is a $ref, the referenced schema name @@ -66,6 +67,10 @@ type Schema struct { Dependencies []*Dependency // Parent resource dependencies from path parameters OneOf []*Property // Root-level oneOf variants (for union type schemas) Items *Property // For array-type schemas, the items type + + // Map type support + AdditionalProperties *Property // For object types with additionalProperties (map value schema) + MaxProperties *int64 // Maximum number of map entries } // ParsedSpec is the result of parsing an OpenAPI spec via ParsePaths. @@ -319,6 +324,17 @@ func (p *Parser) parseSchema(name string, schemaValue *openapi3.Schema) *Schema schema.Items = ParseProperty("items", schemaValue.Items, 0, p.visited) } + // Handle additionalProperties (map value schema) + if schemaValue.AdditionalProperties.Schema != nil { + schema.AdditionalProperties = ParseProperty("value", schemaValue.AdditionalProperties.Schema, 0, p.visited) + } + + // Handle maxProperties + if schemaValue.MaxProps != nil { + maxProps := int64(*schemaValue.MaxProps) + schema.MaxProperties = &maxProps + } + // Handle root-level oneOf (union type schemas) if len(schemaValue.OneOf) > 0 { for _, oneOfRef := range schemaValue.OneOf { diff --git a/crd-from-oas/pkg/parser/property.go b/crd-from-oas/pkg/parser/property.go index e414e9d83d..6c26ba57f7 100644 --- a/crd-from-oas/pkg/parser/property.go +++ b/crd-from-oas/pkg/parser/property.go @@ -88,6 +88,12 @@ func ParseProperty(name string, schemaRef *openapi3.SchemaRef, depth int, visite }) } + // Handle maxProperties (map size constraint) + if schemaValue.MaxProps != nil { + maxProps := int64(*schemaValue.MaxProps) + prop.MaxProperties = &maxProps + } + // Handle additionalProperties (map types) if schemaValue.AdditionalProperties.Schema != nil { prop.AdditionalProperties = ParseProperty("value", schemaValue.AdditionalProperties.Schema, depth+1, visited) diff --git a/crd-from-oas/pkg/parser/property_test.go b/crd-from-oas/pkg/parser/property_test.go index 5cdeb6069a..1d45329c44 100644 --- a/crd-from-oas/pkg/parser/property_test.go +++ b/crd-from-oas/pkg/parser/property_test.go @@ -443,6 +443,40 @@ func TestParseProperty_DeeplyNestedObject(t *testing.T) { assert.Equal(t, "string", value.Type) } +func TestParseProperty_MapTypeWithValidations(t *testing.T) { + maxProps := uint64(50) + maxLen := uint64(63) + schemaRef := &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"object"}, + MaxProps: &maxProps, + AdditionalProperties: openapi3.AdditionalProperties{ + Schema: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + MinLength: 1, + MaxLength: &maxLen, + Pattern: `^[a-z0-9A-Z]{1}([a-z0-9A-Z-._]*[a-z0-9A-Z]+)?$`, + }, + }, + }, + }, + } + + prop := ParseProperty("labels", schemaRef, 0, make(map[string]bool)) + + assert.Equal(t, "object", prop.Type) + require.NotNil(t, prop.MaxProperties) + assert.Equal(t, int64(50), *prop.MaxProperties) + require.NotNil(t, prop.AdditionalProperties) + assert.Equal(t, "string", prop.AdditionalProperties.Type) + require.NotNil(t, prop.AdditionalProperties.MinLength) + assert.Equal(t, int64(1), *prop.AdditionalProperties.MinLength) + require.NotNil(t, prop.AdditionalProperties.MaxLength) + assert.Equal(t, int64(63), *prop.AdditionalProperties.MaxLength) + assert.Equal(t, `^[a-z0-9A-Z]{1}([a-z0-9A-Z-._]*[a-z0-9A-Z]+)?$`, prop.AdditionalProperties.Pattern) +} + func TestParseProperty_AllValidations(t *testing.T) { schemaRef := &openapi3.SchemaRef{ Value: &openapi3.Schema{