kubernetes-plugin: integrate with top method from kubernetes client library (#8248)
* kubernetes-plugin: integrate with top method from kubernetes client library Signed-off-by: mclarke <mclarke@spotify.com> * fix existing tests Signed-off-by: mclarke <mclarke@spotify.com> * prettier Signed-off-by: mclarke <mclarke@spotify.com> * add fetcher test Signed-off-by: mclarke <mclarke@spotify.com> * update api report Signed-off-by: mclarke <mclarke@spotify.com> * fix fe tests Signed-off-by: mclarke <mclarke@spotify.com> * fe tests Signed-off-by: mclarke <mclarke@spotify.com> * changeset Signed-off-by: mclarke <mclarke@spotify.com> * add skip metrics lookup flag Signed-off-by: mclarke <mclarke@spotify.com> * add skip metrics lookup to gke locator and docs Signed-off-by: mclarke <mclarke@spotify.com> * fix tests Signed-off-by: mclarke <mclarke@spotify.com> * minor change Signed-off-by: mclarke <mclarke@spotify.com> * missed prettier Signed-off-by: mclarke <mclarke@spotify.com> * missed file Signed-off-by: mclarke <mclarke@spotify.com> * more details to changeset Signed-off-by: mclarke <mclarke@spotify.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import type { KubernetesFetchError } from '@backstage/plugin-kubernetes-common';
|
||||
import type { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
import type { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common';
|
||||
import { PodStatus } from '@kubernetes/client-node/dist/top';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "AWSClusterDetails" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
@@ -34,6 +35,7 @@ export interface ClusterDetails {
|
||||
name: string;
|
||||
// (undocumented)
|
||||
serviceAccountToken?: string | undefined;
|
||||
skipMetricsLookup?: boolean;
|
||||
// (undocumented)
|
||||
skipTLSVerify?: boolean;
|
||||
// (undocumented)
|
||||
@@ -164,6 +166,11 @@ export interface KubernetesFetcher {
|
||||
fetchObjectsForService(
|
||||
params: ObjectFetchParams,
|
||||
): Promise<FetchResponseWrapper>;
|
||||
// (undocumented)
|
||||
fetchPodMetricsByNamespace(
|
||||
clusterDetails: ClusterDetails,
|
||||
namespace: string,
|
||||
): Promise<PodStatus[]>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "KubernetesObjectsProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: system:aggregated-metrics-reader
|
||||
labels:
|
||||
rbac.authorization.k8s.io/aggregate-to-view: 'true'
|
||||
rbac.authorization.k8s.io/aggregate-to-edit: 'true'
|
||||
rbac.authorization.k8s.io/aggregate-to-admin: 'true'
|
||||
rules:
|
||||
- apiGroups: ['metrics.k8s.io']
|
||||
resources: ['pods']
|
||||
verbs: ['get', 'list', 'watch']
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: metrics-server:system:auth-delegator
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: system:auth-delegator
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: metrics-server
|
||||
namespace: kube-system
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: metrics-server-auth-reader
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: extension-apiserver-authentication-reader
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: metrics-server
|
||||
namespace: kube-system
|
||||
---
|
||||
apiVersion: apiregistration.k8s.io/v1beta1
|
||||
kind: APIService
|
||||
metadata:
|
||||
name: v1beta1.metrics.k8s.io
|
||||
spec:
|
||||
service:
|
||||
name: metrics-server
|
||||
namespace: kube-system
|
||||
group: metrics.k8s.io
|
||||
version: v1beta1
|
||||
insecureSkipTLSVerify: true
|
||||
groupPriorityMinimum: 100
|
||||
versionPriority: 100
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: metrics-server
|
||||
namespace: kube-system
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: metrics-server
|
||||
namespace: kube-system
|
||||
labels:
|
||||
k8s-app: metrics-server
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
k8s-app: metrics-server
|
||||
template:
|
||||
metadata:
|
||||
name: metrics-server
|
||||
labels:
|
||||
k8s-app: metrics-server
|
||||
spec:
|
||||
serviceAccountName: metrics-server
|
||||
volumes:
|
||||
# mount in tmp so we can safely use from-scratch images and/or read-only containers
|
||||
- name: tmp-dir
|
||||
emptyDir: {}
|
||||
containers:
|
||||
- name: metrics-server
|
||||
image: k8s.gcr.io/metrics-server-amd64:v0.3.1
|
||||
args:
|
||||
- --kubelet-insecure-tls
|
||||
- --kubelet-preferred-address-types=InternalIP
|
||||
imagePullPolicy: Always
|
||||
volumeMounts:
|
||||
- name: tmp-dir
|
||||
mountPath: /tmp
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: metrics-server
|
||||
namespace: kube-system
|
||||
labels:
|
||||
kubernetes.io/name: 'Metrics-server'
|
||||
spec:
|
||||
selector:
|
||||
k8s-app: metrics-server
|
||||
ports:
|
||||
- port: 443
|
||||
protocol: TCP
|
||||
targetPort: 443
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: system:metrics-server
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ''
|
||||
resources:
|
||||
- pods
|
||||
- nodes
|
||||
- nodes/stats
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: system:metrics-server
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: system:metrics-server
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: metrics-server
|
||||
namespace: kube-system
|
||||
@@ -38,7 +38,7 @@
|
||||
"@backstage/errors": "^0.1.5",
|
||||
"@backstage/plugin-kubernetes-common": "^0.1.7",
|
||||
"@google-cloud/container": "^2.2.0",
|
||||
"@kubernetes/client-node": "^0.15.0",
|
||||
"@kubernetes/client-node": "^0.16.0",
|
||||
"@types/express": "^4.17.6",
|
||||
"aws-sdk": "^2.840.0",
|
||||
"aws4": "^1.11.0",
|
||||
|
||||
@@ -52,6 +52,7 @@ describe('ConfigClusterLocator', () => {
|
||||
serviceAccountToken: undefined,
|
||||
url: 'http://localhost:8080',
|
||||
authProvider: 'serviceAccount',
|
||||
skipMetricsLookup: false,
|
||||
skipTLSVerify: false,
|
||||
caData: undefined,
|
||||
},
|
||||
@@ -67,6 +68,7 @@ describe('ConfigClusterLocator', () => {
|
||||
url: 'http://localhost:8080',
|
||||
authProvider: 'serviceAccount',
|
||||
skipTLSVerify: false,
|
||||
skipMetricsLookup: true,
|
||||
dashboardUrl: 'https://k8s.foo.com',
|
||||
},
|
||||
{
|
||||
@@ -74,6 +76,7 @@ describe('ConfigClusterLocator', () => {
|
||||
url: 'http://localhost:8081',
|
||||
authProvider: 'google',
|
||||
skipTLSVerify: true,
|
||||
skipMetricsLookup: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -90,6 +93,7 @@ describe('ConfigClusterLocator', () => {
|
||||
url: 'http://localhost:8080',
|
||||
authProvider: 'serviceAccount',
|
||||
skipTLSVerify: false,
|
||||
skipMetricsLookup: true,
|
||||
caData: undefined,
|
||||
},
|
||||
{
|
||||
@@ -98,6 +102,7 @@ describe('ConfigClusterLocator', () => {
|
||||
url: 'http://localhost:8081',
|
||||
authProvider: 'google',
|
||||
skipTLSVerify: true,
|
||||
skipMetricsLookup: false,
|
||||
caData: undefined,
|
||||
},
|
||||
]);
|
||||
@@ -144,6 +149,7 @@ describe('ConfigClusterLocator', () => {
|
||||
url: 'http://localhost:8080',
|
||||
authProvider: 'aws',
|
||||
skipTLSVerify: false,
|
||||
skipMetricsLookup: false,
|
||||
caData: undefined,
|
||||
},
|
||||
{
|
||||
@@ -154,6 +160,7 @@ describe('ConfigClusterLocator', () => {
|
||||
url: 'http://localhost:8081',
|
||||
authProvider: 'aws',
|
||||
skipTLSVerify: true,
|
||||
skipMetricsLookup: false,
|
||||
caData: undefined,
|
||||
},
|
||||
{
|
||||
@@ -164,6 +171,7 @@ describe('ConfigClusterLocator', () => {
|
||||
serviceAccountToken: undefined,
|
||||
authProvider: 'aws',
|
||||
skipTLSVerify: true,
|
||||
skipMetricsLookup: false,
|
||||
caData: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -35,6 +35,7 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier {
|
||||
url: c.getString('url'),
|
||||
serviceAccountToken: c.getOptionalString('serviceAccountToken'),
|
||||
skipTLSVerify: c.getOptionalBoolean('skipTLSVerify') ?? false,
|
||||
skipMetricsLookup: c.getOptionalBoolean('skipMetricsLookup') ?? false,
|
||||
caData: c.getOptionalString('caData'),
|
||||
authProvider: authProvider,
|
||||
};
|
||||
|
||||
@@ -93,6 +93,7 @@ describe('GkeClusterLocator', () => {
|
||||
type: 'gke',
|
||||
projectId: 'some-project',
|
||||
region: 'some-region',
|
||||
skipMetricsLookup: true,
|
||||
});
|
||||
|
||||
const sut = GkeClusterLocator.fromConfigWithClient(config, {
|
||||
@@ -107,6 +108,7 @@ describe('GkeClusterLocator', () => {
|
||||
name: 'some-cluster',
|
||||
url: 'https://1.2.3.4',
|
||||
skipTLSVerify: false,
|
||||
skipMetricsLookup: true,
|
||||
},
|
||||
]);
|
||||
expect(mockedListClusters).toBeCalledTimes(1);
|
||||
@@ -143,6 +145,7 @@ describe('GkeClusterLocator', () => {
|
||||
name: 'some-cluster',
|
||||
url: 'https://1.2.3.4',
|
||||
skipTLSVerify: false,
|
||||
skipMetricsLookup: false,
|
||||
},
|
||||
]);
|
||||
expect(mockedListClusters).toBeCalledTimes(1);
|
||||
@@ -184,12 +187,14 @@ describe('GkeClusterLocator', () => {
|
||||
name: 'some-cluster',
|
||||
url: 'https://1.2.3.4',
|
||||
skipTLSVerify: false,
|
||||
skipMetricsLookup: false,
|
||||
},
|
||||
{
|
||||
authProvider: 'google',
|
||||
name: 'some-other-cluster',
|
||||
url: 'https://6.7.8.9',
|
||||
skipTLSVerify: false,
|
||||
skipMetricsLookup: false,
|
||||
},
|
||||
]);
|
||||
expect(mockedListClusters).toBeCalledTimes(1);
|
||||
|
||||
@@ -23,6 +23,7 @@ type GkeClusterLocatorOptions = {
|
||||
projectId: string;
|
||||
region?: string;
|
||||
skipTLSVerify?: boolean;
|
||||
skipMetricsLookup?: boolean;
|
||||
};
|
||||
|
||||
export class GkeClusterLocator implements KubernetesClustersSupplier {
|
||||
@@ -39,6 +40,8 @@ export class GkeClusterLocator implements KubernetesClustersSupplier {
|
||||
projectId: config.getString('projectId'),
|
||||
region: config.getOptionalString('region') ?? '-',
|
||||
skipTLSVerify: config.getOptionalBoolean('skipTLSVerify') ?? false,
|
||||
skipMetricsLookup:
|
||||
config.getOptionalBoolean('skipMetricsLookup') ?? false,
|
||||
};
|
||||
return new GkeClusterLocator(options, client);
|
||||
}
|
||||
@@ -52,7 +55,8 @@ export class GkeClusterLocator implements KubernetesClustersSupplier {
|
||||
|
||||
// TODO pass caData into the object
|
||||
async getClusters(): Promise<GKEClusterDetails[]> {
|
||||
const { projectId, region, skipTLSVerify } = this.options;
|
||||
const { projectId, region, skipTLSVerify, skipMetricsLookup } =
|
||||
this.options;
|
||||
const request = {
|
||||
parent: `projects/${projectId}/locations/${region}`,
|
||||
};
|
||||
@@ -65,6 +69,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier {
|
||||
url: `https://${r.endpoint ?? ''}`,
|
||||
authProvider: 'google',
|
||||
skipTLSVerify,
|
||||
skipMetricsLookup,
|
||||
}));
|
||||
} catch (e) {
|
||||
throw new ForwardedError(
|
||||
|
||||
@@ -53,6 +53,7 @@ describe('getCombinedClusterDetails', () => {
|
||||
serviceAccountToken: 'token',
|
||||
url: 'http://localhost:8080',
|
||||
authProvider: 'serviceAccount',
|
||||
skipMetricsLookup: false,
|
||||
skipTLSVerify: false,
|
||||
caData: undefined,
|
||||
},
|
||||
@@ -61,6 +62,7 @@ describe('getCombinedClusterDetails', () => {
|
||||
serviceAccountToken: undefined,
|
||||
url: 'http://localhost:8081',
|
||||
authProvider: 'google',
|
||||
skipMetricsLookup: false,
|
||||
skipTLSVerify: false,
|
||||
caData: undefined,
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
} from '../types/types';
|
||||
import { KubernetesBuilder } from './KubernetesBuilder';
|
||||
import { KubernetesFanOutHandler } from './KubernetesFanOutHandler';
|
||||
import { PodStatus } from '@kubernetes/client-node';
|
||||
|
||||
describe('KubernetesBuilder', () => {
|
||||
let app: express.Express;
|
||||
@@ -194,6 +195,7 @@ describe('KubernetesBuilder', () => {
|
||||
name: someCluster.name,
|
||||
},
|
||||
errors: [],
|
||||
podMetrics: [],
|
||||
resources: [
|
||||
{
|
||||
type: 'pods',
|
||||
@@ -211,6 +213,12 @@ describe('KubernetesBuilder', () => {
|
||||
};
|
||||
|
||||
const fetcher: KubernetesFetcher = {
|
||||
fetchPodMetricsByNamespace(
|
||||
_clusterDetails: ClusterDetails,
|
||||
_namespace: string,
|
||||
): Promise<PodStatus[]> {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
fetchObjectsForService(
|
||||
_params: ObjectFetchParams,
|
||||
): Promise<FetchResponseWrapper> {
|
||||
|
||||
@@ -47,14 +47,14 @@ describe('KubernetesClientProvider', () => {
|
||||
expect(mockGetKubeConfig.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('can get apps client by cluster details', async () => {
|
||||
it('can get custom objects client by cluster details', async () => {
|
||||
const sut = new KubernetesClientProvider();
|
||||
|
||||
const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({}));
|
||||
|
||||
sut.getKubeConfig = mockGetKubeConfig;
|
||||
|
||||
const result = sut.getAppsClientByClusterDetails({
|
||||
const result = sut.getCustomObjectsClient({
|
||||
name: 'cluster-name',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
|
||||
@@ -15,12 +15,9 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AppsV1Api,
|
||||
BatchV1beta1Api,
|
||||
AutoscalingV1Api,
|
||||
CoreV1Api,
|
||||
KubeConfig,
|
||||
NetworkingV1beta1Api,
|
||||
Metrics,
|
||||
CustomObjectsApi,
|
||||
} from '@kubernetes/client-node';
|
||||
import { ClusterDetails } from '../types/types';
|
||||
@@ -63,28 +60,10 @@ export class KubernetesClientProvider {
|
||||
return kc.makeApiClient(CoreV1Api);
|
||||
}
|
||||
|
||||
getAppsClientByClusterDetails(clusterDetails: ClusterDetails) {
|
||||
getMetricsClient(clusterDetails: ClusterDetails) {
|
||||
const kc = this.getKubeConfig(clusterDetails);
|
||||
|
||||
return kc.makeApiClient(AppsV1Api);
|
||||
}
|
||||
|
||||
getAutoscalingClientByClusterDetails(clusterDetails: ClusterDetails) {
|
||||
const kc = this.getKubeConfig(clusterDetails);
|
||||
|
||||
return kc.makeApiClient(AutoscalingV1Api);
|
||||
}
|
||||
|
||||
getBatchClientByClusterDetails(clusterDetails: ClusterDetails) {
|
||||
const kc = this.getKubeConfig(clusterDetails);
|
||||
|
||||
return kc.makeApiClient(BatchV1beta1Api);
|
||||
}
|
||||
|
||||
getNetworkingBeta1Client(clusterDetails: ClusterDetails) {
|
||||
const kc = this.getKubeConfig(clusterDetails);
|
||||
|
||||
return kc.makeApiClient(NetworkingV1beta1Api);
|
||||
return new Metrics(kc);
|
||||
}
|
||||
|
||||
getCustomObjectsClient(clusterDetails: ClusterDetails) {
|
||||
|
||||
@@ -15,13 +15,30 @@
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { ObjectFetchParams } from '../types/types';
|
||||
import { ClusterDetails, ObjectFetchParams } from '../types/types';
|
||||
import { KubernetesFanOutHandler } from './KubernetesFanOutHandler';
|
||||
import { PodStatus } from '@kubernetes/client-node/dist/top';
|
||||
|
||||
const fetchObjectsForService = jest.fn();
|
||||
const fetchPodMetricsByNamespace = jest.fn();
|
||||
|
||||
const getClustersByServiceId = jest.fn();
|
||||
|
||||
const POD_METRICS_FIXTURE = {
|
||||
containers: [],
|
||||
cpu: {
|
||||
currentUsage: 100,
|
||||
limitTotal: 102,
|
||||
requestTotal: 101,
|
||||
},
|
||||
memory: {
|
||||
currentUsage: '1000',
|
||||
limitTotal: '1002',
|
||||
requestTotal: '1001',
|
||||
},
|
||||
pod: {},
|
||||
};
|
||||
|
||||
const mockFetch = (mock: jest.Mock) => {
|
||||
mock.mockImplementation((params: ObjectFetchParams) =>
|
||||
Promise.resolve(
|
||||
@@ -33,6 +50,34 @@ const mockFetch = (mock: jest.Mock) => {
|
||||
);
|
||||
};
|
||||
|
||||
const mockMetrics = (mock: jest.Mock) => {
|
||||
mock.mockImplementation((clusterDetails: ClusterDetails, namespace: string) =>
|
||||
Promise.resolve(generatePodStatus(clusterDetails.name, namespace)),
|
||||
);
|
||||
};
|
||||
|
||||
function generatePodStatus(
|
||||
_clusterName: string,
|
||||
_namespace: string,
|
||||
): PodStatus[] {
|
||||
return [
|
||||
{
|
||||
Pod: {},
|
||||
CPU: {
|
||||
CurrentUsage: 100,
|
||||
RequestTotal: 101,
|
||||
LimitTotal: 102,
|
||||
},
|
||||
Memory: {
|
||||
CurrentUsage: BigInt('1000'),
|
||||
RequestTotal: BigInt('1001'),
|
||||
LimitTotal: BigInt('1002'),
|
||||
},
|
||||
Containers: [],
|
||||
},
|
||||
] as any;
|
||||
}
|
||||
|
||||
function generateMockResourcesAndErrors(
|
||||
serviceId: string,
|
||||
clusterName: string,
|
||||
@@ -84,6 +129,7 @@ function generateMockResourcesAndErrors(
|
||||
{
|
||||
metadata: {
|
||||
name: `my-pods-${serviceId}-${clusterName}`,
|
||||
namespace: `ns-${serviceId}-${clusterName}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -94,6 +140,7 @@ function generateMockResourcesAndErrors(
|
||||
{
|
||||
metadata: {
|
||||
name: `my-configmaps-${serviceId}-${clusterName}`,
|
||||
namespace: `ns-${serviceId}-${clusterName}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -104,6 +151,7 @@ function generateMockResourcesAndErrors(
|
||||
{
|
||||
metadata: {
|
||||
name: `my-services-${serviceId}-${clusterName}`,
|
||||
namespace: `ns-${serviceId}-${clusterName}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -128,11 +176,13 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
);
|
||||
|
||||
mockFetch(fetchObjectsForService);
|
||||
mockMetrics(fetchPodMetricsByNamespace);
|
||||
|
||||
const sut = new KubernetesFanOutHandler({
|
||||
logger: getVoidLogger(),
|
||||
fetcher: {
|
||||
fetchObjectsForService,
|
||||
fetchPodMetricsByNamespace,
|
||||
},
|
||||
serviceLocator: {
|
||||
getClustersByServiceId,
|
||||
@@ -161,6 +211,11 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
|
||||
expect(getClustersByServiceId.mock.calls.length).toBe(1);
|
||||
expect(fetchObjectsForService.mock.calls.length).toBe(1);
|
||||
expect(fetchPodMetricsByNamespace.mock.calls.length).toBe(1);
|
||||
expect(fetchPodMetricsByNamespace.mock.calls[0][1]).toBe(
|
||||
'ns-test-component-test-cluster',
|
||||
);
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
items: [
|
||||
{
|
||||
@@ -168,12 +223,14 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
name: 'test-cluster',
|
||||
},
|
||||
errors: [],
|
||||
podMetrics: [POD_METRICS_FIXTURE],
|
||||
resources: [
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-pods-test-component-test-cluster',
|
||||
namespace: 'ns-test-component-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -184,6 +241,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-configmaps-test-component-test-cluster',
|
||||
namespace: 'ns-test-component-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -194,6 +252,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-services-test-component-test-cluster',
|
||||
namespace: 'ns-test-component-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -205,6 +264,124 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('dont call top for the same namespace twice', async () => {
|
||||
getClustersByServiceId.mockImplementation(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
name: 'test-cluster',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
fetchObjectsForService.mockImplementation((_: ObjectFetchParams) =>
|
||||
Promise.resolve({
|
||||
errors: [],
|
||||
responses: [
|
||||
{
|
||||
type: 'pods',
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: `pod1`,
|
||||
namespace: `ns-a`,
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: {
|
||||
name: `pod2`,
|
||||
namespace: `ns-a`,
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: {
|
||||
name: `pod3`,
|
||||
namespace: `ns-b`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
mockMetrics(fetchPodMetricsByNamespace);
|
||||
|
||||
const sut = new KubernetesFanOutHandler({
|
||||
logger: getVoidLogger(),
|
||||
fetcher: {
|
||||
fetchObjectsForService,
|
||||
fetchPodMetricsByNamespace,
|
||||
},
|
||||
serviceLocator: {
|
||||
getClustersByServiceId,
|
||||
},
|
||||
customResources: [],
|
||||
});
|
||||
|
||||
const result = await sut.getKubernetesObjectsByEntity({
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test-component',
|
||||
annotations: {
|
||||
'backstage.io/kubernetes-labels-selector':
|
||||
'backstage.io/test-label=test-component',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
owner: 'joe',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getClustersByServiceId.mock.calls.length).toBe(1);
|
||||
expect(fetchObjectsForService.mock.calls.length).toBe(1);
|
||||
expect(fetchPodMetricsByNamespace.mock.calls.length).toBe(2);
|
||||
expect(fetchPodMetricsByNamespace.mock.calls[0][1]).toBe('ns-a');
|
||||
expect(fetchPodMetricsByNamespace.mock.calls[1][1]).toBe('ns-b');
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
items: [
|
||||
{
|
||||
cluster: {
|
||||
name: 'test-cluster',
|
||||
},
|
||||
errors: [],
|
||||
podMetrics: [POD_METRICS_FIXTURE, POD_METRICS_FIXTURE],
|
||||
resources: [
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'pod1',
|
||||
namespace: 'ns-a',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: {
|
||||
name: 'pod2',
|
||||
namespace: 'ns-a',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: {
|
||||
name: 'pod3',
|
||||
namespace: 'ns-b',
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'pods',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('retrieve objects for two clusters', async () => {
|
||||
getClustersByServiceId.mockImplementation(() =>
|
||||
Promise.resolve([
|
||||
@@ -221,11 +398,13 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
);
|
||||
|
||||
mockFetch(fetchObjectsForService);
|
||||
mockMetrics(fetchPodMetricsByNamespace);
|
||||
|
||||
const sut = new KubernetesFanOutHandler({
|
||||
logger: getVoidLogger(),
|
||||
fetcher: {
|
||||
fetchObjectsForService,
|
||||
fetchPodMetricsByNamespace,
|
||||
},
|
||||
serviceLocator: {
|
||||
getClustersByServiceId,
|
||||
@@ -265,12 +444,14 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
name: 'test-cluster',
|
||||
},
|
||||
errors: [],
|
||||
podMetrics: [POD_METRICS_FIXTURE],
|
||||
resources: [
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-pods-test-component-test-cluster',
|
||||
namespace: 'ns-test-component-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -281,6 +462,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-configmaps-test-component-test-cluster',
|
||||
namespace: 'ns-test-component-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -291,6 +473,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-services-test-component-test-cluster',
|
||||
namespace: 'ns-test-component-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -303,12 +486,14 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
name: 'other-cluster',
|
||||
},
|
||||
errors: [],
|
||||
podMetrics: [POD_METRICS_FIXTURE],
|
||||
resources: [
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-pods-test-component-other-cluster',
|
||||
namespace: 'ns-test-component-other-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -319,6 +504,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-configmaps-test-component-other-cluster',
|
||||
namespace: 'ns-test-component-other-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -329,6 +515,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-services-test-component-other-cluster',
|
||||
namespace: 'ns-test-component-other-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -358,11 +545,13 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
);
|
||||
|
||||
mockFetch(fetchObjectsForService);
|
||||
mockMetrics(fetchPodMetricsByNamespace);
|
||||
|
||||
const sut = new KubernetesFanOutHandler({
|
||||
logger: getVoidLogger(),
|
||||
fetcher: {
|
||||
fetchObjectsForService,
|
||||
fetchPodMetricsByNamespace,
|
||||
},
|
||||
serviceLocator: {
|
||||
getClustersByServiceId,
|
||||
@@ -401,12 +590,14 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
name: 'test-cluster',
|
||||
},
|
||||
errors: [],
|
||||
podMetrics: [POD_METRICS_FIXTURE],
|
||||
resources: [
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-pods-test-component-test-cluster',
|
||||
namespace: 'ns-test-component-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -417,6 +608,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-configmaps-test-component-test-cluster',
|
||||
namespace: 'ns-test-component-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -427,6 +619,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-services-test-component-test-cluster',
|
||||
namespace: 'ns-test-component-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -439,12 +632,14 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
name: 'other-cluster',
|
||||
},
|
||||
errors: [],
|
||||
podMetrics: [POD_METRICS_FIXTURE],
|
||||
resources: [
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-pods-test-component-other-cluster',
|
||||
namespace: 'ns-test-component-other-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -455,6 +650,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-configmaps-test-component-other-cluster',
|
||||
namespace: 'ns-test-component-other-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -465,6 +661,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-services-test-component-other-cluster',
|
||||
namespace: 'ns-test-component-other-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -498,11 +695,13 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
);
|
||||
|
||||
mockFetch(fetchObjectsForService);
|
||||
mockMetrics(fetchPodMetricsByNamespace);
|
||||
|
||||
const sut = new KubernetesFanOutHandler({
|
||||
logger: getVoidLogger(),
|
||||
fetcher: {
|
||||
fetchObjectsForService,
|
||||
fetchPodMetricsByNamespace,
|
||||
},
|
||||
serviceLocator: {
|
||||
getClustersByServiceId,
|
||||
@@ -541,12 +740,14 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
name: 'test-cluster',
|
||||
},
|
||||
errors: [],
|
||||
podMetrics: [POD_METRICS_FIXTURE],
|
||||
resources: [
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-pods-test-component-test-cluster',
|
||||
namespace: 'ns-test-component-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -557,6 +758,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-configmaps-test-component-test-cluster',
|
||||
namespace: 'ns-test-component-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -567,6 +769,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-services-test-component-test-cluster',
|
||||
namespace: 'ns-test-component-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -579,12 +782,14 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
name: 'other-cluster',
|
||||
},
|
||||
errors: [],
|
||||
podMetrics: [POD_METRICS_FIXTURE],
|
||||
resources: [
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-pods-test-component-other-cluster',
|
||||
namespace: 'ns-test-component-other-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -595,6 +800,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-configmaps-test-component-other-cluster',
|
||||
namespace: 'ns-test-component-other-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -605,6 +811,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-services-test-component-other-cluster',
|
||||
namespace: 'ns-test-component-other-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -617,6 +824,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
name: 'error-cluster',
|
||||
},
|
||||
errors: ['some random cluster error'],
|
||||
podMetrics: [],
|
||||
resources: [
|
||||
{
|
||||
type: 'pods',
|
||||
|
||||
@@ -27,9 +27,19 @@ import {
|
||||
import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types';
|
||||
import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator';
|
||||
import {
|
||||
ClientContainerStatus,
|
||||
ClientCurrentResourceUsage,
|
||||
ClientPodStatus,
|
||||
ClusterObjects,
|
||||
FetchResponse,
|
||||
ObjectsByEntityResponse,
|
||||
PodFetchResponse,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
import {
|
||||
ContainerStatus,
|
||||
CurrentResourceUsage,
|
||||
PodStatus,
|
||||
} from '@kubernetes/client-node';
|
||||
|
||||
export const DEFAULT_OBJECTS: ObjectToFetch[] = [
|
||||
{
|
||||
@@ -93,6 +103,50 @@ export interface KubernetesFanOutHandlerOptions
|
||||
|
||||
export interface KubernetesRequestBody extends ObjectsByEntityRequest {}
|
||||
|
||||
const isPodFetchResponse = (fr: FetchResponse): fr is PodFetchResponse =>
|
||||
fr.type === 'pods';
|
||||
const isString = (str: string | undefined): str is string => str !== undefined;
|
||||
|
||||
const numberOrBigIntToNumberOrString = (
|
||||
value: number | BigInt,
|
||||
): number | string => {
|
||||
// @ts-ignore
|
||||
return typeof value === 'bigint' ? value.toString() : value;
|
||||
};
|
||||
|
||||
const toClientSafeResource = (
|
||||
current: CurrentResourceUsage,
|
||||
): ClientCurrentResourceUsage => {
|
||||
return {
|
||||
currentUsage: numberOrBigIntToNumberOrString(current.CurrentUsage),
|
||||
requestTotal: numberOrBigIntToNumberOrString(current.RequestTotal),
|
||||
limitTotal: numberOrBigIntToNumberOrString(current.LimitTotal),
|
||||
};
|
||||
};
|
||||
|
||||
const toClientSafeContainer = (
|
||||
container: ContainerStatus,
|
||||
): ClientContainerStatus => {
|
||||
return {
|
||||
container: container.Container,
|
||||
cpuUsage: toClientSafeResource(container.CPUUsage),
|
||||
memoryUsage: toClientSafeResource(container.MemoryUsage),
|
||||
};
|
||||
};
|
||||
|
||||
const toClientSafePodMetrics = (
|
||||
podMetrics: PodStatus[][],
|
||||
): ClientPodStatus[] => {
|
||||
return podMetrics.flat().map((pd: PodStatus): ClientPodStatus => {
|
||||
return {
|
||||
pod: pd.Pod,
|
||||
memory: toClientSafeResource(pd.Memory),
|
||||
cpu: toClientSafeResource(pd.CPU),
|
||||
containers: pd.Containers.map(toClientSafeContainer),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export class KubernetesFanOutHandler {
|
||||
private readonly logger: Logger;
|
||||
private readonly fetcher: KubernetesFetcher;
|
||||
@@ -162,10 +216,36 @@ export class KubernetesFanOutHandler {
|
||||
customResources: this.customResources,
|
||||
})
|
||||
.then(result => {
|
||||
if (clusterDetailsItem.skipMetricsLookup) {
|
||||
return Promise.all([
|
||||
Promise.resolve(result),
|
||||
Promise.resolve([]),
|
||||
]);
|
||||
}
|
||||
// TODO refactor, extract as method
|
||||
const namespaces: Set<string> = new Set<string>(
|
||||
result.responses
|
||||
.filter(isPodFetchResponse)
|
||||
.flatMap(r => r.resources)
|
||||
.map(p => p.metadata?.namespace)
|
||||
.filter(isString),
|
||||
);
|
||||
|
||||
const podMetrics = Array.from(namespaces).map(ns =>
|
||||
this.fetcher.fetchPodMetricsByNamespace(clusterDetailsItem, ns),
|
||||
);
|
||||
|
||||
return Promise.all([
|
||||
Promise.resolve(result),
|
||||
Promise.all(podMetrics),
|
||||
]);
|
||||
})
|
||||
.then(([result, metrics]) => {
|
||||
const objects: ClusterObjects = {
|
||||
cluster: {
|
||||
name: clusterDetailsItem.name,
|
||||
},
|
||||
podMetrics: toClientSafePodMetrics(metrics),
|
||||
resources: result.responses,
|
||||
errors: result.errors,
|
||||
};
|
||||
|
||||
@@ -106,6 +106,7 @@ describe('KubernetesFetcher', () => {
|
||||
'v1',
|
||||
'pods',
|
||||
'',
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
'backstage.io/kubernetes-id=some-service',
|
||||
@@ -116,6 +117,7 @@ describe('KubernetesFetcher', () => {
|
||||
'v1',
|
||||
'services',
|
||||
'',
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
'backstage.io/kubernetes-id=some-service',
|
||||
@@ -197,6 +199,7 @@ describe('KubernetesFetcher', () => {
|
||||
'v1',
|
||||
'pods',
|
||||
'',
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
'backstage.io/kubernetes-id=some-service',
|
||||
@@ -207,6 +210,7 @@ describe('KubernetesFetcher', () => {
|
||||
'v1',
|
||||
'services',
|
||||
'',
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
'backstage.io/kubernetes-id=some-service',
|
||||
@@ -316,6 +320,7 @@ describe('KubernetesFetcher', () => {
|
||||
'v1',
|
||||
'pods',
|
||||
'',
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
'backstage.io/kubernetes-id=some-service',
|
||||
@@ -326,6 +331,7 @@ describe('KubernetesFetcher', () => {
|
||||
'v1',
|
||||
'services',
|
||||
'',
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
'backstage.io/kubernetes-id=some-service',
|
||||
@@ -336,6 +342,7 @@ describe('KubernetesFetcher', () => {
|
||||
'v2',
|
||||
'things',
|
||||
'',
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
'backstage.io/kubernetes-id=some-service',
|
||||
|
||||
@@ -14,13 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AppsV1Api,
|
||||
AutoscalingV1Api,
|
||||
BatchV1beta1Api,
|
||||
CoreV1Api,
|
||||
NetworkingV1beta1Api,
|
||||
} from '@kubernetes/client-node';
|
||||
import { CoreV1Api, topPods } from '@kubernetes/client-node';
|
||||
import lodash, { Dictionary } from 'lodash';
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
@@ -37,13 +31,10 @@ import {
|
||||
KubernetesErrorTypes,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
import { KubernetesClientProvider } from './KubernetesClientProvider';
|
||||
import { PodStatus } from '@kubernetes/client-node/dist/top';
|
||||
|
||||
export interface Clients {
|
||||
core: CoreV1Api;
|
||||
apps: AppsV1Api;
|
||||
autoscaling: AutoscalingV1Api;
|
||||
batch: BatchV1beta1Api;
|
||||
networkingBeta1: NetworkingV1beta1Api;
|
||||
}
|
||||
|
||||
export interface KubernetesClientBasedFetcherOptions {
|
||||
@@ -112,10 +103,26 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
return Promise.all(fetchResults).then(fetchResultsToResponseWrapper);
|
||||
}
|
||||
|
||||
fetchPodMetricsByNamespace(
|
||||
clusterDetails: ClusterDetails,
|
||||
namespace: string,
|
||||
): Promise<PodStatus[]> {
|
||||
const metricsClient =
|
||||
this.kubernetesClientProvider.getMetricsClient(clusterDetails);
|
||||
const coreApi =
|
||||
this.kubernetesClientProvider.getCoreClientByClusterDetails(
|
||||
clusterDetails,
|
||||
);
|
||||
|
||||
return topPods(coreApi, metricsClient, namespace);
|
||||
}
|
||||
|
||||
private captureKubernetesErrorsRethrowOthers(e: any): KubernetesFetchError {
|
||||
if (e.response && e.response.statusCode) {
|
||||
this.logger.info(
|
||||
`statusCode=${e.response.statusCode} for resource ${e.response.request.uri.pathname}`,
|
||||
this.logger.warn(
|
||||
`statusCode=${e.response.statusCode} for resource ${
|
||||
e.response.request.uri.pathname
|
||||
} body=[${JSON.stringify(e.response.body)}]`,
|
||||
);
|
||||
return {
|
||||
errorType: statusCodeToErrorType(e.response.statusCode),
|
||||
@@ -145,6 +152,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
resource.apiVersion,
|
||||
resource.plural,
|
||||
'',
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
labelSelector,
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
KubernetesRequestBody,
|
||||
ObjectsByEntityResponse,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
import { PodStatus } from '@kubernetes/client-node/dist/top';
|
||||
|
||||
export interface ObjectFetchParams {
|
||||
serviceId: string;
|
||||
@@ -40,6 +41,10 @@ export interface KubernetesFetcher {
|
||||
fetchObjectsForService(
|
||||
params: ObjectFetchParams,
|
||||
): Promise<FetchResponseWrapper>;
|
||||
fetchPodMetricsByNamespace(
|
||||
clusterDetails: ClusterDetails,
|
||||
namespace: string,
|
||||
): Promise<PodStatus[]>;
|
||||
}
|
||||
|
||||
export interface FetchResponseWrapper {
|
||||
@@ -93,6 +98,11 @@ export interface ClusterDetails {
|
||||
authProvider: string;
|
||||
serviceAccountToken?: string | undefined;
|
||||
skipTLSVerify?: boolean;
|
||||
/**
|
||||
* Whether to skip the lookup to the metrics server to retrieve pod resource usage.
|
||||
* It is not guaranteed that the Kubernetes distro has the metrics server installed.
|
||||
*/
|
||||
skipMetricsLookup?: boolean;
|
||||
caData?: string | undefined;
|
||||
/**
|
||||
* Specifies the link to the Kubernetes dashboard managing this cluster.
|
||||
|
||||
Reference in New Issue
Block a user