Merge pull request #15005 from jamieklassen/k8s-cluster-cafile

[kubernetes-backend] support caFile on clusters defined in app-config
This commit is contained in:
Fredrik Adelöw
2022-12-12 11:46:57 +01:00
committed by GitHub
12 changed files with 90 additions and 30 deletions
+2
View File
@@ -78,6 +78,8 @@ export interface ClusterDetails {
authProvider: string;
// (undocumented)
caData?: string | undefined;
// (undocumented)
caFile?: string | undefined;
customResources?: CustomResourceMatcher[];
dashboardApp?: string;
dashboardParameters?: JsonObject;
+4
View File
@@ -54,6 +54,10 @@ export interface Config {
skipTLSVerify?: boolean;
/** @visibility frontend */
skipMetricsLookup?: boolean;
/** @visibility secret */
caData?: string;
/** @visibility secret */
caFile?: string;
}>;
}
| {
+1
View File
@@ -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