node-fetch-based implementation for pod metrics
which eliminates the need for the KubernetesClientProvider Signed-off-by: Jamie Klassen <jklassen@vmware.com>
This commit is contained in:
@@ -5,21 +5,17 @@
|
||||
```ts
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { Config } from '@backstage/config';
|
||||
import { CoreV1Api } from '@kubernetes/client-node';
|
||||
import { Credentials } from 'aws-sdk';
|
||||
import { CustomObjectsApi } from '@kubernetes/client-node';
|
||||
import type { CustomResourceMatcher } from '@backstage/plugin-kubernetes-common';
|
||||
import { Duration } from 'luxon';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import express from 'express';
|
||||
import type { FetchResponse } from '@backstage/plugin-kubernetes-common';
|
||||
import type { JsonObject } from '@backstage/types';
|
||||
import { KubeConfig } from '@kubernetes/client-node';
|
||||
import type { KubernetesFetchError } from '@backstage/plugin-kubernetes-common';
|
||||
import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
|
||||
import type { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
||||
import { Logger } from 'winston';
|
||||
import { Metrics } from '@kubernetes/client-node';
|
||||
import type { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import type { RequestHandler } from 'express';
|
||||
@@ -262,18 +258,6 @@ export type KubernetesBuilderReturn = Promise<{
|
||||
serviceLocator: KubernetesServiceLocator;
|
||||
}>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export class KubernetesClientProvider {
|
||||
// (undocumented)
|
||||
getCoreClientByClusterDetails(clusterDetails: ClusterDetails): CoreV1Api;
|
||||
// (undocumented)
|
||||
getCustomObjectsClient(clusterDetails: ClusterDetails): CustomObjectsApi;
|
||||
// (undocumented)
|
||||
getKubeConfig(clusterDetails: ClusterDetails): KubeConfig;
|
||||
// (undocumented)
|
||||
getMetricsClient(clusterDetails: ClusterDetails): Metrics;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export interface KubernetesClustersSupplier {
|
||||
getClusters(): Promise<ClusterDetails[]>;
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
ObjectsByEntityRequest,
|
||||
ServiceLocatorMethod,
|
||||
} from '../types/types';
|
||||
import { KubernetesClientProvider } from './KubernetesClientProvider';
|
||||
import {
|
||||
DEFAULT_OBJECTS,
|
||||
KubernetesFanOutHandler,
|
||||
@@ -211,7 +210,6 @@ export class KubernetesBuilder {
|
||||
|
||||
protected buildFetcher(): KubernetesFetcher {
|
||||
this.fetcher = new KubernetesClientBasedFetcher({
|
||||
kubernetesClientProvider: new KubernetesClientProvider(),
|
||||
logger: this.env.logger,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import '@backstage/backend-common';
|
||||
import { KubernetesClientProvider } from './KubernetesClientProvider';
|
||||
import { ClusterDetails } from '../types/types';
|
||||
import * as https from 'https';
|
||||
import mockFs from 'mock-fs';
|
||||
|
||||
describe('KubernetesClientProvider', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('can get core client by cluster details', () => {
|
||||
const sut = new KubernetesClientProvider();
|
||||
const getKubeConfig = jest.spyOn(sut, 'getKubeConfig');
|
||||
|
||||
const clusterDetails: ClusterDetails = {
|
||||
name: 'cluster-name',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
authProvider: 'serviceAccount',
|
||||
};
|
||||
const result = sut.getCoreClientByClusterDetails(clusterDetails);
|
||||
|
||||
expect(result.basePath).toBe('http://localhost:9999');
|
||||
// These fields aren't on the type but are there
|
||||
const auth = (result as any).authentications.default;
|
||||
expect(auth.users[0].token).toBe('TOKEN');
|
||||
expect(auth.clusters[0].name).toBe('cluster-name');
|
||||
expect(auth.clusters[0].skipTLSVerify).toBe(false);
|
||||
|
||||
expect(getKubeConfig).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('can get custom objects client by cluster details', () => {
|
||||
const sut = new KubernetesClientProvider();
|
||||
const getKubeConfig = jest.spyOn(sut, 'getKubeConfig');
|
||||
|
||||
const clusterDetails: ClusterDetails = {
|
||||
name: 'cluster-name',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
authProvider: 'serviceAccount',
|
||||
skipTLSVerify: false,
|
||||
};
|
||||
const result = sut.getCustomObjectsClient(clusterDetails);
|
||||
|
||||
expect(result.basePath).toBe('http://localhost:9999');
|
||||
// These fields aren't on the type but are there
|
||||
const auth = (result as any).authentications.default;
|
||||
expect(auth.users[0].token).toBe('TOKEN');
|
||||
expect(auth.clusters[0].name).toBe('cluster-name');
|
||||
|
||||
expect(getKubeConfig).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('respects caFile', async () => {
|
||||
mockFs({
|
||||
'/path/to/ca.crt': 'my-ca',
|
||||
});
|
||||
const clusterDetails: ClusterDetails = {
|
||||
name: 'cluster-name',
|
||||
url: 'https://localhost:9999',
|
||||
authProvider: 'serviceAccount',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
caFile: '/path/to/ca.crt',
|
||||
};
|
||||
const kubeConfig = new KubernetesClientProvider().getKubeConfig(
|
||||
clusterDetails,
|
||||
);
|
||||
|
||||
const options: https.RequestOptions = {};
|
||||
await kubeConfig.applytoHTTPSOptions(options);
|
||||
|
||||
expect(options.ca?.toString()).toEqual('my-ca');
|
||||
});
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Cluster,
|
||||
Context,
|
||||
CoreV1Api,
|
||||
CustomObjectsApi,
|
||||
KubeConfig,
|
||||
Metrics,
|
||||
User,
|
||||
} from '@kubernetes/client-node';
|
||||
import { ClusterDetails } from '../types/types';
|
||||
|
||||
/**
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export class KubernetesClientProvider {
|
||||
// visible for testing
|
||||
getKubeConfig(clusterDetails: ClusterDetails): KubeConfig {
|
||||
const cluster: Cluster = {
|
||||
name: clusterDetails.name,
|
||||
server: clusterDetails.url,
|
||||
skipTLSVerify: clusterDetails.skipTLSVerify || false,
|
||||
caData: clusterDetails.caData,
|
||||
caFile: clusterDetails.caFile,
|
||||
};
|
||||
|
||||
// TODO configure
|
||||
const user: User = {
|
||||
name: 'backstage',
|
||||
token: clusterDetails.serviceAccountToken,
|
||||
};
|
||||
|
||||
const context: Context = {
|
||||
name: `${clusterDetails.name}`,
|
||||
user: user.name,
|
||||
cluster: cluster.name,
|
||||
};
|
||||
|
||||
const kc: KubeConfig = new KubeConfig();
|
||||
if (clusterDetails.serviceAccountToken) {
|
||||
kc.loadFromOptions({
|
||||
clusters: [cluster],
|
||||
users: [user],
|
||||
contexts: [context],
|
||||
currentContext: context.name,
|
||||
});
|
||||
} else {
|
||||
kc.loadFromDefault();
|
||||
}
|
||||
|
||||
return kc;
|
||||
}
|
||||
|
||||
getCoreClientByClusterDetails(clusterDetails: ClusterDetails): CoreV1Api {
|
||||
const kc = this.getKubeConfig(clusterDetails);
|
||||
|
||||
return kc.makeApiClient(CoreV1Api);
|
||||
}
|
||||
|
||||
getMetricsClient(clusterDetails: ClusterDetails): Metrics {
|
||||
const kc = this.getKubeConfig(clusterDetails);
|
||||
|
||||
return new Metrics(kc);
|
||||
}
|
||||
|
||||
getCustomObjectsClient(clusterDetails: ClusterDetails): CustomObjectsApi {
|
||||
const kc = this.getKubeConfig(clusterDetails);
|
||||
|
||||
return kc.makeApiClient(CustomObjectsApi);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
|
||||
import { KubernetesClientProvider } from './KubernetesClientProvider';
|
||||
import { ObjectToFetch } from '../types/types';
|
||||
import {
|
||||
MockedRequest,
|
||||
@@ -167,7 +166,6 @@ describe('KubernetesFetcher', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new KubernetesClientBasedFetcher({
|
||||
kubernetesClientProvider: new KubernetesClientProvider(),
|
||||
logger,
|
||||
});
|
||||
});
|
||||
@@ -827,7 +825,6 @@ describe('KubernetesFetcher', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new KubernetesClientBasedFetcher({
|
||||
kubernetesClientProvider: new KubernetesClientProvider(),
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
import {
|
||||
Cluster,
|
||||
CoreV1Api,
|
||||
KubeConfig,
|
||||
Metrics,
|
||||
User,
|
||||
bufferFromFileOrString,
|
||||
topPods,
|
||||
@@ -27,9 +29,7 @@ import {
|
||||
ClusterDetails,
|
||||
FetchResponseWrapper,
|
||||
KubernetesFetcher,
|
||||
KubernetesObjectTypes,
|
||||
ObjectFetchParams,
|
||||
ObjectToFetch,
|
||||
} from '../types/types';
|
||||
import {
|
||||
FetchResponse,
|
||||
@@ -37,13 +37,11 @@ import {
|
||||
KubernetesErrorTypes,
|
||||
PodStatusFetchResponse,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
import { KubernetesClientProvider } from './KubernetesClientProvider';
|
||||
import fetch, { Headers, RequestInit } from 'node-fetch';
|
||||
import fetch, { RequestInit, Response } from 'node-fetch';
|
||||
import * as https from 'https';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
export interface KubernetesClientBasedFetcherOptions {
|
||||
kubernetesClientProvider: KubernetesClientProvider;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
@@ -81,14 +79,9 @@ const statusCodeToErrorType = (statusCode: number): KubernetesErrorTypes => {
|
||||
};
|
||||
|
||||
export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
private readonly kubernetesClientProvider: KubernetesClientProvider;
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor({
|
||||
kubernetesClientProvider,
|
||||
logger,
|
||||
}: KubernetesClientBasedFetcherOptions) {
|
||||
this.kubernetesClientProvider = kubernetesClientProvider;
|
||||
constructor({ logger }: KubernetesClientBasedFetcherOptions) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@@ -97,16 +90,27 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
): Promise<FetchResponseWrapper> {
|
||||
const fetchResults = Array.from(params.objectTypesToFetch)
|
||||
.concat(params.customResources)
|
||||
.map(toFetch => {
|
||||
return this.fetchResource(
|
||||
.map(({ objectType, group, apiVersion, plural }) =>
|
||||
this.fetchResource(
|
||||
params.clusterDetails,
|
||||
toFetch,
|
||||
group,
|
||||
apiVersion,
|
||||
plural,
|
||||
params.namespace,
|
||||
params.labelSelector ||
|
||||
`backstage.io/kubernetes-id=${params.serviceId}`,
|
||||
toFetch.objectType,
|
||||
params.namespace,
|
||||
).catch(this.captureKubernetesErrorsRethrowOthers.bind(this));
|
||||
});
|
||||
).then(
|
||||
(r: Response): Promise<FetchResult> =>
|
||||
r.ok
|
||||
? r.json().then(
|
||||
({ items }): FetchResponse => ({
|
||||
type: objectType,
|
||||
resources: items,
|
||||
}),
|
||||
)
|
||||
: this.handleUnsuccessfulResponse(r),
|
||||
),
|
||||
);
|
||||
|
||||
return Promise.all(fetchResults).then(fetchResultsToResponseWrapper);
|
||||
}
|
||||
@@ -115,51 +119,65 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
clusterDetails: ClusterDetails,
|
||||
namespaces: Set<string>,
|
||||
): Promise<FetchResponseWrapper> {
|
||||
const metricsClient =
|
||||
this.kubernetesClientProvider.getMetricsClient(clusterDetails);
|
||||
const coreApi =
|
||||
this.kubernetesClientProvider.getCoreClientByClusterDetails(
|
||||
clusterDetails,
|
||||
);
|
||||
|
||||
const fetchResults = Array.from(namespaces).map(ns =>
|
||||
topPods(coreApi, metricsClient, ns)
|
||||
.then(r => {
|
||||
return {
|
||||
const fetchResults = Array.from(namespaces).map(async ns => {
|
||||
const [podMetrics, podList] = await Promise.all([
|
||||
this.fetchResource(
|
||||
clusterDetails,
|
||||
'metrics.k8s.io',
|
||||
'v1beta1',
|
||||
'pods',
|
||||
ns,
|
||||
),
|
||||
this.fetchResource(clusterDetails, '', 'v1', 'pods', ns),
|
||||
]);
|
||||
if (podMetrics.ok && podList.ok) {
|
||||
return topPods(
|
||||
{
|
||||
listPodForAllNamespaces: () =>
|
||||
podList.json().then(b => ({ body: b })),
|
||||
} as unknown as CoreV1Api,
|
||||
{
|
||||
getPodMetrics: () => podMetrics.json(),
|
||||
} as unknown as Metrics,
|
||||
).then(
|
||||
(resources): PodStatusFetchResponse => ({
|
||||
type: 'podstatus',
|
||||
resources: r,
|
||||
} as PodStatusFetchResponse;
|
||||
})
|
||||
.catch(this.captureKubernetesErrorsRethrowOthers.bind(this)),
|
||||
);
|
||||
resources,
|
||||
}),
|
||||
);
|
||||
} else if (podMetrics.ok) {
|
||||
return this.handleUnsuccessfulResponse(podList);
|
||||
}
|
||||
return this.handleUnsuccessfulResponse(podMetrics);
|
||||
});
|
||||
|
||||
return Promise.all(fetchResults).then(fetchResultsToResponseWrapper);
|
||||
}
|
||||
|
||||
private captureKubernetesErrorsRethrowOthers(e: any): KubernetesFetchError {
|
||||
if (e.response && e.response.statusCode) {
|
||||
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),
|
||||
statusCode: e.response.statusCode,
|
||||
resourcePath: e.response.request.uri.pathname,
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
private async handleUnsuccessfulResponse(
|
||||
res: Response,
|
||||
): Promise<KubernetesFetchError> {
|
||||
const resourcePath = new URL(res.url).pathname;
|
||||
this.logger.warn(
|
||||
`statusCode=${
|
||||
res.status
|
||||
} for resource ${resourcePath} body=[${await res.text()}]`,
|
||||
);
|
||||
return {
|
||||
errorType: statusCodeToErrorType(res.status),
|
||||
statusCode: res.status,
|
||||
resourcePath,
|
||||
};
|
||||
}
|
||||
|
||||
private fetchResource(
|
||||
clusterDetails: ClusterDetails,
|
||||
resource: ObjectToFetch,
|
||||
labelSelector: string,
|
||||
objectType: KubernetesObjectTypes,
|
||||
group: string,
|
||||
apiVersion: string,
|
||||
plural: string,
|
||||
namespace?: string,
|
||||
): Promise<FetchResult> {
|
||||
const { group, apiVersion, plural } = resource;
|
||||
labelSelector?: string,
|
||||
): Promise<Response> {
|
||||
const encode = (s: string) => encodeURIComponent(s);
|
||||
let resourcePath = group
|
||||
? `/apis/${encode(group)}/${encode(apiVersion)}`
|
||||
@@ -169,71 +187,68 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
}
|
||||
resourcePath += `/${encode(plural)}`;
|
||||
|
||||
const headers: Headers = new Headers({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
const fetchOptions: RequestInit = {
|
||||
method: 'GET',
|
||||
};
|
||||
let token: Buffer | string;
|
||||
let url: URL;
|
||||
if (clusterDetails.serviceAccountToken) {
|
||||
url = new URL(`${clusterDetails.url}${resourcePath}`);
|
||||
const [url, requestInit]: [URL, RequestInit] =
|
||||
clusterDetails.serviceAccountToken
|
||||
? this.fetchArgsFromClusterDetails(clusterDetails)
|
||||
: this.fetchArgsInCluster();
|
||||
|
||||
if (url.protocol === 'https:') {
|
||||
fetchOptions.agent = new https.Agent({
|
||||
ca:
|
||||
bufferFromFileOrString(
|
||||
clusterDetails.caFile,
|
||||
clusterDetails.caData,
|
||||
) ?? undefined,
|
||||
rejectUnauthorized: !clusterDetails.skipTLSVerify,
|
||||
});
|
||||
}
|
||||
|
||||
token = clusterDetails.serviceAccountToken;
|
||||
} else {
|
||||
const kc = new KubeConfig();
|
||||
kc.loadFromCluster();
|
||||
// loadFromCluster never fails (unless an exception is thrown) and is
|
||||
// guaranteed to populate the cluster/user/context correctly
|
||||
const cluster = kc.getCurrentCluster() as Cluster;
|
||||
const user = kc.getCurrentUser() as User;
|
||||
url = new URL(`${cluster.server}${resourcePath}`);
|
||||
|
||||
if (url.protocol === 'https:') {
|
||||
fetchOptions.agent = new https.Agent({
|
||||
ca: fs.readFileSync(cluster.caFile as string),
|
||||
});
|
||||
}
|
||||
|
||||
token = fs.readFileSync(user.authProvider.config.tokenFile);
|
||||
url.pathname = resourcePath;
|
||||
if (labelSelector) {
|
||||
url.search = `labelSelector=${labelSelector}`;
|
||||
}
|
||||
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
fetchOptions.headers = headers;
|
||||
url.search = `labelSelector=${labelSelector}`;
|
||||
return fetch(url, requestInit);
|
||||
}
|
||||
|
||||
return fetch(url.toString(), fetchOptions).then(r => {
|
||||
return r.json().then(j => {
|
||||
if (r.ok) {
|
||||
return {
|
||||
type: objectType,
|
||||
resources: j.items,
|
||||
};
|
||||
}
|
||||
this.logger.warn(
|
||||
`statusCode=${
|
||||
r.status
|
||||
} for resource ${resourcePath} body=[${JSON.stringify(j)}]`,
|
||||
);
|
||||
return {
|
||||
errorType: statusCodeToErrorType(r.status),
|
||||
statusCode: r.status,
|
||||
resourcePath,
|
||||
};
|
||||
private fetchArgsFromClusterDetails(
|
||||
clusterDetails: ClusterDetails,
|
||||
): [URL, RequestInit] {
|
||||
const requestInit: RequestInit = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${clusterDetails.serviceAccountToken}`,
|
||||
},
|
||||
};
|
||||
|
||||
const url: URL = new URL(clusterDetails.url);
|
||||
if (url.protocol === 'https:') {
|
||||
requestInit.agent = new https.Agent({
|
||||
ca:
|
||||
bufferFromFileOrString(
|
||||
clusterDetails.caFile,
|
||||
clusterDetails.caData,
|
||||
) ?? undefined,
|
||||
rejectUnauthorized: !clusterDetails.skipTLSVerify,
|
||||
});
|
||||
});
|
||||
}
|
||||
return [url, requestInit];
|
||||
}
|
||||
private fetchArgsInCluster(): [URL, RequestInit] {
|
||||
const kc = new KubeConfig();
|
||||
kc.loadFromCluster();
|
||||
// loadFromCluster is guaranteed to populate the cluster/user/context
|
||||
const cluster = kc.getCurrentCluster() as Cluster;
|
||||
const user = kc.getCurrentUser() as User;
|
||||
|
||||
const token = fs.readFileSync(user.authProvider.config.tokenFile);
|
||||
|
||||
const requestInit: RequestInit = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
};
|
||||
|
||||
const url = new URL(cluster.server);
|
||||
if (url.protocol === 'https:') {
|
||||
requestInit.agent = new https.Agent({
|
||||
ca: fs.readFileSync(cluster.caFile as string),
|
||||
});
|
||||
}
|
||||
return [url, requestInit];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
export * from './KubernetesBuilder';
|
||||
export * from './KubernetesClientProvider';
|
||||
export { DEFAULT_OBJECTS } from './KubernetesFanOutHandler';
|
||||
export { HEADER_KUBERNETES_CLUSTER, KubernetesProxy } from './KubernetesProxy';
|
||||
export * from './router';
|
||||
|
||||
Reference in New Issue
Block a user