diff --git a/pkg/schema/default_schema/console.json b/pkg/schema/default_schema/console.json index 8885d77..b4fc717 100644 --- a/pkg/schema/default_schema/console.json +++ b/pkg/schema/default_schema/console.json @@ -146,7 +146,7 @@ "Type": "string" } }, - "ApplyExample": "apiVersion: v1\nkind: ApplicationInstance\nmetadata:\n application: my-app\n labels:\n env: prod\n team: my-team\n name: my-app-instance-prod\nspec:\n cluster: prod-cluster\n defaultCatalogVisibility: PUBLIC\n policyRef:\n - my-policy\n resources:\n - name: my-topic\n patternType: LITERAL\n type: TOPIC\n - name: my-consumer-group\n patternType: LITERAL\n type: CONSUMER_GROUP\n serviceAccount: my-service-account\n topicPolicyRef:\n - my-topic-policy\n", + "ApplyExample": "apiVersion: v1\nkind: ApplicationInstance\nmetadata:\n application: my-app\n labels:\n env: prod\n team: my-team\n name: my-app-instance-prod\nspec:\n cluster: prod-cluster\n defaultCatalogVisibility: PUBLIC\n managedServiceAccountNamespace: my-app-.*\n policyRef:\n - my-policy\n resources:\n - name: my-topic\n patternType: LITERAL\n type: TOPIC\n - name: my-consumer-group\n patternType: LITERAL\n type: CONSUMER_GROUP\n serviceAccount: my-service-account\n topicPolicyRef:\n - my-topic-policy\n", "Order": 8 } } @@ -204,7 +204,7 @@ "ParentPathParam": [], "ParentQueryParam": null, "ListQueryParameter": {}, - "ApplyExample": "apiVersion: v2\nkind: ConnectorTemplate\nmetadata:\n labels:\n category: sink\n name: s3-sink-standard\nspec:\n defaults:\n metadata:\n labels:\n category: sink\n name: s3-sink-${dept}\n spec:\n class: io.confluent.connect.s3.S3SinkConnector\n config:\n flush.size: \"10000\"\n format.class: io.confluent.connect.s3.format.avro.AvroFormat\n rotate.interval.ms: \"60000\"\n storage.class: io.confluent.connect.s3.storage.S3Storage\n tasks.max: \"2\"\n description: Standard S3 sink connector with flush every 10,000 records or 60 seconds.\n displayName: S3 Sink (Standard)\n", + "ApplyExample": "apiVersion: v2\nkind: ConnectorTemplate\nmetadata:\n labels:\n category: sink\n name: s3-sink-standard\nspec:\n defaults:\n metadata:\n labels:\n category: sink\n name: s3-sink-{{dept}}\n spec:\n class: io.confluent.connect.s3.S3SinkConnector\n config:\n flush.size: \"10000\"\n format.class: io.confluent.connect.s3.format.avro.AvroFormat\n rotate.interval.ms: \"60000\"\n storage.class: io.confluent.connect.s3.storage.S3Storage\n tasks.max: \"2\"\n description: Standard S3 sink connector with flush every 10,000 records or 60 seconds.\n displayName: S3 Sink (Standard)\n", "Order": 23 } } @@ -465,7 +465,7 @@ "ParentPathParam": [], "ParentQueryParam": null, "ListQueryParameter": {}, - "ApplyExample": "apiVersion: v2\nkind: TopicTemplate\nmetadata:\n labels:\n category: template\n name: high-partition-topic\nspec:\n defaults:\n metadata:\n labels:\n throughput: high\n name: my-topic-${department}\n spec:\n configs:\n cleanup.policy: delete\n min.insync.replicas: \"2\"\n retention.ms: \"604800000\"\n partitions: 24\n replicationFactor: 3\n description: Optimised for high-throughput workloads. 24 partitions, 3× replication, 7-day retention.\n displayName: High Partition Topic\n", + "ApplyExample": "apiVersion: v2\nkind: TopicTemplate\nmetadata:\n labels:\n category: template\n name: high-partition-topic\nspec:\n defaults:\n metadata:\n labels:\n throughput: high\n name: my-topic-{{department}}\n spec:\n configs:\n cleanup.policy: delete\n min.insync.replicas: \"2\"\n retention.ms: \"604800000\"\n partitions: 24\n replicationFactor: 3\n description: Optimised for high-throughput workloads. 24 partitions, 3× replication, 7-day retention.\n displayName: High Partition Topic\n", "Order": 22 } } @@ -582,6 +582,126 @@ "BodyFields": {}, "Method": "PUT" }, + "consumerGroupDelete": { + "Path": "/public/v1/clusters/{clusterName}/consumergroups/{consumerGroup}", + "Name": "consumerGroupDelete", + "Doc": "Delete a consumer group", + "QueryParameter": {}, + "PathParameter": [ + "clusterName", + "consumerGroup" + ], + "BodyFields": {}, + "Method": "DELETE" + }, + "consumerGroupDescribe": { + "Path": "/public/v1/clusters/{clusterName}/consumergroups/{consumerGroup}", + "Name": "consumerGroupDescribe", + "Doc": "Describe a consumer group", + "QueryParameter": {}, + "PathParameter": [ + "clusterName", + "consumerGroup" + ], + "BodyFields": {}, + "Method": "GET" + }, + "consumerGroupListMembers": { + "Path": "/public/v1/clusters/{clusterName}/consumergroups/{consumerGroup}/members", + "Name": "consumerGroupListMembers", + "Doc": "List the members of a consumer group", + "QueryParameter": {}, + "PathParameter": [ + "clusterName", + "consumerGroup" + ], + "BodyFields": {}, + "Method": "GET" + }, + "consumerGroupListTopics": { + "Path": "/public/v1/clusters/{clusterName}/consumergroups/{consumerGroup}/topics", + "Name": "consumerGroupListTopics", + "Doc": "List the topics consumed by a consumer group", + "QueryParameter": {}, + "PathParameter": [ + "clusterName", + "consumerGroup" + ], + "BodyFields": {}, + "Method": "GET" + }, + "consumerGroupMemberDetail": { + "Path": "/public/v1/clusters/{clusterName}/consumergroups/{consumerGroup}/members/{memberId}", + "Name": "consumerGroupMemberDetail", + "Doc": "Describe a member of a consumer group", + "QueryParameter": {}, + "PathParameter": [ + "clusterName", + "consumerGroup", + "memberId" + ], + "BodyFields": {}, + "Method": "GET" + }, + "consumerGroupResetOffsets": { + "Path": "/public/v1/clusters/{clusterName}/consumergroups/{consumerGroup}/offset-reset", + "Name": "consumerGroupResetOffsets", + "Doc": "Reset a consumer group's offsets", + "QueryParameter": {}, + "PathParameter": [ + "clusterName", + "consumerGroup" + ], + "BodyFields": { + "resetSpecification": { + "FlagName": "reset-specification", + "Required": true, + "Type": "json" + }, + "topicPartitionSelection": { + "FlagName": "topic-partition-selection", + "Required": true, + "Type": "json" + } + }, + "Method": "POST" + }, + "consumerGroupResetOffsetsPreview": { + "Path": "/public/v1/clusters/{clusterName}/consumergroups/{consumerGroup}/offset-reset-preview", + "Name": "consumerGroupResetOffsetsPreview", + "Doc": "Preview the result of resetting a consumer group's offsets", + "QueryParameter": {}, + "PathParameter": [ + "clusterName", + "consumerGroup" + ], + "BodyFields": { + "resetSpecification": { + "FlagName": "reset-specification", + "Required": true, + "Type": "json" + }, + "topicPartitionSelection": { + "FlagName": "topic-partition-selection", + "Required": true, + "Type": "json" + } + }, + "Method": "POST" + }, + "consumerGroupTopicDetail": { + "Path": "/public/v1/clusters/{clusterName}/consumergroups/{consumerGroup}/topics/{topicName}", + "Name": "consumerGroupTopicDetail", + "Doc": "Describe a topic consumed by a consumer group", + "QueryParameter": {}, + "PathParameter": [ + "clusterName", + "consumerGroup", + "topicName" + ], + "BodyFields": {}, + "Method": "GET" + }, "partnerZoneGenerateCredentials": { "Path": "/public/partner-zone/v2/{partner-zone-name}/generate-credentials", "Name": "partnerZoneGenerateCredentials", diff --git a/pkg/schema/openapi_parser.go b/pkg/schema/openapi_parser.go index 23a7d81..6bee102 100644 --- a/pkg/schema/openapi_parser.go +++ b/pkg/schema/openapi_parser.go @@ -98,7 +98,13 @@ func handleExecuteOperation(backendType BackendType, path string, operation *v3h return nil } - nameYaml, present := operation.Extensions.Get("x-cdk-run-name") + // New endpoints are annotated with x-cdk-run-name-v2 so that older CLIs, + // which only know x-cdk-run-name, skip them instead of failing. The current + // CLI must therefore accept both keys, preferring the v2 one when present. + nameYaml, present := operation.Extensions.Get("x-cdk-run-name-v2") + if !present { + nameYaml, present = operation.Extensions.Get("x-cdk-run-name") + } if !present { return nil } @@ -151,19 +157,28 @@ func computeBodyFields(body *v3high.RequestBody) map[string]FlagParameterOption for propertiesPair := bodySchema.Properties.First(); propertiesPair != nil; propertiesPair = propertiesPair.Next() { key := propertiesPair.Key() value := propertiesPair.Value() - if value != nil && value.Schema() != nil && len(value.Schema().Type) > 0 { - valueType := value.Schema().Type[0] + if value == nil || value.Schema() == nil { + continue + } + propSchema := value.Schema() + var valueType string + if len(propSchema.Type) > 0 { + valueType = propSchema.Type[0] // Scalars map to a typed flag; arrays/objects are passed as a // JSON-encoded string flag that we decode back into the body. if valueType == "array" || valueType == "object" { valueType = "json" } - if valueType == "string" || valueType == "boolean" || valueType == "integer" || valueType == "json" { - result[key] = FlagParameterOption{ - FlagName: computeFlagName(key), - Type: valueType, - Required: slices.Contains(bodySchema.Required, key), - } + } else if len(propSchema.OneOf) > 0 || len(propSchema.AnyOf) > 0 || len(propSchema.AllOf) > 0 { + // Polymorphic properties (oneOf/anyOf/allOf) carry no scalar type; + // expose them as a JSON-encoded string flag, like arrays/objects. + valueType = "json" + } + if valueType == "string" || valueType == "boolean" || valueType == "integer" || valueType == "json" { + result[key] = FlagParameterOption{ + FlagName: computeFlagName(key), + Type: valueType, + Required: slices.Contains(bodySchema.Required, key), } } } diff --git a/pkg/schema/openapi_parser_console_test.go b/pkg/schema/openapi_parser_console_test.go index c30b062..e73a7c2 100644 --- a/pkg/schema/openapi_parser_console_test.go +++ b/pkg/schema/openapi_parser_console_test.go @@ -562,4 +562,139 @@ func TestGetConnectorRuns(t *testing.T) { t.Error(spew.Printf("got %v, want %v", result, expected)) } }) + + t.Run("reads x-cdk-run-name-v2 and prefers it over x-cdk-run-name", func(t *testing.T) { + // New endpoints are annotated with x-cdk-run-name-v2 only, so old CLIs + // (which look only for x-cdk-run-name) skip them. The current CLI must + // expose both the v2-only endpoints and the legacy x-cdk-run-name ones, + // preferring the v2 name when an endpoint carries both keys. + spec := []byte(`openapi: 3.0.0 +info: + title: run-version + version: "1.0" +paths: + /v2only: + get: + x-cdk-run-name-v2: newOnlyRun + x-cdk-run-doc: A run only the new CLI can see + responses: + "200": + description: ok + /both: + get: + x-cdk-run-name: legacyName + x-cdk-run-name-v2: preferredName + responses: + "200": + description: ok + /legacyonly: + get: + x-cdk-run-name: legacyOnlyRun + responses: + "200": + description: ok +`) + + schema, err := NewOpenAPIParser(spec) + if err != nil { + t.Fatalf("failed creating new schema: %s", err) + } + + result, err := schema.getRuns(CONSOLE) + if err != nil { + t.Fatalf("failed getting runs: %s", err) + } + + if _, ok := result["newOnlyRun"]; !ok { + t.Errorf("expected v2-only endpoint to be exposed as %q, got %v", "newOnlyRun", result) + } + if _, ok := result["preferredName"]; !ok { + t.Errorf("expected v2 name to take precedence (%q), got %v", "preferredName", result) + } + if _, ok := result["legacyName"]; ok { + t.Errorf("legacy name should be shadowed by the v2 name when both are present, got %v", result) + } + if _, ok := result["legacyOnlyRun"]; !ok { + t.Errorf("expected legacy-only endpoint to still be exposed, got %v", result) + } + }) + + t.Run("exposes polymorphic (oneOf) body properties as json flags", func(t *testing.T) { + // A body property that is a oneOf/anyOf/allOf carries no scalar type; + // it must still surface as a json-encoded string flag (like the + // consumerGroupResetOffsets reset payload) rather than being dropped. + spec := []byte(`openapi: 3.0.0 +info: + title: run-version + version: "1.0" +paths: + /reset: + post: + x-cdk-run-name: resetRun + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ResetRequest' + responses: + "200": + description: ok +components: + schemas: + ResetRequest: + type: object + required: + - selection + properties: + selection: + $ref: '#/components/schemas/Selection' + note: + type: string + Selection: + oneOf: + - $ref: '#/components/schemas/AllTopics' + - $ref: '#/components/schemas/OneTopic' + AllTopics: + type: object + properties: + type: + type: string + OneTopic: + type: object + properties: + type: + type: string + topic: + type: string +`) + + schema, err := NewOpenAPIParser(spec) + if err != nil { + t.Fatalf("failed creating new schema: %s", err) + } + + result, err := schema.getRuns(CONSOLE) + if err != nil { + t.Fatalf("failed getting runs: %s", err) + } + + run, ok := result["resetRun"] + if !ok { + t.Fatalf("expected resetRun to be present, got %v", result) + } + selection, ok := run.BodyFields["selection"] + if !ok { + t.Fatalf("expected oneOf property 'selection' to be exposed as a body field, got %v", run.BodyFields) + } + if selection.Type != "json" { + t.Errorf("expected oneOf property to be a json flag, got %q", selection.Type) + } + if !selection.Required { + t.Errorf("expected 'selection' to be required") + } + if note, ok := run.BodyFields["note"]; !ok || note.Type != "string" { + t.Errorf("expected scalar sibling 'note' to remain a string flag, got %v", run.BodyFields) + } + }) }