Skip to content

fix: make yaml functions use serializer #2520

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 25, 2025
Merged
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
38 changes: 5 additions & 33 deletions src/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ObjectSerializer } from './serializer.js';
import { KubernetesListObject, KubernetesObject } from './types.js';
import { from, mergeMap, of } from './gen/rxjsStub.js';
import { PatchStrategy } from './patch.js';
import { getSerializationType } from './util.js';

/** Kubernetes API verbs. */
type KubernetesApiAction = 'create' | 'delete' | 'patch' | 'read' | 'list' | 'replace';
Expand All @@ -29,11 +30,6 @@ type KubernetesObjectHeader<T extends KubernetesObject | KubernetesObject> = Pic
};
};

interface GroupVersion {
group: string;
version: string;
}

/**
* Dynamically construct Kubernetes API request URIs so client does not have to know what type of object it is acting
* on.
Expand Down Expand Up @@ -107,7 +103,7 @@ export class KubernetesObjectApi {
if (fieldManager !== undefined) {
requestContext.setQueryParam('fieldManager', ObjectSerializer.serialize(fieldManager, 'string'));
}
const type = await this.getSerializationType(spec.apiVersion, spec.kind);
const type = getSerializationType(spec.apiVersion, spec.kind);

// Body Params
const contentType = ObjectSerializer.getPreferredMediaType([]);
Expand Down Expand Up @@ -269,7 +265,7 @@ export class KubernetesObjectApi {
requestContext.setQueryParam('force', ObjectSerializer.serialize(force, 'boolean'));
}

const type = await this.getSerializationType(spec.apiVersion, spec.kind);
const type = getSerializationType(spec.apiVersion, spec.kind);

// Body Params
const serializedBody = ObjectSerializer.stringify(
Expand Down Expand Up @@ -468,7 +464,7 @@ export class KubernetesObjectApi {
requestContext.setQueryParam('fieldManager', ObjectSerializer.serialize(fieldManager, 'string'));
}

const type = await this.getSerializationType(spec.apiVersion, spec.kind);
const type = getSerializationType(spec.apiVersion, spec.kind);

// Body Params
const contentType = ObjectSerializer.getPreferredMediaType([]);
Expand Down Expand Up @@ -593,30 +589,6 @@ export class KubernetesObjectApi {
}
}

protected async getSerializationType(apiVersion?: string, kind?: string): Promise<string> {
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 = this.groupVersion(apiVersion);
const version = gv.version.charAt(0).toUpperCase() + gv.version.slice(1);
return `${version}${kind}`;
}

protected groupVersion(apiVersion: string): GroupVersion {
const v = apiVersion.split('/');
return v.length === 1
? {
group: 'core',
version: apiVersion,
}
: {
group: v[0],
version: v[1],
};
}

protected async requestPromise<T extends KubernetesObject | KubernetesObject>(
requestContext: RequestContext,
type?: string,
Expand Down Expand Up @@ -670,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 = await this.getSerializationType(data.apiVersion, data.kind);
type = getSerializationType(data.apiVersion, data.kind);
}

if (!type) {
Expand Down
29 changes: 29 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,32 @@ export function normalizeResponseHeaders(response: Response): { [key: string]: s

return normalizedHeaders;
}

export function getSerializationType(apiVersion?: string, kind?: string): string {
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);
const version = gv.version.charAt(0).toUpperCase() + gv.version.slice(1);
return `${version}${kind}`;
}

interface GroupVersion {
group: string;
version: string;
}

function groupVersion(apiVersion: string): GroupVersion {
const v = apiVersion.split('/');
return v.length === 1
? {
group: 'core',
version: apiVersion,
}
: {
group: v[0],
version: v[1],
};
}
7 changes: 7 additions & 0 deletions src/util_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
quantityToScalar,
totalCPU,
totalMemory,
getSerializationType,
} from './util.js';

describe('Utils', () => {
Expand Down Expand Up @@ -142,4 +143,10 @@ describe('Utils', () => {

deepStrictEqual(normalizeResponseHeaders(response), { foo: 'bar', baz: 'k8s' });
});

it('should get the serialization type correctly', () => {
strictEqual(getSerializationType('v1', 'Pod'), 'V1Pod');
strictEqual(getSerializationType('apps/v1', 'Deployment'), 'V1Deployment');
strictEqual(getSerializationType(undefined, undefined), 'KubernetesObject');
});
});
41 changes: 38 additions & 3 deletions src/yaml.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,48 @@
import yaml from 'js-yaml';
import { getSerializationType } from './util.js';
import { KubernetesObject } from './types.js';
import { ObjectSerializer } from './serializer.js';

/**
* Load a Kubernetes object from YAML.
* @param data - The YAML string to load.
* @param opts - Optional YAML load options.
* @returns The deserialized Kubernetes object.
*/
export function loadYaml<T>(data: string, opts?: yaml.LoadOptions): T {
return yaml.load(data, opts) as any as T;
const yml = yaml.load(data, opts) as any as KubernetesObject;
if (!yml) {
throw new Error('Failed to load YAML');
}
const type = getSerializationType(yml.apiVersion, yml.kind);

return ObjectSerializer.deserialize(yml, type) as T;
}

