From 33f49e11a3c6587903e74edf88d99564ecb16915 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:11:10 +0000 Subject: [PATCH 1/2] Initial plan From 8c293c32d8ba626ca33e9bef3508f0b526a76be9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:30:01 +0000 Subject: [PATCH 2/2] Fix Knative Service YAML parsing by checking API groups Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com> --- src/object.ts | 8 ++++---- src/util.ts | 47 ++++++++++++++++++++++++++++++++++++++++++++- src/util_test.ts | 10 ++++++++++ src/yaml.ts | 6 +++--- src/yaml_test.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+), 8 deletions(-) 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'); + }); });