Skip to content

Commit 1c01c5b

Browse files
committed
add watch using genericApi
Signed-off-by: Zahar Pecherichny <zfrhv2010@gmail.com>
1 parent a672c58 commit 1c01c5b

File tree

6 files changed

+186
-6
lines changed

6 files changed

+186
-6
lines changed

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"isomorphic-ws": "^5.0.0",
6363
"js-yaml": "^4.1.0",
6464
"jsonpath-plus": "^8.0.0",
65+
"pluralize": "^8.0.0",
6566
"request": "^2.88.0",
6667
"rfc4648": "^1.3.0",
6768
"stream-buffers": "^3.0.2",

src/generic_api.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import querystring = require('querystring');
2+
import pluralize = require('pluralize');
3+
4+
export interface K8sObject {
5+
apiVersion: string;
6+
kind: string;
7+
metadata?: {
8+
name?: string;
9+
namespace?: string;
10+
labels?: object;
11+
};
12+
}
13+
14+
export function CreatePath(object: K8sObject, include_name: boolean = true) {
15+
if (!object.apiVersion) {
16+
throw new Error('The object passed must contain apiVersion field')
17+
}
18+
if (!object.kind) {
19+
throw new Error('The object passed must contain kind field')
20+
}
21+
22+
let path = ""
23+
24+
// add apiVersion
25+
if (object.apiVersion == "v1") {
26+
path += "/api/v1"
27+
} else {
28+
path += "/apis/" + object.apiVersion
29+
}
30+
31+
// add namespace if object namespaced
32+
if (object.metadata?.namespace) {
33+
path += "/namespaces/" + object.metadata.namespace
34+
}
35+
36+
// add object kind
37+
path += "/" + pluralize(object.kind.toLowerCase())
38+
39+
// add object name
40+
if (include_name && object.metadata?.name) {
41+
path += "/" + object.metadata.name
42+
}
43+
44+
// add label selector
45+
const labels = object.metadata?.labels
46+
if (labels) {
47+
path += "?" + querystring.stringify({labelSelector: Object.keys(labels).map(label => label + "=" + labels[label]).join(",")})
48+
}
49+
50+
return path
51+
}

src/generic_api_test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { expect } from 'chai';
2+
import { CreatePath } from './generic_api';
3+
4+
5+
6+
describe('CreatePath', () => {
7+
it('should convert namespace to path', async () => {
8+
const obj1 = {
9+
apiVersion: "v1",
10+
kind: "Namespace"
11+
};
12+
13+
const path1 = CreatePath(obj1);
14+
expect(path1).to.equal("/api/v1/namespaces");
15+
16+
const obj2 = {
17+
apiVersion: "v1",
18+
kind: "Namespace",
19+
metadata: {
20+
name: "test"
21+
}
22+
};
23+
24+
const path2 = CreatePath(obj2);
25+
expect(path2).to.equal("/api/v1/namespaces/test");
26+
});
27+
28+
it('should convert custom resource to path', async () => {
29+
const obj1 = {
30+
apiVersion: "fake.crd.io/v1",
31+
kind: "fakekind",
32+
metadata: {
33+
name: "fake-name",
34+
namespace: "fake-namespace"
35+
}
36+
};
37+
38+
const path1 = CreatePath(obj1);
39+
expect(path1).to.equal("/apis/fake.crd.io/v1/namespaces/fake-namespace/fakekinds/fake-name");
40+
});
41+
42+
it('should convert objects with labels to path with labels selector', async () => {
43+
const obj1 = {
44+
apiVersion: "v1",
45+
kind: "pod",
46+
metadata: {
47+
labels: {
48+
label1: "value1",
49+
label2: "value2"
50+
}
51+
}
52+
};
53+
54+
const path1 = CreatePath(obj1);
55+
expect(path1).to.equal("/api/v1/pods?labelSelector=label1%3Dvalue1%2Clabel2%3Dvalue2");
56+
});
57+
58+
it('should convert cluster wide object to path', async () => {
59+
const obj1 = {
60+
apiVersion: "rbac.authorization.k8s.io/v1",
61+
kind: "ClusterRoleBinding"
62+
};
63+
64+
const path1 = CreatePath(obj1);
65+
expect(path1).to.equal("/apis/rbac.authorization.k8s.io/v1/clusterrolebindings");
66+
});
67+
68+
it('should convert object to path while skipping the name', async () => {
69+
const obj1 = {
70+
apiVersion: "rbac.authorization.k8s.io/v1",
71+
kind: "RoleBinding",
72+
metadata: {
73+
name: "fake-name",
74+
namespace: "fake-namespace"
75+
}
76+
};
77+
78+
const path1 = CreatePath(obj1);
79+
expect(path1).to.equal("/apis/rbac.authorization.k8s.io/v1/namespaces/fake-namespace/rolebindings/fake-name");
80+
81+
const path2 = CreatePath(obj1, false);
82+
expect(path2).to.equal("/apis/rbac.authorization.k8s.io/v1/namespaces/fake-namespace/rolebindings");
83+
});
84+
});

