diff --git a/src/object.ts b/src/object.ts index 099ccfce946..4e07e993d68 100644 --- a/src/object.ts +++ b/src/object.ts @@ -1,15 +1,8 @@ import * as http from 'http'; import request = require('request'); -import { - ApisApi, - HttpError, - ObjectSerializer, - V1APIResource, - V1APIResourceList, - V1DeleteOptions, - V1Status, -} from './api'; +import { ApisApi, HttpError, V1APIResource, V1APIResourceList, V1DeleteOptions, V1Status } from './api'; import { KubeConfig } from './config'; +import ObjectSerializer from './serializer'; import { KubernetesListObject, KubernetesObject } from './types'; /** Union type of body types returned by KubernetesObjectApi. */ @@ -499,7 +492,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 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 6841d54cf20..0d812931962 100644 --- a/src/object_test.ts +++ b/src/object_test.ts @@ -1818,8 +1818,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 00000000000..210bde32215 --- /dev/null +++ b/src/serializer.ts @@ -0,0 +1,115 @@ +import { ObjectSerializer } from './api'; +import { V1ObjectMeta } from './gen/model/v1ObjectMeta'; + +type AttributeType = { + name: string; + baseName: string; + type: 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', + }, + { + name: 'kind', + baseName: 'kind', + type: 'string', + }, + { + name: 'metadata', + baseName: 'metadata', + type: 'V1ObjectMeta', + }, + ]; +} + +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. + */ +class KubernetesObjectSerializer { + private static _instance: KubernetesObjectSerializer; + + public static get instance(): KubernetesObjectSerializer { + if (this._instance) { + return this._instance; + } + this._instance = new KubernetesObjectSerializer(); + return this._instance; + } + + private constructor() {} + + public serialize(data: any, type: string): any { + const obj = ObjectSerializer.serialize(data, type); + if (obj !== data) { + return obj; + } + + if (!isKubernetesObject(data)) { + return obj; + } + + const instance: Record = {}; + for (const attributeType of KubernetesObject.attributeTypeMap) { + instance[attributeType.name] = ObjectSerializer.serialize( + data[attributeType.baseName], + attributeType.type, + ); + } + // 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 deserialize(data: any, type: string): any { + const obj = ObjectSerializer.deserialize(data, type); + 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) { + instance[attributeType.name] = ObjectSerializer.deserialize( + data[attributeType.baseName], + attributeType.type, + ); + } + // 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; + } +} + +export default KubernetesObjectSerializer.instance; diff --git a/src/serializer_test.ts b/src/serializer_test.ts new file mode 100644 index 00000000000..6023519c259 --- /dev/null +++ b/src/serializer_test.ts @@ -0,0 +1,190 @@ +import { expect } from 'chai'; +import KubernetesObjectSerializer from './serializer'; + +describe('KubernetesObjectSerializer', () => { + 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 = KubernetesObjectSerializer.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 = KubernetesObjectSerializer.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 = KubernetesObjectSerializer.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 = KubernetesObjectSerializer.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'), + 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 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 = KubernetesObjectSerializer.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'), + 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 deserialize a unknown primitive', () => { + const s = { + key: 'value', + }; + const res = KubernetesObjectSerializer.serialize(s, 'unknown'); + expect(res).to.deep.equal(s); + }); + }); +});