/**
* Load all Kubernetes objects from YAML.
* @param data - The YAML string to load.
* @param opts - Optional YAML load options.
* @returns An array of deserialized Kubernetes objects.
*/
export function loadAllYaml(data: string, opts?: yaml.LoadOptions): any[] {
return yaml.loadAll(data, undefined, opts);
const ymls = yaml.loadAll(data, undefined, opts);
return ymls.map((yml) => {
const obj = yml as KubernetesObject;
const type = getSerializationType(obj.apiVersion, obj.kind);
return ObjectSerializer.deserialize(yml, type);
});
}

/**
* Dump a Kubernetes object to YAML.
* @param object - The Kubernetes object to dump.
* @param opts - Optional YAML dump options.
* @returns The YAML string representation of the serialized Kubernetes object.
*/
export function dumpYaml(object: any, opts?: yaml.DumpOptions): string {
return yaml.dump(object, opts);
const kubeObject = object as KubernetesObject;
const type = getSerializationType(kubeObject.apiVersion, kubeObject.kind);
const serialized = ObjectSerializer.serialize(kubeObject, type);
return yaml.dump(serialized, opts);
}
119 changes: 115 additions & 4 deletions src/yaml_test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it } from 'node:test';
import { deepStrictEqual, strictEqual } from 'node:assert';
import { V1Namespace } from './api.js';
import { deepEqual, deepStrictEqual, strictEqual } from 'node:assert';
import { V1CustomResourceDefinition, V1Namespace } from './api.js';
import { dumpYaml, loadAllYaml, loadYaml } from './yaml.js';

describe('yaml', () => {
Expand All @@ -12,6 +12,43 @@ describe('yaml', () => {
strictEqual(ns.kind, 'Namespace');
strictEqual(ns.metadata!.name, 'some-namespace');
});
it('should load safely by mapping properties correctly', () => {
const yaml = `
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: my-crd.example.com
spec:
group: example.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
foobar:
anyOf:
- type: integer
- type: string
x-kubernetes-int-or-string: true
`;
const ns = loadYaml<V1CustomResourceDefinition>(yaml);

strictEqual(ns.apiVersion, 'apiextensions.k8s.io/v1');
strictEqual(ns.kind, 'CustomResourceDefinition');
strictEqual(ns.metadata!.name, 'my-crd.example.com');
strictEqual(
ns.spec.versions[0]!.schema!.openAPIV3Schema!.properties!['foobar'].x_kubernetes_int_or_string,
true,
);
strictEqual(
ns.spec.versions[0]!.schema!.openAPIV3Schema!.properties!['foobar']['x-kubernetes-int-or-string'],
undefined,
);
});

it('should load all safely', () => {
const yaml =
'apiVersion: v1\n' +
Expand All @@ -23,15 +60,49 @@ describe('yaml', () => {
'kind: Pod\n' +
'metadata:\n' +
' name: some-pod\n' +
' namespace: some-ns\n';
' namespace: some-ns\n' +
'---\n' +
'apiVersion: apiextensions.k8s.io/v1\n' +
'kind: CustomResourceDefinition\n' +
'metadata:\n' +
' name: my-crd.example.com\n' +
'spec:\n' +
' group: example.com\n' +
' versions:\n' +
' - name: v1\n' +
' served: true\n' +
' storage: true\n' +
' schema:\n' +
' openAPIV3Schema:\n' +
' type: object\n' +
' properties:\n' +
' foobar:\n' +
' anyOf:\n' +
' - type: integer\n' +
' - type: string\n' +
' x-kubernetes-int-or-string: true\n';
const objects = loadAllYaml(yaml);

strictEqual(objects.length, 2);
strictEqual(objects.length, 3);
strictEqual(objects[0].kind, 'Namespace');
strictEqual(objects[1].kind, 'Pod');
strictEqual(objects[0].metadata.name, 'some-namespace');
strictEqual(objects[1].metadata.name, 'some-pod');
strictEqual(objects[1].metadata.namespace, 'some-ns');
strictEqual(objects[2].apiVersion, 'apiextensions.k8s.io/v1');
strictEqual(objects[2].kind, 'CustomResourceDefinition');
strictEqual(objects[2].metadata!.name, 'my-crd.example.com');
strictEqual(
objects[2].spec.versions[0]!.schema!.openAPIV3Schema!.properties!['foobar']
.x_kubernetes_int_or_string,
true,
);
strictEqual(
objects[2].spec.versions[0]!.schema!.openAPIV3Schema!.properties!['foobar'][
'x-kubernetes-int-or-string'
],
undefined,
);
});
it('should round trip successfully', () => {
const expected = {
Expand All @@ -43,4 +114,44 @@ describe('yaml', () => {
const actual = loadYaml(yamlString);
deepStrictEqual(actual, expected);
});

it('should round trip successfully with mapped properties', () => {
const expected: V1CustomResourceDefinition = {
apiVersion: 'apiextensions.k8s.io/v1',
kind: 'CustomResourceDefinition',
metadata: {
name: 'my-crd.example.com',
},
spec: {
group: 'example.com',
names: {
kind: 'MyCRD',
plural: 'mycrds',
},
scope: 'Namespaced',
versions: [
{
name: 'v1',
schema: {
openAPIV3Schema: {
properties: {
foobar: {
anyOf: [{ type: 'integer' }, { type: 'string' }],
x_kubernetes_int_or_string: true,
},
},
type: 'object',
},
},
served: true,
storage: true,
},
],
},
};
const yamlString = dumpYaml(expected);
const actual = loadYaml(yamlString);
// not using strict equality as types are not matching
deepEqual(actual, expected);
});
});