Skip to content

Commit eaf1b23

Browse files
authored
Merge pull request #854 from schrodit/fix-types
Return strong types in generic client
2 parents 2b6813f + bc00df9 commit eaf1b23

File tree

2 files changed

+157
-35
lines changed

2 files changed

+157
-35
lines changed

src/object.ts

Lines changed: 68 additions & 18 deletions
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+
throw new Error('Required parameter spec.apiVersion was null or undefined when calling read.');
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

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from 'chai';
22
import nock = require('nock');
3-
import { V1APIResource, V1APIResourceList } from './api';
3+
import { V1APIResource, V1APIResourceList, V1Secret } from './api';
44
import { KubeConfig } from './config';
55
import { KubernetesObjectApi } from './object';
66
import { KubernetesObject } from './types';
@@ -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',
@@ -1748,11 +1748,93 @@ describe('KubernetesObject', () => {
17481748
scope.done();
17491749
});
17501750

1751+
it('should read a resource', async () => {
1752+
const scope = nock('https://d.i.y')
1753+
.get('/api/v1/namespaces/default/secrets/test-secret-1')
1754+
.reply(200, {
1755+
apiVersion: 'v1',
1756+
kind: 'Secret',
1757+
metadata: {
1758+
name: 'test-secret-1',
1759+
namespace: 'default',
1760+
uid: 'a4fd7a65-2af5-4ef1-a0bc-cb34a308b821',
1761+
creationTimestamp: '2022-01-01T00:00:00.000Z',
1762+
},
1763+
data: {
1764+
key: 'value',
1765+
},
1766+
});
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',
1807+
metadata: {
1808+
name: 'test-1',
1809+
namespace: 'default',
1810+
uid: 'a4fd7a65-2af5-4ef1-a0bc-cb34a308b821',
1811+
creationTimestamp: '2022-01-01T00:00:00.000Z',
1812+
},
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',
1823+
},
1824+
});
1825+
const custom = res.body;
1826+
expect(custom.spec).to.deep.equal({
1827+
key: 'value',
1828+
});
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');
1832+
scope.done();
1833+
});
1834+
17511835
it('should list resources in a namespace', async () => {
17521836
const scope = nock('https://d.i.y')
1753-
.get(
1754-
'/api/v1/namespaces/default/secrets?fieldSelector=metadata.name%3Dtest-secret1&labelSelector=app%3Dmy-app&limit=5&continue=abc',
1755-
)
1837+
.get('/api/v1/namespaces/default/secrets')
17561838
.reply(200, {
17571839
apiVersion: 'v1',
17581840
kind: 'SecretList',
@@ -1771,20 +1853,10 @@ describe('KubernetesObject', () => {
17711853
continue: 'abc',
17721854
},
17731855
});
1774-
const lr = await client.list(
1775-
'v1',
1776-
'Secret',
1777-
'default',
1778-
undefined,
1779-
undefined,
1780-
undefined,
1781-
'metadata.name=test-secret1',
1782-
'app=my-app',
1783-
5,
1784-
'abc',
1785-
);
1856+
const lr = await client.list<V1Secret>('v1', 'Secret', 'default');
17861857
const items = lr.body.items;
17871858
expect(items).to.have.length(1);
1859+
expect(items[0]).to.be.instanceof(V1Secret);
17881860
scope.done();
17891861
});
17901862

0 commit comments

Comments
 (0)