Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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([]);
Expand Down Expand Up @@ -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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than requiring the caller to do this, just modify getSerializationType to return KubernetesOjbect for the non-native case.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Modified getSerializationType to return 'KubernetesObject' for non-native cases and removed the ?? fallback operators from all callers in commit 3bce4d5.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Modified getSerializationType to return 'KubernetesObject' for non-native cases and removed the ?? fallback operators from all callers in commit b0f3f4b.

}

if (!type) {
Expand Down
47 changes: 46 additions & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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><Kind>".
// 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return 'KubernetesObject' here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Changed to return 'KubernetesObject' instead of undefined in commit 3bce4d5.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Changed to return 'KubernetesObject' instead of undefined in commit b0f3f4b.

}

const version = gv.version.charAt(0).toUpperCase() + gv.version.slice(1);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const version = gv.version.charAt(0).toUpperCase() + gv.version.slice(1);
const version = gv.version ? gv.version.charAt(0).toUpperCase() + gv.version.slice(1) : '';

Perhaps add check so code doesn't throw when gv.version is undefined or malformed. (Keep getSerializationType protected for edge cases)

return `${version}${kind}`;
}
Expand Down
10 changes: 10 additions & 0 deletions src/util_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
6 changes: 3 additions & 3 deletions src/yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function loadYaml<T>(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;
}
Expand All @@ -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);
});
}
Expand All @@ -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);
}
50 changes: 50 additions & 0 deletions src/yaml_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});