feat: add gke cluster locator resource label filtering (#10908)

* feat: add gke cluster locator resource label filtering

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* test: add gke label filter tests

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* docs: add gke label filtering changeset

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* style: prettier

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* docs: add gke label matching docs

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* style: prettier

Signed-off-by: Matthew Clarke <mclarke@spotify.com>
This commit is contained in:
Matthew Clarke
2022-04-20 13:36:54 -04:00
committed by GitHub
parent 40429e9695
commit b9f7ffb162
4 changed files with 169 additions and 22 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes-backend': patch
---
Add filtering to GKE cluster locator
+15 -4
View File
@@ -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,
@@ -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');
@@ -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(