From a43bb74834325243060cba99847e7b04be2e6f12 Mon Sep 17 00:00:00 2001 From: OP Date: Tue, 3 Feb 2026 01:14:23 +0000 Subject: [PATCH] fix(import): aggregate CRD imports from same API group When importing multiple CRD files from the same Kubernetes API group (e.g., `compute.gcp.upbound.io`) using separate entries in `cdk8s.yaml`, all CRDs should be consolidated into a single module. Previously, each import was processed independently, causing later imports to overwrite earlier ones from the same API group. This change aggregates all unprefixed CRD imports, downloads all their manifests, combines them with YAML document separators, and processes them together. This ensures CRDs from the same API group are properly consolidated regardless of whether they come from the same file or different files/URLs. CRD imports with a `moduleNamePrefix` continue to be processed individually to preserve the user's intent for separate modules. Fixes #3797 Signed-off-by: OP --- src/import/crd.ts | 27 + src/import/dispatch.ts | 123 ++- .../__snapshots__/import-crd.test.ts.snap | 877 ++++++++++++++++++ test/import/fixtures/same_group_crd_1.yaml | 25 + test/import/fixtures/same_group_crd_2.yaml | 28 + test/import/import-crd.test.ts | 86 ++ 6 files changed, 1159 insertions(+), 7 deletions(-) create mode 100644 test/import/fixtures/same_group_crd_1.yaml create mode 100644 test/import/fixtures/same_group_crd_2.yaml diff --git a/src/import/crd.ts b/src/import/crd.ts index 048a6cb21..087eb41c2 100644 --- a/src/import/crd.ts +++ b/src/import/crd.ts @@ -128,6 +128,33 @@ export class ImportCustomResourceDefinition extends ImportBase { return new ImportCustomResourceDefinition(manifest); } + /** + * Creates an importer from multiple import specs by downloading and combining + * their manifests. This ensures CRDs from the same API group across different + * sources are consolidated into a single module. + * + * @param importSpecs Array of import specifications to aggregate + * @returns A single ImportCustomResourceDefinition containing all CRDs + */ + public static async fromSpecs(importSpecs: ImportSpec[]): Promise { + const manifests = await Promise.all( + importSpecs.map(spec => download(spec.source)), + ); + // Combine all manifests with YAML document separator + const combinedManifest = manifests.join('\n---\n'); + return new ImportCustomResourceDefinition(combinedManifest); + } + + /** + * Creates an importer directly from a raw manifest string. + * + * @param manifest Raw YAML manifest string containing one or more CRDs + * @returns An ImportCustomResourceDefinition instance + */ + public static fromManifest(manifest: string): ImportCustomResourceDefinition { + return new ImportCustomResourceDefinition(manifest); + } + public readonly rawManifest: string; private readonly groups: Record = { }; diff --git a/src/import/dispatch.ts b/src/import/dispatch.ts index c2f0c947f..e23fbf543 100644 --- a/src/import/dispatch.ts +++ b/src/import/dispatch.ts @@ -6,24 +6,133 @@ import { ImportKubernetesApi } from './k8s'; import { ImportSpec, addImportToConfig } from '../config'; import { PREFIX_DELIM } from '../util'; -export async function importDispatch(imports: ImportSpec[], argv: any, options: ImportOptions) { +/** + * Categorized import specs for processing. + */ +interface CategorizedImports { + /** Kubernetes API imports (k8s@version) */ + k8s: Array<{ spec: ImportSpec; importer: ImportKubernetesApi }>; + /** Helm chart imports */ + helm: ImportSpec[]; + /** CRD imports without module name prefix (will be aggregated) */ + crdUnprefixed: ImportSpec[]; + /** CRD imports with module name prefix (processed individually to preserve prefix) */ + crdPrefixed: ImportSpec[]; +} + +/** + * Categorizes import specs by type for appropriate processing. + * + * CRD imports without a module name prefix are grouped together so they can be + * aggregated - this ensures CRDs from the same API group across different sources + * are consolidated into a single module instead of overwriting each other. + * + * CRD imports with a module name prefix are kept separate since the prefix + * indicates the user wants them in distinct modules. + */ +async function categorizeImports(imports: ImportSpec[], argv: any): Promise { + const result: CategorizedImports = { + k8s: [], + helm: [], + crdUnprefixed: [], + crdPrefixed: [], + }; + for (const importSpec of imports) { - const importer = await matchImporter(importSpec, argv); + // Check if it's a k8s@ import + const k8sMatch = await ImportKubernetesApi.match(importSpec, argv); + if (k8sMatch) { + result.k8s.push({ spec: importSpec, importer: new ImportKubernetesApi(k8sMatch) }); + continue; + } + + const prefix = importSpec.source.split(':')[0]; + + // Check if it's a helm import + if (prefix === 'helm') { + result.helm.push(importSpec); + continue; + } + + // It's a CRD import - categorize by whether it has a module name prefix + if (importSpec.moduleNamePrefix) { + result.crdPrefixed.push(importSpec); + } else { + result.crdUnprefixed.push(importSpec); + } + } - if (!importer) { - throw new Error(`unable to determine import type for "${importSpec}"`); + return result; +} + +export async function importDispatch(imports: ImportSpec[], argv: any, options: ImportOptions) { + const categorized = await categorizeImports(imports, argv); + + // Process k8s imports + for (const { spec, importer } of categorized.k8s) { + console.error('Importing resources, this may take a few moments...'); + await importer.import({ + moduleNamePrefix: spec.moduleNamePrefix, + ...options, + }); + if (options.save ?? true) { + const specStr = spec.moduleNamePrefix ? `${spec.moduleNamePrefix}${PREFIX_DELIM}${spec.source}` : spec.source; + await addImportToConfig(specStr); } + } + // Process helm imports + for (const spec of categorized.helm) { + const importer = await ImportHelm.fromSpec(spec); console.error('Importing resources, this may take a few moments...'); + await importer.import({ + moduleNamePrefix: spec.moduleNamePrefix, + ...options, + }); + if (options.save ?? true) { + const specStr = spec.moduleNamePrefix ? `${spec.moduleNamePrefix}${PREFIX_DELIM}${spec.source}` : spec.source; + await addImportToConfig(specStr); + } + } + // Process CRD imports with prefix (individually, to preserve their prefixes) + for (const spec of categorized.crdPrefixed) { + // Check for crds.dev URL format + const crdsDevUrl = matchCrdsDevUrl(spec.source); + const importer = crdsDevUrl + ? await ImportCustomResourceDefinition.fromSpec({ source: crdsDevUrl, moduleNamePrefix: spec.moduleNamePrefix }) + : await ImportCustomResourceDefinition.fromSpec(spec); + + console.error('Importing resources, this may take a few moments...'); await importer.import({ - moduleNamePrefix: importSpec.moduleNamePrefix, + moduleNamePrefix: spec.moduleNamePrefix, ...options, }); + if (options.save ?? true) { + const specStr = `${spec.moduleNamePrefix}${PREFIX_DELIM}${spec.source}`; + await addImportToConfig(specStr); + } + } + + // Process unprefixed CRD imports together (aggregated) + // This ensures CRDs from the same API group across different sources + // are consolidated into a single module + if (categorized.crdUnprefixed.length > 0) { + // Transform sources to handle crds.dev URLs + const resolvedSpecs = categorized.crdUnprefixed.map(spec => { + const crdsDevUrl = matchCrdsDevUrl(spec.source); + return crdsDevUrl ? { ...spec, source: crdsDevUrl } : spec; + }); + + console.error('Importing resources, this may take a few moments...'); + const importer = await ImportCustomResourceDefinition.fromSpecs(resolvedSpecs); + await importer.import(options); + // Save all sources to config if (options.save ?? true) { - const spec = importSpec.moduleNamePrefix ? `${importSpec.moduleNamePrefix}${PREFIX_DELIM}${importSpec.source}` : importSpec.source; - await addImportToConfig(spec); + for (const spec of categorized.crdUnprefixed) { + await addImportToConfig(spec.source); + } } } } diff --git a/test/import/__snapshots__/import-crd.test.ts.snap b/test/import/__snapshots__/import-crd.test.ts.snap index 152eb5495..fce301bc5 100644 --- a/test/import/__snapshots__/import-crd.test.ts.snap +++ b/test/import/__snapshots__/import-crd.test.ts.snap @@ -237607,6 +237607,883 @@ export function toJson_ThanosRulerSpecVolumesProjectedSourcesDownwardApiItemsRes } `; +exports[`snapshots same_group_crd_1.yaml 1`] = ` +Object { + "author": Object { + "name": "generated@generated.com", + "roles": Array [ + "author", + ], + }, + "dependencies": "__omitted__", + "dependencyClosure": Object { + "cdk8s": Object { + "targets": Object { + "dotnet": Object { + "namespace": "Org.Cdk8s", + "packageId": "Org.Cdk8s", + }, + "go": Object { + "moduleName": "github.com/cdk8s-team/cdk8s-core-go", + }, + "java": Object { + "maven": Object { + "artifactId": "cdk8s", + "groupId": "org.cdk8s", + }, + "package": "org.cdk8s", + }, + "js": Object { + "npm": "cdk8s", + }, + "python": Object { + "distName": "cdk8s", + "module": "cdk8s", + }, + }, + }, + "constructs": Object { + "targets": Object { + "dotnet": Object { + "namespace": "Constructs", + "packageId": "Constructs", + }, + "go": Object { + "moduleName": "github.com/aws/constructs-go", + }, + "java": Object { + "maven": Object { + "artifactId": "constructs", + "groupId": "software.constructs", + }, + "package": "software.constructs", + }, + "js": Object { + "npm": "constructs", + }, + "python": Object { + "distName": "constructs", + "module": "constructs", + }, + }, + }, + }, + "description": "computeexamplecom", + "fingerprint": "", + "homepage": "http://generated", + "jsiiVersion": "__omitted__", + "license": "UNLICENSED", + "metadata": Object { + "jsii": Object { + "pacmak": Object { + "hasDefaultInterfaces": true, + }, + }, + }, + "name": "computeexamplecom", + "repository": Object { + "type": "git", + "url": "http://generated", + }, + "schema": "jsii/0.10.0", + "targets": Object { + "js": Object { + "npm": "computeexamplecom", + }, + }, + "types": Object { + "computeexamplecom.Network": Object { + "assembly": "computeexamplecom", + "base": "cdk8s.ApiObject", + "docs": Object { + "custom": Object { + "schema": "Network", + }, + }, + "fqn": "computeexamplecom.Network", + "initializer": Object { + "docs": Object { + "summary": "Defines a \\"Network\\" API object.", + }, + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 40, + }, + "parameters": Array [ + Object { + "docs": Object { + "summary": "the scope in which to define this object.", + }, + "name": "scope", + "type": Object { + "fqn": "constructs.Construct", + }, + }, + Object { + "docs": Object { + "summary": "a scope-local name for the object.", + }, + "name": "id", + "type": Object { + "primitive": "string", + }, + }, + Object { + "docs": Object { + "summary": "initialization props.", + }, + "name": "props", + "optional": true, + "type": Object { + "fqn": "computeexamplecom.NetworkProps", + }, + }, + ], + }, + "kind": "class", + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 11, + }, + "methods": Array [ + Object { + "docs": Object { + "remarks": "This can be used to inline resource manifests inside other objects (e.g. as templates).", + "summary": "Renders a Kubernetes manifest for \\"Network\\".", + }, + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 27, + }, + "name": "manifest", + "parameters": Array [ + Object { + "docs": Object { + "summary": "initialization props.", + }, + "name": "props", + "optional": true, + "type": Object { + "fqn": "computeexamplecom.NetworkProps", + }, + }, + ], + "returns": Object { + "type": Object { + "primitive": "any", + }, + }, + "static": true, + }, + Object { + "docs": Object { + "summary": "Renders the object to Kubernetes JSON.", + }, + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 50, + }, + "name": "toJson", + "overrides": "cdk8s.ApiObject", + "returns": Object { + "type": Object { + "primitive": "any", + }, + }, + }, + ], + "name": "Network", + "properties": Array [ + Object { + "const": true, + "docs": Object { + "summary": "Returns the apiVersion and kind for \\"Network\\".", + }, + "immutable": true, + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 15, + }, + "name": "GVK", + "static": true, + "type": Object { + "fqn": "cdk8s.GroupVersionKind", + }, + }, + ], + "symbolId": "compute.example.com:Network", + }, + "computeexamplecom.NetworkProps": Object { + "assembly": "computeexamplecom", + "datatype": true, + "docs": Object { + "custom": Object { + "schema": "Network", + }, + }, + "fqn": "computeexamplecom.NetworkProps", + "kind": "interface", + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 63, + }, + "name": "NetworkProps", + "properties": Array [ + Object { + "abstract": true, + "docs": Object { + "custom": Object { + "schema": "Network#metadata", + }, + }, + "immutable": true, + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 67, + }, + "name": "metadata", + "optional": true, + "type": Object { + "fqn": "cdk8s.ApiObjectMetadata", + }, + }, + Object { + "abstract": true, + "docs": Object { + "custom": Object { + "schema": "Network#spec", + }, + }, + "immutable": true, + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 72, + }, + "name": "spec", + "optional": true, + "type": Object { + "fqn": "computeexamplecom.NetworkSpec", + }, + }, + ], + "symbolId": "compute.example.com:NetworkProps", + }, + "computeexamplecom.NetworkSpec": Object { + "assembly": "computeexamplecom", + "datatype": true, + "docs": Object { + "custom": Object { + "schema": "NetworkSpec", + }, + }, + "fqn": "computeexamplecom.NetworkSpec", + "kind": "interface", + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 93, + }, + "name": "NetworkSpec", + "properties": Array [ + Object { + "abstract": true, + "docs": Object { + "custom": Object { + "schema": "NetworkSpec#cidr", + }, + "summary": "CIDR block for the network.", + }, + "immutable": true, + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 99, + }, + "name": "cidr", + "optional": true, + "type": Object { + "primitive": "string", + }, + }, + ], + "symbolId": "compute.example.com:NetworkSpec", + }, + }, + "version": "0.0.0", +} +`; + +exports[`snapshots same_group_crd_1.yaml 2`] = ` +Object { + "compute.example.com.ts": "// generated by cdk8s +import { ApiObject, ApiObjectMetadata, GroupVersionKind } from 'cdk8s'; +import { Construct } from 'constructs'; + + +/** + * + * + * @schema Network + */ +export class Network extends ApiObject { + /** + * Returns the apiVersion and kind for \\"Network\\" + */ + public static readonly GVK: GroupVersionKind = { + apiVersion: 'compute.example.com/v1', + kind: 'Network', + } + + /** + * Renders a Kubernetes manifest for \\"Network\\". + * + * This can be used to inline resource manifests inside other objects (e.g. as templates). + * + * @param props initialization props + */ + public static manifest(props: NetworkProps = {}): any { + return { + ...Network.GVK, + ...toJson_NetworkProps(props), + }; + } + + /** + * Defines a \\"Network\\" API object + * @param scope the scope in which to define this object + * @param id a scope-local name for the object + * @param props initialization props + */ + public constructor(scope: Construct, id: string, props: NetworkProps = {}) { + super(scope, id, { + ...Network.GVK, + ...props, + }); + } + + /** + * Renders the object to Kubernetes JSON. + */ + public override toJson(): any { + const resolved = super.toJson(); + + return { + ...Network.GVK, + ...toJson_NetworkProps(resolved), + }; + } +} + +/** + * @schema Network + */ +export interface NetworkProps { + /** + * @schema Network#metadata + */ + readonly metadata?: ApiObjectMetadata; + + /** + * @schema Network#spec + */ + readonly spec?: NetworkSpec; +} + +/** + * Converts an object of type 'NetworkProps' to JSON representation. + */ +/* eslint-disable max-len, @stylistic/max-len, quote-props, @stylistic/quote-props */ +export function toJson_NetworkProps(obj: NetworkProps | undefined): Record | undefined { + if (obj === undefined) { return undefined; } + const result = { + 'metadata': obj.metadata, + 'spec': toJson_NetworkSpec(obj.spec), + }; + // filter undefined values + return Object.entries(result).reduce((r, i) => (i[1] === undefined) ? r : ({ ...r, [i[0]]: i[1] }), {}); +} +/* eslint-enable max-len, @stylistic/max-len, quote-props, @stylistic/quote-props */ + +/** + * @schema NetworkSpec + */ +export interface NetworkSpec { + /** + * CIDR block for the network + * + * @schema NetworkSpec#cidr + */ + readonly cidr?: string; +} + +/** + * Converts an object of type 'NetworkSpec' to JSON representation. + */ +/* eslint-disable max-len, @stylistic/max-len, quote-props, @stylistic/quote-props */ +export function toJson_NetworkSpec(obj: NetworkSpec | undefined): Record | undefined { + if (obj === undefined) { return undefined; } + const result = { + 'cidr': obj.cidr, + }; + // filter undefined values + return Object.entries(result).reduce((r, i) => (i[1] === undefined) ? r : ({ ...r, [i[0]]: i[1] }), {}); +} +/* eslint-enable max-len, @stylistic/max-len, quote-props, @stylistic/quote-props */ + +", +} +`; + +exports[`snapshots same_group_crd_2.yaml 1`] = ` +Object { + "author": Object { + "name": "generated@generated.com", + "roles": Array [ + "author", + ], + }, + "dependencies": "__omitted__", + "dependencyClosure": Object { + "cdk8s": Object { + "targets": Object { + "dotnet": Object { + "namespace": "Org.Cdk8s", + "packageId": "Org.Cdk8s", + }, + "go": Object { + "moduleName": "github.com/cdk8s-team/cdk8s-core-go", + }, + "java": Object { + "maven": Object { + "artifactId": "cdk8s", + "groupId": "org.cdk8s", + }, + "package": "org.cdk8s", + }, + "js": Object { + "npm": "cdk8s", + }, + "python": Object { + "distName": "cdk8s", + "module": "cdk8s", + }, + }, + }, + "constructs": Object { + "targets": Object { + "dotnet": Object { + "namespace": "Constructs", + "packageId": "Constructs", + }, + "go": Object { + "moduleName": "github.com/aws/constructs-go", + }, + "java": Object { + "maven": Object { + "artifactId": "constructs", + "groupId": "software.constructs", + }, + "package": "software.constructs", + }, + "js": Object { + "npm": "constructs", + }, + "python": Object { + "distName": "constructs", + "module": "constructs", + }, + }, + }, + }, + "description": "computeexamplecom", + "fingerprint": "", + "homepage": "http://generated", + "jsiiVersion": "__omitted__", + "license": "UNLICENSED", + "metadata": Object { + "jsii": Object { + "pacmak": Object { + "hasDefaultInterfaces": true, + }, + }, + }, + "name": "computeexamplecom", + "repository": Object { + "type": "git", + "url": "http://generated", + }, + "schema": "jsii/0.10.0", + "targets": Object { + "js": Object { + "npm": "computeexamplecom", + }, + }, + "types": Object { + "computeexamplecom.Subnetwork": Object { + "assembly": "computeexamplecom", + "base": "cdk8s.ApiObject", + "docs": Object { + "custom": Object { + "schema": "Subnetwork", + }, + }, + "fqn": "computeexamplecom.Subnetwork", + "initializer": Object { + "docs": Object { + "summary": "Defines a \\"Subnetwork\\" API object.", + }, + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 40, + }, + "parameters": Array [ + Object { + "docs": Object { + "summary": "the scope in which to define this object.", + }, + "name": "scope", + "type": Object { + "fqn": "constructs.Construct", + }, + }, + Object { + "docs": Object { + "summary": "a scope-local name for the object.", + }, + "name": "id", + "type": Object { + "primitive": "string", + }, + }, + Object { + "docs": Object { + "summary": "initialization props.", + }, + "name": "props", + "optional": true, + "type": Object { + "fqn": "computeexamplecom.SubnetworkProps", + }, + }, + ], + }, + "kind": "class", + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 11, + }, + "methods": Array [ + Object { + "docs": Object { + "remarks": "This can be used to inline resource manifests inside other objects (e.g. as templates).", + "summary": "Renders a Kubernetes manifest for \\"Subnetwork\\".", + }, + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 27, + }, + "name": "manifest", + "parameters": Array [ + Object { + "docs": Object { + "summary": "initialization props.", + }, + "name": "props", + "optional": true, + "type": Object { + "fqn": "computeexamplecom.SubnetworkProps", + }, + }, + ], + "returns": Object { + "type": Object { + "primitive": "any", + }, + }, + "static": true, + }, + Object { + "docs": Object { + "summary": "Renders the object to Kubernetes JSON.", + }, + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 50, + }, + "name": "toJson", + "overrides": "cdk8s.ApiObject", + "returns": Object { + "type": Object { + "primitive": "any", + }, + }, + }, + ], + "name": "Subnetwork", + "properties": Array [ + Object { + "const": true, + "docs": Object { + "summary": "Returns the apiVersion and kind for \\"Subnetwork\\".", + }, + "immutable": true, + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 15, + }, + "name": "GVK", + "static": true, + "type": Object { + "fqn": "cdk8s.GroupVersionKind", + }, + }, + ], + "symbolId": "compute.example.com:Subnetwork", + }, + "computeexamplecom.SubnetworkProps": Object { + "assembly": "computeexamplecom", + "datatype": true, + "docs": Object { + "custom": Object { + "schema": "Subnetwork", + }, + }, + "fqn": "computeexamplecom.SubnetworkProps", + "kind": "interface", + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 63, + }, + "name": "SubnetworkProps", + "properties": Array [ + Object { + "abstract": true, + "docs": Object { + "custom": Object { + "schema": "Subnetwork#metadata", + }, + }, + "immutable": true, + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 67, + }, + "name": "metadata", + "optional": true, + "type": Object { + "fqn": "cdk8s.ApiObjectMetadata", + }, + }, + Object { + "abstract": true, + "docs": Object { + "custom": Object { + "schema": "Subnetwork#spec", + }, + }, + "immutable": true, + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 72, + }, + "name": "spec", + "optional": true, + "type": Object { + "fqn": "computeexamplecom.SubnetworkSpec", + }, + }, + ], + "symbolId": "compute.example.com:SubnetworkProps", + }, + "computeexamplecom.SubnetworkSpec": Object { + "assembly": "computeexamplecom", + "datatype": true, + "docs": Object { + "custom": Object { + "schema": "SubnetworkSpec", + }, + }, + "fqn": "computeexamplecom.SubnetworkSpec", + "kind": "interface", + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 93, + }, + "name": "SubnetworkSpec", + "properties": Array [ + Object { + "abstract": true, + "docs": Object { + "custom": Object { + "schema": "SubnetworkSpec#ipRange", + }, + "summary": "IP range for the subnetwork.", + }, + "immutable": true, + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 106, + }, + "name": "ipRange", + "optional": true, + "type": Object { + "primitive": "string", + }, + }, + Object { + "abstract": true, + "docs": Object { + "custom": Object { + "schema": "SubnetworkSpec#networkRef", + }, + "summary": "Reference to the parent network.", + }, + "immutable": true, + "locationInModule": Object { + "filename": "compute.example.com.ts", + "line": 99, + }, + "name": "networkRef", + "optional": true, + "type": Object { + "primitive": "string", + }, + }, + ], + "symbolId": "compute.example.com:SubnetworkSpec", + }, + }, + "version": "0.0.0", +} +`; + +exports[`snapshots same_group_crd_2.yaml 2`] = ` +Object { + "compute.example.com.ts": "// generated by cdk8s +import { ApiObject, ApiObjectMetadata, GroupVersionKind } from 'cdk8s'; +import { Construct } from 'constructs'; + + +/** + * + * + * @schema Subnetwork + */ +export class Subnetwork extends ApiObject { + /** + * Returns the apiVersion and kind for \\"Subnetwork\\" + */ + public static readonly GVK: GroupVersionKind = { + apiVersion: 'compute.example.com/v1', + kind: 'Subnetwork', + } + + /** + * Renders a Kubernetes manifest for \\"Subnetwork\\". + * + * This can be used to inline resource manifests inside other objects (e.g. as templates). + * + * @param props initialization props + */ + public static manifest(props: SubnetworkProps = {}): any { + return { + ...Subnetwork.GVK, + ...toJson_SubnetworkProps(props), + }; + } + + /** + * Defines a \\"Subnetwork\\" API object + * @param scope the scope in which to define this object + * @param id a scope-local name for the object + * @param props initialization props + */ + public constructor(scope: Construct, id: string, props: SubnetworkProps = {}) { + super(scope, id, { + ...Subnetwork.GVK, + ...props, + }); + } + + /** + * Renders the object to Kubernetes JSON. + */ + public override toJson(): any { + const resolved = super.toJson(); + + return { + ...Subnetwork.GVK, + ...toJson_SubnetworkProps(resolved), + }; + } +} + +/** + * @schema Subnetwork + */ +export interface SubnetworkProps { + /** + * @schema Subnetwork#metadata + */ + readonly metadata?: ApiObjectMetadata; + + /** + * @schema Subnetwork#spec + */ + readonly spec?: SubnetworkSpec; +} + +/** + * Converts an object of type 'SubnetworkProps' to JSON representation. + */ +/* eslint-disable max-len, @stylistic/max-len, quote-props, @stylistic/quote-props */ +export function toJson_SubnetworkProps(obj: SubnetworkProps | undefined): Record | undefined { + if (obj === undefined) { return undefined; } + const result = { + 'metadata': obj.metadata, + 'spec': toJson_SubnetworkSpec(obj.spec), + }; + // filter undefined values + return Object.entries(result).reduce((r, i) => (i[1] === undefined) ? r : ({ ...r, [i[0]]: i[1] }), {}); +} +/* eslint-enable max-len, @stylistic/max-len, quote-props, @stylistic/quote-props */ + +/** + * @schema SubnetworkSpec + */ +export interface SubnetworkSpec { + /** + * Reference to the parent network + * + * @schema SubnetworkSpec#networkRef + */ + readonly networkRef?: string; + + /** + * IP range for the subnetwork + * + * @schema SubnetworkSpec#ipRange + */ + readonly ipRange?: string; +} + +/** + * Converts an object of type 'SubnetworkSpec' to JSON representation. + */ +/* eslint-disable max-len, @stylistic/max-len, quote-props, @stylistic/quote-props */ +export function toJson_SubnetworkSpec(obj: SubnetworkSpec | undefined): Record | undefined { + if (obj === undefined) { return undefined; } + const result = { + 'networkRef': obj.networkRef, + 'ipRange': obj.ipRange, + }; + // filter undefined values + return Object.entries(result).reduce((r, i) => (i[1] === undefined) ? r : ({ ...r, [i[0]]: i[1] }), {}); +} +/* eslint-enable max-len, @stylistic/max-len, quote-props, @stylistic/quote-props */ + +", +} +`; + exports[`snapshots version.yaml 1`] = ` Object { "author": Object { diff --git a/test/import/fixtures/same_group_crd_1.yaml b/test/import/fixtures/same_group_crd_1.yaml new file mode 100644 index 000000000..82bc30a84 --- /dev/null +++ b/test/import/fixtures/same_group_crd_1.yaml @@ -0,0 +1,25 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networks.compute.example.com +spec: + group: compute.example.com + names: + kind: Network + plural: networks + singular: network + scope: Cluster + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + cidr: + type: string + description: CIDR block for the network diff --git a/test/import/fixtures/same_group_crd_2.yaml b/test/import/fixtures/same_group_crd_2.yaml new file mode 100644 index 000000000..b2f255b09 --- /dev/null +++ b/test/import/fixtures/same_group_crd_2.yaml @@ -0,0 +1,28 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: subnetworks.compute.example.com +spec: + group: compute.example.com + names: + kind: Subnetwork + plural: subnetworks + singular: subnetwork + scope: Cluster + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + networkRef: + type: string + description: Reference to the parent network + ipRange: + type: string + description: IP range for the subnetwork diff --git a/test/import/import-crd.test.ts b/test/import/import-crd.test.ts index d3500b12f..838887e00 100644 --- a/test/import/import-crd.test.ts +++ b/test/import/import-crd.test.ts @@ -640,3 +640,89 @@ describe('cdk8s.yaml file', () => { }); }); + +describe('aggregating multiple CRD imports from the same API group', () => { + + test('consolidates CRDs from same API group when importing multiple files without prefix', async () => { + // This test verifies that when importing multiple CRD files from the same API group + // (e.g., compute.example.com), they are consolidated into a single module instead + // of the second import overwriting the first. + const crd1Path = path.join(fixtures, 'same_group_crd_1.yaml'); + const crd2Path = path.join(fixtures, 'same_group_crd_2.yaml'); + + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'crd-aggregate-test')); + const importOptions: ImportOptions = { + targetLanguage: Language.TYPESCRIPT, + outdir: tempDir, + save: false, + }; + + try { + // Import both CRDs from the same API group + await importDispatch([ + { source: crd1Path }, + { source: crd2Path }, + ], {}, importOptions); + + // Both Network and Subnetwork should be in the same module file + const moduleFile = path.join(tempDir, 'compute.example.com.ts'); + expect(fs.existsSync(moduleFile)).toBe(true); + + const content = fs.readFileSync(moduleFile, 'utf-8'); + // Both classes should be present in the same file + expect(content).toContain('export class Network'); + expect(content).toContain('export class Subnetwork'); + } finally { + fs.removeSync(tempDir); + } + }); + + test('fromSpecs aggregates multiple import specs into single importer', async () => { + const crd1Path = path.join(fixtures, 'same_group_crd_1.yaml'); + const crd2Path = path.join(fixtures, 'same_group_crd_2.yaml'); + + const importer = await ImportCustomResourceDefinition.fromSpecs([ + { source: crd1Path }, + { source: crd2Path }, + ]); + + // Should have one module for the shared API group + expect(importer.moduleNames).toEqual(['compute.example.com']); + }); + + test('prefixed imports are kept separate even from same API group', async () => { + const crd1Path = path.join(fixtures, 'same_group_crd_1.yaml'); + const crd2Path = path.join(fixtures, 'same_group_crd_2.yaml'); + + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'crd-prefix-test')); + const importOptions: ImportOptions = { + targetLanguage: Language.TYPESCRIPT, + outdir: tempDir, + save: false, + }; + + try { + // Import with different prefixes - should create separate modules + await importDispatch([ + { source: crd1Path, moduleNamePrefix: 'net' }, + { source: crd2Path, moduleNamePrefix: 'subnet' }, + ], {}, importOptions); + + // Each should have its own prefixed file + expect(fs.existsSync(path.join(tempDir, 'net-compute.example.com.ts'))).toBe(true); + expect(fs.existsSync(path.join(tempDir, 'subnet-compute.example.com.ts'))).toBe(true); + + const netContent = fs.readFileSync(path.join(tempDir, 'net-compute.example.com.ts'), 'utf-8'); + const subnetContent = fs.readFileSync(path.join(tempDir, 'subnet-compute.example.com.ts'), 'utf-8'); + + expect(netContent).toContain('export class Network'); + expect(netContent).not.toContain('export class Subnetwork'); + + expect(subnetContent).toContain('export class Subnetwork'); + expect(subnetContent).not.toContain('export class Network'); + } finally { + fs.removeSync(tempDir); + } + }); + +});