diff --git a/pkg/schema/definitions/handler_test.go b/pkg/schema/definitions/handler_test.go index 630afc90..54507685 100644 --- a/pkg/schema/definitions/handler_test.go +++ b/pkg/schema/definitions/handler_test.go @@ -305,13 +305,11 @@ func Test_byID(t *testing.T) { Description: "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", }, "binaryData": { - Type: "map", - SubType: "string", + Type: "map[string]", Description: "BinaryData contains the binary data. Each key must consist of alphanumeric characters, '-', '_' or '.'. BinaryData can contain byte sequences that are not in the UTF-8 range. The keys stored in BinaryData must not overlap with the ones in the Data field, this is enforced during validation process. Using this field will require 1.10+ apiserver and kubelet.", }, "data": { - Type: "map", - SubType: "string", + Type: "map[string]", Description: "Data contains the configuration data. Each key must consist of alphanumeric characters, '-', '_' or '.'. Values with non-UTF-8 byte sequences must use the BinaryData field. The keys stored in Data must not overlap with the keys in the BinaryData field, this is enforced during validation process.", }, "immutable": { @@ -323,8 +321,7 @@ func Test_byID(t *testing.T) { "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { ResourceFields: map[string]definitionField{ "annotations": { - Type: "map", - SubType: "string", + Type: "map[string]", Description: "annotations of the resource", }, "name": { @@ -393,8 +390,7 @@ func Test_byID(t *testing.T) { "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { ResourceFields: map[string]definitionField{ "annotations": { - Type: "map", - SubType: "string", + Type: "map[string]", Description: "annotations of the resource", }, "name": { @@ -443,8 +439,7 @@ func Test_byID(t *testing.T) { "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { ResourceFields: map[string]definitionField{ "annotations": { - Type: "map", - SubType: "string", + Type: "map[string]", Description: "annotations of the resource", }, "name": { @@ -517,8 +512,7 @@ func Test_byID(t *testing.T) { "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { ResourceFields: map[string]definitionField{ "annotations": { - Type: "map", - SubType: "string", + Type: "map[string]", Description: "annotations of the resource", }, "name": { @@ -566,8 +560,7 @@ func Test_byID(t *testing.T) { "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { ResourceFields: map[string]definitionField{ "annotations": { - Type: "map", - SubType: "string", + Type: "map[string]", Description: "annotations of the resource", }, "name": { diff --git a/pkg/schema/definitions/visitor.go b/pkg/schema/definitions/visitor.go index 518bfb07..b5272664 100644 --- a/pkg/schema/definitions/visitor.go +++ b/pkg/schema/definitions/visitor.go @@ -1,6 +1,8 @@ package definitions import ( + "fmt" + "k8s.io/kube-openapi/pkg/util/proto" ) @@ -16,12 +18,12 @@ func (s *schemaFieldVisitor) VisitArray(array *proto.Array) { field := definitionField{ Description: array.GetDescription(), } - // this currently is not recursive and provides little information for nested types- while this isn't optimal, - // it was kept this way to provide backwards compat with previous endpoints. - array.SubType.Accept(s) - subField := s.field - field.Type = "array" - field.SubType = subField.Type + // Recursively visit the value subtype + subVisitor := &schemaFieldVisitor{definitions: s.definitions} + array.SubType.Accept(subVisitor) + subField := subVisitor.field + // Represent the map as "array[]" + field.Type = fmt.Sprintf("array[%s]", subField.Type) s.field = field } @@ -31,12 +33,12 @@ func (s *schemaFieldVisitor) VisitMap(protoMap *proto.Map) { field := definitionField{ Description: protoMap.GetDescription(), } - // this currently is not recursive and provides little information for nested types- while this isn't optimal, - // it was kept this way to provide backwards compat with previous endpoints. - protoMap.SubType.Accept(s) - subField := s.field - field.Type = "map" - field.SubType = subField.Type + // Recursively visit the value subtype + subVisitor := &schemaFieldVisitor{definitions: s.definitions} + protoMap.SubType.Accept(subVisitor) + subField := subVisitor.field + // Represent the map as "map[]" + field.Type = fmt.Sprintf("map[%s]", subField.Type) s.field = field } diff --git a/pkg/schema/definitions/visitor_test.go b/pkg/schema/definitions/visitor_test.go index c9497d4f..a4bff366 100644 --- a/pkg/schema/definitions/visitor_test.go +++ b/pkg/schema/definitions/visitor_test.go @@ -72,6 +72,18 @@ var ( Description: "testArbitrary", }, } + protoNestedMap = proto.Map{ + BaseSchema: proto.BaseSchema{ + Description: "nestedMap", + }, + SubType: &protoKind, + } + protoEmpty = proto.Kind{ + BaseSchema: proto.BaseSchema{ + Description: "emptySchema", + Path: proto.NewPath("io.cattle.empty"), + }, + } ) func TestSchemaFieldVisitor(t *testing.T) { @@ -87,9 +99,8 @@ func TestSchemaFieldVisitor(t *testing.T) { inputSchema: &protoArray, wantDefinitions: map[string]definition{}, wantField: definitionField{ - Type: "array", + Type: "array[string]", Description: protoArray.Description, - SubType: protoPrimitive.Type, }, }, { @@ -97,9 +108,8 @@ func TestSchemaFieldVisitor(t *testing.T) { inputSchema: &protoMap, wantDefinitions: map[string]definition{}, wantField: definitionField{ - Type: "map", + Type: "map[string]", Description: protoMap.Description, - SubType: protoPrimitive.Type, }, }, { @@ -136,15 +146,13 @@ func TestSchemaFieldVisitor(t *testing.T) { protoKind.Path.String(): { ResourceFields: map[string]definitionField{ "protoArray": { - Type: "array", + Type: "array[" + protoPrimitive.Type + "]", Description: protoArray.Description, - SubType: protoPrimitive.Type, Required: true, }, "protoMap": { - Type: "map", + Type: "map[" + protoPrimitive.Type + "]", Description: protoMap.Description, - SubType: protoPrimitive.Type, }, "protoPrimitive": { Type: protoPrimitive.Type, @@ -181,15 +189,13 @@ func TestSchemaFieldVisitor(t *testing.T) { protoKind.Path.String(): { ResourceFields: map[string]definitionField{ "protoArray": { - Type: "array", + Type: "array[string]", Description: protoArray.Description, - SubType: protoPrimitive.Type, Required: true, }, "protoMap": { - Type: "map", + Type: "map[string]", Description: protoMap.Description, - SubType: protoPrimitive.Type, }, "protoPrimitive": { Type: protoPrimitive.Type, @@ -219,6 +225,79 @@ func TestSchemaFieldVisitor(t *testing.T) { Description: protoArbitrary.Description, }, }, + { + name: "nested map with kind", + inputSchema: &protoNestedMap, + wantDefinitions: map[string]definition{ + protoKind.Path.String(): { + ResourceFields: map[string]definitionField{ + "protoArray": { + Type: "array[string]", + Description: protoArray.Description, + Required: true, + }, + "protoMap": { + Type: "map[string]", + Description: protoMap.Description, + }, + "protoPrimitive": { + Type: protoPrimitive.Type, + Description: protoPrimitive.Description, + Required: true, + }, + "protoRef": { + Type: protoKind.Path.String(), + Description: protoRef.Description, + }, + }, + Type: protoKind.Path.String(), + Description: protoKind.Description, + }, + }, + wantField: definitionField{ + Type: "map[io.cattle.test]", + Description: protoNestedMap.Description, + }, + }, + { + name: "multi-level nested maps and arrays", + inputSchema: &proto.Map{ + BaseSchema: proto.BaseSchema{ + Description: "multi-level nested structure", + }, + SubType: &proto.Array{ + BaseSchema: proto.BaseSchema{ + Description: "nested array", + }, + SubType: &proto.Map{ + BaseSchema: proto.BaseSchema{ + Description: "deeply nested map", + }, + SubType: &protoPrimitive, + }, + }, + }, + wantDefinitions: map[string]definition{}, + wantField: definitionField{ + Type: "map[array[map[string]]]", + Description: "multi-level nested structure", + }, + }, + { + name: "empty schema", + inputSchema: &protoEmpty, + wantDefinitions: map[string]definition{ + "io.cattle.empty": { + ResourceFields: map[string]definitionField{}, + Type: "io.cattle.empty", + Description: protoEmpty.Description, + }, + }, + wantField: definitionField{ + Type: "io.cattle.empty", + Description: protoEmpty.Description, + }, + }, } for _, test := range tests {