clusters in app-config support caFile
Signed-off-by: Jamie Klassen <jklassen@vmware.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user