diff --git a/src/object.ts b/src/object.ts index 350b8a733c9..17f4eb1a6c5 100644 --- a/src/object.ts +++ b/src/object.ts @@ -22,6 +22,21 @@ type KubernetesObjectResponseBody = /** Kubernetes API verbs. */ type KubernetesApiAction = 'create' | 'delete' | 'patch' | 'read' | 'list' | 'replace'; +type KubernetesObjectHeader = Pick< + T, + 'apiVersion' | 'kind' +> & { + metadata: { + name: string; + namespace: string; + }; +}; + +interface GroupVersion { + group: string; + version: string; +} + /** * Valid Content-Type header values for patch operations. See * https://kubernetes.io/docs/tasks/run-application/update-api-object-kubectl-patch/ @@ -74,13 +89,13 @@ export class KubernetesObjectApi extends ApisApi { * @param options Optional headers to use in the request. * @return Promise containing the request response and [[KubernetesObject]]. */ - public async create( - spec: KubernetesObject, + public async create( + spec: T, pretty?: string, dryRun?: string, fieldManager?: string, options: { headers: { [name: string]: string } } = { headers: {} }, - ): Promise<{ body: KubernetesObject; response: http.IncomingMessage }> { + ): Promise<{ body: T; response: http.IncomingMessage }> { // verify required parameter 'spec' is not null or undefined if (spec === null || spec === undefined) { throw new Error('Required parameter spec was null or undefined when calling create.'); @@ -218,14 +233,14 @@ export class KubernetesObjectApi extends ApisApi { * @param options Optional headers to use in the request. * @return Promise containing the request response and [[KubernetesObject]]. */ - public async patch( - spec: KubernetesObject, + public async patch( + spec: T, pretty?: string, dryRun?: string, fieldManager?: string, force?: boolean, options: { headers: { [name: string]: string } } = { headers: {} }, - ): Promise<{ body: KubernetesObject; response: http.IncomingMessage }> { + ): Promise<{ body: T; response: http.IncomingMessage }> { // verify required parameter 'spec' is not null or undefined if (spec === null || spec === undefined) { throw new Error('Required parameter spec was null or undefined when calling patch.'); @@ -275,17 +290,24 @@ export class KubernetesObjectApi extends ApisApi { * @param options Optional headers to use in the request. * @return Promise containing the request response and [[KubernetesObject]]. */ - public async read( - spec: KubernetesObject, + public async read( + spec: KubernetesObjectHeader, pretty?: string, exact?: boolean, exportt?: boolean, options: { headers: { [name: string]: string } } = { headers: {} }, - ): Promise<{ body: KubernetesObject; response: http.IncomingMessage }> { + ): Promise<{ body: T; response: http.IncomingMessage }> { // verify required parameter 'spec' is not null or undefined if (spec === null || spec === undefined) { throw new Error('Required parameter spec was null or undefined when calling read.'); } + // verify required parameter 'kind' is not null or undefined + if (spec.kind === null || spec.kind === undefined) { + throw new Error('Required parameter spec.kind was null or undefined when calling read.'); + } + if (!spec.apiVersion) { + throw new Error('Required parameter spec.apiVersion was null or undefined when calling read.'); + } const localVarPath = await this.specUriPath(spec, 'read'); const localVarQueryParameters: any = {}; @@ -331,7 +353,7 @@ export class KubernetesObjectApi extends ApisApi { * @param options Optional headers to use in the request. * @return Promise containing the request response and [[KubernetesListObject]]. */ - public async list( + public async list( apiVersion: string, kind: string, namespace?: string, @@ -343,7 +365,7 @@ export class KubernetesObjectApi extends ApisApi { limit?: number, continueToken?: string, options: { headers: { [name: string]: string } } = { headers: {} }, - ): Promise<{ body: KubernetesListObject; response: http.IncomingMessage }> { + ): Promise<{ body: KubernetesListObject; response: http.IncomingMessage }> { // verify required parameters 'apiVersion', 'kind' is not null or undefined if (apiVersion === null || apiVersion === undefined) { throw new Error('Required parameter apiVersion was null or undefined when calling list.'); @@ -418,13 +440,13 @@ export class KubernetesObjectApi extends ApisApi { * @param options Optional headers to use in the request. * @return Promise containing the request response and [[KubernetesObject]]. */ - public async replace( - spec: KubernetesObject, + public async replace( + spec: T, pretty?: string, dryRun?: string, fieldManager?: string, options: { headers: { [name: string]: string } } = { headers: {} }, - ): Promise<{ body: KubernetesObject; response: http.IncomingMessage }> { + ): Promise<{ body: T; response: http.IncomingMessage }> { // verify required parameter 'spec' is not null or undefined if (spec === null || spec === undefined) { throw new Error('Required parameter spec was null or undefined when calling replace.'); @@ -477,7 +499,7 @@ export class KubernetesObjectApi extends ApisApi { * * @param spec Kubernetes resource spec which must define kind and apiVersion properties. * @param action API action, see [[K8sApiAction]]. - * @return tail of resource-specific URI + * @return tail of resource-specific URIDeploym */ protected async specUriPath(spec: KubernetesObject, action: KubernetesApiAction): Promise { if (!spec.kind) { @@ -592,12 +614,36 @@ export class KubernetesObjectApi extends ApisApi { } } + 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], + }; + } + /** * Standard Kubernetes request wrapped in a Promise. */ protected async requestPromise( requestOptions: request.Options, - tipe: string = 'KubernetesObject', + type?: string, ): Promise<{ body: T; response: http.IncomingMessage }> { let authenticationPromise = Promise.resolve(); if (this.authentications.BearerToken.apiKey) { @@ -616,11 +662,15 @@ export class KubernetesObjectApi extends ApisApi { await interceptorPromise; return new Promise<{ body: T; response: http.IncomingMessage }>((resolve, reject) => { - request(requestOptions, (error, response, body) => { + request(requestOptions, async (error, response, body) => { if (error) { reject(error); } else { - body = ObjectSerializer.deserialize(body, tipe); + // TODO(schrodit): support correct deserialization to KubernetesObject. + if (type === undefined) { + type = await this.getSerializationType(body.apiVersion, body.kind); + } + body = ObjectSerializer.deserialize(body, type); if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) { resolve({ response, body }); } else { diff --git a/src/object_test.ts b/src/object_test.ts index 77a258855d0..dc3bb32e0f2 100644 --- a/src/object_test.ts +++ b/src/object_test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import nock = require('nock'); -import { V1APIResource, V1APIResourceList } from './api'; +import { V1APIResource, V1APIResourceList, V1Secret } from './api'; import { KubeConfig } from './config'; import { KubernetesObjectApi } from './object'; import { KubernetesObject } from './types'; @@ -1657,7 +1657,7 @@ describe('KubernetesObject', () => { selfLink: '/api/v1/namespaces/default/services/k8s-js-client-test', uid: 'a4fd7a65-2af5-4ef1-a0bc-cb34a308b821', resourceVersion: '41183', - creationTimestamp: '2020-05-11T19:35:01Z', + creationTimestamp: '2020-05-11T19:35:01.000Z', annotations: { owner: 'test', test: '1', @@ -1748,11 +1748,93 @@ describe('KubernetesObject', () => { scope.done(); }); + it('should read a resource', async () => { + const scope = nock('https://d.i.y') + .get('/api/v1/namespaces/default/secrets/test-secret-1') + .reply(200, { + apiVersion: 'v1', + kind: 'Secret', + metadata: { + name: 'test-secret-1', + namespace: 'default', + uid: 'a4fd7a65-2af5-4ef1-a0bc-cb34a308b821', + creationTimestamp: '2022-01-01T00:00:00.000Z', + }, + data: { + key: 'value', + }, + }); + const res = await client.read({ + apiVersion: 'v1', + kind: 'Secret', + metadata: { + name: 'test-secret-1', + namespace: 'default', + }, + }); + const secret = res.body; + expect(secret).to.be.instanceof(V1Secret); + expect(secret.data).to.deep.equal({ + key: 'value', + }); + expect(secret.metadata).to.be.ok; + expect(secret.metadata!.creationTimestamp).to.deep.equal(new Date('2022-01-01T00:00:00.000Z')); + scope.done(); + }); + + it('should read a custom resource', async () => { + interface CustomTestResource extends KubernetesObject { + spec: { + key: string; + }; + } + (client as any).apiVersionResourceCache['example.com/v1'] = { + groupVersion: 'example.com/v1', + kind: 'APIResourceList', + resources: [ + { + kind: 'CustomTestResource', + name: 'customtestresources', + namespaced: true, + }, + ], + }; + const scope = nock('https://d.i.y') + .get('/apis/example.com/v1/namespaces/default/customtestresources/test-1') + .reply(200, { + apiVersion: 'example.com/v1', + kind: 'CustomTestResource', + metadata: { + name: 'test-1', + namespace: 'default', + uid: 'a4fd7a65-2af5-4ef1-a0bc-cb34a308b821', + creationTimestamp: '2022-01-01T00:00:00.000Z', + }, + spec: { + key: 'value', + }, + }); + const res = await client.read({ + apiVersion: 'example.com/v1', + kind: 'CustomTestResource', + metadata: { + name: 'test-1', + namespace: 'default', + }, + }); + const custom = res.body; + expect(custom.spec).to.deep.equal({ + key: 'value', + }); + expect(custom.metadata).to.be.ok; + // TODO(schrodit): this should be a Date rather than a string + expect(custom.metadata!.creationTimestamp).to.equal('2022-01-01T00:00:00.000Z'); + scope.done(); + }); + it('should list resources in a namespace', async () => { const scope = nock('https://d.i.y') - .get( - '/api/v1/namespaces/default/secrets?fieldSelector=metadata.name%3Dtest-secret1&labelSelector=app%3Dmy-app&limit=5&continue=abc', - ) + .get('/api/v1/namespaces/default/secrets') .reply(200, { apiVersion: 'v1', kind: 'SecretList', @@ -1771,20 +1853,10 @@ describe('KubernetesObject', () => { continue: 'abc', }, }); - const lr = await client.list( - 'v1', - 'Secret', - 'default', - undefined, - undefined, - undefined, - 'metadata.name=test-secret1', - 'app=my-app', - 5, - 'abc', - ); + const lr = await client.list('v1', 'Secret', 'default'); const items = lr.body.items; expect(items).to.have.length(1); + expect(items[0]).to.be.instanceof(V1Secret); scope.done(); });