Skip to content

Commit 88ceded

Browse files
committed
add top pods function
1 parent 2ff44b1 commit 88ceded

File tree

3 files changed

+330
-1
lines changed

3 files changed

+330
-1
lines changed

src/top.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { CoreV1Api, V1Node, V1Pod } from './gen/api';
1+
import { CoreV1Api, V1Node, V1Pod, V1PodList, V1Container } from './gen/api';
2+
import { Metrics, PodMetric } from './metrics';
23
import { add, podsForNode, quantityToScalar, totalCPU, totalMemory } from './util';
34

45
export class ResourceUsage {
@@ -9,6 +10,14 @@ export class ResourceUsage {
910
) {}
1011
}
1112

13+
export class CurrentResourceUsage {
14+
constructor(
15+
public readonly CurrentUsage: number | BigInt,
16+
public readonly RequestTotal: number | BigInt,
17+
public readonly LimitTotal: number | BigInt,
18+
) {}
19+
}
20+
1221
export class NodeStatus {
1322
constructor(
1423
public readonly Node: V1Node,
@@ -17,6 +26,25 @@ export class NodeStatus {
1726
) {}
1827
}
1928

29+
export class ContainerStatus {
30+
constructor(
31+
public readonly Container: string,
32+
public readonly CPUUsage: number | BigInt,
33+
public readonly Memory: number | BigInt,
34+
) {}
35+
}
36+
37+
export class PodStatus {
38+
constructor(
39+
public readonly Pod: V1Pod,
40+
public readonly CPU: CurrentResourceUsage,
41+
public readonly Memory: CurrentResourceUsage,
42+
public readonly Containers: ContainerStatus[],
43+
) {}
44+
}
45+
46+
47+
2048
export async function topNodes(api: CoreV1Api): Promise<NodeStatus[]> {
2149
// TODO: Support metrics APIs in the client and this library
2250
const nodes = await api.listNode();
@@ -46,3 +74,51 @@ export async function topNodes(api: CoreV1Api): Promise<NodeStatus[]> {
4674
}
4775
return result;
4876
}
77+
78+
export async function topPods(api: CoreV1Api, metrics: Metrics, namespace?: string): Promise<PodStatus[]> {
79+
80+
const getPodList = async ():Promise<V1PodList> => {
81+
if (namespace) {
82+
return (await api.listNamespacedPod(namespace)).body
83+
} else {
84+
return (await api.listPodForAllNamespaces()).body
85+
}
86+
}
87+
88+
const [podMetrics, podList] = await Promise.all([metrics.getPodMetrics(namespace), getPodList()])
89+
90+
const podMetricsMap = podMetrics.items.reduce((accum, next) => {
91+
accum.set(next.metadata.name, next)
92+
return accum
93+
}, (new Map<string, PodMetric>()))
94+
95+
const result: PodStatus[] = [];
96+
for (const pod of podList.items) {
97+
98+
const podMetric = podMetricsMap.get(pod.metadata!.name!)
99+
100+
const cpuTotal = totalCPU(pod);
101+
const memTotal = totalMemory(pod);
102+
const containerStatuses: ContainerStatus[] = [];
103+
let currentPodCPU: number | bigint = 0;
104+
let currentPodMem: number | bigint = 0;
105+
106+
if (podMetric !== undefined){
107+
podMetric.containers.forEach(container => {
108+
const containerCPUUsage = quantityToScalar(container.usage.cpu);
109+
const containerMemUsage = quantityToScalar(container.usage.memory);
110+
currentPodCPU = add(currentPodCPU, containerCPUUsage)
111+
currentPodMem = add(currentPodMem, containerMemUsage)
112+
containerStatuses.push(new ContainerStatus(container.name, containerCPUUsage, containerMemUsage))
113+
})
114+
}
115+
116+
const cpuUsage = new CurrentResourceUsage(currentPodCPU, cpuTotal.request, cpuTotal.limit);
117+
const memUsage = new CurrentResourceUsage(currentPodMem, memTotal.request, memTotal.limit);
118+
result.push(new PodStatus(pod, cpuUsage, memUsage, containerStatuses));
119+
120+
}
121+
return result;
122+
123+
124+
}

src/top_test.ts

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
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, V1Pod } from './gen/api';
6+
import { Metrics, NodeMetricsList, PodMetricsList } from './metrics';
7+
import { topPods } from './top';
8+
import { CoreV1Api, } from './gen/api';
9+
10+
11+
const emptyPodMetrics: PodMetricsList = {
12+
kind: 'PodMetricsList',
13+
apiVersion: 'metrics.k8s.io/v1beta1',
14+
metadata: {
15+
selfLink: '/apis/metrics.k8s.io/v1beta1/pods',
16+
},
17+
items: [],
18+
};
19+
20+
const mockedPodMetrics: PodMetricsList = {
21+
kind: 'PodMetricsList',
22+
apiVersion: 'metrics.k8s.io/v1beta1',
23+
metadata: { selfLink: '/apis/metrics.k8s.io/v1beta1/pods/' },
24+
items: [
25+
{
26+
metadata: {
27+
name: 'dice-roller-7c76898b4d-shm9p',
28+
namespace: 'default',
29+
selfLink: '/apis/metrics.k8s.io/v1beta1/namespaces/default/pods/dice-roller-7c76898b4d-shm9p',
30+
creationTimestamp: '2021-09-26T11:57:27Z',
31+
},
32+
timestamp: '2021-09-26T11:57:21Z',
33+
window: '30s',
34+
containers: [{ name: 'nginx', usage: { cpu: '10', memory: '3912Ki' } }],
35+
},
36+
{
37+
metadata: {
38+
name: 'other-pod-7c76898b4e-12kj',
39+
namespace: 'default',
40+
selfLink: '/apis/metrics.k8s.io/v1beta1/namespaces/default/pods/other-pod-7c76898b4e-12kj',
41+
creationTimestamp: '2021-09-26T11:57:27Z',
42+
},
43+
timestamp: '2021-09-26T11:57:21Z',
44+
window: '30s',
45+
containers: [
46+
{ name: 'nginx', usage: { cpu: '15', memory: '4012Ki' } },
47+
{ name: 'sidecar', usage: { cpu: '16', memory: '3012Ki' } },
48+
],
49+
},
50+
],
51+
};
52+
53+
const podList: V1Pod[] = [
54+
{
55+
"metadata": {
56+
"name": "dice-roller-7c76898b4d-shm9p"
57+
},
58+
'spec': {
59+
containers: [
60+
{
61+
name: "nginx",
62+
resources: {
63+
requests: {
64+
memory: "100Mi",
65+
cpu: "100m"
66+
},
67+
limits: {
68+
memory: "100Mi",
69+
cpu: "100m"
70+
},
71+
}
72+
}
73+
]
74+
}
75+
},
76+
{
77+
"metadata": {
78+
"name": "other-pod-7c76898b4e-12kj"
79+
},
80+
'spec': {
81+
containers: [
82+
{
83+
name: "nginx",
84+
resources: {
85+
requests: {
86+
memory: "100Mi",
87+
cpu: "100m"
88+
},
89+
limits: {
90+
memory: "100Mi",
91+
cpu: "100m"
92+
},
93+
}
94+
},
95+
{
96+
name: "sidecar",
97+
resources: {
98+
requests: {
99+
memory: "50Mi",
100+
cpu: "1"
101+
},
102+
limits: {
103+
memory: "100Mi",
104+
cpu: "1"
105+
},
106+
}
107+
}
108+
]
109+
}
110+
}
111+
]
112+
113+
114+
const TEST_NAMESPACE = 'test-namespace';
115+
116+
const testConfigOptions: any = {
117+
clusters: [{ name: 'cluster', server: 'https://127.0.0.1:51010' }],
118+
users: [{ name: 'user', password: 'password' }],
119+
contexts: [{ name: 'currentContext', cluster: 'cluster', user: 'user' }],
120+
currentContext: 'currentContext',
121+
};
122+
123+
const systemUnderTest = (namespace?: string, options: any = testConfigOptions): [() => ReturnType<typeof topPods>, nock.Scope] => {
124+
const kc = new KubeConfig();
125+
kc.loadFromOptions(options);
126+
const metricsClient = new Metrics(kc);
127+
const core = kc.makeApiClient(CoreV1Api);
128+
const topPodsFunc = () => topPods(core, metricsClient, namespace);
129+
130+
const scope = nock(testConfigOptions.clusters[0].server);
131+
132+
return [topPodsFunc, scope];
133+
};
134+
135+
describe('Top', () => {
136+
describe('topPods', () => {
137+
it('should return empty when no pods', async () => {
138+
const [topPodsFunc, scope] = systemUnderTest();
139+
const podMetrics = scope.get('/apis/metrics.k8s.io/v1beta1/pods').reply(200, emptyPodMetrics);
140+
const pods = scope.get('/api/v1/pods').reply(200, {
141+
items: []
142+
});
143+
const result = await topPodsFunc();
144+
expect(result).to.deep.equal([]);
145+
podMetrics.done();
146+
pods.done();
147+
});
148+
it('should return cluster wide pod metrics', async () => {
149+
const [topPodsFunc, scope] = systemUnderTest();
150+
const podMetrics = scope.get('/apis/metrics.k8s.io/v1beta1/pods').reply(200, mockedPodMetrics);
151+
const pods = scope.get('/api/v1/pods').reply(200, {
152+
items: podList
153+
});
154+
const result = await topPodsFunc();
155+
expect(result.length).to.equal(2);
156+
expect(result[0].CPU).to.deep.equal({
157+
// TODO fix this
158+
"CurrentUsage": 10,
159+
"LimitTotal": 0.1,
160+
"RequestTotal": 0.1
161+
});
162+
expect(result[0].Memory).to.deep.equal({
163+
"CurrentUsage": BigInt("4005888"),
164+
"RequestTotal": BigInt("104857600"),
165+
"LimitTotal": BigInt("104857600"),
166+
});
167+
expect(result[0].Containers).to.deep.equal([
168+
{
169+
"CPUUsage": 10,
170+
"Container": "nginx",
171+
"Memory": BigInt("4005888")
172+
}
173+
]);
174+
expect(result[1].CPU).to.deep.equal({
175+
// TODO fix this
176+
"CurrentUsage": 31,
177+
"LimitTotal": 1.1,
178+
"RequestTotal": 1.1
179+
});
180+
expect(result[1].Memory).to.deep.equal({
181+
"CurrentUsage": BigInt("7192576"),
182+
"LimitTotal": BigInt("209715200"),
183+
"RequestTotal": BigInt("157286400")
184+
});
185+
expect(result[1].Containers).to.deep.equal( [
186+
{
187+
"CPUUsage": 15,
188+
"Container": "nginx",
189+
"Memory": BigInt("4108288"),
190+
},
191+
{
192+
"CPUUsage": 16,
193+
"Container": "sidecar",
194+
"Memory": BigInt("3084288"),
195+
}
196+
]);
197+
podMetrics.done();
198+
pods.done();
199+
});
200+
it('should return namespace pod metrics', async () => {
201+
const [topPodsFunc, scope] = systemUnderTest(TEST_NAMESPACE);
202+
const podMetrics = scope.get(`/apis/metrics.k8s.io/v1beta1/namespaces/${TEST_NAMESPACE}/pods`).reply(200, mockedPodMetrics);
203+
const pods = scope.get(`/api/v1/namespaces/${TEST_NAMESPACE}/pods`).reply(200, {
204+
items: podList
205+
});
206+
const result = await topPodsFunc();
207+
expect(result.length).to.equal(2);
208+
expect(result[0].CPU).to.deep.equal({
209+
"CurrentUsage": 10,
210+
"LimitTotal": 0.1,
211+
"RequestTotal": 0.1
212+
});
213+
expect(result[0].Memory).to.deep.equal({
214+
"CurrentUsage": BigInt("4005888"),
215+
"RequestTotal": BigInt("104857600"),
216+
"LimitTotal": BigInt("104857600"),
217+
});
218+
expect(result[0].Containers).to.deep.equal([
219+
{
220+
"CPUUsage": 10,
221+
"Container": "nginx",
222+
"Memory": BigInt("4005888")
223+
}
224+
]);
225+
expect(result[1].CPU).to.deep.equal({
226+
"CurrentUsage": 31,
227+
"LimitTotal": 1.1,
228+
"RequestTotal": 1.1
229+
});
230+
expect(result[1].Memory).to.deep.equal({
231+
"CurrentUsage": BigInt("7192576"),
232+
"LimitTotal": BigInt("209715200"),
233+
"RequestTotal": BigInt("157286400")
234+
});
235+
expect(result[1].Containers).to.deep.equal( [
236+
{
237+
"CPUUsage": 15,
238+
"Container": "nginx",
239+
"Memory": BigInt("4108288"),
240+
},
241+
{
242+
"CPUUsage": 16,
243+
"Container": "sidecar",
244+
"Memory": BigInt("3084288"),
245+
}
246+
]);
247+
podMetrics.done();
248+
pods.done();
249+
});
250+
});
251+
});

src/util.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export function quantityToScalar(quantity: string): number | bigint {
2727
return num;
2828
}
2929
switch (suffix) {
30+
case 'n':
31+
return Number(quantity.substr(0, quantity.length - 1)).valueOf() / 1_000_000.0;
3032
case 'm':
3133
return Number(quantity.substr(0, quantity.length - 1)).valueOf() / 1000.0;
3234
case 'Ki':

0 commit comments

Comments
 (0)