Skip to content

Commit

Permalink
feat(metrics): add single node metrics and query options
Browse files Browse the repository at this point in the history
  • Loading branch information
0fatal committed Oct 24, 2023
1 parent f1f8949 commit 3db5472
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 18 deletions.
75 changes: 58 additions & 17 deletions src/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface NodeMetric {
name: string;
selfLink?: string;
creationTimestamp: string;
labels?: { [key: string]: string };
};
timestamp: string;
window: string;
Expand Down Expand Up @@ -60,43 +61,82 @@ export interface SinglePodMetrics extends PodMetric {
apiVersion: 'metrics.k8s.io/v1beta1';
}

export interface SingleNodeMetrics extends NodeMetric {
kind: 'NodeMetrics';
apiVersion: 'metrics.k8s.io/v1beta1';
}

export interface GetPodMetricsOptions {
/**
* restrict the list of returned objects by labels
*/
labelSelector?: string;
}

export class Metrics {
private config: KubeConfig;

public constructor(config: KubeConfig) {
this.config = config;
}

public async getNodeMetrics(): Promise<NodeMetricsList> {
return this.metricsApiRequest<NodeMetricsList>('/apis/metrics.k8s.io/v1beta1/nodes');
public async getNodeMetrics(options?: GetPodMetricsOptions): Promise<NodeMetricsList>;
public async getNodeMetrics(node: string, options?: GetPodMetricsOptions): Promise<SingleNodeMetrics>;
public async getNodeMetrics(
nodeOrOptions?: string | GetPodMetricsOptions,
options?: GetPodMetricsOptions,
): Promise<NodeMetricsList | SingleNodeMetrics> {
if (typeof nodeOrOptions !== 'string' || nodeOrOptions === '') {
if (nodeOrOptions !== '') {
options = nodeOrOptions;
}
return this.metricsApiRequest<NodeMetricsList>('/apis/metrics.k8s.io/v1beta1/nodes', options);
}
return this.metricsApiRequest<SingleNodeMetrics>(
`/apis/metrics.k8s.io/v1beta1/nodes/${nodeOrOptions}`,
options,
);
}

public async getPodMetrics(namespace?: string): Promise<PodMetricsList>;
public async getPodMetrics(namespace: string, name: string): Promise<SinglePodMetrics>;

public async getPodMetrics(options?: GetPodMetricsOptions): Promise<PodMetricsList>;
public async getPodMetrics(namespace?: string, options?: GetPodMetricsOptions): Promise<PodMetricsList>;
public async getPodMetrics(
namespace: string,
name: string,
options?: GetPodMetricsOptions,
): Promise<SinglePodMetrics>;
public async getPodMetrics(
namespace?: string,
name?: string,
namespaceOrOptions?: string | GetPodMetricsOptions,
nameOrOptions?: string | GetPodMetricsOptions,
options?: GetPodMetricsOptions,
): Promise<SinglePodMetrics | PodMetricsList> {
let path: string;

if (namespace !== undefined && namespace.length > 0 && name !== undefined && name.length > 0) {
path = `/apis/metrics.k8s.io/v1beta1/namespaces/${namespace}/pods/${name}`;
return this.metricsApiRequest<SinglePodMetrics>(path);
}
if (typeof namespaceOrOptions === 'string' && namespaceOrOptions !== '') {
const namespace = namespaceOrOptions;

if (namespace !== undefined && namespace.length > 0) {
path = `/apis/metrics.k8s.io/v1beta1/namespaces/${namespace}/pods`;
if (typeof nameOrOptions === 'string') {
path = `/apis/metrics.k8s.io/v1beta1/namespaces/${namespace}/pods/${nameOrOptions}`;
} else {
path = `/apis/metrics.k8s.io/v1beta1/namespaces/${namespace}/pods`;
options = nameOrOptions;
}
} else {
path = '/apis/metrics.k8s.io/v1beta1/pods';

if (typeof namespaceOrOptions !== 'string') {
options = namespaceOrOptions;
} else if (typeof nameOrOptions !== 'string') {
options = nameOrOptions;
}
}

return this.metricsApiRequest<PodMetricsList>(path);
return this.metricsApiRequest<PodMetricsList | SinglePodMetrics>(path, options);
}

private async metricsApiRequest<T extends PodMetricsList | NodeMetricsList | SinglePodMetrics>(
path: string,
): Promise<T> {
private async metricsApiRequest<
T extends PodMetricsList | NodeMetricsList | SinglePodMetrics | SingleNodeMetrics,
>(path: string, options?: GetPodMetricsOptions): Promise<T> {
const cluster = this.config.getCurrentCluster();
if (!cluster) {
throw new Error('No currently active cluster');
Expand All @@ -105,6 +145,7 @@ export class Metrics {
const requestOptions: request.Options = {
method: 'GET',
uri: cluster.server + path,
qs: options,
};

await this.config.applyToRequest(requestOptions);
Expand Down
138 changes: 137 additions & 1 deletion src/metrics_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { expect } from 'chai';
import nock = require('nock');
import { KubeConfig } from './config';
import { V1Status, HttpError } from './gen/api';
import { Metrics, NodeMetricsList, PodMetricsList, SinglePodMetrics } from './metrics';
import { Metrics, NodeMetricsList, PodMetricsList, SingleNodeMetrics, SinglePodMetrics } from './metrics';

const emptyPodMetrics: PodMetricsList = {
kind: 'PodMetricsList',
Expand Down Expand Up @@ -47,6 +47,28 @@ const mockedPodMetrics: PodMetricsList = {
],
};

const mockedPodMetricsWithLabels: PodMetricsList = {
kind: 'PodMetricsList',
apiVersion: 'metrics.k8s.io/v1beta1',
metadata: { selfLink: '/apis/metrics.k8s.io/v1beta1/pods/' },
items: [
{
metadata: {
name: 'dice-roller-7c76898b4d-shm9p',
namespace: 'default',
selfLink: '/apis/metrics.k8s.io/v1beta1/namespaces/default/pods/dice-roller-7c76898b4d-shm9p',
creationTimestamp: '2021-09-26T11:57:27Z',
labels: {
label: 'aLabel',
},
},
timestamp: '2021-09-26T11:57:21Z',
window: '30s',
containers: [{ name: 'nginx', usage: { cpu: '10', memory: '3912Ki' } }],
},
],
};

const emptyNodeMetrics: NodeMetricsList = {
kind: 'NodeMetricsList',
apiVersion: 'metrics.k8s.io/v1beta1',
Expand All @@ -64,6 +86,9 @@ const mockedNodeMetrics: NodeMetricsList = {
name: 'a-node',
selfLink: '/apis/metrics.k8s.io/v1beta1/nodes/a-node',
creationTimestamp: '2021-09-26T16:01:53Z',
labels: {
label: 'aLabel',
},
},
timestamp: '2021-09-26T16:01:11Z',
window: '30s',
Expand All @@ -72,6 +97,21 @@ const mockedNodeMetrics: NodeMetricsList = {
],
};

const mockedSingleNodeMetrics: SingleNodeMetrics = {
kind: 'NodeMetrics',
apiVersion: 'metrics.k8s.io/v1beta1',
metadata: {
name: 'a-node',
creationTimestamp: '2021-09-26T16:01:53Z',
labels: {
label: 'aLabel',
},
},
timestamp: '2021-09-26T16:01:11Z',
window: '30s',
usage: { cpu: '214650124n', memory: '801480Ki' },
};

const mockedSinglePodMetrics: SinglePodMetrics = {
kind: 'PodMetrics',
apiVersion: 'metrics.k8s.io/v1beta1',
Expand Down Expand Up @@ -167,6 +207,54 @@ describe('Metrics', () => {

s.done();
});

it('should return specified cluster scope pods metric list if given options', async () => {
const [metricsClient, scope] = systemUnderTest();
const options = {
labelSelector: 'label=aLabel',
};
const s = scope
.get('/apis/metrics.k8s.io/v1beta1/pods')
.query(options)
.reply(200, mockedPodMetricsWithLabels);

const response = await metricsClient.getPodMetrics(options);
expect(response).to.deep.equal(mockedPodMetricsWithLabels);
s.done();
});

it('should return specified namespace scope pods metric list if given options', async () => {
const [metricsClient, scope] = systemUnderTest();
const options = {
labelSelector: 'label=aLabel',
};
const s = scope
.get(`/apis/metrics.k8s.io/v1beta1/namespaces/${TEST_NAMESPACE}/pods`)
.query(options)
.reply(200, mockedPodMetricsWithLabels);

const response = await metricsClient.getPodMetrics(TEST_NAMESPACE, options);
expect(response).to.deep.equal(mockedPodMetricsWithLabels);
s.done();
});

it('should return specified single pod metrics if given namespace and pod name and options', async () => {
const podName = 'pod-name';
const [metricsClient, scope] = systemUnderTest();
const options = {
labelSelector: 'label=aLabel',
};
const s = scope
.get(`/apis/metrics.k8s.io/v1beta1/namespaces/${TEST_NAMESPACE}/pods/${podName}`)
.query(options)
.reply(200, mockedSinglePodMetrics);

const response = await metricsClient.getPodMetrics(TEST_NAMESPACE, podName, options);
expect(response).to.deep.equal(mockedSinglePodMetrics);

s.done();
});

it('should when connection refused', async () => {
const kc = new KubeConfig();
kc.loadFromOptions({
Expand Down Expand Up @@ -261,6 +349,54 @@ describe('Metrics', () => {

s.done();
});

it('should return single node metrics if given node name', async () => {
const [metricsClient, scope] = systemUnderTest();
const nodeName = 'a-node';

const s = scope
.get(`/apis/metrics.k8s.io/v1beta1/nodes/${nodeName}`)
.reply(200, mockedSingleNodeMetrics);

const response = await metricsClient.getNodeMetrics(nodeName);
expect(response).to.deep.equal(mockedSingleNodeMetrics);

s.done();
});

it('should return specified nodes metrics list if given options', async () => {
const [metricsClient, scope] = systemUnderTest();
const options = {
labelSelector: 'label=aLabel',
};
const s = scope
.get('/apis/metrics.k8s.io/v1beta1/nodes')
.query(options)
.reply(200, mockedNodeMetrics);

const response = await metricsClient.getNodeMetrics(options);
expect(response).to.deep.equal(mockedNodeMetrics);

s.done();
});

it('should return specified single node metrics if given node name and options', async () => {
const [metricsClient, scope] = systemUnderTest();
const nodeName = 'a-node';
const options = {
labelSelector: 'label=aLabel',
};
const s = scope
.get(`/apis/metrics.k8s.io/v1beta1/nodes/${nodeName}`)
.query(options)
.reply(200, mockedSingleNodeMetrics);

const response = await metricsClient.getNodeMetrics(nodeName, options);
expect(response).to.deep.equal(mockedSingleNodeMetrics);

s.done();
});

it('should resolve to error when 500', async () => {
const response: V1Status = {
code: 12345,
Expand Down

0 comments on commit 3db5472

Please sign in to comment.