Skip to content

Commit 5bdd3f9

Browse files
committed
fix: make yaml functions use serializer
1 parent eee23eb commit 5bdd3f9

File tree

5 files changed

+194
-40
lines changed

5 files changed

+194
-40
lines changed

src/object.ts

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ObjectSerializer } from './serializer.js';
1515
import { KubernetesListObject, KubernetesObject } from './types.js';
1616
import { from, mergeMap, of } from './gen/rxjsStub.js';
1717
import { PatchStrategy } from './patch.js';
18+
import { getSerializationType } from './util.js';
1819

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

32-
interface GroupVersion {
33-
group: string;
34-
version: string;
35-
}
36-
3733
/**
3834
* Dynamically construct Kubernetes API request URIs so client does not have to know what type of object it is acting
3935
* on.
@@ -107,7 +103,7 @@ export class KubernetesObjectApi {
107103
if (fieldManager !== undefined) {
108104
requestContext.setQueryParam('fieldManager', ObjectSerializer.serialize(fieldManager, 'string'));
109105
}
110-
const type = await this.getSerializationType(spec.apiVersion, spec.kind);
106+
const type = getSerializationType(spec.apiVersion, spec.kind);
111107

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

272-
const type = await this.getSerializationType(spec.apiVersion, spec.kind);
268+
const type = getSerializationType(spec.apiVersion, spec.kind);
273269

274270
// Body Params
275271
const serializedBody = ObjectSerializer.stringify(
@@ -468,7 +464,7 @@ export class KubernetesObjectApi {
468464
requestContext.setQueryParam('fieldManager', ObjectSerializer.serialize(fieldManager, 'string'));
469465
}
470466

471-
const type = await this.getSerializationType(spec.apiVersion, spec.kind);
467+
const type = getSerializationType(spec.apiVersion, spec.kind);
472468

473469
// Body Params
474470
const contentType = ObjectSerializer.getPreferredMediaType([]);
@@ -593,30 +589,6 @@ export class KubernetesObjectApi {
593589
}
594590
}
595591

596-
protected async getSerializationType(apiVersion?: string, kind?: string): Promise<string> {
597-
if (apiVersion === undefined || kind === undefined) {
598-
return 'KubernetesObject';
599-
}
600-
// Types are defined in src/gen/api/models with the format "<Version><Kind>".
601-
// Version and Kind are in PascalCase.
602-
const gv = this.groupVersion(apiVersion);
603-
const version = gv.version.charAt(0).toUpperCase() + gv.version.slice(1);
604-
return `${version}${kind}`;
605-
}
606-
607-
protected groupVersion(apiVersion: string): GroupVersion {
608-
const v = apiVersion.split('/');
609-
return v.length === 1
610-
? {
611-
group: 'core',
612-
version: apiVersion,
613-
}
614-
: {
615-
group: v[0],
616-
version: v[1],
617-
};
618-
}
619-
620592
protected async requestPromise<T extends KubernetesObject | KubernetesObject>(
621593
requestContext: RequestContext,
622594
type?: string,
@@ -670,7 +642,7 @@ export class KubernetesObjectApi {
670642
if (response.httpStatusCode >= 200 && response.httpStatusCode <= 299) {
671643
const data = ObjectSerializer.parse(await response.body.text(), contentType);
672644
if (type === undefined) {
673-
type = await this.getSerializationType(data.apiVersion, data.kind);
645+
type = getSerializationType(data.apiVersion, data.kind);
674646
}
675647

676648
if (!type) {

src/util.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,32 @@ export function normalizeResponseHeaders(response: Response): { [key: string]: s
164164

165165
return normalizedHeaders;
166166
}
167+
168+
export function getSerializationType(apiVersion?: string, kind?: string): string {
169+
if (apiVersion === undefined || kind === undefined) {
170+
return 'KubernetesObject';
171+
}
172+
// Types are defined in src/gen/api/models with the format "<Version><Kind>".
173+
// Version and Kind are in PascalCase.
174+
const gv = groupVersion(apiVersion);
175+
const version = gv.version.charAt(0).toUpperCase() + gv.version.slice(1);
176+
return `${version}${kind}`;
177+
}
178+
179+
interface GroupVersion {
180+
group: string;
181+
version: string;
182+
}
183+
184+
function groupVersion(apiVersion: string): GroupVersion {
185+
const v = apiVersion.split('/');
186+
return v.length === 1
187+
? {
188+
group: 'core',
189+
version: apiVersion,
190+
}
191+
: {
192+
group: v[0],
193+
version: v[1],
194+
};
195+
}

src/util_test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
quantityToScalar,
1010
totalCPU,
1111
totalMemory,
12+
getSerializationType,
1213
} from './util.js';
1314

1415
describe('Utils', () => {
@@ -142,4 +143,10 @@ describe('Utils', () => {
142143

143144
deepStrictEqual(normalizeResponseHeaders(response), { foo: 'bar', baz: 'k8s' });
144145
});
146+
147+
it('should get the serialization type correctly', () => {
148+
strictEqual(getSerializationType('v1', 'Pod'), 'V1Pod');
149+
strictEqual(getSerializationType('apps/v1', 'Deployment'), 'V1Deployment');
150+
strictEqual(getSerializationType(undefined, undefined), 'KubernetesObject');
151+
});
145152
});

src/yaml.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,48 @@
11
import yaml from 'js-yaml';
2+
import { getSerializationType } from './util.js';
3+
import { KubernetesObject } from './types.js';
4+
import { ObjectSerializer } from './serializer.js';
25

6+
/**
7+
* Load a Kubernetes object from YAML.
8+
* @param data - The YAML string to load.
9+
* @param opts - Optional YAML load options.
10+
* @returns The deserialized Kubernetes object.
11+
*/
312
export function loadYaml<T>(data: string, opts?: yaml.LoadOptions): T {
4-
return yaml.load(data, opts) as any as T;
13+
const yml = yaml.load(data, opts) as any as KubernetesObject;
14+
if (!yml) {
15+
throw new Error('Failed to load YAML');
16+
}
17+
const type = getSerializationType(yml.apiVersion, yml.kind);
18+
19+
return ObjectSerializer.deserialize(yml, type) as T;
520
}
621

