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:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-backend': patch
|
||||
---
|
||||
|
||||
Add filtering to GKE cluster locator
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user