Skip to content

Commit 1e9aa80

Browse files
committed
feat(crd-from-oas): add support for additionalProperties
1 parent e6769ee commit 1e9aa80

File tree

14 files changed

+388
-24
lines changed

14 files changed

+388
-24
lines changed

.mise.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ run = "golangci-lint-kube-api-linter run -v --config .golangci-kube-api.yaml ./a
116116

117117
[tasks.test-crdsvalidation]
118118
description = "Run CRD validation tests"
119-
dir = "./{{env.DIR}}"
120119
# Update test paths below if more API groups are added
121120
run = '''
122121
export KUBEBUILDER_ASSETS=$(setup-envtest use -p path)

api/x-konnect/v1alpha1/eventgateway_types.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/x-konnect/v1alpha1/portal_types.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/x-konnect/v1alpha1/schema_types.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,26 @@ type GatewayDescription string
66
// GatewayName The name of the Gateway.
77
type GatewayName string
88

9+
// LabelsValue is the value type for Labels.
10+
//
11+
// +kubebuilder:validation:MinLength=1
12+
// +kubebuilder:validation:MaxLength=63
13+
// +kubebuilder:validation:Pattern=`^[a-z0-9A-Z]{1}([a-z0-9A-Z-._]*[a-z0-9A-Z]+)?$`
14+
type LabelsValue string
15+
916
// Labels store metadata of an entity that can be used for filtering an entity
1017
// list or for searching across entity types.
1118
//
1219
// Keys must be of length 1-63 characters, and cannot start with "kong",
1320
// "konnect", "mesh", "kic", or "_".
14-
type Labels map[string]string
21+
type Labels map[string]LabelsValue
22+
23+
// LabelsUpdateValue is the value type for LabelsUpdate.
24+
//
25+
// +kubebuilder:validation:MinLength=1
26+
// +kubebuilder:validation:MaxLength=63
27+
// +kubebuilder:validation:Pattern=`^[a-z0-9A-Z]{1}([a-z0-9A-Z-._]*[a-z0-9A-Z]+)?$`
28+
type LabelsUpdateValue string
1529

1630
// LabelsUpdate Labels store metadata of an entity that can be used for
1731
// filtering an entity list or for searching across entity types.
@@ -20,7 +34,7 @@ type Labels map[string]string
2034
//
2135
// Keys must be of length 1-63 characters, and cannot start with "kong",
2236
// "konnect", "mesh", "kic", or "_".
23-
type LabelsUpdate map[string]string
37+
type LabelsUpdate map[string]LabelsUpdateValue
2438

2539
// MinRuntimeVersion The minimum runtime version supported by the API.
2640
// This is the lowest version of the data plane

config/crd/kong-operator/x-konnect.konghq.com_eventgateways.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ spec:
6666
type: string
6767
labels:
6868
additionalProperties:
69+
description: LabelsValue is the value type for Labels.
70+
maxLength: 63
71+
minLength: 1
72+
pattern: ^[a-z0-9A-Z]{1}([a-z0-9A-Z-._]*[a-z0-9A-Z]+)?$
6973
type: string
7074
description: |-
7175
Labels store metadata of an entity that can be used for filtering an entity

config/crd/kong-operator/x-konnect.konghq.com_portals.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ spec:
178178
type: string
179179
labels:
180180
additionalProperties:
181+
description: LabelsUpdateValue is the value type for LabelsUpdate.
182+
maxLength: 63
183+
minLength: 1
184+
pattern: ^[a-z0-9A-Z]{1}([a-z0-9A-Z-._]*[a-z0-9A-Z]+)?$
181185
type: string
182186
description: |-
183187
Labels store metadata of an entity that can be used for filtering an entity

crd-from-oas/pkg/generator/generator.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,8 @@ func (g *Generator) generateSchemaTypes(refs map[string]bool, parsed *parser.Par
187187
comment := formatSchemaComment(refName, schema.Description)
188188

189189
// Generate based on schema type
190-
if len(schema.Properties) > 0 {
190+
switch {
191+
case len(schema.Properties) > 0:
191192
// It's an object type - generate a struct
192193
buf.WriteString(comment)
193194
fmt.Fprintf(&buf, "type %s struct {\n", refName)
@@ -198,7 +199,26 @@ func (g *Generator) generateSchemaTypes(refs map[string]bool, parsed *parser.Par
198199
fmt.Fprintf(&buf, "\t%s %s `json:\"%s\"`\n", goFieldName(prop.Name), g.goType(prop), jsonTag(prop))
199200
}
200201
buf.WriteString("}\n\n")
201-
} else {
202+
203+
case schema.AdditionalProperties != nil:
204+
// Map type with value constraints: generate a dedicated value type
205+
// with native kubebuilder markers, then define the map using it.
206+
valueTypeName := refName + "Value"
207+
valueBaseType := propertyToGoBaseType(schema.AdditionalProperties)
208+
209+
fmt.Fprintf(&buf, "// %s is the value type for %s.\n", valueTypeName, refName)
210+
if markers := valueTypeMarkers(schema.AdditionalProperties); len(markers) > 0 {
211+
buf.WriteString("//\n")
212+
for _, marker := range markers {
213+
fmt.Fprintf(&buf, "// %s\n", marker)
214+
}
215+
}
216+
fmt.Fprintf(&buf, "type %s %s\n\n", valueTypeName, valueBaseType)
217+
218+
buf.WriteString(comment)
219+
fmt.Fprintf(&buf, "type %s map[string]%s\n\n", refName, valueTypeName)
220+
221+
default:
202222
// Generate based on the schema's actual type
203223
buf.WriteString(comment)
204224
goType := schemaToGoType(schema)

crd-from-oas/pkg/generator/generator_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,89 @@ func TestFormatSchemaComment(t *testing.T) {
596596
}
597597
}
598598

599+
func TestGenerateSchemaTypes_MapWithValueTypes(t *testing.T) {
600+
g := NewGenerator(Config{
601+
APIVersion: "v1alpha1",
602+
})
603+
604+
parsed := &parser.ParsedSpec{
605+
Schemas: map[string]*parser.Schema{
606+
"Labels": {
607+
Name: "Labels",
608+
Description: "Labels store metadata.",
609+
Type: "object",
610+
MaxProperties: func() *int64 { v := int64(50); return &v }(),
611+
AdditionalProperties: &parser.Property{
612+
Type: "string",
613+
MinLength: func() *int64 { v := int64(1); return &v }(),
614+
MaxLength: func() *int64 { v := int64(63); return &v }(),
615+
Pattern: `^[a-z0-9A-Z]+$`,
616+
},
617+
},
618+
"LabelsUpdate": {
619+
Name: "LabelsUpdate",
620+
Description: "LabelsUpdate store metadata.",
621+
Type: "object",
622+
AdditionalProperties: &parser.Property{
623+
Type: "string",
624+
MinLength: func() *int64 { v := int64(1); return &v }(),
625+
MaxLength: func() *int64 { v := int64(63); return &v }(),
626+
Pattern: `^[a-z0-9A-Z]+$`,
627+
},
628+
},
629+
},
630+
}
631+
632+
refs := map[string]bool{
633+
"Labels": true,
634+
"LabelsUpdate": true,
635+
}
636+
637+
content := g.generateSchemaTypes(refs, parsed)
638+
639+
// Labels should generate a value type with native markers, then a map type using it
640+
assert.Contains(t, content, "type LabelsValue string")
641+
assert.Contains(t, content, "type Labels map[string]LabelsValue")
642+
assert.Contains(t, content, "+kubebuilder:validation:MinLength=1")
643+
assert.Contains(t, content, "+kubebuilder:validation:MaxLength=63")
644+
assert.Contains(t, content, "+kubebuilder:validation:Pattern=`^[a-z0-9A-Z]+$`")
645+
646+
// LabelsUpdate should also generate a value type
647+
assert.Contains(t, content, "type LabelsUpdateValue string")
648+
assert.Contains(t, content, "type LabelsUpdate map[string]LabelsUpdateValue")
649+
650+
// No CEL XValidation rules or MaxProperties on the type (goes on the field)
651+
assert.NotContains(t, content, "XValidation")
652+
assert.NotContains(t, content, "MaxProperties")
653+
}
654+
655+
func TestGenerateSchemaTypes_NoValueTypeForNonMapTypes(t *testing.T) {
656+
g := NewGenerator(Config{
657+
APIVersion: "v1alpha1",
658+
})
659+
660+
parsed := &parser.ParsedSpec{
661+
Schemas: map[string]*parser.Schema{
662+
"GatewayName": {
663+
Name: "GatewayName",
664+
Description: "The name of the Gateway.",
665+
Type: "string",
666+
},
667+
},
668+
}
669+
670+
refs := map[string]bool{
671+
"GatewayName": true,
672+
}
673+
674+
content := g.generateSchemaTypes(refs, parsed)
675+
676+
assert.Contains(t, content, "type GatewayName string")
677+
assert.NotContains(t, content, "Value")
678+
assert.NotContains(t, content, "XValidation")
679+
assert.NotContains(t, content, "MaxProperties")
680+
}
681+
599682
func TestParseSDKTypePath(t *testing.T) {
600683
tests := []struct {
601684
name string

crd-from-oas/pkg/generator/markers.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ const (
66
kbOptional = "+optional"
77
kbRequired = "+required"
88

9-
kbValidationMaxLengthFmt = "+kubebuilder:validation:MaxLength=%d"
10-
kbValidationMinLengthFmt = "+kubebuilder:validation:MinLength=%d"
11-
kbValidationPatternFmt = "+kubebuilder:validation:Pattern=`%s`"
12-
kbValidationMinimumFmt = "+kubebuilder:validation:Minimum=%v"
13-
kbValidationMaximumFmt = "+kubebuilder:validation:Maximum=%v"
14-
kbValidationEnumFmt = "+kubebuilder:validation:Enum=%s"
9+
kbValidationMaxLengthFmt = "+kubebuilder:validation:MaxLength=%d"
10+
kbValidationMinLengthFmt = "+kubebuilder:validation:MinLength=%d"
11+
kbValidationPatternFmt = "+kubebuilder:validation:Pattern=`%s`"
12+
kbValidationMinimumFmt = "+kubebuilder:validation:Minimum=%v"
13+
kbValidationMaximumFmt = "+kubebuilder:validation:Maximum=%v"
14+
kbValidationEnumFmt = "+kubebuilder:validation:Enum=%s"
15+
kbValidationMaxPropertiesFmt = "+kubebuilder:validation:MaxProperties=%d"
1516

1617
kbDefaultBoolFmt = "+kubebuilder:default=%t"
1718
kbDefaultStringFmt = "+kubebuilder:default=%s"
@@ -20,12 +21,13 @@ const (
2021
func markerOptional() string { return kbOptional }
2122
func markerRequired() string { return kbRequired }
2223

23-
func markerValidationMaxLength(v int) string { return fmt.Sprintf(kbValidationMaxLengthFmt, v) }
24-
func markerValidationMinLength(v int) string { return fmt.Sprintf(kbValidationMinLengthFmt, v) }
25-
func markerValidationPattern(v string) string { return fmt.Sprintf(kbValidationPatternFmt, v) }
26-
func markerValidationMinimum(v any) string { return fmt.Sprintf(kbValidationMinimumFmt, v) }
27-
func markerValidationMaximum(v any) string { return fmt.Sprintf(kbValidationMaximumFmt, v) }
28-
func markerValidationEnum(v string) string { return fmt.Sprintf(kbValidationEnumFmt, v) }
24+
func markerValidationMaxLength(v int) string { return fmt.Sprintf(kbValidationMaxLengthFmt, v) }
25+
func markerValidationMinLength(v int) string { return fmt.Sprintf(kbValidationMinLengthFmt, v) }
26+
func markerValidationPattern(v string) string { return fmt.Sprintf(kbValidationPatternFmt, v) }
27+
func markerValidationMinimum(v any) string { return fmt.Sprintf(kbValidationMinimumFmt, v) }
28+
func markerValidationMaximum(v any) string { return fmt.Sprintf(kbValidationMaximumFmt, v) }
29+
func markerValidationEnum(v string) string { return fmt.Sprintf(kbValidationEnumFmt, v) }
30+
func markerValidationMaxProperties(v int) string { return fmt.Sprintf(kbValidationMaxPropertiesFmt, v) }
2931

3032
func markerDefaultBool(v bool) string { return fmt.Sprintf(kbDefaultBoolFmt, v) }
3133
func markerDefaultString(v string) string { return fmt.Sprintf(kbDefaultStringFmt, v) }

crd-from-oas/pkg/generator/tags.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ func KubebuilderTags(prop *parser.Property, entityName string, fieldConfig *conf
7272
}
7373
}
7474

75+
// Map MaxProperties constraint (applies to both ref and inline map types)
76+
if prop.MaxProperties != nil {
77+
tags = append(tags, markerValidationMaxProperties(int(*prop.MaxProperties)))
78+
}
79+
7580
// Add custom validations from config
7681
if fieldConfig != nil {
7782
customValidations := fieldConfig.GetFieldValidations(entityName, prop.Name)
@@ -80,3 +85,37 @@ func KubebuilderTags(prop *parser.Property, entityName string, fieldConfig *conf
8085

8186
return tags
8287
}
88+
89+
// valueTypeMarkers generates kubebuilder validation markers for a map value type
90+
// based on the additionalProperties constraints from the OpenAPI spec.
91+
func valueTypeMarkers(ap *parser.Property) []string {
92+
var markers []string
93+
if ap.Type == "string" {
94+
if ap.MinLength != nil {
95+
markers = append(markers, markerValidationMinLength(int(*ap.MinLength)))
96+
}
97+
if ap.MaxLength != nil {
98+
markers = append(markers, markerValidationMaxLength(int(*ap.MaxLength)))
99+
}
100+
if ap.Pattern != "" {
101+
markers = append(markers, markerValidationPattern(ap.Pattern))
102+
}
103+
}
104+
return markers
105+
}
106+
107+
// propertyToGoBaseType returns the Go base type for a simple property.
108+
func propertyToGoBaseType(prop *parser.Property) string {
109+
switch prop.Type {
110+
case "string":
111+
return "string"
112+
case "integer":
113+
return "int"
114+
case "boolean":
115+
return "bool"
116+
case "number":
117+
return "float64"
118+
default:
119+
return "string"
120+
}
121+
}

0 commit comments

Comments
 (0)