diff --git a/.changeset/sharp-seas-remain.md b/.changeset/sharp-seas-remain.md new file mode 100644 index 0000000000..b482587aa3 --- /dev/null +++ b/.changeset/sharp-seas-remain.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes-common': patch +--- + +implement dashboard link formatter for GKE diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 32ab830322..492cd86022 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -39,6 +39,7 @@ kubernetes: region: 'europe-west1' skipTLSVerify: true skipMetricsLookup: true + exposeDashboard: true ``` ### `serviceLocatorMethod` @@ -113,9 +114,13 @@ kubectl -n get secret $(kubectl -n get sa { }, ]); }); + + it('one cluster with dashboardParameters', async () => { + const config: Config = new ConfigReader({ + clusters: [ + { + name: 'cluster1', + url: 'http://localhost:8080', + authProvider: 'serviceAccount', + dashboardApp: 'gke', + dashboardParameters: { + projectId: 'some-project', + region: 'some-region', + clusterName: 'cluster1', + }, + }, + ], + }); + + const sut = ConfigClusterLocator.fromConfig(config); + + const result = await sut.getClusters(); + + expect(result).toStrictEqual([ + { + name: 'cluster1', + serviceAccountToken: undefined, + url: 'http://localhost:8080', + authProvider: 'serviceAccount', + skipMetricsLookup: false, + skipTLSVerify: false, + caData: undefined, + dashboardApp: 'gke', + dashboardParameters: { + projectId: 'some-project', + region: 'some-region', + clusterName: 'cluster1', + }, + }, + ]); + }); + + it('one cluster with dashboardUrl', async () => { + const config: Config = new ConfigReader({ + clusters: [ + { + name: 'cluster1', + url: 'http://localhost:8080', + authProvider: 'serviceAccount', + dashboardApp: 'standard', + dashboardUrl: 'http://someurl', + }, + ], + }); + + const sut = ConfigClusterLocator.fromConfig(config); + + const result = await sut.getClusters(); + + expect(result).toStrictEqual([ + { + name: 'cluster1', + serviceAccountToken: undefined, + url: 'http://localhost:8080', + authProvider: 'serviceAccount', + skipMetricsLookup: false, + skipTLSVerify: false, + caData: undefined, + dashboardApp: 'standard', + dashboardUrl: 'http://someurl', + }, + ]); + }); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index 36b5f58f2c..d8436b6c24 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -47,6 +47,9 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { if (dashboardApp) { clusterDetails.dashboardApp = dashboardApp; } + if (c.has('dashboardParameters')) { + clusterDetails.dashboardParameters = c.get('dashboardParameters'); + } switch (authProvider) { case 'google': { diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts index bd342f566b..6bef8e0009 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts @@ -226,5 +226,51 @@ describe('GkeClusterLocator', () => { parent: 'projects/some-project/locations/some-region', }); }); + it('expose GKE dashboard', async () => { + mockedListClusters.mockReturnValueOnce([ + { + clusters: [ + { + name: 'some-cluster', + endpoint: '1.2.3.4', + }, + ], + }, + ]); + + const config: Config = new ConfigReader({ + type: 'gke', + projectId: 'some-project', + region: 'some-region', + skipMetricsLookup: true, + exposeDashboard: true, + }); + + 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: true, + dashboardApp: 'gke', + dashboardParameters: { + clusterName: 'some-cluster', + projectId: 'some-project', + region: 'some-region', + }, + }, + ]); + expect(mockedListClusters).toBeCalledTimes(1); + expect(mockedListClusters).toHaveBeenCalledWith({ + parent: 'projects/some-project/locations/some-region', + }); + }); }); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts index d7f5050399..8b70db40eb 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts @@ -24,6 +24,7 @@ type GkeClusterLocatorOptions = { region?: string; skipTLSVerify?: boolean; skipMetricsLookup?: boolean; + exposeDashboard?: boolean; }; export class GkeClusterLocator implements KubernetesClustersSupplier { @@ -42,6 +43,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { skipTLSVerify: config.getOptionalBoolean('skipTLSVerify') ?? false, skipMetricsLookup: config.getOptionalBoolean('skipMetricsLookup') ?? false, + exposeDashboard: config.getOptionalBoolean('exposeDashboard') ?? false, }; return new GkeClusterLocator(options, client); } @@ -55,8 +57,13 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { // TODO pass caData into the object async getClusters(): Promise { - const { projectId, region, skipTLSVerify, skipMetricsLookup } = - this.options; + const { + projectId, + region, + skipTLSVerify, + skipMetricsLookup, + exposeDashboard, + } = this.options; const request = { parent: `projects/${projectId}/locations/${region}`, }; @@ -70,6 +77,16 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { authProvider: 'google', skipTLSVerify, skipMetricsLookup, + ...(exposeDashboard + ? { + dashboardApp: 'gke', + dashboardParameters: { + projectId, + region, + clusterName: r.name, + }, + } + : {}), })); } catch (e) { throw new ForwardedError( diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index a25c220b80..b15ec1386e 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -255,6 +255,10 @@ export class KubernetesFanOutHandler { if (clusterDetailsItem.dashboardApp) { objects.cluster.dashboardApp = clusterDetailsItem.dashboardApp; } + if (clusterDetailsItem.dashboardParameters) { + objects.cluster.dashboardParameters = + clusterDetailsItem.dashboardParameters; + } return objects; }); }), diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index a0ec0832c1..4f25805063 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -15,6 +15,7 @@ */ import { Logger } from 'winston'; +import type { JsonObject } from '@backstage/types'; import type { FetchResponse, KubernetesFetchError, @@ -111,6 +112,7 @@ export interface ClusterDetails { * using the dashboardApp property, in order to properly format * links to kubernetes resources, otherwise it will assume that you're running the standard one. * @see dashboardApp + * @see dashboardParameters */ dashboardUrl?: string; /** @@ -129,6 +131,12 @@ export interface ClusterDetails { * ``` */ dashboardApp?: string; + /** + * Specifies specific parameters used by some dashboard URL formatters. + * This is used by the GKE formatter which requires the project, region and cluster name. + * @see dashboardApp + */ + dashboardParameters?: JsonObject; } export interface GKEClusterDetails extends ClusterDetails {} diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index f3500aa271..15019d71c3 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -4,6 +4,7 @@ ```ts import { Entity } from '@backstage/catalog-model'; +import type { JsonObject } from '@backstage/types'; import { V1ConfigMap } from '@kubernetes/client-node'; import { V1CronJob } from '@kubernetes/client-node'; import { V1Deployment } from '@kubernetes/client-node'; @@ -62,6 +63,7 @@ export interface ClientPodStatus { // @public (undocumented) export interface ClusterAttributes { dashboardApp?: string; + dashboardParameters?: JsonObject; dashboardUrl?: string; name: string; } diff --git a/plugins/kubernetes-common/src/types.ts b/plugins/kubernetes-common/src/types.ts index 0c5af00a97..afda9b5299 100644 --- a/plugins/kubernetes-common/src/types.ts +++ b/plugins/kubernetes-common/src/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import type { JsonObject } from '@backstage/types'; import { V1ConfigMap, V1CronJob, @@ -45,6 +46,7 @@ export interface ClusterAttributes { * Note that you should specify the app used for the dashboard * using the dashboardApp property, in order to properly format * links to kubernetes resources, otherwise it will assume that you're running the standard one. + * Also, for cloud clusters such as GKE, you should provide addititonal parameters using dashboardParameters. * @see dashboardApp */ dashboardUrl?: string; @@ -64,6 +66,11 @@ export interface ClusterAttributes { * ``` */ dashboardApp?: string; + /** + * Specifies specific parameters used by some dashboard URL formatters. + * This is used by the GKE formatter which requires the project, region and cluster name. + */ + dashboardParameters?: JsonObject; } export interface ClusterObjects { diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index 33ac98fb76..8dc0114738 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -10,6 +10,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; +import type { JsonObject } from '@backstage/types'; import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; import { OAuthApi } from '@backstage/core-plugin-api'; import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common'; diff --git a/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx b/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx index b4ea31dc8d..8a177cf6dd 100644 --- a/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx +++ b/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx @@ -155,6 +155,7 @@ const KubernetesDrawerContent = ({ const { clusterLink, errorMessage } = tryFormatClusterLink({ dashboardUrl: cluster.dashboardUrl, dashboardApp: cluster.dashboardApp, + dashboardParameters: cluster.dashboardParameters, object, kind, }); diff --git a/plugins/kubernetes/src/types/types.ts b/plugins/kubernetes/src/types/types.ts index d6ef8fa784..1bdfa181d9 100644 --- a/plugins/kubernetes/src/types/types.ts +++ b/plugins/kubernetes/src/types/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import type { JsonObject } from '@backstage/types'; import { V1Deployment, V1Pod, @@ -43,7 +44,8 @@ export interface GroupedResponses extends DeploymentResources { } export interface ClusterLinksFormatterOptions { - dashboardUrl: URL; + dashboardUrl?: URL; + dashboardParameters?: JsonObject; object: any; kind: string; } diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.test.ts b/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.test.ts index 31e07a0f14..b767d496a2 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.test.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.test.ts @@ -114,5 +114,47 @@ describe('clusterLinks', () => { ); }); }); + describe('GKE app', () => { + it('should return an url on the deployment', () => { + const url = formatClusterLink({ + dashboardApp: 'gke', + dashboardParameters: { + projectId: 'foobar-333614', + region: 'us-east1-c', + clusterName: 'cluster-1', + }, + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }); + expect(url).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', () => { + const url = formatClusterLink({ + dashboardApp: 'gke', + dashboardParameters: { + projectId: 'foobar-333614', + region: 'us-east1-c', + clusterName: 'cluster-1', + }, + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Service', + }); + expect(url).toBe( + 'https://console.cloud.google.com/kubernetes/service/us-east1-c/cluster-1/bar/foobar/overview?project=foobar-333614', + ); + }); + }); }); }); diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.ts b/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.ts index a8f83a9c76..6a998bbce2 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.ts @@ -14,20 +14,22 @@ * limitations under the License. */ +import type { JsonObject } from '@backstage/types'; import { defaultFormatterName, clusterLinksFormatters } from './formatters'; export type FormatClusterLinkOptions = { dashboardUrl?: string; dashboardApp?: string; + dashboardParameters?: JsonObject; object: any; kind: string; }; export function formatClusterLink(options: FormatClusterLinkOptions) { - if (!options.dashboardUrl) { + if (!options.dashboardUrl && !options.dashboardParameters) { return undefined; } - if (!options.object) { + if (options.dashboardUrl && !options.object) { return options.dashboardUrl; } const app = options.dashboardApp || defaultFormatterName; @@ -36,7 +38,10 @@ export function formatClusterLink(options: FormatClusterLinkOptions) { throw new Error(`Could not find Kubernetes dashboard app named '${app}'`); } const url = formatter({ - dashboardUrl: new URL(options.dashboardUrl), + dashboardUrl: options.dashboardUrl + ? new URL(options.dashboardUrl) + : undefined, + dashboardParameters: options.dashboardParameters, object: options.object, kind: options.kind, }); diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/gke.test.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/gke.test.ts index e566404daf..20541f216d 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatters/gke.test.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/gke.test.ts @@ -16,10 +16,9 @@ import { gkeFormatter } from './gke'; describe('clusterLinks - GKE formatter', () => { - it('should return an url on the workloads when there is a namespace only', () => { + it('should provide a dashboardParameters in the options', () => { expect(() => gkeFormatter({ - dashboardUrl: new URL('https://k8s.foo.com'), object: { metadata: { name: 'foobar', @@ -28,6 +27,196 @@ describe('clusterLinks - GKE formatter', () => { }, kind: 'Deployment', }), - ).toThrowError('GKE formatter is not yet implemented. Please, contribute!'); + ).toThrowError('GKE dashboard requires a dashboardParameters option'); + }); + it('should provide a projectId in the dashboardParameters options', () => { + expect(() => + gkeFormatter({ + dashboardParameters: { + region: 'us-east1-c', + clusterName: 'cluster-1', + }, + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }), + ).toThrowError( + 'GKE dashboard requires a "projectId" of type string in the dashboardParameters option', + ); + }); + it('should provide a region in the dashboardParameters options', () => { + expect(() => + gkeFormatter({ + dashboardParameters: { + projectId: 'foobar-333614', + clusterName: 'cluster-1', + }, + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }), + ).toThrowError( + 'GKE dashboard requires a "region" of type string in the dashboardParameters option', + ); + }); + it('should provide a clusterName in the dashboardParameters options', () => { + expect(() => + gkeFormatter({ + dashboardParameters: { + projectId: 'foobar-333614', + region: 'us-east1-c', + }, + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }), + ).toThrowError( + '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', () => { + const url = gkeFormatter({ + 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 recognizeed', () => { + const url = gkeFormatter({ + dashboardParameters: { + projectId: 'foobar-333614', + region: 'us-east1-c', + clusterName: 'cluster-1', + }, + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + 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', () => { + const url = gkeFormatter({ + dashboardParameters: { + projectId: 'foobar-333614', + region: 'us-east1-c', + clusterName: 'cluster-1', + }, + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }); + 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', () => { + const url = gkeFormatter({ + 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', () => { + const url = gkeFormatter({ + 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', () => { + const url = gkeFormatter({ + 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', () => { + const url = gkeFormatter({ + 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/src/utils/clusterLinks/formatters/gke.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/gke.ts index 30e7fa3123..957032e6e2 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatters/gke.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/gke.ts @@ -15,6 +15,53 @@ */ import { ClusterLinksFormatterOptions } from '../../../types/types'; -export function gkeFormatter(_options: ClusterLinksFormatterOptions): URL { - throw new Error('GKE formatter is not yet implemented. Please, contribute!'); +const kindMappings: Record = { + deployment: 'deployment', + pod: 'pod', + ingress: 'ingress', + service: 'service', + horizontalpodautoscaler: 'deployment', +}; + +export function gkeFormatter(options: ClusterLinksFormatterOptions): URL { + if (!options.dashboardParameters) { + throw new Error('GKE dashboard requires a dashboardParameters option'); + } + const args = options.dashboardParameters; + if (typeof args.projectId !== 'string') { + throw new Error( + 'GKE dashboard requires a "projectId" of type string in the dashboardParameters option', + ); + } + if (typeof args.region !== 'string') { + throw new Error( + 'GKE dashboard requires a "region" of type string in the dashboardParameters option', + ); + } + if (typeof args.clusterName !== 'string') { + throw new Error( + 'GKE dashboard requires a "clusterName" of type string in the dashboardParameters option', + ); + } + const basePath = new URL('https://console.cloud.google.com/'); + const region = encodeURIComponent(args.region); + const clusterName = encodeURIComponent(args.clusterName); + const name = encodeURIComponent(options.object.metadata?.name ?? ''); + const namespace = encodeURIComponent( + options.object.metadata?.namespace ?? '', + ); + const validKind = kindMappings[options.kind.toLocaleLowerCase('en-US')]; + let path = ''; + if (namespace && name && validKind) { + const kindsWithDetails = ['ingress', 'pod']; + const landingPage = kindsWithDetails.includes(validKind) + ? 'details' + : 'overview'; + path = `kubernetes/${validKind}/${region}/${clusterName}/${namespace}/${name}/${landingPage}`; + } else { + path = `kubernetes/clusters/details/${region}/${clusterName}/details`; + } + const result = new URL(path, basePath); + result.searchParams.set('project', args.projectId); + return result; } diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.test.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.test.ts index f0b85cad49..2380d3e2d5 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.test.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.test.ts @@ -16,6 +16,19 @@ import { openshiftFormatter } from './openshift'; describe('clusterLinks - OpenShift formatter', () => { + it('should provide a dashboardUrl in the options', () => { + expect(() => + openshiftFormatter({ + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }), + ).toThrowError('OpenShift dashboard requires a dashboardUrl option'); + }); it('should return an url on the workloads when there is a namespace only', () => { const url = openshiftFormatter({ dashboardUrl: new URL('https://k8s.foo.com'), diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.ts index 6c20cd4720..c5b7313061 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.ts @@ -24,6 +24,9 @@ const kindMappings: Record = { }; export function openshiftFormatter(options: ClusterLinksFormatterOptions): URL { + if (!options.dashboardUrl) { + throw new Error('OpenShift dashboard requires a dashboardUrl option'); + } const basePath = new URL(options.dashboardUrl.href); const name = encodeURIComponent(options.object.metadata?.name ?? ''); const namespace = encodeURIComponent( diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.test.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.test.ts index cb200387a3..45983deab0 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.test.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.test.ts @@ -16,6 +16,19 @@ import { rancherFormatter } from './rancher'; describe('clusterLinks - rancher formatter', () => { + it('should provide a dashboardUrl in the options', () => { + expect(() => + rancherFormatter({ + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }), + ).toThrowError('Rancher dashboard requires a dashboardUrl option'); + }); it('should return a url on the workloads when there is a namespace only', () => { const url = rancherFormatter({ dashboardUrl: new URL('https://k8s.foo.com'), diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.ts index 1ace39ec59..6c72730181 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.ts @@ -23,6 +23,9 @@ const kindMappings: Record = { }; export function rancherFormatter(options: ClusterLinksFormatterOptions): URL { + if (!options.dashboardUrl) { + throw new Error('Rancher dashboard requires a dashboardUrl option'); + } const basePath = new URL(options.dashboardUrl.href); const name = encodeURIComponent(options.object.metadata?.name ?? ''); const namespace = encodeURIComponent( diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.test.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.test.ts index 0b3c21a627..c0ee3f534d 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.test.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.test.ts @@ -23,6 +23,19 @@ function formatUrl(url: URL) { } describe('clusterLinks - standard formatter', () => { + it('should provide a dashboardUrl in the options', () => { + expect(() => + standardFormatter({ + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }), + ).toThrowError('standard dashboard requires a dashboardUrl option'); + }); it('should return an url on the workloads when there is a namespace only', () => { const url = standardFormatter({ dashboardUrl: new URL('https://k8s.foo.com'), @@ -67,6 +80,21 @@ describe('clusterLinks - standard formatter', () => { 'https://k8s.foo.com/#/deployment/bar/foobar?namespace=bar', ); }); + it('should return an url on the pod', () => { + const url = standardFormatter({ + dashboardUrl: new URL('https://k8s.foo.com/'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Pod', + }); + expect(formatUrl(url)).toBe( + 'https://k8s.foo.com/#/pod/bar/foobar?namespace=bar', + ); + }); it('should return an url on the deployment with a prefix 1', () => { const url = standardFormatter({ dashboardUrl: new URL('https://k8s.foo.com/some/prefix'), diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.ts index e28c9fae2b..a7428a8722 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.ts @@ -17,12 +17,16 @@ import { ClusterLinksFormatterOptions } from '../../../types/types'; const kindMappings: Record = { deployment: 'deployment', + pod: 'pod', ingress: 'ingress', service: 'service', horizontalpodautoscaler: 'deployment', }; export function standardFormatter(options: ClusterLinksFormatterOptions) { + if (!options.dashboardUrl) { + throw new Error('standard dashboard requires a dashboardUrl option'); + } const result = new URL(options.dashboardUrl.href); const name = encodeURIComponent(options.object.metadata?.name ?? ''); const namespace = encodeURIComponent(