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); + } + }); + +});