From 3db54728b523363657d48a82d5d2c12b29a39799 Mon Sep 17 00:00:00 2001 From: 0fatal <2816813070@qq.com> Date: Tue, 24 Oct 2023 15:28:33 +0800 Subject: [PATCH] feat(metrics): add single node metrics and query options --- src/metrics.ts | 75 ++++++++++++++++++------ src/metrics_test.ts | 138 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 195 insertions(+), 18 deletions(-) diff --git a/src/metrics.ts b/src/metrics.ts index 7b8954a775..09c012e08e 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -31,6 +31,7 @@ export interface NodeMetric { name: string; selfLink?: string; creationTimestamp: string; + labels?: { [key: string]: string }; }; timestamp: string; window: string; @@ -60,6 +61,18 @@ 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; @@ -67,36 +80,63 @@ export class Metrics { this.config = config; } - public async getNodeMetrics(): Promise { - return this.metricsApiRequest('/apis/metrics.k8s.io/v1beta1/nodes'); + public async getNodeMetrics(options?: GetPodMetricsOptions): Promise; + public async getNodeMetrics(node: string, options?: GetPodMetricsOptions): Promise; + public async getNodeMetrics( + nodeOrOptions?: string | GetPodMetricsOptions, + options?: GetPodMetricsOptions, + ): Promise { + if (typeof nodeOrOptions !== 'string' || nodeOrOptions === '') { + if (nodeOrOptions !== '') { + options = nodeOrOptions; + } + return this.metricsApiRequest('/apis/metrics.k8s.io/v1beta1/nodes', options); + } + return this.metricsApiRequest( + `/apis/metrics.k8s.io/v1beta1/nodes/${nodeOrOptions}`, + options, + ); } - public async getPodMetrics(namespace?: string): Promise; - public async getPodMetrics(namespace: string, name: string): Promise; - + public async getPodMetrics(options?: GetPodMetricsOptions): Promise; + public async getPodMetrics(namespace?: string, options?: GetPodMetricsOptions): Promise; + public async getPodMetrics( + namespace: string, + name: string, + options?: GetPodMetricsOptions, + ): Promise; public async getPodMetrics( - namespace?: string, - name?: string, + namespaceOrOptions?: string | GetPodMetricsOptions, + nameOrOptions?: string | GetPodMetricsOptions, + options?: GetPodMetricsOptions, ): Promise { 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(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(path); + return this.metricsApiRequest(path, options); } - private async metricsApiRequest( - path: string, - ): Promise { + private async metricsApiRequest< + T extends PodMetricsList | NodeMetricsList | SinglePodMetrics | SingleNodeMetrics, + >(path: string, options?: GetPodMetricsOptions): Promise { const cluster = this.config.getCurrentCluster(); if (!cluster) { throw new Error('No currently active cluster'); @@ -105,6 +145,7 @@ export class Metrics { const requestOptions: request.Options = { method: 'GET', uri: cluster.server + path, + qs: options, }; await this.config.applyToRequest(requestOptions); diff --git a/src/metrics_test.ts b/src/metrics_test.ts index edc6a54bc5..869bfff18d 100644 --- a/src/metrics_test.ts +++ b/src/metrics_test.ts @@ -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', @@ -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', @@ -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', @@ -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', @@ -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({ @@ -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,