Skip to content

Commit fa8e512

Browse files
committed
Add versioning to create, read and replace endpoint and fix resource deserialization
1 parent 20ff54c commit fa8e512

File tree

2 files changed

+132
-46
lines changed

2 files changed

+132
-46
lines changed

src/object.ts

+68-18
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@ type KubernetesObjectResponseBody =
2222
/** Kubernetes API verbs. */
2323
type KubernetesApiAction = 'create' | 'delete' | 'patch' | 'read' | 'list' | 'replace';
2424

25+
type KubernetesObjectHeader<T extends KubernetesObject | KubernetesObject> = Pick<
26+
T,
27+
'apiVersion' | 'kind'
28+
> & {
29+
metadata: {
30+
name: string;
31+
namespace: string;
32+
};
33+
};
34+
35+
interface GroupVersion {
36+
group: string;
37+
version: string;
38+
}
39+
2540
/**
2641
* Valid Content-Type header values for patch operations. See
2742
* https://kubernetes.io/docs/tasks/run-application/update-api-object-kubectl-patch/
@@ -74,13 +89,13 @@ export class KubernetesObjectApi extends ApisApi {
7489
* @param options Optional headers to use in the request.
7590
* @return Promise containing the request response and [[KubernetesObject]].
7691
*/
77-
public async create(
78-
spec: KubernetesObject,
92+
public async create<T extends KubernetesObject | KubernetesObject>(
93+
spec: T,
7994
pretty?: string,
8095
dryRun?: string,
8196
fieldManager?: string,
8297
options: { headers: { [name: string]: string } } = { headers: {} },
83-
): Promise<{ body: KubernetesObject; response: http.IncomingMessage }> {
98+
): Promise<{ body: T; response: http.IncomingMessage }> {
8499
// verify required parameter 'spec' is not null or undefined
85100
if (spec === null || spec === undefined) {
86101
throw new Error('Required parameter spec was null or undefined when calling create.');
@@ -218,14 +233,14 @@ export class KubernetesObjectApi extends ApisApi {
218233
* @param options Optional headers to use in the request.
219234
* @return Promise containing the request response and [[KubernetesObject]].
220235
*/
221-
public async patch(
222-
spec: KubernetesObject,
236+
public async patch<T extends KubernetesObject | KubernetesObject>(
237+
spec: T,
223238
pretty?: string,
224239
dryRun?: string,
225240
fieldManager?: string,
226241
force?: boolean,
227242
options: { headers: { [name: string]: string } } = { headers: {} },
228-
): Promise<{ body: KubernetesObject; response: http.IncomingMessage }> {
243+
): Promise<{ body: T; response: http.IncomingMessage }> {
229244
// verify required parameter 'spec' is not null or undefined
230245
if (spec === null || spec === undefined) {
231246
throw new Error('Required parameter spec was null or undefined when calling patch.');
@@ -275,17 +290,24 @@ export class KubernetesObjectApi extends ApisApi {
275290
* @param options Optional headers to use in the request.
276291
* @return Promise containing the request response and [[KubernetesObject]].
277292
*/
278-
public async read(
279-
spec: KubernetesObject,
293+
public async read<T extends KubernetesObject | KubernetesObject>(
294+
spec: KubernetesObjectHeader<T>,
280295
pretty?: string,
281296
exact?: boolean,
282297
exportt?: boolean,
283298
options: { headers: { [name: string]: string } } = { headers: {} },
284-
): Promise<{ body: KubernetesObject; response: http.IncomingMessage }> {
299+
): Promise<{ body: T; response: http.IncomingMessage }> {
285300
// verify required parameter 'spec' is not null or undefined
286301
if (spec === null || spec === undefined) {
287302
throw new Error('Required parameter spec was null or undefined when calling read.');
288303
}
304+
// verify required parameter 'kind' is not null or undefined
305+
if (spec.kind === null || spec.kind === undefined) {
306+
throw new Error('Required parameter spec.kind was null or undefined when calling read.');
307+
}
308+
if (!spec.apiVersion) {
309+
spec.apiVersion = 'v1';
310+
}
289311

290312
const localVarPath = await this.specUriPath(spec, 'read');
291313
const localVarQueryParameters: any = {};
@@ -331,7 +353,7 @@ export class KubernetesObjectApi extends ApisApi {
331353
* @param options Optional headers to use in the request.
332354
* @return Promise containing the request response and [[KubernetesListObject<KubernetesObject>]].
333355
*/
334-
public async list(
356+
public async list<T extends KubernetesObject | KubernetesObject>(
335357
apiVersion: string,
336358
kind: string,
337359
namespace?: string,
@@ -343,7 +365,7 @@ export class KubernetesObjectApi extends ApisApi {
343365
limit?: number,
344366
continueToken?: string,
345367
options: { headers: { [name: string]: string } } = { headers: {} },
346-
): Promise<{ body: KubernetesListObject<KubernetesObject>; response: http.IncomingMessage }> {
368+
): Promise<{ body: KubernetesListObject<T>; response: http.IncomingMessage }> {
347369
// verify required parameters 'apiVersion', 'kind' is not null or undefined
348370
if (apiVersion === null || apiVersion === undefined) {
349371
throw new Error('Required parameter apiVersion was null or undefined when calling list.');
@@ -418,13 +440,13 @@ export class KubernetesObjectApi extends ApisApi {
418440
* @param options Optional headers to use in the request.
419441
* @return Promise containing the request response and [[KubernetesObject]].
420442
*/
421-
public async replace(
422-
spec: KubernetesObject,
443+
public async replace<T extends KubernetesObject | KubernetesObject>(
444+
spec: T,
423445
pretty?: string,
424446
dryRun?: string,
425447
fieldManager?: string,
426448
options: { headers: { [name: string]: string } } = { headers: {} },
427-
): Promise<{ body: KubernetesObject; response: http.IncomingMessage }> {
449+
): Promise<{ body: T; response: http.IncomingMessage }> {
428450
// verify required parameter 'spec' is not null or undefined
429451
if (spec === null || spec === undefined) {
430452
throw new Error('Required parameter spec was null or undefined when calling replace.');
@@ -477,7 +499,7 @@ export class KubernetesObjectApi extends ApisApi {
477499
*
478500
* @param spec Kubernetes resource spec which must define kind and apiVersion properties.
479501
* @param action API action, see [[K8sApiAction]].
480-
* @return tail of resource-specific URI
502+
* @return tail of resource-specific URIDeploym
481503
*/
482504
protected async specUriPath(spec: KubernetesObject, action: KubernetesApiAction): Promise<string> {
483505
if (!spec.kind) {
@@ -592,12 +614,36 @@ export class KubernetesObjectApi extends ApisApi {
592614
}
593615
}
594616

617+
protected async getSerializationType(apiVersion?: string, kind?: string): Promise<string> {
618+
if (apiVersion === undefined || kind === undefined) {
619+
return 'KubernetesObject';
620+
}
621+
// Types are defined in src/gen/api/models with the format "<Version><Kind>".
622+
// Version and Kind are in PascalCase.
623+
const gv = this.groupVersion(apiVersion);
624+
const version = gv.version.charAt(0).toUpperCase() + gv.version.slice(1);
625+
return `${version}${kind}`;
626+
}
627+
628+
protected groupVersion(apiVersion: string): GroupVersion {
629+
const v = apiVersion.split('/');
630+
return v.length === 1
631+
? {
632+
group: 'core',
633+
version: apiVersion,
634+
}
635+
: {
636+
group: v[0],
637+
version: v[1],
638+
};
639+
}
640+
595641
/**
596642
* Standard Kubernetes request wrapped in a Promise.
597643
*/
598644
protected async requestPromise<T extends KubernetesObjectResponseBody = KubernetesObject>(
599645
requestOptions: request.Options,
600-
tipe: string = 'KubernetesObject',
646+
type?: string,
601647
): Promise<{ body: T; response: http.IncomingMessage }> {
602648
let authenticationPromise = Promise.resolve();
603649
if (this.authentications.BearerToken.apiKey) {
@@ -616,11 +662,15 @@ export class KubernetesObjectApi extends ApisApi {
616662
await interceptorPromise;
617663

618664
return new Promise<{ body: T; response: http.IncomingMessage }>((resolve, reject) => {
619-
request(requestOptions, (error, response, body) => {
665+
request(requestOptions, async (error, response, body) => {
620666
if (error) {
621667
reject(error);
622668
} else {
623-
body = ObjectSerializer.deserialize(body, tipe);
669+
// TODO(schrodit): support correct deserialization to KubernetesObject.
670+
if (type === undefined) {
671+
type = await this.getSerializationType(body.apiVersion, body.kind);
672+
}
673+
body = ObjectSerializer.deserialize(body, type);
624674
if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) {
625675
resolve({ response, body });
626676
} else {

src/object_test.ts

+64-28
Original file line numberDiff line numberDiff line change
@@ -1657,7 +1657,7 @@ describe('KubernetesObject', () => {
16571657
selfLink: '/api/v1/namespaces/default/services/k8s-js-client-test',
16581658
uid: 'a4fd7a65-2af5-4ef1-a0bc-cb34a308b821',
16591659
resourceVersion: '41183',
1660-
creationTimestamp: '2020-05-11T19:35:01Z',
1660+
creationTimestamp: '2020-05-11T19:35:01.000Z',
16611661
annotations: {
16621662
owner: 'test',
16631663
test: '1',
@@ -1750,9 +1750,7 @@ describe('KubernetesObject', () => {
17501750

17511751
it('should read a resource', async () => {
17521752
const scope = nock('https://d.i.y')
1753-
.get(
1754-
'/api/v1/namespaces/default/secrets/test-secret-1',
1755-
)
1753+
.get('/api/v1/namespaces/default/secrets/test-secret-1')
17561754
.reply(200, {
17571755
apiVersion: 'v1',
17581756
kind: 'Secret',
@@ -1766,29 +1764,77 @@ describe('KubernetesObject', () => {
17661764
key: 'value',
17671765
},
17681766
});
1769-
const res = await client.read(
1770-
{
1771-
apiVersion: 'v1',
1772-
kind: 'Secret',
1767+
const res = await client.read<V1Secret>({
1768+
apiVersion: 'v1',
1769+
kind: 'Secret',
1770+
metadata: {
1771+
name: 'test-secret-1',
1772+
namespace: 'default',
1773+
},
1774+
});
1775+
const secret = res.body;
1776+
expect(secret).to.be.instanceof(V1Secret);
1777+
expect(secret.data).to.deep.equal({
1778+
key: 'value',
1779+
});
1780+
expect(secret.metadata).to.be.ok;
1781+
expect(secret.metadata!.creationTimestamp).to.deep.equal(new Date('2022-01-01T00:00:00.000Z'));
1782+
scope.done();
1783+
});
1784+
1785+
it('should read a custom resource', async () => {
1786+
interface CustomTestResource extends KubernetesObject {
1787+
spec: {
1788+
key: string;
1789+
};
1790+
}
1791+
(client as any).apiVersionResourceCache['example.com/v1'] = {
1792+
groupVersion: 'example.com/v1',
1793+
kind: 'APIResourceList',
1794+
resources: [
1795+
{
1796+
kind: 'CustomTestResource',
1797+
name: 'customtestresources',
1798+
namespaced: true,
1799+
},
1800+
],
1801+
};
1802+
const scope = nock('https://d.i.y')
1803+
.get('/apis/example.com/v1/namespaces/default/customtestresources/test-1')
1804+
.reply(200, {
1805+
apiVersion: 'example.com/v1',
1806+
kind: 'CustomTestResource',
17731807
metadata: {
1774-
name: 'test-secret-1',
1808+
name: 'test-1',
17751809
namespace: 'default',
1810+
uid: 'a4fd7a65-2af5-4ef1-a0bc-cb34a308b821',
1811+
creationTimestamp: '2022-01-01T00:00:00.000Z',
17761812
},
1813+
spec: {
1814+
key: 'value',
1815+
},
1816+
});
1817+
const res = await client.read<CustomTestResource>({
1818+
apiVersion: 'example.com/v1',
1819+
kind: 'CustomTestResource',
1820+
metadata: {
1821+
name: 'test-1',
1822+
namespace: 'default',
17771823
},
1778-
);
1779-
const secret = res.body as V1Secret;
1780-
expect(secret.data).to.contain({
1824+
});
1825+
const custom = res.body;
1826+
expect(custom.spec).to.deep.equal({
17811827
key: 'value',
17821828
});
1783-
expect(secret.metadata?.creationTimestamp).to.equal(new Date('2022-01-01T00:00:00.000Z'));
1829+
expect(custom.metadata).to.be.ok;
1830+
// TODO(schrodit): this should be a Date rather than a string
1831+
expect(custom.metadata!.creationTimestamp).to.equal('2022-01-01T00:00:00.000Z');
17841832
scope.done();
17851833
});
17861834

17871835
it('should list resources in a namespace', async () => {
17881836
const scope = nock('https://d.i.y')
1789-
.get(
1790-
'/api/v1/namespaces/default/secrets?fieldSelector=metadata.name%3Dtest-secret1&labelSelector=app%3Dmy-app&limit=5&continue=abc',
1791-
)
1837+
.get('/api/v1/namespaces/default/secrets')
17921838
.reply(200, {
17931839
apiVersion: 'v1',
17941840
kind: 'SecretList',
@@ -1807,20 +1853,10 @@ describe('KubernetesObject', () => {
18071853
continue: 'abc',
18081854
},
18091855
});
1810-
const lr = await client.list(
1811-
'v1',
1812-
'Secret',
1813-
'default',
1814-
undefined,
1815-
undefined,
1816-
undefined,
1817-
'metadata.name=test-secret1',
1818-
'app=my-app',
1819-
5,
1820-
'abc',
1821-
);
1856+
const lr = await client.list<V1Secret>('v1', 'Secret', 'default');
18221857
const items = lr.body.items;
18231858
expect(items).to.have.length(1);
1859+
expect(items[0]).to.be.instanceof(V1Secret);
18241860
scope.done();
18251861
});
18261862

0 commit comments

Comments
 (0)