diff --git a/src/cache.ts b/src/cache.ts index 16d49cb5bc..65b5ccc98e 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -11,7 +11,7 @@ import { UPDATE, } from './informer'; import { KubernetesObject } from './types'; -import { ObjectSerializer } from './util'; +import { ObjectSerializer } from './serializer'; import { Watch } from './watch'; export interface ObjectCache { diff --git a/src/object.ts b/src/object.ts index 5207fe3801..7fd2cd3802 100644 --- a/src/object.ts +++ b/src/object.ts @@ -11,8 +11,8 @@ import { V1Status, } from './api'; import { KubeConfig } from './config'; +import { ObjectSerializer } from './serializer'; import { KubernetesListObject, KubernetesObject } from './types'; -import { ObjectSerializer } from './util'; import { from, mergeMap, of } from './gen/rxjsStub'; import { PatchStrategy } from './patch'; @@ -482,7 +482,7 @@ export class KubernetesObjectApi { * * @param spec Kubernetes resource spec which must define kind and apiVersion properties. * @param action API action, see [[K8sApiAction]]. - * @return tail of resource-specific URIDeploym + * @return tail of resource-specific URI */ protected async specUriPath(spec: KubernetesObject, action: KubernetesApiAction): Promise { if (!spec.kind) { diff --git a/src/object_test.ts b/src/object_test.ts index 805a883720..ba735611b0 100644 --- a/src/object_test.ts +++ b/src/object_test.ts @@ -1819,8 +1819,7 @@ describe('KubernetesObject', () => { 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'); + expect(custom.metadata!.creationTimestamp).to.deep.equal(new Date('2022-01-01T00:00:00.000Z')); scope.done(); }); diff --git a/src/serializer.ts b/src/serializer.ts new file mode 100644 index 0000000000..112ef08bd7 --- /dev/null +++ b/src/serializer.ts @@ -0,0 +1,112 @@ +import { ObjectSerializer as InternalSerializer, V1ObjectMeta } from './gen/models/ObjectSerializer'; + +type AttributeType = { + name: string; + baseName: string; + type: string; + format: string; +}; + +class KubernetesObject { + /** + * APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + */ + 'apiVersion'?: string; + /** + * Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + */ + 'kind'?: string; + 'metadata'?: V1ObjectMeta; + + static attributeTypeMap: AttributeType[] = [ + { + name: 'apiVersion', + baseName: 'apiVersion', + type: 'string', + format: '', + }, + { + name: 'kind', + baseName: 'kind', + type: 'string', + format: '', + }, + { + name: 'metadata', + baseName: 'metadata', + type: 'V1ObjectMeta', + format: '', + }, + ]; +} + +const isKubernetesObject = (data: unknown): boolean => + !!data && typeof data === 'object' && 'apiVersion' in data && 'kind' in data; + +/** + * Wraps the ObjectSerializer to support custom resources and generic Kubernetes objects. + */ +export class ObjectSerializer extends InternalSerializer { + public static serialize(data: any, type: string, format: string = ''): any { + const obj = InternalSerializer.serialize(data, type, format); + if (obj !== data) { + return obj; + } + + if (!isKubernetesObject(data)) { + return obj; + } + + const instance: Record = {}; + for (const attributeType of KubernetesObject.attributeTypeMap) { + const value = data[attributeType.baseName]; + if (value !== undefined) { + instance[attributeType.name] = InternalSerializer.serialize( + data[attributeType.baseName], + attributeType.type, + attributeType.format, + ); + } + } + // add all unknown properties as is. + for (const [key, value] of Object.entries(data)) { + if (KubernetesObject.attributeTypeMap.find((t) => t.name === key)) { + continue; + } + instance[key] = value; + } + return instance; + } + + public static deserialize(data: any, type: string, format: string = ''): any { + const obj = InternalSerializer.deserialize(data, type, format); + if (obj !== data) { + // the serializer knows the type and already deserialized it. + return obj; + } + + if (!isKubernetesObject(data)) { + return obj; + } + + const instance = new KubernetesObject(); + for (const attributeType of KubernetesObject.attributeTypeMap) { + const value = data[attributeType.baseName]; + if (value !== undefined) { + instance[attributeType.name] = InternalSerializer.deserialize( + data[attributeType.baseName], + attributeType.type, + attributeType.format, + ); + } + } + // add all unknown properties as is. + for (const [key, value] of Object.entries(data)) { + if (KubernetesObject.attributeTypeMap.find((t) => t.name === key)) { + continue; + } + instance[key] = value; + } + return instance; + } +} diff --git a/src/serializer_test.ts b/src/serializer_test.ts new file mode 100644 index 0000000000..51dbf7a6bd --- /dev/null +++ b/src/serializer_test.ts @@ -0,0 +1,163 @@ +import { expect } from 'chai'; +import { ObjectSerializer } from './serializer'; + +describe('ObjectSerializer', () => { + describe('serialize', () => { + it('should serialize a known object', () => { + const s = { + apiVersion: 'v1', + kind: 'Secret', + metadata: { + name: 'k8s-js-client-test', + namespace: 'default', + creationTimestamp: new Date('2022-01-01T00:00:00.000Z'), + }, + data: { + key: 'value', + }, + }; + const res = ObjectSerializer.serialize(s, 'V1Secret'); + expect(res).to.deep.equal({ + apiVersion: 'v1', + kind: 'Secret', + metadata: { + name: 'k8s-js-client-test', + namespace: 'default', + creationTimestamp: '2022-01-01T00:00:00.000Z', + uid: undefined, + annotations: undefined, + labels: undefined, + finalizers: undefined, + generateName: undefined, + selfLink: undefined, + resourceVersion: undefined, + generation: undefined, + ownerReferences: undefined, + deletionTimestamp: undefined, + deletionGracePeriodSeconds: undefined, + managedFields: undefined, + }, + data: { + key: 'value', + }, + type: undefined, + immutable: undefined, + stringData: undefined, + }); + }); + + it('should serialize a unknown kubernetes object', () => { + const s = { + apiVersion: 'v1alpha1', + kind: 'MyCustomResource', + metadata: { + name: 'k8s-js-client-test', + namespace: 'default', + creationTimestamp: new Date('2022-01-01T00:00:00.000Z'), + }, + data: { + key: 'value', + }, + }; + const res = ObjectSerializer.serialize(s, 'v1alpha1MyCustomResource'); + expect(res).to.deep.equal({ + apiVersion: 'v1alpha1', + kind: 'MyCustomResource', + metadata: { + name: 'k8s-js-client-test', + namespace: 'default', + creationTimestamp: '2022-01-01T00:00:00.000Z', + uid: undefined, + annotations: undefined, + labels: undefined, + finalizers: undefined, + generateName: undefined, + selfLink: undefined, + resourceVersion: undefined, + generation: undefined, + ownerReferences: undefined, + deletionTimestamp: undefined, + deletionGracePeriodSeconds: undefined, + managedFields: undefined, + }, + data: { + key: 'value', + }, + }); + }); + + it('should serialize a unknown primitive', () => { + const s = { + key: 'value', + }; + const res = ObjectSerializer.serialize(s, 'unknown'); + expect(res).to.deep.equal(s); + }); + }); + + describe('deserialize', () => { + it('should deserialize a known object', () => { + const s = { + apiVersion: 'v1', + kind: 'Secret', + metadata: { + name: 'k8s-js-client-test', + namespace: 'default', + creationTimestamp: '2022-01-01T00:00:00.000Z', + }, + data: { + key: 'value', + }, + }; + const res = ObjectSerializer.deserialize(s, 'V1Secret'); + expect(res).to.deep.equal({ + apiVersion: 'v1', + kind: 'Secret', + metadata: { + name: 'k8s-js-client-test', + namespace: 'default', + creationTimestamp: new Date('2022-01-01T00:00:00.000Z'), + }, + data: { + key: 'value', + }, + }); + }); + + it('should deserialize a unknown object', () => { + const s = { + apiVersion: 'v1alpha1', + kind: 'MyCustomResource', + metadata: { + name: 'k8s-js-client-test', + namespace: 'default', + creationTimestamp: '2022-01-01T00:00:00.000Z', + }, + data: { + key: 'value', + }, + }; + const res = ObjectSerializer.deserialize(s, 'v1alpha1MyCustomResource'); + expect(res).to.deep.equal({ + apiVersion: 'v1alpha1', + kind: 'MyCustomResource', + metadata: { + name: 'k8s-js-client-test', + namespace: 'default', + creationTimestamp: new Date('2022-01-01T00:00:00.000Z'), + }, + data: { + key: 'value', + }, + }); + }); + + it('should deserialize a unknown primitive', () => { + const s = { + key: 'value', + }; + const res = ObjectSerializer.serialize(s, 'unknown'); + expect(res).to.deep.equal(s); + }); + }); +}); diff --git a/src/util.ts b/src/util.ts index 720aa068b7..02422af9af 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,16 +1,6 @@ import { Response } from 'node-fetch'; import { isNumber } from 'underscore'; import { CoreV1Api, V1Container, V1Pod } from './gen'; -import { ObjectSerializer as InternalSerializer } from './gen/models/ObjectSerializer'; - -export class ObjectSerializer extends InternalSerializer { - public static serialize(data: any, type: string, format: string = ''): string { - return InternalSerializer.serialize(data, type, format); - } - public static deserialize(data: any, type: string, format: string = ''): any { - return InternalSerializer.deserialize(data, type, format); - } -} export async function podsForNode(api: CoreV1Api, nodeName: string): Promise { const allPods = await api.listPodForAllNamespaces();