Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Properly parse metadata of custom Kubernetes objects #1695

Merged
merged 4 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions src/object.ts
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down Expand Up @@ -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<string> {
if (!spec.kind) {
Expand Down
3 changes: 1 addition & 2 deletions src/object_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down
115 changes: 115 additions & 0 deletions src/serializer.ts
Original file line number Diff line number Diff line change
@@ -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<string, any> = {};
for (const attributeType of KubernetesObject.attributeTypeMap) {
instance[attributeType.name] = ObjectSerializer.serialize(
data[attributeType.baseName],
attributeType.type,
);
}
mstruebing marked this conversation as resolved.
Show resolved Hide resolved
// 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,
);
}
mstruebing marked this conversation as resolved.
Show resolved Hide resolved
// 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;
190 changes: 190 additions & 0 deletions src/serializer_test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});