From b9f7ffb162b6d9ed0d982c972011816e8bd6f981 Mon Sep 17 00:00:00 2001 From: Matthew Clarke Date: Wed, 20 Apr 2022 13:36:54 -0400 Subject: [PATCH] feat: add gke cluster locator resource label filtering (#10908) * feat: add gke cluster locator resource label filtering Signed-off-by: Matthew Clarke * test: add gke label filter tests Signed-off-by: Matthew Clarke * docs: add gke label filtering changeset Signed-off-by: Matthew Clarke * style: prettier Signed-off-by: Matthew Clarke * docs: add gke label matching docs Signed-off-by: Matthew Clarke * style: prettier Signed-off-by: Matthew Clarke --- .changeset/eighty-chicken-retire.md | 5 + docs/features/kubernetes/configuration.md | 19 ++- .../cluster-locator/GkeClusterLocator.test.ts | 109 ++++++++++++++++++ .../src/cluster-locator/GkeClusterLocator.ts | 58 +++++++--- 4 files changed, 169 insertions(+), 22 deletions(-) create mode 100644 .changeset/eighty-chicken-retire.md diff --git a/.changeset/eighty-chicken-retire.md b/.changeset/eighty-chicken-retire.md new file mode 100644 index 0000000000..f6d66f5eba --- /dev/null +++ b/.changeset/eighty-chicken-retire.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Add filtering to GKE cluster locator diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 78f386ae70..cb30451c7d 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -228,7 +228,13 @@ For example: ```yaml - type: 'gke' projectId: 'gke-clusters' - region: 'europe-west1' + region: 'europe-west1' # optional + skipTLSVerify: false # optional + skipMetricsLookup: false # optional + exposeDashboard: false # optional + matchingResourceLabels: # optional + - key: 'environment' + value: 'production' ``` Will configure the Kubernetes plugin to connect to all GKE clusters in the @@ -247,17 +253,17 @@ The Google Cloud project to look for Kubernetes clusters in. The Google Cloud region to look for Kubernetes clusters in. Defaults to all regions. -##### `skipTLSVerify` +##### `skipTLSVerify` (optional) This determines whether the Kubernetes client verifies the TLS certificate presented by the API server. Defaults to `false`. -##### `skipMetricsLookup` +##### `skipMetricsLookup` (optional) This determines whether the Kubernetes client looks up resource metrics CPU/Memory for pods returned by the API server. Defaults to `false`. -##### `exposeDashboard` +##### `exposeDashboard` (optional) This determines whether the `dashboardApp` and `dashboardParameters` should be automatically configured in order to expose the GKE dashboard from the @@ -265,6 +271,11 @@ Kubernetes plugin. Defaults to `false`. +##### `matchingResourceLabels` (optional) + +Array of key value labels used to filter out clusters which don't have the matching +[resource labels](https://cloud.google.com/resource-manager/docs/creating-managing-labels). + #### Custom `KubernetesClustersSupplier` If the configuration-based cluster locators do not work for your use-case, diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts index 6bef8e0009..783ec16904 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts @@ -202,6 +202,115 @@ describe('GkeClusterLocator', () => { parent: 'projects/some-project/locations/some-region', }); }); + it('dont filter out clusters when no label matcher provided', async () => { + mockedListClusters.mockReturnValueOnce([ + { + clusters: [ + { + name: 'some-cluster', + endpoint: '1.2.3.4', + resourceLabels: { + foo: 'bar', + }, + }, + { + name: 'some-other-cluster', + endpoint: '6.7.8.9', + resourceLabels: { + something: 'other', + }, + }, + ], + }, + ]); + + const config: Config = new ConfigReader({ + type: 'gke', + projectId: 'some-project', + region: 'some-region', + }); + + const sut = GkeClusterLocator.fromConfigWithClient(config, { + listClusters: mockedListClusters, + } as any); + + const result = await sut.getClusters(); + + expect(result).toStrictEqual([ + { + authProvider: 'google', + name: 'some-cluster', + url: 'https://1.2.3.4', + skipTLSVerify: false, + skipMetricsLookup: false, + }, + { + authProvider: 'google', + name: 'some-other-cluster', + url: 'https://6.7.8.9', + skipTLSVerify: false, + skipMetricsLookup: false, + }, + ]); + expect(mockedListClusters).toBeCalledTimes(1); + expect(mockedListClusters).toHaveBeenCalledWith({ + parent: 'projects/some-project/locations/some-region', + }); + }); + it('filter out clusters without matching resource labels', async () => { + mockedListClusters.mockReturnValueOnce([ + { + clusters: [ + { + name: 'some-cluster', + endpoint: '1.2.3.4', + resourceLabels: { + foo: 'bar', + }, + }, + { + name: 'some-other-cluster', + endpoint: '6.7.8.9', + resourceLabels: { + something: 'other', + }, + }, + ], + }, + ]); + + const config: Config = new ConfigReader({ + type: 'gke', + projectId: 'some-project', + region: 'some-region', + matchingResourceLabels: [ + { + key: 'foo', + value: 'bar', + }, + ], + }); + + const sut = GkeClusterLocator.fromConfigWithClient(config, { + listClusters: mockedListClusters, + } as any); + + const result = await sut.getClusters(); + + expect(result).toStrictEqual([ + { + authProvider: 'google', + name: 'some-cluster', + url: 'https://1.2.3.4', + skipTLSVerify: false, + skipMetricsLookup: false, + }, + ]); + expect(mockedListClusters).toBeCalledTimes(1); + expect(mockedListClusters).toHaveBeenCalledWith({ + parent: 'projects/some-project/locations/some-region', + }); + }); it('Handle errors gracefully', async () => { mockedListClusters.mockImplementation(() => { throw new Error('some error'); diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts index 0eb25c2acd..7a81b98032 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts @@ -25,12 +25,18 @@ import { KubernetesClustersSupplier, } from '../types/types'; +interface MatchResourceLabelEntry { + key: string; + value: string; +} + type GkeClusterLocatorOptions = { projectId: string; region?: string; skipTLSVerify?: boolean; skipMetricsLookup?: boolean; exposeDashboard?: boolean; + matchingResourceLabels?: MatchResourceLabelEntry[]; }; export class GkeClusterLocator implements KubernetesClustersSupplier { @@ -44,8 +50,13 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { static fromConfigWithClient( config: Config, client: container.v1.ClusterManagerClient, - refreshInterval: Duration | undefined = undefined, + refreshInterval?: Duration, ): GkeClusterLocator { + const matchingResourceLabels: MatchResourceLabelEntry[] = + config.getOptionalConfigArray('matchingResourceLabels')?.map(mrl => { + return { key: mrl.getString('key'), value: mrl.getString('value') }; + }) ?? []; + const options = { projectId: config.getString('projectId'), region: config.getOptionalString('region') ?? '-', @@ -53,6 +64,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { skipMetricsLookup: config.getOptionalBoolean('skipMetricsLookup') ?? false, exposeDashboard: config.getOptionalBoolean('exposeDashboard') ?? false, + matchingResourceLabels, }; const gkeClusterLocator = new GkeClusterLocator(options, client); if (refreshInterval) { @@ -91,6 +103,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { skipTLSVerify, skipMetricsLookup, exposeDashboard, + matchingResourceLabels, } = this.options; const request = { parent: `projects/${projectId}/locations/${region}`, @@ -98,24 +111,33 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { try { const [response] = await this.client.listClusters(request); - this.clusterDetails = (response.clusters ?? []).map(r => ({ - // TODO filter out clusters which don't have name or endpoint - name: r.name ?? 'unknown', - url: `https://${r.endpoint ?? ''}`, - authProvider: 'google', - skipTLSVerify, - skipMetricsLookup, - ...(exposeDashboard - ? { - dashboardApp: 'gke', - dashboardParameters: { - projectId, - region, - clusterName: r.name, - }, + this.clusterDetails = (response.clusters ?? []) + .filter(r => { + return matchingResourceLabels?.every(mrl => { + if (!r.resourceLabels) { + return false; } - : {}), - })); + return r.resourceLabels[mrl.key] === mrl.value; + }); + }) + .map(r => ({ + // TODO filter out clusters which don't have name or endpoint + name: r.name ?? 'unknown', + url: `https://${r.endpoint ?? ''}`, + authProvider: 'google', + skipTLSVerify, + skipMetricsLookup, + ...(exposeDashboard + ? { + dashboardApp: 'gke', + dashboardParameters: { + projectId, + region, + clusterName: r.name, + }, + } + : {}), + })); this.hasClusterDetails = true; } catch (e) { throw new ForwardedError(