From d5d2c67ca88415caab943d73ebb28ddbf1ce7740 Mon Sep 17 00:00:00 2001 From: Tomasz Szuba Date: Wed, 13 Dec 2023 17:36:30 +0100 Subject: [PATCH] Add `authuser` param to GKE cluster link formatter in k8s plugin Signed-off-by: Tomasz Szuba --- .changeset/new-plants-sort.md | 8 + .../GkeClusterLinksFormatter.test.ts | 341 ++++++++++-------- .../formatters/GkeClusterLinksFormatter.ts | 8 + .../src/api/formatters/index.ts | 10 +- plugins/kubernetes/src/plugin.ts | 2 +- 5 files changed, 212 insertions(+), 157 deletions(-) create mode 100644 .changeset/new-plants-sort.md diff --git a/.changeset/new-plants-sort.md b/.changeset/new-plants-sort.md new file mode 100644 index 0000000000..00570e9482 --- /dev/null +++ b/.changeset/new-plants-sort.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-kubernetes-react': patch +'@backstage/plugin-kubernetes': patch +--- + +Add `authuser` search parameter to GKE cluster link formatter in k8s plugin + +Thanks to this, people with multiple simultaneously logged-in accounts to be automatically picked. diff --git a/plugins/kubernetes-react/src/api/formatters/GkeClusterLinksFormatter.test.ts b/plugins/kubernetes-react/src/api/formatters/GkeClusterLinksFormatter.test.ts index 250be75214..9b9e707a6c 100644 --- a/plugins/kubernetes-react/src/api/formatters/GkeClusterLinksFormatter.test.ts +++ b/plugins/kubernetes-react/src/api/formatters/GkeClusterLinksFormatter.test.ts @@ -17,25 +17,120 @@ import { GkeClusterLinksFormatter } from './GkeClusterLinksFormatter'; describe('clusterLinks - GKE formatter', () => { - const formatter = new GkeClusterLinksFormatter(); + describe('without googleAuthApi provided', () => { + const formatter = new GkeClusterLinksFormatter(undefined); - it('should provide a dashboardParameters in the options', async () => { - await expect( - formatter.formatClusterLink({ + it('should provide a dashboardParameters in the options', async () => { + await expect( + formatter.formatClusterLink({ + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }), + ).rejects.toThrow('GKE dashboard requires a dashboardParameters option'); + }); + it('should provide a projectId in the dashboardParameters options', async () => { + await expect( + formatter.formatClusterLink({ + dashboardParameters: { + region: 'us-east1-c', + clusterName: 'cluster-1', + }, + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }), + ).rejects.toThrow( + 'GKE dashboard requires a "projectId" of type string in the dashboardParameters option', + ); + }); + it('should provide a region in the dashboardParameters options', async () => { + await expect( + formatter.formatClusterLink({ + dashboardParameters: { + projectId: 'foobar-333614', + clusterName: 'cluster-1', + }, + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }), + ).rejects.toThrow( + 'GKE dashboard requires a "region" of type string in the dashboardParameters option', + ); + }); + it('should provide a clusterName in the dashboardParameters options', async () => { + await expect(() => + formatter.formatClusterLink({ + dashboardParameters: { + projectId: 'foobar-333614', + region: 'us-east1-c', + }, + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }), + ).rejects.toThrow( + 'GKE dashboard requires a "clusterName" of type string in the dashboardParameters option', + ); + }); + it('should return an url on the cluster when there is a namespace only', async () => { + const url = await formatter.formatClusterLink({ + dashboardParameters: { + projectId: 'foobar-333614', + region: 'us-east1-c', + clusterName: 'cluster-1', + }, + object: { + metadata: { + namespace: 'bar', + }, + }, + kind: 'foo', + }); + expect(url.href).toBe( + 'https://console.cloud.google.com/kubernetes/clusters/details/us-east1-c/cluster-1/details?project=foobar-333614', + ); + }); + it('should return an url on the cluster when the kind is not recognized', async () => { + const url = await formatter.formatClusterLink({ + dashboardParameters: { + projectId: 'foobar-333614', + region: 'us-east1-c', + clusterName: 'cluster-1', + }, object: { metadata: { name: 'foobar', namespace: 'bar', }, }, - kind: 'Deployment', - }), - ).rejects.toThrow('GKE dashboard requires a dashboardParameters option'); - }); - it('should provide a projectId in the dashboardParameters options', async () => { - await expect( - formatter.formatClusterLink({ + kind: 'UnknownKind', + }); + expect(url.href).toBe( + 'https://console.cloud.google.com/kubernetes/clusters/details/us-east1-c/cluster-1/details?project=foobar-333614', + ); + }); + it('should return an url on the deployment', async () => { + const url = await formatter.formatClusterLink({ dashboardParameters: { + projectId: 'foobar-333614', region: 'us-east1-c', clusterName: 'cluster-1', }, @@ -46,16 +141,17 @@ describe('clusterLinks - GKE formatter', () => { }, }, kind: 'Deployment', - }), - ).rejects.toThrow( - 'GKE dashboard requires a "projectId" of type string in the dashboardParameters option', - ); - }); - it('should provide a region in the dashboardParameters options', async () => { - await expect( - formatter.formatClusterLink({ + }); + expect(url.href).toBe( + 'https://console.cloud.google.com/kubernetes/deployment/us-east1-c/cluster-1/bar/foobar/overview?project=foobar-333614', + ); + }); + + it('should return an url on the service', async () => { + const url = await formatter.formatClusterLink({ dashboardParameters: { projectId: 'foobar-333614', + region: 'us-east1-c', clusterName: 'cluster-1', }, object: { @@ -64,18 +160,18 @@ describe('clusterLinks - GKE formatter', () => { namespace: 'bar', }, }, - kind: 'Deployment', - }), - ).rejects.toThrow( - 'GKE dashboard requires a "region" of type string in the dashboardParameters option', - ); - }); - it('should provide a clusterName in the dashboardParameters options', async () => { - await expect(() => - formatter.formatClusterLink({ + kind: 'Service', + }); + expect(url.href).toBe( + 'https://console.cloud.google.com/kubernetes/service/us-east1-c/cluster-1/bar/foobar/overview?project=foobar-333614', + ); + }); + it('should return an url on the pod', async () => { + const url = await formatter.formatClusterLink({ dashboardParameters: { projectId: 'foobar-333614', region: 'us-east1-c', + clusterName: 'cluster-1', }, object: { metadata: { @@ -83,14 +179,53 @@ describe('clusterLinks - GKE formatter', () => { namespace: 'bar', }, }, - kind: 'Deployment', - }), - ).rejects.toThrow( - 'GKE dashboard requires a "clusterName" of type string in the dashboardParameters option', - ); + kind: 'Pod', + }); + expect(url.href).toBe( + 'https://console.cloud.google.com/kubernetes/pod/us-east1-c/cluster-1/bar/foobar/details?project=foobar-333614', + ); + }); + it('should return an url on the ingress', async () => { + const url = await formatter.formatClusterLink({ + dashboardParameters: { + projectId: 'foobar-333614', + region: 'us-east1-c', + clusterName: 'cluster-1', + }, + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Ingress', + }); + expect(url.href).toBe( + 'https://console.cloud.google.com/kubernetes/ingress/us-east1-c/cluster-1/bar/foobar/details?project=foobar-333614', + ); + }); + it('should return an url on the deployment for a hpa', async () => { + const url = await formatter.formatClusterLink({ + dashboardParameters: { + projectId: 'foobar-333614', + region: 'us-east1-c', + clusterName: 'cluster-1', + }, + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'HorizontalPodAutoscaler', + }); + expect(url.href).toBe( + 'https://console.cloud.google.com/kubernetes/deployment/us-east1-c/cluster-1/bar/foobar/overview?project=foobar-333614', + ); + }); }); - it('should return an url on the cluster when there is a namespace only', async () => { - const url = await formatter.formatClusterLink({ + describe('with googleAuthApi provided', () => { + const object = { dashboardParameters: { projectId: 'foobar-333614', region: 'us-east1-c', @@ -102,124 +237,28 @@ describe('clusterLinks - GKE formatter', () => { }, }, kind: 'foo', + }; + it('should not fail, when undefined is returned as profile', async () => { + const formatter = new GkeClusterLinksFormatter({ + getProfile: async _ => undefined, + }); + const url = await formatter.formatClusterLink(object); + expect(url.searchParams.has('authuser')).toBeFalsy(); }); - expect(url.href).toBe( - 'https://console.cloud.google.com/kubernetes/clusters/details/us-east1-c/cluster-1/details?project=foobar-333614', - ); - }); - it('should return an url on the cluster when the kind is not recognizeed', async () => { - const url = await formatter.formatClusterLink({ - dashboardParameters: { - projectId: 'foobar-333614', - region: 'us-east1-c', - clusterName: 'cluster-1', - }, - object: { - metadata: { - name: 'foobar', - namespace: 'bar', - }, - }, - kind: 'UnknownKind', + it('should not fail, when profile returns no email', async () => { + const formatter = new GkeClusterLinksFormatter({ + getProfile: async _ => ({ email: undefined }), + }); + const url = await formatter.formatClusterLink(object); + expect(url.searchParams.has('authuser')).toBeFalsy(); }); - expect(url.href).toBe( - 'https://console.cloud.google.com/kubernetes/clusters/details/us-east1-c/cluster-1/details?project=foobar-333614', - ); - }); - it('should return an url on the deployment', async () => { - const url = await formatter.formatClusterLink({ - dashboardParameters: { - projectId: 'foobar-333614', - region: 'us-east1-c', - clusterName: 'cluster-1', - }, - object: { - metadata: { - name: 'foobar', - namespace: 'bar', - }, - }, - kind: 'Deployment', + it('should add authuser with email when provided', async () => { + const email = 'email@example.com'; + const formatter = new GkeClusterLinksFormatter({ + getProfile: async _ => ({ email }), + }); + const url = await formatter.formatClusterLink(object); + expect(url.searchParams.get('authuser')).toBe(email); }); - expect(url.href).toBe( - 'https://console.cloud.google.com/kubernetes/deployment/us-east1-c/cluster-1/bar/foobar/overview?project=foobar-333614', - ); - }); - - it('should return an url on the service', async () => { - const url = await formatter.formatClusterLink({ - dashboardParameters: { - projectId: 'foobar-333614', - region: 'us-east1-c', - clusterName: 'cluster-1', - }, - object: { - metadata: { - name: 'foobar', - namespace: 'bar', - }, - }, - kind: 'Service', - }); - expect(url.href).toBe( - 'https://console.cloud.google.com/kubernetes/service/us-east1-c/cluster-1/bar/foobar/overview?project=foobar-333614', - ); - }); - it('should return an url on the pod', async () => { - const url = await formatter.formatClusterLink({ - dashboardParameters: { - projectId: 'foobar-333614', - region: 'us-east1-c', - clusterName: 'cluster-1', - }, - object: { - metadata: { - name: 'foobar', - namespace: 'bar', - }, - }, - kind: 'Pod', - }); - expect(url.href).toBe( - 'https://console.cloud.google.com/kubernetes/pod/us-east1-c/cluster-1/bar/foobar/details?project=foobar-333614', - ); - }); - it('should return an url on the ingress', async () => { - const url = await formatter.formatClusterLink({ - dashboardParameters: { - projectId: 'foobar-333614', - region: 'us-east1-c', - clusterName: 'cluster-1', - }, - object: { - metadata: { - name: 'foobar', - namespace: 'bar', - }, - }, - kind: 'Ingress', - }); - expect(url.href).toBe( - 'https://console.cloud.google.com/kubernetes/ingress/us-east1-c/cluster-1/bar/foobar/details?project=foobar-333614', - ); - }); - it('should return an url on the deployment for a hpa', async () => { - const url = await formatter.formatClusterLink({ - dashboardParameters: { - projectId: 'foobar-333614', - region: 'us-east1-c', - clusterName: 'cluster-1', - }, - object: { - metadata: { - name: 'foobar', - namespace: 'bar', - }, - }, - kind: 'HorizontalPodAutoscaler', - }); - expect(url.href).toBe( - 'https://console.cloud.google.com/kubernetes/deployment/us-east1-c/cluster-1/bar/foobar/overview?project=foobar-333614', - ); }); }); diff --git a/plugins/kubernetes-react/src/api/formatters/GkeClusterLinksFormatter.ts b/plugins/kubernetes-react/src/api/formatters/GkeClusterLinksFormatter.ts index c8dec85f71..4bf3c28f45 100644 --- a/plugins/kubernetes-react/src/api/formatters/GkeClusterLinksFormatter.ts +++ b/plugins/kubernetes-react/src/api/formatters/GkeClusterLinksFormatter.ts @@ -17,6 +17,7 @@ import { ClusterLinksFormatter, ClusterLinksFormatterOptions, } from '../../types'; +import { ProfileInfoApi } from '@backstage/core-plugin-api'; const kindMappings: Record = { deployment: 'deployment', @@ -28,6 +29,7 @@ const kindMappings: Record = { /** @public */ export class GkeClusterLinksFormatter implements ClusterLinksFormatter { + constructor(private readonly googleAuthApi: ProfileInfoApi | undefined) {} async formatClusterLink(options: ClusterLinksFormatterOptions): Promise { if (!options.dashboardParameters) { throw new Error('GKE dashboard requires a dashboardParameters option'); @@ -68,6 +70,12 @@ export class GkeClusterLinksFormatter implements ClusterLinksFormatter { } const result = new URL(path, basePath); result.searchParams.set('project', args.projectId); + if (this.googleAuthApi) { + const profile = await this.googleAuthApi.getProfile({ optional: true }); + if (profile?.email) { + result.searchParams.set('authuser', profile.email); + } + } return result; } } diff --git a/plugins/kubernetes-react/src/api/formatters/index.ts b/plugins/kubernetes-react/src/api/formatters/index.ts index 97a3140d50..c4bf52d181 100644 --- a/plugins/kubernetes-react/src/api/formatters/index.ts +++ b/plugins/kubernetes-react/src/api/formatters/index.ts @@ -21,6 +21,7 @@ import { GkeClusterLinksFormatter } from './GkeClusterLinksFormatter'; import { StandardClusterLinksFormatter } from './StandardClusterLinksFormatter'; import { OpenshiftClusterLinksFormatter } from './OpenshiftClusterLinksFormatter'; import { RancherClusterLinksFormatter } from './RancherClusterLinksFormatter'; +import { ProfileInfoApi } from '@backstage/core-plugin-api'; export { StandardClusterLinksFormatter, @@ -35,15 +36,14 @@ export { export const DEFAULT_FORMATTER_NAME = 'standard'; /** @public */ -export function getDefaultFormatters(_deps: {}): Record< - string, - ClusterLinksFormatter -> { +export function getDefaultFormatters(deps: { + googleAuthApi: ProfileInfoApi; +}): Record { return { standard: new StandardClusterLinksFormatter(), aks: new AksClusterLinksFormatter(), eks: new EksClusterLinksFormatter(), - gke: new GkeClusterLinksFormatter(), + gke: new GkeClusterLinksFormatter(deps.googleAuthApi), openshift: new OpenshiftClusterLinksFormatter(), rancher: new RancherClusterLinksFormatter(), }; diff --git a/plugins/kubernetes/src/plugin.ts b/plugins/kubernetes/src/plugin.ts index c479f99aa0..afe2287633 100644 --- a/plugins/kubernetes/src/plugin.ts +++ b/plugins/kubernetes/src/plugin.ts @@ -103,7 +103,7 @@ export const kubernetesPlugin = createPlugin({ }), createApiFactory({ api: kubernetesClusterLinkFormatterApiRef, - deps: {}, + deps: { googleAuthApi: googleAuthApiRef }, factory: deps => { const formatters = getDefaultFormatters(deps); return new KubernetesClusterLinkFormatter({