Skip to content

Commit b85a634

Browse files
k8s-ci-robotschrodit
authored andcommitted
Merge pull request #1695 from schrodit/fix-obj-typing
Properly parse metadata of custom Kubernetes objects
1 parent b8705f0 commit b85a634

File tree

6 files changed

+279
-15
lines changed

6 files changed

+279
-15
lines changed

src/cache.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
UPDATE,
1212
} from './informer';
1313
import { KubernetesObject } from './types';
14-
import { ObjectSerializer } from './util';
14+
import { ObjectSerializer } from './serializer';
1515
import { Watch } from './watch';
1616

1717
export interface ObjectCache<T> {

src/object.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import {
1111
V1Status,
1212
} from './api';
1313
import { KubeConfig } from './config';
14+
import { ObjectSerializer } from './serializer';
1415
import { KubernetesListObject, KubernetesObject } from './types';
15-
import { ObjectSerializer } from './util';
1616
import { from, mergeMap, of } from './gen/rxjsStub';
1717
import { PatchStrategy } from './patch';
1818

@@ -482,7 +482,7 @@ export class KubernetesObjectApi {
482482
*
483483
* @param spec Kubernetes resource spec which must define kind and apiVersion properties.
484484
* @param action API action, see [[K8sApiAction]].
485-
* @return tail of resource-specific URIDeploym
485+
* @return tail of resource-specific URI
486486
*/
487487
protected async specUriPath(spec: KubernetesObject, action: KubernetesApiAction): Promise<string> {
488488
if (!spec.kind) {

src/object_test.ts

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

src/serializer.ts

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

src/serializer_test.ts

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { expect } from 'chai';
2+
import { ObjectSerializer } from './serializer';
3+
4+
describe('ObjectSerializer', () => {
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 = ObjectSerializer.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 = ObjectSerializer.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 = ObjectSerializer.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 = ObjectSerializer.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+
},
121+
data: {
122+
key: 'value',
123+
},
124+
});
125+
});
126+
127+
it('should deserialize a unknown object', () => {
128+
const s = {
129+
apiVersion: 'v1alpha1',
130+
kind: 'MyCustomResource',
131+
metadata: {
132+
name: 'k8s-js-client-test',
133+
namespace: 'default',
134+
creationTimestamp: '2022-01-01T00:00:00.000Z',
135+
},
136+
data: {
137+
key: 'value',
138+
},
139+
};
140+
const res = ObjectSerializer.deserialize(s, 'v1alpha1MyCustomResource');
141+
expect(res).to.deep.equal({
142+
apiVersion: 'v1alpha1',
143+
kind: 'MyCustomResource',
144+
metadata: {
145+
name: 'k8s-js-client-test',
146+
namespace: 'default',
147+
creationTimestamp: new Date('2022-01-01T00:00:00.000Z'),
148+
},
149+
data: {
150+
key: 'value',
151+
},
152+
});
153+
});
154+
155+
it('should deserialize a unknown primitive', () => {
156+
const s = {
157+
key: 'value',
158+
};
159+
const res = ObjectSerializer.serialize(s, 'unknown');
160+
expect(res).to.deep.equal(s);
161+
});
162+
});
163+
});

src/util.ts

-10
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,6 @@
11
import { Response } from 'node-fetch';
22
import { isNumber } from 'underscore';
33
import { CoreV1Api, V1Container, V1Pod } from './gen';
4-
import { ObjectSerializer as InternalSerializer } from './gen/models/ObjectSerializer';
5-
6-
export class ObjectSerializer extends InternalSerializer {
7-
public static serialize(data: any, type: string, format: string = ''): string {
8-
return InternalSerializer.serialize(data, type, format);
9-
}
10-
public static deserialize(data: any, type: string, format: string = ''): any {
11-
return InternalSerializer.deserialize(data, type, format);
12-
}
13-
}
144

155
export async function podsForNode(api: CoreV1Api, nodeName: string): Promise<V1Pod[]> {
166
const allPods = await api.listPodForAllNamespaces();

0 commit comments

Comments
 (0)