22+
/**
23+
* Load all Kubernetes objects from YAML.
24+
* @param data - The YAML string to load.
25+
* @param opts - Optional YAML load options.
26+
* @returns An array of deserialized Kubernetes objects.
27+
*/
728
export function loadAllYaml(data: string, opts?: yaml.LoadOptions): any[] {
8-
return yaml.loadAll(data, undefined, opts);
29+
const ymls = yaml.loadAll(data, undefined, opts);
30+
return ymls.map((yml) => {
31+
const obj = yml as KubernetesObject;
32+
const type = getSerializationType(obj.apiVersion, obj.kind);
33+
return ObjectSerializer.deserialize(yml, type);
34+
});
935
}
1036

37+
/**
38+
* Dump a Kubernetes object to YAML.
39+
* @param object - The Kubernetes object to dump.
40+
* @param opts - Optional YAML dump options.
41+
* @returns The YAML string representation of the serialized Kubernetes object.
42+
*/
1143
export function dumpYaml(object: any, opts?: yaml.DumpOptions): string {
12-
return yaml.dump(object, opts);
44+
const kubeObject = object as KubernetesObject;
45+
const type = getSerializationType(kubeObject.apiVersion, kubeObject.kind);
46+
const serialized = ObjectSerializer.serialize(kubeObject, type);
47+
return yaml.dump(serialized, opts);
1348
}

src/yaml_test.ts

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it } from 'node:test';
2-
import { deepStrictEqual, strictEqual } from 'node:assert';
3-
import { V1Namespace } from './api.js';
2+
import { deepEqual, deepStrictEqual, strictEqual } from 'node:assert';
3+
import { V1CustomResourceDefinition, V1Namespace } from './api.js';
44
import { dumpYaml, loadAllYaml, loadYaml } from './yaml.js';
55

