Skip to content

Commit ba4423e

Browse files
authored
Merge pull request kubernetes-client#415 from brendandburns/top
Initial implementation of "top" command for nodes
2 parents e4e34dd + 0ba4d5d commit ba4423e

File tree

5 files changed

+218
-0
lines changed

5 files changed

+218
-0
lines changed

examples/top.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const k8s = require('../dist/index');
2+
3+
const kc = new k8s.KubeConfig();
4+
kc.loadFromDefault();
5+
6+
const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
7+
8+
k8s.topNodes(k8sApi).then((obj) => console.log(obj));

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export * from './types';
99
export * from './yaml';
1010
export * from './log';
1111
export * from './informer';
12+
export * from './top';

src/top.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { CoreV1Api, V1Node, V1Pod } from './gen/api';
2+
import { podsForNode, quantityToScalar, totalCPU, totalMemory } from './util';
3+
4+
export class ResourceUsage {
5+
constructor(
6+
public readonly Capacity: number,
7+
public readonly RequestTotal: number,
8+
public readonly LimitTotal: number,
9+
) {}
10+
}
11+
12+
export class NodeStatus {
13+
constructor(
14+
public readonly Node: V1Node,
15+
public readonly CPU: ResourceUsage,
16+
public readonly Memory: ResourceUsage,
17+
) {}
18+
}
19+
20+
export async function topNodes(api: CoreV1Api): Promise<NodeStatus[]> {
21+
// TODO: Support metrics APIs in the client and this library
22+
const nodes = await api.listNode();
23+
const result: NodeStatus[] = [];
24+
for (const node of nodes.body!.items) {
25+
const availableCPU = quantityToScalar(node.status!.allocatable!.cpu);
26+
const availableMem = quantityToScalar(node.status!.allocatable!.memory);
27+
let totalPodCPU = 0;
28+
let totalPodCPULimit = 0;
29+
let totalPodMem = 0;
30+
let totalPodMemLimit = 0;
31+
let pods = await podsForNode(api, node.metadata!.name!);
32+
pods = pods.filter((pod: V1Pod) => pod.status!.phase === 'Running');
33+
pods.forEach((pod: V1Pod) => {
34+
const cpuTotal = totalCPU(pod);
35+
totalPodCPU += cpuTotal.request;
36+
totalPodCPULimit += cpuTotal.limit;
37+
38+
const memTotal = totalMemory(pod);
39+
totalPodMem += memTotal.request;
40+
totalPodMemLimit += memTotal.limit;
41+
});
42+
43+
const cpuUsage = new ResourceUsage(availableCPU, totalPodCPU, totalPodCPULimit);
44+
const memUsage = new ResourceUsage(availableMem, totalPodMem, totalPodMemLimit);
45+
result.push(new NodeStatus(node, cpuUsage, memUsage));
46+
}
47+
return result;
48+
}

src/util.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { CoreV1Api, V1Container, V1Pod } from './gen/api';
2+
3+
export async function podsForNode(api: CoreV1Api, nodeName: string): Promise<V1Pod[]> {
4+
const allPods = await api.listPodForAllNamespaces();
5+
return allPods.body.items.filter((pod: V1Pod) => pod.spec!.nodeName === nodeName);
6+
}
7+
8+
export function quantityToScalar(quantity: string): number {
9+
if (!quantity) {
10+
return 0;
11+
}
12+
if (quantity.endsWith('m')) {
13+
return parseInt(quantity.substr(0, quantity.length - 1), 10) / 1000.0;
14+
}
15+
if (quantity.endsWith('Ki')) {
16+
return parseInt(quantity.substr(0, quantity.length - 2), 10) * 1024;
17+
}
18+
const num = parseInt(quantity, 10);
19+
if (isNaN(num)) {
20+
throw new Error('Unknown quantity ' + quantity);
21+
}
22+
return num;
23+
}
24+
25+
export class ResourceStatus {
26+
constructor(
27+
public readonly request: number,
28+
public readonly limit: number,
29+
public readonly resourceType: string,
30+
) {}
31+
}
32+
33+
export function totalCPU(pod: V1Pod): ResourceStatus {
34+
return totalForResource(pod, 'cpu');
35+
}
36+
37+
export function totalMemory(pod: V1Pod): ResourceStatus {
38+
return totalForResource(pod, 'memory');
39+
}
40+
41+
export function totalForResource(pod: V1Pod, resource: string): ResourceStatus {
42+
let reqTotal = 0;
43+
let limitTotal = 0;
44+
pod.spec!.containers.forEach((container: V1Container) => {
45+
if (container.resources) {
46+
if (container.resources.requests) {
47+
reqTotal += quantityToScalar(container.resources.requests[resource]);
48+
}
49+
if (container.resources.limits) {
50+
limitTotal += quantityToScalar(container.resources.limits[resource]);
51+
}
52+
}
53+
});
54+
return new ResourceStatus(reqTotal, limitTotal, resource);
55+
}