src/watch.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import byline = require('byline');
2+
import querystring = require('querystring');
23
import request = require('request');
34
import { Duplex } from 'stream';
45
import { KubeConfig } from './config';
6+
import { K8sObject, CreatePath } from './generic_api'
57

68
export interface WatchUpdate {
79
type: string;
@@ -137,4 +139,15 @@ export class Watch {
137139
req.pipe(stream);
138140
return req;
139141
}
142+
143+
// watch by object instead of url path
144+
public async genericWatch(
145+
object: K8sObject,
146+
queryParams: any,
147+
callback: (phase: string, apiObj: any, watchObj?: any) => void,
148+
done: (err: any) => void,
149+
): Promise<any> {
150+
let path = CreatePath(object, false)
151+
return this.watch(path, queryParams, callback, done)
152+
}
140153
}

src/watch_test.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ describe('Watch', () => {
108108
},
109109
});
110110

111-
const path = '/some/path/to/object';
111+
const path = '/api/v1/pods';
112112

113113
let doneCalled = false;
114114
let doneErr: any;
@@ -125,6 +125,23 @@ describe('Watch', () => {
125125
expect(doneCalled).to.equal(true);
126126
expect(doneErr.toString()).to.equal('Error: some error');
127127
expect(aborted).to.equal(true);
128+
129+
doneCalled = false;
130+
doneErr = null;
131+
await watch.genericWatch(
132+
{
133+
apiVersion: "v1",
134+
kind: "pod"
135+
},
136+
{},
137+
(phase: string, obj: string) => {},
138+
(err: any) => {
139+
doneCalled = true;
140+
doneErr = err;
141+
},
142+
);
143+
expect(doneCalled).to.equal(true);
144+
expect(doneErr.toString()).to.equal('Error: some error');
128145
});
129146

130147
it('should not call watch done callback more than once', async () => {
@@ -157,7 +174,7 @@ describe('Watch', () => {
157174

158175
when(fakeRequestor.webRequest(anything())).thenReturn(fakeRequest);
159176

160-
const path = '/some/path/to/object';
177+
const path = '/api/v1/pods';
161178

162179
const receivedTypes: string[] = [];
163180
const receivedObjects: string[] = [];
@@ -223,7 +240,7 @@ describe('Watch', () => {
223240

224241
when(fakeRequestor.webRequest(anything())).thenReturn(fakeRequest);
225242

226-
const path = '/some/path/to/object';
243+
const path = '/api/v1/pods';
227244

228245
const receivedTypes: string[] = [];
229246
const receivedObjects: string[] = [];
@@ -281,7 +298,7 @@ describe('Watch', () => {
281298

282299
when(fakeRequestor.webRequest(anything())).thenReturn(fakeRequest);
283300

284-
const path = '/some/path/to/object';
301+
const path = '/api/v1/pods';
285302

286303
const receivedTypes: string[] = [];
287304
const receivedObjects: string[] = [];
@@ -338,7 +355,7 @@ describe('Watch', () => {
338355

339356
when(fakeRequestor.webRequest(anything())).thenReturn(fakeRequest);
340357

341-
const path = '/some/path/to/object';
358+
const path = '/api/v1/pods';
342359

343360
const receivedTypes: string[] = [];
344361
const receivedObjects: string[] = [];
@@ -385,7 +402,7 @@ describe('Watch', () => {
385402

386403
when(fakeRequestor.webRequest(anything())).thenReturn(fakeRequest);
387404

388-
const path = '/some/path/to/object';
405+
const path = '/api/v1/pods';
389406

390407
const receivedTypes: string[] = [];
391408
const receivedObjects: string[] = [];

0 commit comments

Comments
 (0)