Skip to content

Commit 2ff44b1

Browse files
authored
Merge pull request #722 from mclarke47/add-metrics-client
Add metrics client
2 parents 2b3c837 + 1747406 commit 2ff44b1

File tree

3 files changed

+359
-0
lines changed

3 files changed

+359
-0
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ export * from './top';
1313
export * from './object';
1414
export * from './cp';
1515
export * from './patch';
16+
export * from './metrics';
1617
export { ConfigOptions, User, Cluster, Context } from './config_types';

src/metrics.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import * as request from 'request';
2+
3+
import { KubeConfig } from './config';
4+
import { HttpError, ObjectSerializer } from './gen/api';
5+
6+
export interface Usage {
7+
cpu: string;
8+
memory: string;
9+
}
10+
11+
export interface ContainerMetric {
12+
name: string;
13+
usage: Usage;
14+
}
15+
16+
export interface PodMetric {
17+
metadata: {
18+
name: string;
19+
namespace: string;
20+
selfLink: string;
21+
creationTimestamp: string;
22+
};
23+
timestamp: string;
24+
window: string;
25+
containers: ContainerMetric[];
26+
}
27+
28+
export interface NodeMetric {
29+
metadata: {
30+
name: string;
31+
selfLink: string;
32+
creationTimestamp: string;
33+
};
34+
timestamp: string;
35+
window: string;
36+
usage: Usage;
37+
}
38+
39+
export interface PodMetricsList {
40+
kind: 'PodMetricsList';
41+
apiVersion: 'metrics.k8s.io/v1beta1';
42+
metadata: {
43+
selfLink: string;
44+
};
45+
items: PodMetric[];
46+
}
47+
48+
export interface NodeMetricsList {
49+
kind: 'NodeMetricsList';
50+
apiVersion: 'metrics.k8s.io/v1beta1';
51+
metadata: {
52+
selfLink: string;
53+
};
54+
items: NodeMetric[];
55+
}
56+
57+
export class Metrics {
58+
private config: KubeConfig;
59+
60+
public constructor(config: KubeConfig) {
61+
this.config = config;
62+
}
63+
64+
public async getNodeMetrics(): Promise<NodeMetricsList> {
65+
return this.metricsApiRequest<NodeMetricsList>('/apis/metrics.k8s.io/v1beta1/nodes');
66+
}
67+
68+
public async getPodMetrics(namespace?: string): Promise<PodMetricsList> {
69+
let path: string;
70+
71+
if (namespace !== undefined && namespace.length > 0) {
72+
path = `/apis/metrics.k8s.io/v1beta1/namespaces/${namespace}/pods`;
73+
} else {
74+
path = '/apis/metrics.k8s.io/v1beta1/pods';
75+
}
76+
77+
return this.metricsApiRequest<PodMetricsList>(path);
78+
}
79+
80+
private async metricsApiRequest<T extends PodMetricsList | NodeMetricsList>(path: string): Promise<T> {
81+
const cluster = this.config.getCurrentCluster();
82+
if (!cluster) {
83+
throw new Error('No currently active cluster');
84+
}
85+
86+
const requestOptions: request.Options = {
87+
method: 'GET',
88+
uri: cluster.server + path,
89+
};
90+
91+
await this.config.applyToRequest(requestOptions);
92+
93+
return new Promise((resolve, reject) => {
94+
const req = request(requestOptions, (error, response, body) => {
95+
if (error) {
96+
reject(error);
97+
} else if (response.statusCode !== 200) {
98+
try {
99+
const deserializedBody = ObjectSerializer.deserialize(JSON.parse(body), 'V1Status');
100+
reject(new HttpError(response, deserializedBody, response.statusCode));
101+
} catch (e) {
102+
reject(new HttpError(response, body, response.statusCode));
103+
}
104+
} else {
105+
resolve(JSON.parse(body) as T);
106+
}
107+
});
108+
});
109+
}
110+
}