src/util_test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { expect } from 'chai';
2+
import { CoreV1Api, V1Container, V1Pod } from './api';
3+
import { podsForNode, quantityToScalar, totalCPU, totalMemory } from './util';
4+
5+
describe('Utils', () => {
6+
it('should get zero pods for a node', async () => {
7+
const podList = {
8+
body: {
9+
items: [],
10+
},
11+
};
12+
const mockApi = {
13+
listPodForAllNamespaces: (): Promise<any> => {
14+
return new Promise<any>((resolve) => {
15+
resolve(podList);
16+
});
17+
},
18+
};
19+
20+
const pods = await podsForNode(mockApi as CoreV1Api, 'foo');
21+
expect(pods.length).to.equal(0);
22+
});
23+
24+
it('should only gets for pods named node', async () => {
25+
const podList = {
26+
body: {
27+
items: [
28+
{
29+
spec: {
30+
nodeName: 'foo',
31+
},
32+
},
33+
{
34+
spec: {
35+
nodeName: 'bar',
36+
},
37+
},
38+
],
39+
},
40+
};
41+
const mockApi = {
42+
listPodForAllNamespaces: (): Promise<any> => {
43+
return new Promise<any>((resolve) => {
44+
resolve(podList);
45+
});
46+
},
47+
};
48+
49+
const pods = await podsForNode(mockApi as CoreV1Api, 'foo');
50+
expect(pods.length).to.equal(1);
51+
expect(pods[0].spec!.nodeName).to.equal('foo');
52+
});
53+
54+
it('should parse quantities', () => {
55+
expect(quantityToScalar('')).to.equal(0);
56+
57+
expect(quantityToScalar('100m')).to.equal(0.1);
58+
expect(quantityToScalar('1976m')).to.equal(1.976);
59+
60+
expect(quantityToScalar('10Ki')).to.equal(10240);
61+
62+
expect(quantityToScalar('1024')).to.equal(1024);
63+
64+
expect(() => quantityToScalar('foobar')).to.throw('Unknown quantity foobar');
65+
});
66+
67+
it('should get resources for pod', () => {
68+
const pod = {
69+
spec: {
70+
containers: [
71+
{
72+
name: 'foo',
73+
resources: {
74+
requests: {
75+
cpu: '100m',
76+
memory: '10Ki',
77+
},
78+
limits: {
79+
cpu: '200m',
80+
},
81+
},
82+
} as V1Container,
83+
{
84+
name: 'bar',
85+
resources: {
86+
requests: {
87+
cpu: '1.0',
88+
},
89+
limits: {
90+
memory: '20Ki',
91+
},
92+
},
93+
} as V1Container,
94+
] as V1Container[],
95+
},
96+
} as V1Pod;
97+
98+
const cpuResult = totalCPU(pod);
99+
expect(cpuResult.request).to.equal(1.1);
100+
expect(cpuResult.limit).to.equal(0.2);
101+
102+
const memResult = totalMemory(pod);
103+
expect(memResult.request).to.equal(10240);
104+
expect(memResult.limit).to.equal(20480);
105+
});
106+
});

0 commit comments

Comments
 (0)