diff --git a/src/object.ts b/src/object.ts index 7761cd26dd..5c17528fa5 100644 --- a/src/object.ts +++ b/src/object.ts @@ -103,7 +103,7 @@ export class KubernetesObjectApi { if (fieldManager !== undefined) { requestContext.setQueryParam('fieldManager', ObjectSerializer.serialize(fieldManager, 'string')); } - const type = getSerializationType(spec.apiVersion, spec.kind); + const type = getSerializationType(spec.apiVersion, spec.kind) ?? 'KubernetesObject'; // Body Params const contentType = ObjectSerializer.getPreferredMediaType([]); @@ -265,7 +265,7 @@ export class KubernetesObjectApi { requestContext.setQueryParam('force', ObjectSerializer.serialize(force, 'boolean')); } - const type = getSerializationType(spec.apiVersion, spec.kind); + const type = getSerializationType(spec.apiVersion, spec.kind) ?? 'KubernetesObject'; // Body Params const serializedBody = ObjectSerializer.stringify( @@ -464,7 +464,7 @@ export class KubernetesObjectApi { requestContext.setQueryParam('fieldManager', ObjectSerializer.serialize(fieldManager, 'string')); } - const type = getSerializationType(spec.apiVersion, spec.kind); + const type = getSerializationType(spec.apiVersion, spec.kind) ?? 'KubernetesObject'; // Body Params const contentType = ObjectSerializer.getPreferredMediaType([]); @@ -642,7 +642,7 @@ export class KubernetesObjectApi { if (response.httpStatusCode >= 200 && response.httpStatusCode <= 299) { const data = ObjectSerializer.parse(await response.body.text(), contentType); if (type === undefined) { - type = getSerializationType(data.apiVersion, data.kind); + type = getSerializationType(data.apiVersion, data.kind) ?? 'KubernetesObject'; } if (!type) { diff --git a/src/util.ts b/src/util.ts index e502e600d9..4b3d7fff16 100644 --- a/src/util.ts +++ b/src/util.ts @@ -165,13 +165,58 @@ export function normalizeResponseHeaders(response: Response): { [key: string]: s return normalizedHeaders; } -export function getSerializationType(apiVersion?: string, kind?: string): string { +/** + * Built-in Kubernetes API groups that have generated TypeScript models. + * Custom resources and third-party API groups (like Knative) are not included. + */ +const BUILT_IN_API_GROUPS = new Set([ + 'core', // maps to "" (empty string) for core resources like Pod, Service, etc. + 'admissionregistration.k8s.io', + 'apiextensions.k8s.io', + 'apiregistration.k8s.io', + 'apps', + 'authentication.k8s.io', + 'authorization.k8s.io', + 'autoscaling', + 'batch', + 'certificates.k8s.io', + 'coordination.k8s.io', + 'discovery.k8s.io', + 'events.k8s.io', + 'flowcontrol.apiserver.k8s.io', + 'internal.apiserver.k8s.io', + 'networking.k8s.io', + 'node.k8s.io', + 'policy', + 'rbac.authorization.k8s.io', + 'resource.k8s.io', + 'scheduling.k8s.io', + 'storage.k8s.io', + 'storagemigration.k8s.io', +]); + +/** + * Check if the given API group is a built-in Kubernetes API group. + * @param group - The API group to check (e.g., "apps", "serving.knative.dev", "core") + * @returns true if the group is a built-in Kubernetes API group, false otherwise + */ +function isBuiltInApiGroup(group: string): boolean { + return BUILT_IN_API_GROUPS.has(group); +} + +export function getSerializationType(apiVersion?: string, kind?: string): string | undefined { if (apiVersion === undefined || kind === undefined) { return 'KubernetesObject'; } // Types are defined in src/gen/api/models with the format "". // Version and Kind are in PascalCase. const gv = groupVersion(apiVersion); + + // Only return a type name if this is a built-in Kubernetes API group + if (!isBuiltInApiGroup(gv.group)) { + return undefined; + } + const version = gv.version.charAt(0).toUpperCase() + gv.version.slice(1); return `${version}${kind}`; } diff --git a/src/util_test.ts b/src/util_test.ts index 74a323a866..1ed94bcdd8 100644 --- a/src/util_test.ts +++ b/src/util_test.ts @@ -145,8 +145,18 @@ describe('Utils', () => { }); it('should get the serialization type correctly', () => { + // Built-in Kubernetes resources should return a type strictEqual(getSerializationType('v1', 'Pod'), 'V1Pod'); strictEqual(getSerializationType('apps/v1', 'Deployment'), 'V1Deployment'); + strictEqual(getSerializationType('v1', 'Service'), 'V1Service'); + strictEqual(getSerializationType('batch/v1', 'Job'), 'V1Job'); + + // Non-built-in resources should return undefined + strictEqual(getSerializationType('serving.knative.dev/v1', 'Service'), undefined); + strictEqual(getSerializationType('example.com/v1', 'MyCustomResource'), undefined); + strictEqual(getSerializationType('custom.io/v1alpha1', 'CustomThing'), undefined); + + // Undefined inputs should return 'KubernetesObject' strictEqual(getSerializationType(undefined, undefined), 'KubernetesObject'); }); }); diff --git a/src/yaml.ts b/src/yaml.ts index 09468a3e5d..de4ce5bd75 100644 --- a/src/yaml.ts +++ b/src/yaml.ts @@ -14,7 +14,7 @@ export function loadYaml(data: string, opts?: yaml.LoadOptions): T { if (!yml) { throw new Error('Failed to load YAML'); } - const type = getSerializationType(yml.apiVersion, yml.kind); + const type = getSerializationType(yml.apiVersion, yml.kind) ?? 'KubernetesObject'; return ObjectSerializer.deserialize(yml, type) as T; } @@ -29,7 +29,7 @@ export function loadAllYaml(data: string, opts?: yaml.LoadOptions): any[] { const ymls = yaml.loadAll(data, undefined, opts); return ymls.map((yml) => { const obj = yml as KubernetesObject; - const type = getSerializationType(obj.apiVersion, obj.kind); + const type = getSerializationType(obj.apiVersion, obj.kind) ?? 'KubernetesObject'; return ObjectSerializer.deserialize(yml, type); }); } @@ -42,7 +42,7 @@ export function loadAllYaml(data: string, opts?: yaml.LoadOptions): any[] { */ export function dumpYaml(object: any, opts?: yaml.DumpOptions): string { const kubeObject = object as KubernetesObject; - const type = getSerializationType(kubeObject.apiVersion, kubeObject.kind); + const type = getSerializationType(kubeObject.apiVersion, kubeObject.kind) ?? 'KubernetesObject'; const serialized = ObjectSerializer.serialize(kubeObject, type); return yaml.dump(serialized, opts); } diff --git a/src/yaml_test.ts b/src/yaml_test.ts index 958f7ba45b..c08d1dd158 100644 --- a/src/yaml_test.ts +++ b/src/yaml_test.ts @@ -154,4 +154,54 @@ spec: // not using strict equality as types are not matching deepEqual(actual, expected); }); + + it('should load Knative Service correctly preserving spec', () => { + const yaml = `apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: hello-world +spec: + template: + spec: + containers: + - image: ghcr.io/knative/helloworld-go:latest + ports: + - containerPort: 8080 + env: + - name: TARGET + value: "World"`; + const knativeService = loadYaml(yaml); + + strictEqual(knativeService.apiVersion, 'serving.knative.dev/v1'); + strictEqual(knativeService.kind, 'Service'); + strictEqual(knativeService.metadata.name, 'hello-world'); + // Verify that the spec is preserved + strictEqual( + knativeService.spec.template.spec.containers[0].image, + 'ghcr.io/knative/helloworld-go:latest', + ); + strictEqual(knativeService.spec.template.spec.containers[0].ports[0].containerPort, 8080); + strictEqual(knativeService.spec.template.spec.containers[0].env[0].name, 'TARGET'); + strictEqual(knativeService.spec.template.spec.containers[0].env[0].value, 'World'); + }); + + it('should load custom resources correctly', () => { + const yaml = `apiVersion: example.com/v1 +kind: MyCustomResource +metadata: + name: my-resource +spec: + customField: customValue + nestedObject: + key1: value1 + key2: value2`; + const customResource = loadYaml(yaml); + + strictEqual(customResource.apiVersion, 'example.com/v1'); + strictEqual(customResource.kind, 'MyCustomResource'); + strictEqual(customResource.metadata.name, 'my-resource'); + strictEqual(customResource.spec.customField, 'customValue'); + strictEqual(customResource.spec.nestedObject.key1, 'value1'); + strictEqual(customResource.spec.nestedObject.key2, 'value2'); + }); });