66
describe('yaml', () => {
@@ -12,6 +12,43 @@ describe('yaml', () => {
1212
strictEqual(ns.kind, 'Namespace');
1313
strictEqual(ns.metadata!.name, 'some-namespace');
1414
});
15+
it('should load safely by mapping properties correctly', () => {
16+
const yaml = `
17+
apiVersion: apiextensions.k8s.io/v1
18+
kind: CustomResourceDefinition
19+
metadata:
20+
name: my-crd.example.com
21+
spec:
22+
group: example.com
23+
versions:
24+
- name: v1
25+
served: true
26+
storage: true
27+
schema:
28+
openAPIV3Schema:
29+
type: object
30+
properties:
31+
foobar:
32+
anyOf:
33+
- type: integer
34+
- type: string
35+
x-kubernetes-int-or-string: true
36+
`;
37+
const ns = loadYaml<V1CustomResourceDefinition>(yaml);
38+
39+
strictEqual(ns.apiVersion, 'apiextensions.k8s.io/v1');
40+
strictEqual(ns.kind, 'CustomResourceDefinition');
41+
strictEqual(ns.metadata!.name, 'my-crd.example.com');
42+
strictEqual(
43+
ns.spec.versions[0]!.schema!.openAPIV3Schema!.properties!['foobar'].x_kubernetes_int_or_string,
44+
true,
45+
);
46+
strictEqual(
47+
ns.spec.versions[0]!.schema!.openAPIV3Schema!.properties!['foobar']['x-kubernetes-int-or-string'],
48+
undefined,
49+
);
50+
});
51+
1552
it('should load all safely', () => {
1653
const yaml =
1754
'apiVersion: v1\n' +
@@ -23,15 +60,49 @@ describe('yaml', () => {
2360
'kind: Pod\n' +
2461
'metadata:\n' +
2562
' name: some-pod\n' +
26-
' namespace: some-ns\n';
63+
' namespace: some-ns\n' +
64+
'---\n' +
65+
'apiVersion: apiextensions.k8s.io/v1\n' +
66+
'kind: CustomResourceDefinition\n' +
67+
'metadata:\n' +
68+
' name: my-crd.example.com\n' +
69+
'spec:\n' +
70+
' group: example.com\n' +
71+
' versions:\n' +
72+
' - name: v1\n' +
73+
' served: true\n' +
74+
' storage: true\n' +
75+
' schema:\n' +
76+
' openAPIV3Schema:\n' +
77+
' type: object\n' +
78+
' properties:\n' +
79+
' foobar:\n' +
80+
' anyOf:\n' +
81+
' - type: integer\n' +
82+
' - type: string\n' +
83+
' x-kubernetes-int-or-string: true\n';
2784
const objects = loadAllYaml(yaml);
2885

29-
strictEqual(objects.length, 2);
86+
strictEqual(objects.length, 3);
3087
strictEqual(objects[0].kind, 'Namespace');
3188
strictEqual(objects[1].kind, 'Pod');
3289
strictEqual(objects[0].metadata.name, 'some-namespace');
3390
strictEqual(objects[1].metadata.name, 'some-pod');
3491
strictEqual(objects[1].metadata.namespace, 'some-ns');
92+
strictEqual(objects[2].apiVersion, 'apiextensions.k8s.io/v1');
93+
strictEqual(objects[2].kind, 'CustomResourceDefinition');
94+
strictEqual(objects[2].metadata!.name, 'my-crd.example.com');
95+
strictEqual(
96+
objects[2].spec.versions[0]!.schema!.openAPIV3Schema!.properties!['foobar']
97+
.x_kubernetes_int_or_string,
98+
true,
99+
);
100+
strictEqual(
101+
objects[2].spec.versions[0]!.schema!.openAPIV3Schema!.properties!['foobar'][
102+
'x-kubernetes-int-or-string'
103+
],
104+
undefined,
105+
);
35106
});
36107
it('should round trip successfully', () => {
37108
const expected = {
@@ -43,4 +114,44 @@ describe('yaml', () => {
43114
const actual = loadYaml(yamlString);
44115
deepStrictEqual(actual, expected);
45116
});
117+
118+
it('should round trip successfully with mapped properties', () => {
119+
const expected: V1CustomResourceDefinition = {
120+
apiVersion: 'apiextensions.k8s.io/v1',
121+
kind: 'CustomResourceDefinition',
122+
metadata: {
123+
name: 'my-crd.example.com',
124+
},
125+
spec: {
126+
group: 'example.com',
127+
names: {
128+
kind: 'MyCRD',
129+
plural: 'mycrds',
130+
},
131+
scope: 'Namespaced',
132+
versions: [
133+
{
134+
name: 'v1',
135+
schema: {
136+
openAPIV3Schema: {
137+
properties: {
138+
foobar: {
139+
anyOf: [{ type: 'integer' }, { type: 'string' }],
140+
x_kubernetes_int_or_string: true,
141+
},
142+
},
143+
type: 'object',
144+
},
145+
},
146+
served: true,
147+
storage: true,
148+
},
149+
],
150+
},
151+
};
152+
const yamlString = dumpYaml(expected);
153+
const actual = loadYaml(yamlString);
154+
// not using strict equality as types are not matching
155+
deepEqual(actual, expected);
156+
});
46157
});

0 commit comments

Comments
 (0)