diff --git a/src/object.ts b/src/object.ts index 0548b03a04..7761cd26dd 100644 --- a/src/object.ts +++ b/src/object.ts @@ -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'; @@ -29,11 +30,6 @@ type KubernetesObjectHeader = 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. @@ -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([]); @@ -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( @@ -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([]); @@ -593,30 +589,6 @@ export class KubernetesObjectApi { } } - protected async getSerializationType(apiVersion?: string, kind?: string): Promise { - 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 = 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( requestContext: RequestContext, type?: string, @@ -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) { diff --git a/src/util.ts b/src/util.ts index 791d89c728..e502e600d9 100644 --- a/src/util.ts +++ b/src/util.ts @@ -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 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], + }; +} diff --git a/src/util_test.ts b/src/util_test.ts index dd1061f8d1..74a323a866 100644 --- a/src/util_test.ts +++ b/src/util_test.ts @@ -9,6 +9,7 @@ import { quantityToScalar, totalCPU, totalMemory, + getSerializationType, } from './util.js'; describe('Utils', () => { @@ -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'); + }); }); diff --git a/src/yaml.ts b/src/yaml.ts index 7682392ae6..09468a3e5d 100644 --- a/src/yaml.ts +++ b/src/yaml.ts @@ -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(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); } diff --git a/src/yaml_test.ts b/src/yaml_test.ts index ba53293dc0..958f7ba45b 100644 --- a/src/yaml_test.ts +++ b/src/yaml_test.ts @@ -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', () => { @@ -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(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' + @@ -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 = { @@ -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); + }); });