Skip to content

Commit

Permalink
Merge pull request kubernetes-client#1695 from schrodit/fix-obj-typing
Browse files Browse the repository at this point in the history
Properly parse metadata of custom Kubernetes objects
  • Loading branch information
k8s-ci-robot authored and schrodit committed May 30, 2024
1 parent b8705f0 commit b85a634
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 15 deletions.
2 changes: 1 addition & 1 deletion src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
4 changes: 2 additions & 2 deletions src/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 1 addition & 2 deletions src/object_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down
112 changes: 112 additions & 0 deletions src/serializer.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
163 changes: 163 additions & 0 deletions src/serializer_test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
10 changes: 0 additions & 10 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down

0 comments on commit b85a634

Please sign in to comment.