Repository: kubernetes-client/javascript
Pull request #1700 (2024-05-30 09:15:43)

Cherry-Pick #1695
Cherry-picks https://github.com/kubernetes-client/javascript/pull/1695 to the v1 branch.

The original change to be adjusted to the v1 branch mostly because the object serializer interface and some behavior changed.


Patch:
diff --git a/src/cache.ts b/src/cache.ts
index 16d49cb5..65b5ccc9 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<T> {
diff --git a/src/object.ts b/src/object.ts
index 5207fe38..7fd2cd38 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<string> {
         if (!spec.kind) {
diff --git a/src/serializer.ts b/src/serializer.ts
new file mode 100644
index 00000000..112ef08b
--- /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<string, any> = {};
+        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/util.ts b/src/util.ts
index 720aa068..02422af9 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<V1Pod[]> {
     const allPods = await api.listPodForAllNamespaces();


Test patch:
diff --git a/src/object_test.ts b/src/object_test.ts
index 805a8837..ba735611 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_test.ts b/src/serializer_test.ts
new file mode 100644
index 00000000..51dbf7a6
--- /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);
+        });
+    });
+});