src/metrics_test.ts

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import { fail } from 'assert';
2+
import { expect } from 'chai';
3+
import * as nock from 'nock';
4+
import { KubeConfig } from './config';
5+
import { V1Status, HttpError } from './gen/api';
6+
import { Metrics, NodeMetricsList, PodMetricsList } from './metrics';
7+
8+
const emptyPodMetrics: PodMetricsList = {
9+
kind: 'PodMetricsList',
10+
apiVersion: 'metrics.k8s.io/v1beta1',
11+
metadata: {
12+
selfLink: '/apis/metrics.k8s.io/v1beta1/pods',
13+
},
14+
items: [],
15+
};
16+
17+
const mockedPodMetrics: PodMetricsList = {
18+
kind: 'PodMetricsList',
19+
apiVersion: 'metrics.k8s.io/v1beta1',
20+
metadata: { selfLink: '/apis/metrics.k8s.io/v1beta1/pods/' },
21+
items: [
22+
{
23+
metadata: {
24+
name: 'dice-roller-7c76898b4d-shm9p',
25+
namespace: 'default',
26+
selfLink: '/apis/metrics.k8s.io/v1beta1/namespaces/default/pods/dice-roller-7c76898b4d-shm9p',
27+
creationTimestamp: '2021-09-26T11:57:27Z',
28+
},
29+
timestamp: '2021-09-26T11:57:21Z',
30+
window: '30s',
31+
containers: [{ name: 'nginx', usage: { cpu: '10', memory: '3912Ki' } }],
32+
},
33+
{
34+
metadata: {
35+
name: 'other-pod-7c76898b4e-12kj',
36+
namespace: 'default',
37+
selfLink: '/apis/metrics.k8s.io/v1beta1/namespaces/default/pods/other-pod-7c76898b4e-12kj',
38+
creationTimestamp: '2021-09-26T11:57:27Z',
39+
},
40+
timestamp: '2021-09-26T11:57:21Z',
41+
window: '30s',
42+
containers: [
43+
{ name: 'nginx', usage: { cpu: '15', memory: '4012Ki' } },
44+
{ name: 'sidecar', usage: { cpu: '16', memory: '3012Ki' } },
45+
],
46+
},
47+
],
48+
};
49+
50+
const emptyNodeMetrics: NodeMetricsList = {
51+
kind: 'NodeMetricsList',
52+
apiVersion: 'metrics.k8s.io/v1beta1',
53+
metadata: { selfLink: '/apis/metrics.k8s.io/v1beta1/nodes/' },
54+
items: [],
55+
};
56+
57+
const mockedNodeMetrics: NodeMetricsList = {
58+
kind: 'NodeMetricsList',
59+
apiVersion: 'metrics.k8s.io/v1beta1',
60+
metadata: { selfLink: '/apis/metrics.k8s.io/v1beta1/nodes/' },
61+
items: [
62+
{
63+
metadata: {
64+
name: 'a-node',
65+
selfLink: '/apis/metrics.k8s.io/v1beta1/nodes/a-node',
66+
creationTimestamp: '2021-09-26T16:01:53Z',
67+
},
68+
timestamp: '2021-09-26T16:01:11Z',
69+
window: '30s',
70+
usage: { cpu: '214650124n', memory: '801480Ki' },
71+
},
72+
],
73+
};
74+
75+
const TEST_NAMESPACE = 'test-namespace';
76+
77+
const testConfigOptions: any = {
78+
clusters: [{ name: 'cluster', server: 'https://127.0.0.1:51010' }],
79+
users: [{ name: 'user', password: 'password' }],
80+
contexts: [{ name: 'currentContext', cluster: 'cluster', user: 'user' }],
81+
currentContext: 'currentContext',
82+
};
83+
84+
const systemUnderTest = (options: any = testConfigOptions): [Metrics, nock.Scope] => {
85+
const kc = new KubeConfig();
86+
kc.loadFromOptions(options);
87+
const metricsClient = new Metrics(kc);
88+
89+
const scope = nock(testConfigOptions.clusters[0].server);
90+
91+
return [metricsClient, scope];
92+
};
93+
94+
describe('Metrics', () => {
95+
describe('getPodMetrics', () => {
96+
it('should return cluster scope empty pods list', async () => {
97+
const [metricsClient, scope] = systemUnderTest();
98+
const s = scope.get('/apis/metrics.k8s.io/v1beta1/pods').reply(200, emptyPodMetrics);
99+
100+
const response = await metricsClient.getPodMetrics();
101+
expect(response).to.deep.equal(emptyPodMetrics);
102+
s.done();
103+
});
104+
it('should return cluster scope empty pods list when namespace is empty string', async () => {
105+
const [metricsClient, scope] = systemUnderTest();
106+
const s = scope.get('/apis/metrics.k8s.io/v1beta1/pods').reply(200, emptyPodMetrics);
107+
108+
const response = await metricsClient.getPodMetrics('');
109+
expect(response).to.deep.equal(emptyPodMetrics);
110+
s.done();
111+
});
112+
it('should return namespace scope empty pods list', async () => {
113+
const [metricsClient, scope] = systemUnderTest();
114+
const s = scope
115+
.get(`/apis/metrics.k8s.io/v1beta1/namespaces/${TEST_NAMESPACE}/pods`)
116+
.reply(200, emptyPodMetrics);
117+
118+
const response = await metricsClient.getPodMetrics(TEST_NAMESPACE);
119+
expect(response).to.deep.equal(emptyPodMetrics);
120+
s.done();
121+
});
122+
it('should return cluster scope pods metrics list', async () => {
123+
const [metricsClient, scope] = systemUnderTest();
124+
const s = scope.get('/apis/metrics.k8s.io/v1beta1/pods').reply(200, mockedPodMetrics);
125+
126+
const response = await metricsClient.getPodMetrics();
127+
expect(response).to.deep.equal(mockedPodMetrics);
128+
129+
s.done();
130+
});
131+
it('should return namespace scope pods metric list', async () => {
132+
const [metricsClient, scope] = systemUnderTest();
133+
const s = scope
134+
.get(`/apis/metrics.k8s.io/v1beta1/namespaces/${TEST_NAMESPACE}/pods`)
135+
.reply(200, mockedPodMetrics);
136+
137+
const response = await metricsClient.getPodMetrics(TEST_NAMESPACE);
138+
expect(response).to.deep.equal(mockedPodMetrics);
139+
s.done();
140+
});
141+
it('should when connection refused', async () => {
142+
const kc = new KubeConfig();
143+
kc.loadFromOptions({
144+
clusters: [{ name: 'cluster', server: 'https://127.0.0.1:51011' }],
145+
users: [{ name: 'user', password: 'password' }],
146+
contexts: [{ name: 'currentContext', cluster: 'cluster', user: 'user' }],
147+
currentContext: 'currentContext',
148+
});
149+
const metricsClient = new Metrics(kc);
150+
try {
151+
await metricsClient.getPodMetrics();
152+
fail('expected thrown error');
153+
} catch (e) {
154+
expect(e.message).to.equal('connect ECONNREFUSED 127.0.0.1:51011');
155+
}
156+
});
157+
it('should throw when no current cluster', async () => {
158+
const [metricsClient, scope] = systemUnderTest({
159+
clusters: [{ name: 'cluster', server: 'https://127.0.0.1:51010' }],
160+
users: [{ name: 'user', password: 'password' }],
161+
contexts: [{ name: 'currentContext', cluster: 'cluster', user: 'user' }],
162+
});
163+
164+
try {
165+
await metricsClient.getPodMetrics();
166+
fail('expected thrown error');
167+
} catch (e) {
168+
expect(e.message).to.equal('No currently active cluster');
169+
}
170+
scope.done();
171+
});
172+
it('should resolve to error when 500 - V1 Status', async () => {
173+
const response: V1Status = {
174+
code: 12345,
175+
message: 'some message',
176+
};
177+
const [metricsClient, scope] = systemUnderTest();
178+
const s = scope.get('/apis/metrics.k8s.io/v1beta1/pods').reply(500, response);
179+
180+
try {
181+
await metricsClient.getPodMetrics();
182+
fail('expected thrown error');
183+
} catch (e) {
184+
if (!(e instanceof HttpError)) {
185+
fail('expected HttpError error');
186+
}
187+
expect(e.body.code).to.equal(response.code);
188+
expect(e.body.message).to.equal(response.message);
189+
}
190+
s.done();
191+
});
192+
it('should resolve to error when 500 - non-V1Status', async () => {
193+
const response = 'some other response';
194+
const [metricsClient, scope] = systemUnderTest();
195+
const s = scope.get('/apis/metrics.k8s.io/v1beta1/pods').reply(500, response);
196+
197+
try {
198+
await metricsClient.getPodMetrics();
199+
fail('expected thrown error');
200+
} catch (e) {
201+
if (!(e instanceof HttpError)) {
202+
fail('expected HttpError error');
203+
}
204+
expect(e.message).to.equal('HTTP request failed');
205+
}
206+
s.done();
207+
});
208+
});
209+
describe('getNodeMetrics', () => {
210+
it('should return empty nodes list', async () => {
211+
const [metricsClient, scope] = systemUnderTest();
212+
const s = scope.get('/apis/metrics.k8s.io/v1beta1/nodes').reply(200, emptyNodeMetrics);
213+
214+
const response = await metricsClient.getNodeMetrics();
215+
expect(response).to.deep.equal(emptyNodeMetrics);
216+
s.done();
217+
});
218+
it('should return nodes metrics list', async () => {
219+
const [metricsClient, scope] = systemUnderTest();
220+
const s = scope.get('/apis/metrics.k8s.io/v1beta1/nodes').reply(200, mockedNodeMetrics);
221+
222+
const response = await metricsClient.getNodeMetrics();
223+
expect(response).to.deep.equal(mockedNodeMetrics);
224+
225+
s.done();
226+
});
227+
it('should resolve to error when 500', async () => {
228+
const response: V1Status = {
229+
code: 12345,
230+
message: 'some message',
231+
};
232+
const [metricsClient, scope] = systemUnderTest();
233+
const s = scope.get('/apis/metrics.k8s.io/v1beta1/nodes').reply(500, response);
234+
235+
try {
236+
await metricsClient.getNodeMetrics();
237+
fail('expected thrown error');
238+
} catch (e) {
239+
if (!(e instanceof HttpError)) {
240+
fail('expected HttpError error');
241+
}
242+
expect(e.body.code).to.equal(response.code);
243+
expect(e.body.message).to.equal(response.message);
244+
}
245+
s.done();
246+
});
247+
});
248+
});

0 commit comments

Comments
 (0)