Skip to content

Commit 153a808

Browse files
authored
Merge pull request #1695 from schrodit/fix-obj-typing
Properly parse metadata of custom Kubernetes objects
2 parents 5edaa91 + 4df1858 commit 153a808

File tree

4 files changed

+309
-12
lines changed

4 files changed

+309
-12
lines changed

src/object.ts

+3-10
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
import * as http from 'http';
22
import request = require('request');
3-
import {
4-
ApisApi,
5-
HttpError,
6-
ObjectSerializer,
7-
V1APIResource,
8-
V1APIResourceList,
9-
V1DeleteOptions,
10-
V1Status,
11-
} from './api';
3+
import { ApisApi, HttpError, V1APIResource, V1APIResourceList, V1DeleteOptions, V1Status } from './api';
124
import { KubeConfig } from './config';
5+
import ObjectSerializer from './serializer';
136
import { KubernetesListObject, KubernetesObject } from './types';
147

158
/** Union type of body types returned by KubernetesObjectApi. */
@@ -499,7 +492,7 @@ export class KubernetesObjectApi extends ApisApi {
499492
*
500493
* @param spec Kubernetes resource spec which must define kind and apiVersion properties.
501494
* @param action API action, see [[K8sApiAction]].
502-
* @return tail of resource-specific URIDeploym
495+
* @return tail of resource-specific URI
503496
*/
504497
protected async specUriPath(spec: KubernetesObject, action: KubernetesApiAction): Promise<string> {
505498
if (!spec.kind) {

src/object_test.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1818,8 +1818,7 @@ describe('KubernetesObject', () => {
18181818
key: 'value',
18191819
});
18201820
expect(custom.metadata).to.be.ok;
1821-
// TODO(schrodit): this should be a Date rather than a string
1822-
expect(custom.metadata!.creationTimestamp).to.equal('2022-01-01T00:00:00.000Z');
1821+
expect(custom.metadata!.creationTimestamp).to.deep.equal(new Date('2022-01-01T00:00:00.000Z'));
18231822
scope.done();
18241823
});
18251824

src/serializer.ts

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { ObjectSerializer } from './api';
2+
import { V1ObjectMeta } from './gen/model/v1ObjectMeta';
3+
4+
type AttributeType = {
5+
name: string;
6+
baseName: string;
7+
type: string;
8+
};
9+
10+
class KubernetesObject {
11+
/**
12+
* 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
13+
*/
14+
'apiVersion'?: string;
15+
/**
16+
* 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
17+
*/
18+
'kind'?: string;
19+
'metadata'?: V1ObjectMeta;
20+
21+
static attributeTypeMap: AttributeType[] = [
22+
{
23+
name: 'apiVersion',
24+
baseName: 'apiVersion',
25+
type: 'string',
26+
},
27+
{
28+
name: 'kind',
29+
baseName: 'kind',
30+
type: 'string',
31+
},
32+
{
33+
name: 'metadata',
34+
baseName: 'metadata',
35+
type: 'V1ObjectMeta',
36+
},
37+
];
38+
}
39+
40+
const isKubernetesObject = (data: unknown): boolean =>
41+
!!data && typeof data === 'object' && 'apiVersion' in data && 'kind' in data;
42+
43+
/**
44+
* Wraps the ObjectSerializer to support custom resources and generic Kubernetes objects.
45+
*/
46+
class KubernetesObjectSerializer {
47+
private static _instance: KubernetesObjectSerializer;
48+
49+
public static get instance(): KubernetesObjectSerializer {
50+
if (this._instance) {
51+
return this._instance;
52+
}
53+
this._instance = new KubernetesObjectSerializer();
54+
return this._instance;
55+
}
56+
57+
private constructor() {}
58+
59+
public serialize(data: any, type: string): any {
60+
const obj = ObjectSerializer.serialize(data, type);
61+
if (obj !== data) {
62+
return obj;
63+
}
64+
65+
if (!isKubernetesObject(data)) {
66+
return obj;
67+
}
68+
69+
const instance: Record<string, any> = {};
70+
for (const attributeType of KubernetesObject.attributeTypeMap) {
71+
instance[attributeType.name] = ObjectSerializer.serialize(
72+
data[attributeType.baseName],
73+
attributeType.type,
74+
);
75+
}
76+
// add all unknown properties as is.
77+
for (const [key, value] of Object.entries(data)) {
78+
if (KubernetesObject.attributeTypeMap.find((t) => t.name === key)) {
79+
continue;
80+
}
81+
instance[key] = value;
82+
}
83+
return instance;
84+
}
85+
86+
public deserialize(data: any, type: string): any {
87+
const obj = ObjectSerializer.deserialize(data, type);
88+
if (obj !== data) {
89+
// the serializer knows the type and already deserialized it.
90+
return obj;
91+
}
92+
93+
if (!isKubernetesObject(data)) {
94+
return obj;
95+
}
96+
97+
const instance = new KubernetesObject();
98+
for (const attributeType of KubernetesObject.attributeTypeMap) {
99+
instance[attributeType.name] = ObjectSerializer.deserialize(
100+
data[attributeType.baseName],
101+
attributeType.type,
102+
);
103+
}
104+
// add all unknown properties as is.
105+
for (const [key, value] of Object.entries(data)) {
106+
if (KubernetesObject.attributeTypeMap.find((t) => t.name === key)) {
107+
continue;
108+
}
109+
instance[key] = value;
110+
}
111+
return instance;
112+
}
113+
}
114+
115+
export default KubernetesObjectSerializer.instance;

src/serializer_test.ts

+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { expect } from 'chai';
2+
import KubernetesObjectSerializer from './serializer';
3+
4+
describe('KubernetesObjectSerializer', () => {
5+
describe('serialize', () => {
6+
it('should serialize a known object', () => {
7+
const s = {
8+
apiVersion: 'v1',
9+
kind: 'Secret',
10+
metadata: {
11+
name: 'k8s-js-client-test',
12+
namespace: 'default',
13+
creationTimestamp: new Date('2022-01-01T00:00:00.000Z'),
14+
},
15+
data: {
16+
key: 'value',
17+
},
18+
};
19+
const res = KubernetesObjectSerializer.serialize(s, 'V1Secret');
20+
expect(res).to.deep.equal({
21+
apiVersion: 'v1',
22+
kind: 'Secret',
23+
metadata: {
24+
name: 'k8s-js-client-test',
25+
namespace: 'default',
26+
creationTimestamp: '2022-01-01T00:00:00.000Z',
27+
uid: undefined,
28+
annotations: undefined,
29+
labels: undefined,
30+
finalizers: undefined,
31+
generateName: undefined,
32+
selfLink: undefined,
33+
resourceVersion: undefined,
34+
generation: undefined,
35+
ownerReferences: undefined,
36+
deletionTimestamp: undefined,
37+
deletionGracePeriodSeconds: undefined,
38+
managedFields: undefined,
39+
},
40+
data: {
41+
key: 'value',
42+
},
43+
type: undefined,
44+
immutable: undefined,
45+
stringData: undefined,
46+
});
47+
});
48+
49+
it('should serialize a unknown kubernetes object', () => {
50+
const s = {
51+
apiVersion: 'v1alpha1',
52+
kind: 'MyCustomResource',
53+
metadata: {
54+
name: 'k8s-js-client-test',
55+
namespace: 'default',
56+
creationTimestamp: new Date('2022-01-01T00:00:00.000Z'),
57+
},
58+
data: {
59+
key: 'value',
60+
},
61+
};
62+
const res = KubernetesObjectSerializer.serialize(s, 'v1alpha1MyCustomResource');
63+
expect(res).to.deep.equal({
64+
apiVersion: 'v1alpha1',
65+
kind: 'MyCustomResource',
66+
metadata: {
67+
name: 'k8s-js-client-test',
68+
namespace: 'default',
69+
creationTimestamp: '2022-01-01T00:00:00.000Z',
70+
uid: undefined,
71+
annotations: undefined,
72+
labels: undefined,
73+
finalizers: undefined,
74+
generateName: undefined,
75+
selfLink: undefined,
76+
resourceVersion: undefined,
77+
generation: undefined,
78+
ownerReferences: undefined,
79+
deletionTimestamp: undefined,
80+
deletionGracePeriodSeconds: undefined,
81+
managedFields: undefined,
82+
},
83+
data: {
84+
key: 'value',
85+
},
86+
});
87+
});
88+
89+
it('should serialize a unknown primitive', () => {
90+
const s = {
91+
key: 'value',
92+
};
93+
const res = KubernetesObjectSerializer.serialize(s, 'unknown');
94+
expect(res).to.deep.equal(s);
95+
});
96+
});
97+
98+
describe('deserialize', () => {
99+
it('should deserialize a known object', () => {
100+
const s = {
101+
apiVersion: 'v1',
102+
kind: 'Secret',
103+
metadata: {
104+
name: 'k8s-js-client-test',
105+
namespace: 'default',
106+
creationTimestamp: '2022-01-01T00:00:00.000Z',
107+
},
108+
data: {
109+
key: 'value',
110+
},
111+
};
112+
const res = KubernetesObjectSerializer.deserialize(s, 'V1Secret');
113+
expect(res).to.deep.equal({
114+
apiVersion: 'v1',
115+
kind: 'Secret',
116+
metadata: {
117+
name: 'k8s-js-client-test',
118+
namespace: 'default',
119+
creationTimestamp: new Date('2022-01-01T00:00:00.000Z'),
120+
uid: undefined,
121+
annotations: undefined,
122+
labels: undefined,
123+
finalizers: undefined,
124+
generateName: undefined,
125+
selfLink: undefined,
126+
resourceVersion: undefined,
127+
generation: undefined,
128+
ownerReferences: undefined,
129+
deletionTimestamp: undefined,
130+
deletionGracePeriodSeconds: undefined,
131+
managedFields: undefined,
132+
},
133+
data: {
134+
key: 'value',
135+
},
136+
type: undefined,
137+
immutable: undefined,
138+
stringData: undefined,
139+
});
140+
});
141+
142+
it('should deserialize a unknown object', () => {
143+
const s = {
144+
apiVersion: 'v1alpha1',
145+
kind: 'MyCustomResource',
146+
metadata: {
147+
name: 'k8s-js-client-test',
148+
namespace: 'default',
149+
creationTimestamp: '2022-01-01T00:00:00.000Z',
150+
},
151+
data: {
152+
key: 'value',
153+
},
154+
};
155+
const res = KubernetesObjectSerializer.deserialize(s, 'v1alpha1MyCustomResource');
156+
expect(res).to.deep.equal({
157+
apiVersion: 'v1alpha1',
158+
kind: 'MyCustomResource',
159+
metadata: {
160+
name: 'k8s-js-client-test',
161+
namespace: 'default',
162+
creationTimestamp: new Date('2022-01-01T00:00:00.000Z'),
163+
uid: undefined,
164+
annotations: undefined,
165+
labels: undefined,
166+
finalizers: undefined,
167+
generateName: undefined,
168+
selfLink: undefined,
169+
resourceVersion: undefined,
170+
generation: undefined,
171+
ownerReferences: undefined,
172+
deletionTimestamp: undefined,
173+
deletionGracePeriodSeconds: undefined,
174+
managedFields: undefined,
175+
},
176+
data: {
177+
key: 'value',
178+
},
179+
});
180+
});
181+
182+
it('should deserialize a unknown primitive', () => {
183+
const s = {
184+
key: 'value',
185+
};
186+
const res = KubernetesObjectSerializer.serialize(s, 'unknown');
187+
expect(res).to.deep.equal(s);
188+
});
189+
});
190+
});

0 commit comments

Comments
 (0)