clusters in app-config support caFile

Signed-off-by: Jamie Klassen <jklassen@vmware.com>
This commit is contained in:
Jamie Klassen
2022-12-02 18:23:18 -05:00
parent ea4192ff68
commit 22e20b3a59
12 changed files with 78 additions and 21 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes-backend': patch
---
Clusters declared in the app-config can now have their CA configured via a local filesystem path using the `caFile` property.
+11 -2
View File
@@ -31,6 +31,7 @@ kubernetes:
dashboardUrl: http://127.0.0.1:64713 # url copied from running the command: minikube service kubernetes-dashboard -n kubernetes-dashboard
dashboardApp: standard
caData: ${K8S_CONFIG_CA_DATA}
caFile: '' # local path to CA file
customResources:
- group: 'argoproj.io'
apiVersion: 'v1alpha1'
@@ -248,8 +249,8 @@ kubernetes:
##### `clusters.\*.caData` (optional)
Base64-encoded certificate authority bundle in PEM format. The Kubernetes client
will verify that TLS certificate presented by the API server is signed by this
CA.
will verify that the TLS certificate presented by the API server is signed by
this CA.
This value could be obtained via inspecting the kubeconfig file (usually
at `~/.kube/config`) under `clusters[*].cluster.certificate-authority-data`. For
@@ -265,6 +266,14 @@ See also
https://cloud.google.com/kubernetes-engine/docs/how-to/api-server-authentication#environments-without-gcloud
for complete docs about GKE without `gcloud`.
##### `clusters.\*.caFile` (optional)
Filesystem path (on the host where the Backstage process is running) to a
certificate authority bundle in PEM format. The Kubernetes client will verify
that the TLS certificate presented by the API server is signed by this CA. Note
that only clusters defined in the app-config via the [`config`](#config)
cluster locator method can be configured in this way.
##### `clusters.\*.customResources` (optional)
Configures which [custom resources][3] to look for when returning an entity's
+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');
});
});
@@ -31,12 +31,13 @@ import { ClusterDetails } from '../types/types';
*/
export class KubernetesClientProvider {
// visible for testing
getKubeConfig(clusterDetails: ClusterDetails) {
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
@@ -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
+2 -1
View File
@@ -6742,6 +6742,7 @@ __metadata:
http-proxy-middleware: ^2.0.6
lodash: ^4.17.21
luxon: ^3.0.0
mock-fs: ^5.2.0
morgan: ^1.10.0
msw: ^0.49.0
node-fetch: ^2.6.7
@@ -29069,7 +29070,7 @@ __metadata:
languageName: node
linkType: hard
"mock-fs@npm:^5.1.0, mock-fs@npm:^5.1.1":
"mock-fs@npm:^5.1.0, mock-fs@npm:^5.1.1, mock-fs@npm:^5.2.0":
version: 5.2.0
resolution: "mock-fs@npm:5.2.0"
checksum: c25835247bd26fa4e0189addd61f98973f61a72741e4d2a5694b143a2069b84978443a7ac0fdb1a71aead99273ec22ff4e9c968de11bbd076db020264c5b8312