Repository: kubernetes-client/javascript
Pull request #1368 (2023-10-24 07:37:43)

feat(metrics): add single node metrics and query options
Followed the instructions in [resource-metrics-api.md](https://github.com/kubernetes/design-proposals-archive/blob/main/instrumentation/resource-metrics-api.md#endpoints), add single node metrics and query options

Thanks for the code review


Patch:
diff --git a/src/metrics.ts b/src/metrics.ts
index 7b8954a7..81eb9a8a 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 MetricsOptions {
+    /**
+     * 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<NodeMetricsList> {
-        return this.metricsApiRequest<NodeMetricsList>('/apis/metrics.k8s.io/v1beta1/nodes');
+    public async getNodeMetrics(options?: MetricsOptions): Promise<NodeMetricsList>;
+    public async getNodeMetrics(node: string, options?: MetricsOptions): Promise<SingleNodeMetrics>;
+    public async getNodeMetrics(
+        nodeOrOptions?: string | MetricsOptions,
+        options?: MetricsOptions,
+    ): 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?: MetricsOptions): Promise<PodMetricsList>;
+    public async getPodMetrics(namespace?: string, options?: MetricsOptions): Promise<PodMetricsList>;
+    public async getPodMetrics(
+        namespace: string,
+        name: string,
+        options?: MetricsOptions,
+    ): Promise<SinglePodMetrics>;
     public async getPodMetrics(
-        namespace?: string,
-        name?: string,
+        namespaceOrOptions?: string | MetricsOptions,
+        nameOrOptions?: string | MetricsOptions,
+        options?: MetricsOptions,
     ): 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?: MetricsOptions): Promise<T> {
         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);


Test patch:
diff --git a/src/metrics_test.ts b/src/metrics_test.ts
index edc6a54b..869bfff1 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,