Skip to content

Commit 467300e

Browse files
authored
Merge pull request #2035 from feloy/feat/health-1.X
Add Health class to call health check endpoints on current context
2 parents 8e5616f + c234aa5 commit 467300e

File tree

3 files changed

+207
-0
lines changed

3 files changed

+207
-0
lines changed

src/health.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import fetch, { AbortError } from 'node-fetch';
2+
import { KubeConfig } from './config';
3+
import { RequestOptions } from 'node:https';
4+
5+
export class Health {
6+
public config: KubeConfig;
7+
8+
public constructor(config: KubeConfig) {
9+
this.config = config;
10+
}
11+
12+
public async readyz(opts: RequestOptions): Promise<boolean> {
13+
return this.check('/readyz', opts);
14+
}
15+
16+
public async livez(opts: RequestOptions): Promise<boolean> {
17+
return this.check('/livez', opts);
18+
}
19+
20+
private async healthz(opts: RequestOptions): Promise<boolean> {
21+
return this.check('/healthz', opts);
22+
}
23+
24+
private async check(path: string, opts: RequestOptions): Promise<boolean> {
25+
const cluster = this.config.getCurrentCluster();
26+
if (!cluster) {
27+
throw new Error('No currently active cluster');
28+
}
29+
30+
const requestURL = new URL(cluster.server + path);
31+
const requestInit = await this.config.applyToFetchOptions(opts);
32+
if (opts.signal) {
33+
requestInit.signal = opts.signal;
34+
}
35+
requestInit.method = 'GET';
36+
37+
try {
38+
const response = await fetch(requestURL.toString(), requestInit);
39+
const status = response.status;
40+
if (status === 200) {
41+
return true;
42+
}
43+
if (status === 404) {
44+
if (path === '/healthz') {
45+
// /livez/readyz return 404 and healthz also returns 404, let's consider it is live
46+
return true;
47+
}
48+
return this.healthz(opts);
49+
}
50+
return false;
51+
} catch (err: unknown) {
52+
if (err instanceof Error && err.name === 'AbortError') {
53+
throw err;
54+
}
55+
throw new Error('Error occurred in health request');
56+
}
57+
}
58+
}

src/health_test.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { expect } from 'chai';
2+
import nock from 'nock';
3+
4+
import { KubeConfig } from './config';
5+
import { Health } from './health';
6+
import { Cluster, User } from './config_types';
7+
8+
describe('Health', () => {
9+
describe('livez', () => {
10+
it('should throw an error if no current active cluster', async () => {
11+
const kc = new KubeConfig();
12+
const health = new Health(kc);
13+
await expect(health.livez({})).to.be.rejectedWith('No currently active cluster');
14+
});
15+
16+
it('should return true if /livez returns with status 200', async () => {
17+
const kc = new KubeConfig();
18+
const cluster = {
19+
name: 'foo',
20+
server: 'https://server.com',
21+
} as Cluster;
22+
23+
const user = {
24+
name: 'my-user',
25+
password: 'some-password',
26+
} as User;
27+
kc.loadFromClusterAndUser(cluster, user);
28+
29+
const scope = nock('https://server.com').get('/livez').reply(200);
30+
const health = new Health(kc);
31+
32+
const r = await health.livez({});
33+
expect(r).to.be.true;
34+
scope.done();
35+
});
36+
37+
it('should return false if /livez returns with status 500', async () => {
38+
const kc = new KubeConfig();
39+
const cluster = {
40+
name: 'foo',
41+
server: 'https://server.com',
42+
} as Cluster;
43+
44+
const user = {
45+
name: 'my-user',
46+
password: 'some-password',
47+
} as User;
48+
kc.loadFromClusterAndUser(cluster, user);
49+
50+
const scope = nock('https://server.com').get('/livez').reply(500);
51+
const health = new Health(kc);
52+
53+
const r = await health.livez({});
54+
expect(r).to.be.false;
55+
scope.done();
56+
});
57+
58+
it('should return true if /livez returns status 404 and /healthz returns status 200', async () => {
59+
const kc = new KubeConfig();
60+
const cluster = {
61+
name: 'foo',
62+
server: 'https://server.com',
63+
} as Cluster;
64+
65+
const user = {
66+
name: 'my-user',
67+
password: 'some-password',
68+
} as User;
69+
kc.loadFromClusterAndUser(cluster, user);
70+
71+
const scope = nock('https://server.com');
72+
scope.get('/livez').reply(404);
73+
scope.get('/healthz').reply(200);
74+
const health = new Health(kc);
75+
76+
const r = await health.livez({});
77+
expect(r).to.be.true;
78+
scope.done();
79+
});
80+
81+
it('should return false if /livez returns status 404 and /healthz returns status 500', async () => {
82+
const kc = new KubeConfig();
83+
const cluster = {
84+
name: 'foo',
85+
server: 'https://server.com',
86+
} as Cluster;
87+
88+
const user = {
89+
name: 'my-user',
90+
password: 'some-password',
91+
} as User;
92+
kc.loadFromClusterAndUser(cluster, user);
93+
94+
const scope = nock('https://server.com');
95+
scope.get('/livez').reply(404);
96+
scope.get('/healthz').reply(500);
97+
const health = new Health(kc);
98+
99+
const r = await health.livez({});
100+
expect(r).to.be.false;
101+
scope.done();
102+
});
103+
104+
it('should return true if both /livez and /healthz return status 404', async () => {
105+
const kc = new KubeConfig();
106+
const cluster = {
107+
name: 'foo',
108+
server: 'https://server.com',
109+
} as Cluster;
110+
111+
const user = {
112+
name: 'my-user',
113+
password: 'some-password',
114+
} as User;
115+
kc.loadFromClusterAndUser(cluster, user);
116+
117+
const scope = nock('https://server.com');
118+
scope.get('/livez').reply(404);
119+
scope.get('/healthz').reply(200);
120+
const health = new Health(kc);
121+
122+
const r = await health.livez({});
123+
expect(r).to.be.true;
124+
scope.done();
125+
});
126+
127+
it('should throw an error when fetch throws an error', async () => {
128+
const kc = new KubeConfig();
129+
const cluster = {
130+
name: 'foo',
131+
server: 'https://server.com',
132+
} as Cluster;
133+
134+
const user = {
135+
name: 'my-user',
136+
password: 'some-password',
137+
} as User;
138+
kc.loadFromClusterAndUser(cluster, user);
139+
140+
const scope = nock('https://server.com');
141+
scope.get('/livez').replyWithError(new Error('an error'));
142+
const health = new Health(kc);
143+
144+
await expect(health.livez({})).to.be.rejectedWith('Error occurred in health request');
145+
scope.done();
146+
});
147+
});
148+
});

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export * from './cp';
1414
export * from './patch';
1515
export * from './metrics';
1616
export * from './object';
17+
export * from './health';
1718
export { ConfigOptions, User, Cluster, Context } from './config_types';
1819

1920
// Export AbortError and FetchError so that instanceof checks in user code will definitely use the same instances

0 commit comments

Comments
 (0)