Skip to content

Commit d9b7c48

Browse files
committed
properly parse metadata of unknown objects.
1 parent ccfcef9 commit d9b7c48

File tree

4 files changed

+303
-4
lines changed

4 files changed

+303
-4
lines changed

src/object.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import request = require('request');
33
import {
44
ApisApi,
55
HttpError,
6-
ObjectSerializer,
76
V1APIResource,
87
V1APIResourceList,
98
V1DeleteOptions,
109
V1Status,
1110
} from './api';
1211
import { KubeConfig } from './config';
12+
import ObjectSerializer from './serializer';
1313
import { KubernetesListObject, KubernetesObject } from './types';
1414

1515
/** Union type of body types returned by KubernetesObjectApi. */
@@ -51,6 +51,7 @@ enum KubernetesPatchStrategies {
5151
StrategicMergePatch = 'application/strategic-merge-patch+json',
5252
}
5353

54+
5455
/**
5556
* Dynamically construct Kubernetes API request URIs so client does not have to know what type of object it is acting
5657
* on.
@@ -499,7 +500,7 @@ export class KubernetesObjectApi extends ApisApi {
499500
*
500501
* @param spec Kubernetes resource spec which must define kind and apiVersion properties.
501502
* @param action API action, see [[K8sApiAction]].
502-
* @return tail of resource-specific URIDeploym
503+
* @return tail of resource-specific URI
503504
*/
504505
protected async specUriPath(spec: KubernetesObject, action: KubernetesApiAction): Promise<string> {
505506
if (!spec.kind) {

src/object_test.ts

Lines changed: 1 addition & 2 deletions
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

Lines changed: 115 additions & 0 deletions
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+
export type AttributeType = {
5+
name: string;
6+
baseName: string;
7+
type: string;
8+
};
9+
10+
export 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+
48+
private static _instance: KubernetesObjectSerializer;
49+
50+
public static get instance() {
51+
if (this._instance) {
52+
return this._instance;
53+
}
54+
this._instance = new KubernetesObjectSerializer();
55+
return this._instance;
56+
}
57+
58+
private constructor() {}
59+
60+
public serialize(data: any, type: string) {
61+
const obj = ObjectSerializer.serialize(data, type);
62+
if(obj !== data) {
63+
return obj;
64+
}
65+
66+
if (!isKubernetesObject(data)) {
67+
return obj;
68+
}
69+
70+
const instance: Record<string, any> = {};
71+
const attributeTypes = KubernetesObject.attributeTypeMap;
72+
for (let index = 0; index < attributeTypes.length; index++) {
73+
let attributeType = attributeTypes[index];
74+
instance[attributeType.name] = ObjectSerializer.serialize(data[attributeType.baseName], attributeType.type);
75+
}
76+
// add all unknown properties as is.
77+
for (const [key, value] of Object.entries(data)) {
78+
if (attributeTypes.find((t) => t.name === key)) {
79+
continue;
80+
}
81+
instance[key] = value;
82+
}
83+
return instance;
84+
85+
}
86+
87+
public deserialize(data: any, type: string) {
88+
const obj = ObjectSerializer.deserialize(data, type);
89+
if (obj !== data) {
90+
// the serializer knows the type and already deserialized it.
91+
return obj;
92+
}
93+
94+
if (!isKubernetesObject(data)) {
95+
return obj;
96+
}
97+
98+
const instance = new KubernetesObject();
99+
const attributeTypes = KubernetesObject.attributeTypeMap;
100+
for (let index = 0; index < attributeTypes.length; index++) {
101+
let attributeType = attributeTypes[index];
102+
instance[attributeType.name] = ObjectSerializer.deserialize(data[attributeType.baseName], attributeType.type);
103+
}
104+
// add all unknown properties as is.
105+
for (const [key, value] of Object.entries(data)) {
106+
if (attributeTypes.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

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

0 commit comments

Comments
 (0)