From d9b7c481c030725b86a20adec4d931d6e9413c4f Mon Sep 17 00:00:00 2001 From: schrodit Date: Sat, 25 May 2024 16:12:36 +0200 Subject: [PATCH 1/4] properly parse metadata of unknown objects. --- src/object.ts | 5 +- src/object_test.ts | 3 +- src/serializer.ts | 115 ++++++++++++++++++++++++++ src/serializer_test.ts | 184 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 303 insertions(+), 4 deletions(-) create mode 100644 src/serializer.ts create mode 100644 src/serializer_test.ts diff --git a/src/object.ts b/src/object.ts index 099ccfce946..663c37cf53e 100644 --- a/src/object.ts +++ b/src/object.ts @@ -3,13 +3,13 @@ import request = require('request'); import { ApisApi, HttpError, - ObjectSerializer, 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. */ @@ -51,6 +51,7 @@ enum KubernetesPatchStrategies { StrategicMergePatch = 'application/strategic-merge-patch+json', } + /** * Dynamically construct Kubernetes API request URIs so client does not have to know what type of object it is acting * on. @@ -499,7 +500,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..86c986699d9 --- /dev/null +++ b/src/serializer.ts @@ -0,0 +1,115 @@ +import { ObjectSerializer } from "./api"; +import { V1ObjectMeta } from "./gen/model/v1ObjectMeta"; + +export type AttributeType = { + name: string; + baseName: string; + type: string; +}; + +export 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() { + if (this._instance) { + return this._instance; + } + this._instance = new KubernetesObjectSerializer(); + return this._instance; + } + + private constructor() {} + + public serialize(data: any, type: string) { + const obj = ObjectSerializer.serialize(data, type); + if(obj !== data) { + return obj; + } + + if (!isKubernetesObject(data)) { + return obj; + } + + const instance: Record = {}; + const attributeTypes = KubernetesObject.attributeTypeMap; + for (let index = 0; index < attributeTypes.length; index++) { + let attributeType = attributeTypes[index]; + 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 (attributeTypes.find((t) => t.name === key)) { + continue; + } + instance[key] = value; + } + return instance; + + } + + public deserialize(data: any, type: string) { + 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(); + const attributeTypes = KubernetesObject.attributeTypeMap; + for (let index = 0; index < attributeTypes.length; index++) { + let attributeType = attributeTypes[index]; + 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 (attributeTypes.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..bb9e6fdd11f --- /dev/null +++ b/src/serializer_test.ts @@ -0,0 +1,184 @@ +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', + }, + }); + }); + }); + +}); From ab314956a9084a8cd26527529e19c700c5735296 Mon Sep 17 00:00:00 2001 From: tschrodi96 Date: Mon, 27 May 2024 22:04:30 +0200 Subject: [PATCH 2/4] apply review comments --- src/serializer.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/serializer.ts b/src/serializer.ts index 86c986699d9..96626f8ac3a 100644 --- a/src/serializer.ts +++ b/src/serializer.ts @@ -1,13 +1,13 @@ import { ObjectSerializer } from "./api"; import { V1ObjectMeta } from "./gen/model/v1ObjectMeta"; -export type AttributeType = { +type AttributeType = { name: string; baseName: string; type: string; }; -export class KubernetesObject { +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 */ @@ -68,14 +68,12 @@ class KubernetesObjectSerializer { } const instance: Record = {}; - const attributeTypes = KubernetesObject.attributeTypeMap; - for (let index = 0; index < attributeTypes.length; index++) { - let attributeType = attributeTypes[index]; + 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 (attributeTypes.find((t) => t.name === key)) { + if (KubernetesObject.attributeTypeMap.find((t) => t.name === key)) { continue; } instance[key] = value; @@ -96,14 +94,12 @@ class KubernetesObjectSerializer { } const instance = new KubernetesObject(); - const attributeTypes = KubernetesObject.attributeTypeMap; - for (let index = 0; index < attributeTypes.length; index++) { - let attributeType = attributeTypes[index]; + 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 (attributeTypes.find((t) => t.name === key)) { + if (KubernetesObject.attributeTypeMap.find((t) => t.name === key)) { continue; } instance[key] = value; From 7c4ad3ab6131dae531e92ab4b8b7b8b68761b0f6 Mon Sep 17 00:00:00 2001 From: schrodit Date: Tue, 28 May 2024 12:05:04 +0200 Subject: [PATCH 3/4] fix lint issues --- src/serializer.ts | 38 +++++++++++++++++++------------------- src/serializer_test.ts | 8 ++++++++ 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/serializer.ts b/src/serializer.ts index 96626f8ac3a..8b5774657d4 100644 --- a/src/serializer.ts +++ b/src/serializer.ts @@ -1,5 +1,5 @@ -import { ObjectSerializer } from "./api"; -import { V1ObjectMeta } from "./gen/model/v1ObjectMeta"; +import { ObjectSerializer } from './api'; +import { V1ObjectMeta } from './gen/model/v1ObjectMeta'; type AttributeType = { name: string; @@ -9,36 +9,36 @@ type AttributeType = { 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 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 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: 'apiVersion', + baseName: 'apiVersion', + type: 'string' }, { - "name": "kind", - "baseName": "kind", - "type": "string" + name: 'kind', + baseName: 'kind', + type: 'string' }, { - "name": "metadata", - "baseName": "metadata", - "type": "V1ObjectMeta" + name: 'metadata', + baseName: 'metadata', + type: 'V1ObjectMeta' }, ]; } const isKubernetesObject = (data: unknown): boolean => - !!data && typeof data === "object" && 'apiVersion' in data && 'kind' in data; + !!data && typeof data === 'object' && 'apiVersion' in data && 'kind' in data; /** * Wraps the ObjectSerializer to support custom resources and generic Kubernetes objects. @@ -47,7 +47,7 @@ class KubernetesObjectSerializer { private static _instance: KubernetesObjectSerializer; - public static get instance() { + public static get instance(): KubernetesObjectSerializer { if (this._instance) { return this._instance; } @@ -57,7 +57,7 @@ class KubernetesObjectSerializer { private constructor() {} - public serialize(data: any, type: string) { + public serialize(data: any, type: string): any { const obj = ObjectSerializer.serialize(data, type); if(obj !== data) { return obj; @@ -82,7 +82,7 @@ class KubernetesObjectSerializer { } - public deserialize(data: any, type: string) { + 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. diff --git a/src/serializer_test.ts b/src/serializer_test.ts index bb9e6fdd11f..d93fe1985da 100644 --- a/src/serializer_test.ts +++ b/src/serializer_test.ts @@ -179,6 +179,14 @@ describe('KubernetesObjectSerializer', () => { }, }); }); + + it('should deserialize a unknown primitive', () => { + const s = { + key: 'value', + }; + const res = KubernetesObjectSerializer.serialize(s, 'unknown'); + expect(res).to.deep.equal(s); + }); }); }); From 4df1858b09436c10baf9ea0e0b97cf111dd5a4c0 Mon Sep 17 00:00:00 2001 From: schrodit Date: Tue, 28 May 2024 14:03:56 +0200 Subject: [PATCH 4/4] fix formatting --- src/object.ts | 10 +--------- src/serializer.ts | 20 ++++++++++++-------- src/serializer_test.ts | 2 -- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/object.ts b/src/object.ts index 663c37cf53e..4e07e993d68 100644 --- a/src/object.ts +++ b/src/object.ts @@ -1,13 +1,6 @@ import * as http from 'http'; import request = require('request'); -import { - ApisApi, - HttpError, - 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'; @@ -51,7 +44,6 @@ enum KubernetesPatchStrategies { StrategicMergePatch = 'application/strategic-merge-patch+json', } - /** * Dynamically construct Kubernetes API request URIs so client does not have to know what type of object it is acting * on. diff --git a/src/serializer.ts b/src/serializer.ts index 8b5774657d4..210bde32215 100644 --- a/src/serializer.ts +++ b/src/serializer.ts @@ -22,17 +22,17 @@ class KubernetesObject { { name: 'apiVersion', baseName: 'apiVersion', - type: 'string' + type: 'string', }, { name: 'kind', baseName: 'kind', - type: 'string' + type: 'string', }, { name: 'metadata', baseName: 'metadata', - type: 'V1ObjectMeta' + type: 'V1ObjectMeta', }, ]; } @@ -44,7 +44,6 @@ const isKubernetesObject = (data: unknown): boolean => * Wraps the ObjectSerializer to support custom resources and generic Kubernetes objects. */ class KubernetesObjectSerializer { - private static _instance: KubernetesObjectSerializer; public static get instance(): KubernetesObjectSerializer { @@ -59,7 +58,7 @@ class KubernetesObjectSerializer { public serialize(data: any, type: string): any { const obj = ObjectSerializer.serialize(data, type); - if(obj !== data) { + if (obj !== data) { return obj; } @@ -69,7 +68,10 @@ class KubernetesObjectSerializer { const instance: Record = {}; for (const attributeType of KubernetesObject.attributeTypeMap) { - instance[attributeType.name] = ObjectSerializer.serialize(data[attributeType.baseName], attributeType.type); + instance[attributeType.name] = ObjectSerializer.serialize( + data[attributeType.baseName], + attributeType.type, + ); } // add all unknown properties as is. for (const [key, value] of Object.entries(data)) { @@ -79,7 +81,6 @@ class KubernetesObjectSerializer { instance[key] = value; } return instance; - } public deserialize(data: any, type: string): any { @@ -95,7 +96,10 @@ class KubernetesObjectSerializer { const instance = new KubernetesObject(); for (const attributeType of KubernetesObject.attributeTypeMap) { - instance[attributeType.name] = ObjectSerializer.deserialize(data[attributeType.baseName], attributeType.type); + instance[attributeType.name] = ObjectSerializer.deserialize( + data[attributeType.baseName], + attributeType.type, + ); } // add all unknown properties as is. for (const [key, value] of Object.entries(data)) { diff --git a/src/serializer_test.ts b/src/serializer_test.ts index d93fe1985da..6023519c259 100644 --- a/src/serializer_test.ts +++ b/src/serializer_test.ts @@ -2,7 +2,6 @@ import { expect } from 'chai'; import KubernetesObjectSerializer from './serializer'; describe('KubernetesObjectSerializer', () => { - describe('serialize', () => { it('should serialize a known object', () => { const s = { @@ -188,5 +187,4 @@ describe('KubernetesObjectSerializer', () => { expect(res).to.deep.equal(s); }); }); - });