Merge pull request #15005 from jamieklassen/k8s-cluster-cafile
[kubernetes-backend] support caFile on clusters defined in app-config
This commit is contained in:
@@ -78,6 +78,8 @@ export interface ClusterDetails {
|
||||
authProvider: string;
|
||||
// (undocumented)
|
||||
caData?: string | undefined;
|
||||
// (undocumented)
|
||||
caFile?: string | undefined;
|
||||
customResources?: CustomResourceMatcher[];
|
||||
dashboardApp?: string;
|
||||
dashboardParameters?: JsonObject;
|
||||
|
||||
+4
@@ -54,6 +54,10 @@ export interface Config {
|
||||
skipTLSVerify?: boolean;
|
||||
/** @visibility frontend */
|
||||
skipMetricsLookup?: boolean;
|
||||
/** @visibility secret */
|
||||
caData?: string;
|
||||
/** @visibility secret */
|
||||
caFile?: string;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"@types/aws4": "^1.5.1",
|
||||
"@types/http-proxy-middleware": "^0.19.3",
|
||||
"aws-sdk-mock": "^5.2.1",
|
||||
"mock-fs": "^5.2.0",
|
||||
"msw": "^0.49.0",
|
||||
"supertest": "^6.1.3"
|
||||
},
|
||||
|
||||
@@ -55,6 +55,7 @@ describe('ConfigClusterLocator', () => {
|
||||
skipMetricsLookup: false,
|
||||
skipTLSVerify: false,
|
||||
caData: undefined,
|
||||
caFile: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -95,6 +96,7 @@ describe('ConfigClusterLocator', () => {
|
||||
skipTLSVerify: false,
|
||||
skipMetricsLookup: true,
|
||||
caData: undefined,
|
||||
caFile: undefined,
|
||||
},
|
||||
{
|
||||
name: 'cluster2',
|
||||
@@ -104,6 +106,7 @@ describe('ConfigClusterLocator', () => {
|
||||
skipTLSVerify: true,
|
||||
skipMetricsLookup: false,
|
||||
caData: undefined,
|
||||
caFile: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -151,6 +154,7 @@ describe('ConfigClusterLocator', () => {
|
||||
skipTLSVerify: false,
|
||||
skipMetricsLookup: false,
|
||||
caData: undefined,
|
||||
caFile: undefined,
|
||||
},
|
||||
{
|
||||
assumeRole: 'SomeRole',
|
||||
@@ -162,6 +166,7 @@ describe('ConfigClusterLocator', () => {
|
||||
skipTLSVerify: true,
|
||||
skipMetricsLookup: false,
|
||||
caData: undefined,
|
||||
caFile: undefined,
|
||||
},
|
||||
{
|
||||
assumeRole: 'SomeRole',
|
||||
@@ -173,6 +178,7 @@ describe('ConfigClusterLocator', () => {
|
||||
skipTLSVerify: true,
|
||||
skipMetricsLookup: false,
|
||||
caData: undefined,
|
||||
caFile: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -207,6 +213,7 @@ describe('ConfigClusterLocator', () => {
|
||||
skipMetricsLookup: false,
|
||||
skipTLSVerify: false,
|
||||
caData: undefined,
|
||||
caFile: undefined,
|
||||
dashboardApp: 'gke',
|
||||
dashboardParameters: {
|
||||
projectId: 'some-project',
|
||||
@@ -243,6 +250,7 @@ describe('ConfigClusterLocator', () => {
|
||||
skipMetricsLookup: false,
|
||||
skipTLSVerify: false,
|
||||
caData: undefined,
|
||||
caFile: undefined,
|
||||
dashboardApp: 'standard',
|
||||
dashboardUrl: 'http://someurl',
|
||||
},
|
||||
|
||||
@@ -37,6 +37,7 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier {
|
||||
skipTLSVerify: c.getOptionalBoolean('skipTLSVerify') ?? false,
|
||||
skipMetricsLookup: c.getOptionalBoolean('skipMetricsLookup') ?? false,
|
||||
caData: c.getOptionalString('caData'),
|
||||
caFile: c.getOptionalString('caFile'),
|
||||
authProvider: authProvider,
|
||||
};
|
||||
const dashboardUrl = c.getOptionalString('dashboardUrl');
|
||||
|
||||
@@ -60,6 +60,7 @@ describe('getCombinedClusterSupplier', () => {
|
||||
skipMetricsLookup: false,
|
||||
skipTLSVerify: false,
|
||||
caData: undefined,
|
||||
caFile: undefined,
|
||||
},
|
||||
{
|
||||
name: 'cluster2',
|
||||
@@ -69,6 +70,7 @@ describe('getCombinedClusterSupplier', () => {
|
||||
skipMetricsLookup: false,
|
||||
skipTLSVerify: false,
|
||||
caData: undefined,
|
||||
caFile: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -16,26 +16,29 @@
|
||||
|
||||
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', async () => {
|
||||
it('can get core client by cluster details', () => {
|
||||
const sut = new KubernetesClientProvider();
|
||||
const getKubeConfig = jest.spyOn(sut, 'getKubeConfig');
|
||||
|
||||
const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({}));
|
||||
|
||||
sut.getKubeConfig = mockGetKubeConfig;
|
||||
|
||||
const result = sut.getCoreClientByClusterDetails({
|
||||
const clusterDetails: ClusterDetails = {
|
||||
name: 'cluster-name',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
authProvider: 'serviceAccount',
|
||||
skipTLSVerify: false,
|
||||
});
|
||||
};
|
||||
const result = sut.getCoreClientByClusterDetails(clusterDetails);
|
||||
|
||||
expect(result.basePath).toBe('http://localhost:9999');
|
||||
// These fields aren't on the type but are there
|
||||
@@ -44,23 +47,21 @@ describe('KubernetesClientProvider', () => {
|
||||
expect(auth.clusters[0].name).toBe('cluster-name');
|
||||
expect(auth.clusters[0].skipTLSVerify).toBe(false);
|
||||
|
||||
expect(mockGetKubeConfig.mock.calls.length).toBe(1);
|
||||
expect(getKubeConfig).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('can get custom objects client by cluster details', async () => {
|
||||
it('can get custom objects client by cluster details', () => {
|
||||
const sut = new KubernetesClientProvider();
|
||||
const getKubeConfig = jest.spyOn(sut, 'getKubeConfig');
|
||||
|
||||
const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({}));
|
||||
|
||||
sut.getKubeConfig = mockGetKubeConfig;
|
||||
|
||||
const result = sut.getCustomObjectsClient({
|
||||
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
|
||||
@@ -68,6 +69,27 @@ describe('KubernetesClientProvider', () => {
|
||||
expect(auth.users[0].token).toBe('TOKEN');
|
||||
expect(auth.clusters[0].name).toBe('cluster-name');
|
||||
|
||||
expect(mockGetKubeConfig.mock.calls.length).toBe(1);
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,10 +15,13 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
Cluster,
|
||||
Context,
|
||||
CoreV1Api,
|
||||
CustomObjectsApi,
|
||||
KubeConfig,
|
||||
Metrics,
|
||||
CustomObjectsApi,
|
||||
User,
|
||||
} from '@kubernetes/client-node';
|
||||
import { ClusterDetails } from '../types/types';
|
||||
|
||||
@@ -28,27 +31,28 @@ import { ClusterDetails } from '../types/types';
|
||||
*/
|
||||
export class KubernetesClientProvider {
|
||||
// visible for testing
|
||||
getKubeConfig(clusterDetails: ClusterDetails) {
|
||||
const cluster = {
|
||||
getKubeConfig(clusterDetails: ClusterDetails): KubeConfig {
|
||||
const cluster: Cluster = {
|
||||
name: clusterDetails.name,
|
||||
server: clusterDetails.url,
|
||||
skipTLSVerify: clusterDetails.skipTLSVerify,
|
||||
skipTLSVerify: clusterDetails.skipTLSVerify || false,
|
||||
caData: clusterDetails.caData,
|
||||
caFile: clusterDetails.caFile,
|
||||
};
|
||||
|
||||
// TODO configure
|
||||
const user = {
|
||||
const user: User = {
|
||||
name: 'backstage',
|
||||
token: clusterDetails.serviceAccountToken,
|
||||
};
|
||||
|
||||
const context = {
|
||||
const context: Context = {
|
||||
name: `${clusterDetails.name}`,
|
||||
user: user.name,
|
||||
cluster: cluster.name,
|
||||
};
|
||||
|
||||
const kc = new KubeConfig();
|
||||
const kc: KubeConfig = new KubeConfig();
|
||||
if (clusterDetails.serviceAccountToken) {
|
||||
kc.loadFromOptions({
|
||||
clusters: [cluster],
|
||||
@@ -63,19 +67,19 @@ export class KubernetesClientProvider {
|
||||
return kc;
|
||||
}
|
||||
|
||||
getCoreClientByClusterDetails(clusterDetails: ClusterDetails) {
|
||||
getCoreClientByClusterDetails(clusterDetails: ClusterDetails): CoreV1Api {
|
||||
const kc = this.getKubeConfig(clusterDetails);
|
||||
|
||||
return kc.makeApiClient(CoreV1Api);
|
||||
}
|
||||
|
||||
getMetricsClient(clusterDetails: ClusterDetails) {
|
||||
getMetricsClient(clusterDetails: ClusterDetails): Metrics {
|
||||
const kc = this.getKubeConfig(clusterDetails);
|
||||
|
||||
return new Metrics(kc);
|
||||
}
|
||||
|
||||
getCustomObjectsClient(clusterDetails: ClusterDetails) {
|
||||
getCustomObjectsClient(clusterDetails: ClusterDetails): CustomObjectsApi {
|
||||
const kc = this.getKubeConfig(clusterDetails);
|
||||
|
||||
return kc.makeApiClient(CustomObjectsApi);
|
||||
|
||||
@@ -167,6 +167,7 @@ export interface ClusterDetails {
|
||||
*/
|
||||
skipMetricsLookup?: boolean;
|
||||
caData?: string | undefined;
|
||||
caFile?: string | undefined;
|
||||
/**
|
||||
* Specifies the link to the Kubernetes dashboard managing this cluster.
|
||||
* @remarks
|
||||
|
||||
Reference in New Issue
Block a user