diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 068a17249c..737a753635 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -27,6 +27,8 @@ kubernetes: authProvider: 'serviceAccount' skipTLSVerify: false serviceAccountToken: ${K8S_MINIKUBE_TOKEN} + dashboardUrl: http://127.0.0.1:64713 # url copied from running the command: minikube service kubernetes-dashboard -n kubernetes-dashboard + dashboardApp: standard - url: http://127.0.0.2:9999 name: aws-cluster-1 authProvider: 'aws' @@ -98,6 +100,40 @@ kubectl -n get secret $(kubectl -n get sa ...; +``` + +See also +https://github.com/backstage/backstage/tree/master/plugins/kubernetes/src/utils/clusterLinks/formatters +for real examples. + #### `gke` This cluster locator is designed to work with Kubernetes clusters running in diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index ee3bea69f9..2a52089c49 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -27,9 +27,8 @@ export interface AWSClusterDetails extends ClusterDetails { export interface ClusterDetails { // (undocumented) authProvider: string; - // (undocumented) + dashboardApp?: string; dashboardUrl?: string; - // (undocumented) name: string; // (undocumented) serviceAccountToken?: string | undefined; diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index 49ebeaade1..53d3a1d025 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -41,6 +41,10 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { if (dashboardUrl) { clusterDetails.dashboardUrl = dashboardUrl; } + const dashboardApp = c.getOptionalString('dashboardApp'); + if (dashboardApp) { + clusterDetails.dashboardApp = dashboardApp; + } switch (authProvider) { case 'google': { diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index a1b6f90040..fd403f49d1 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -124,6 +124,9 @@ export class KubernetesFanOutHandler { if (clusterDetailsItem.dashboardUrl) { objects.cluster.dashboardUrl = clusterDetailsItem.dashboardUrl; } + if (clusterDetailsItem.dashboardApp) { + objects.cluster.dashboardApp = clusterDetailsItem.dashboardApp; + } return objects; }); }), diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index f6c3f1e3ab..fe62d0fd78 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -75,12 +75,39 @@ export interface KubernetesServiceLocator { export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http export interface ClusterDetails { + /** + * Specifies the name of the Kubernetes cluster. + */ name: string; url: string; authProvider: string; serviceAccountToken?: string | undefined; skipTLSVerify?: boolean; + /** + * Specifies the link to the Kubernetes dashboard managing this cluster. + * @remarks + * Note that you need to specify the app used for the dashboard + * using the dashboardApp property, in order to properly format + * links to kubernetes resources. + * @see dashboardApp + */ dashboardUrl?: string; + /** + * Specifies the app that provides the Kubernetes dashboard. + * This will be used for formatting links to kubernetes objects inside the dashboard. + * @remarks + * The existing apps are: standard, rancher, openshift, gke, aks, eks + * Note that it will default to the regular dashboard provided by the Kubernetes project (standard). + * Note that you can add your own formatter by registering it to the formatters dictionary. + * @defaultValue standard + * @see dashboardUrl + * @example + * ```ts + * import { clusterLinksFormatters } from '@backstage/plugin-kubernetes'; + * clusterLinksFormatters.myDashboard = (options) => ...; + * ``` + */ + dashboardApp?: string; } export interface GKEClusterDetails extends ClusterDetails {} diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index 11ce9d10de..7981c29b4e 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -21,9 +21,8 @@ export type AuthProviderType = 'google' | 'serviceAccount' | 'aws'; // // @public (undocumented) export interface ClusterAttributes { - // (undocumented) + dashboardApp?: string; dashboardUrl?: string; - // (undocumented) name: string; } diff --git a/plugins/kubernetes-common/src/types.ts b/plugins/kubernetes-common/src/types.ts index 1da1390207..8b8aba9a93 100644 --- a/plugins/kubernetes-common/src/types.ts +++ b/plugins/kubernetes-common/src/types.ts @@ -33,8 +33,35 @@ export interface KubernetesRequestBody { } export interface ClusterAttributes { + /** + * Specifies the name of the Kubernetes cluster. + */ name: string; + /** + * Specifies the link to the Kubernetes dashboard managing this cluster. + * @remarks + * Note that you need to specify the app used for the dashboard + * using the dashboardApp property, in order to properly format + * links to kubernetes resources. + * @see dashboardApp + */ dashboardUrl?: string; + /** + * Specifies the app that provides the Kubernetes dashboard. + * This will be used for formatting links to kubernetes objects inside the dashboard. + * @remarks + * The supported dashboards are: standard, rancher, openshift, gke, aks, eks + * Note that it will default to the regular dashboard provided by the Kubernetes project (standard). + * Note that you can add your own formatter by registering it to the formatters dictionary. + * @defaultValue standard + * @see dashboardUrl + * @example + * ```ts + * import { clusterLinksFormatters } from '@backstage/plugin-kubernetes'; + * clusterLinksFormatters.myDashboard = (options) => ...; + * ``` + */ + dashboardApp?: string; } export interface ClusterObjects { diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index 0a3b466942..e3c239949b 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -12,6 +12,12 @@ import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; import { OAuthApi } from '@backstage/core-plugin-api'; import { RouteRef } from '@backstage/core-plugin-api'; +// Warning: (ae-forgotten-export) The symbol "ClusterLinksFormatter" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "clusterLinksFormatters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const clusterLinksFormatters: Record; + // Warning: (ae-missing-release-tag) "EntityKubernetesContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -19,6 +25,16 @@ export const EntityKubernetesContent: (_props: { entity?: Entity | undefined; }) => JSX.Element; +// Warning: (ae-missing-release-tag) "formatClusterLink" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function formatClusterLink(options: { + dashboardUrl?: string; + dashboardApp?: string; + object: any; + kind: string; +}): string | undefined; + // Warning: (ae-forgotten-export) The symbol "KubernetesAuthProvidersApi" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "KubernetesAuthProviders" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx b/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx index 31b87052c5..4a51655484 100644 --- a/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx +++ b/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx @@ -112,6 +112,7 @@ const KubernetesDrawerContent = ({ const cluster = useContext(ClusterContext); const clusterLink = formatClusterLink({ dashboardUrl: cluster.dashboardUrl, + dashboardApp: cluster.dashboardApp, object, kind, }); diff --git a/plugins/kubernetes/src/index.ts b/plugins/kubernetes/src/index.ts index 23acc3e7ca..4f4c3d03da 100644 --- a/plugins/kubernetes/src/index.ts +++ b/plugins/kubernetes/src/index.ts @@ -20,3 +20,4 @@ export { } from './plugin'; export { Router } from './Router'; export * from './kubernetes-auth-provider'; +export * from './utils/clusterLinks'; diff --git a/plugins/kubernetes/src/types/types.ts b/plugins/kubernetes/src/types/types.ts index 0817c90f97..05337086be 100644 --- a/plugins/kubernetes/src/types/types.ts +++ b/plugins/kubernetes/src/types/types.ts @@ -37,3 +37,13 @@ export interface GroupedResponses extends DeploymentResources { ingresses: ExtensionsV1beta1Ingress[]; customResources: any[]; } + +export interface ClusterLinksFormatterOptions { + dashboardUrl: URL; + object: any; + kind: string; +} + +export type ClusterLinksFormatter = ( + options: ClusterLinksFormatterOptions, +) => URL; diff --git a/plugins/kubernetes/src/utils/clusterLinks.test.ts b/plugins/kubernetes/src/utils/clusterLinks.test.ts deleted file mode 100644 index 7c649a34b9..0000000000 --- a/plugins/kubernetes/src/utils/clusterLinks.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { formatClusterLink } from './clusterLinks'; - -describe('clusterLinks', () => { - describe('formatClusterLink', () => { - it('should not return an url when there is no dashboard url', () => { - const url = formatClusterLink({ object: {}, kind: 'foo' }); - expect(url).toBeUndefined(); - }); - it('should return an url even when there is no object', () => { - const url = formatClusterLink({ - dashboardUrl: 'https://k8s.foo.com', - object: undefined, - kind: 'foo', - }); - expect(url).toBe('https://k8s.foo.com'); - }); - it('should return an url on the workloads when there is a namespace only', () => { - const url = formatClusterLink({ - dashboardUrl: 'https://k8s.foo.com', - object: { - metadata: { - namespace: 'bar', - }, - }, - kind: 'foo', - }); - expect(url).toBe('https://k8s.foo.com/#/workloads?namespace=bar'); - }); - it('should return an url on the workloads when the kind is not recognizeed', () => { - const url = formatClusterLink({ - dashboardUrl: 'https://k8s.foo.com', - object: { - metadata: { - name: 'foobar', - namespace: 'bar', - }, - }, - kind: 'UnknownKind', - }); - expect(url).toBe('https://k8s.foo.com/#/workloads?namespace=bar'); - }); - it('should return an url on the deployment', () => { - const url = formatClusterLink({ - dashboardUrl: 'https://k8s.foo.com/', - object: { - metadata: { - name: 'foobar', - namespace: 'bar', - }, - }, - kind: 'Deployment', - }); - expect(url).toBe( - 'https://k8s.foo.com/#/deployment/bar/foobar?namespace=bar', - ); - }); - it('should return an url on the service', () => { - const url = formatClusterLink({ - dashboardUrl: 'https://k8s.foo.com/', - object: { - metadata: { - name: 'foobar', - namespace: 'bar', - }, - }, - kind: 'Service', - }); - expect(url).toBe( - 'https://k8s.foo.com/#/service/bar/foobar?namespace=bar', - ); - }); - it('should return an url on the ingress', () => { - const url = formatClusterLink({ - dashboardUrl: 'https://k8s.foo.com/', - object: { - metadata: { - name: 'foobar', - namespace: 'bar', - }, - }, - kind: 'Ingress', - }); - expect(url).toBe( - 'https://k8s.foo.com/#/ingress/bar/foobar?namespace=bar', - ); - }); - it('should return an url on the deployment for a hpa', () => { - const url = formatClusterLink({ - dashboardUrl: 'https://k8s.foo.com/', - object: { - metadata: { - name: 'foobar', - namespace: 'bar', - }, - }, - kind: 'HorizontalPodAutoscaler', - }); - expect(url).toBe( - 'https://k8s.foo.com/#/deployment/bar/foobar?namespace=bar', - ); - }); - }); -}); diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.test.ts b/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.test.ts new file mode 100644 index 0000000000..afc53d9509 --- /dev/null +++ b/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.test.ts @@ -0,0 +1,133 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { formatClusterLink } from './formatClusterLink'; + +describe('clusterLinks', () => { + describe('formatClusterLink', () => { + it('should not return an url when there is no dashboard url', () => { + const url = formatClusterLink({ object: {}, kind: 'foo' }); + expect(url).toBeUndefined(); + }); + it('should return an url even when there is no object', () => { + const url = formatClusterLink({ + dashboardUrl: 'https://k8s.foo.com', + object: undefined, + kind: 'foo', + }); + expect(url).toBe('https://k8s.foo.com'); + }); + it('should throw when the app is not recognized', () => { + expect(() => + formatClusterLink({ + dashboardUrl: 'https://k8s.foo.com', + dashboardApp: 'unknownapp', + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }), + ).toThrowError( + "Could not find Kubernetes dashboard app named 'unknownapp'", + ); + }); + + describe('default app', () => { + it('should return an url on the deployment', () => { + const url = formatClusterLink({ + dashboardUrl: 'https://k8s.foo.com/', + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }); + expect(url).toBe( + 'https://k8s.foo.com/#/deployment/bar/foobar?namespace=bar', + ); + }); + it('should return an url on the service', () => { + const url = formatClusterLink({ + dashboardUrl: 'https://k8s.foo.com/', + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Service', + }); + expect(url).toBe( + 'https://k8s.foo.com/#/service/bar/foobar?namespace=bar', + ); + }); + it('should return an url on the deployment properly url encoded', () => { + const url = formatClusterLink({ + dashboardUrl: 'https://k8s.foo.com/', + object: { + metadata: { + name: 'foobar', + namespace: 'bar bar', + }, + }, + kind: 'Deployment', + }); + expect(url).toBe( + 'https://k8s.foo.com/#/deployment/bar%20bar/foobar?namespace=bar+bar', + ); + }); + }); + + describe('standard app', () => { + it('should return an url on the deployment', () => { + const url = formatClusterLink({ + dashboardUrl: 'https://k8s.foo.com/', + dashboardApp: 'standard', + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }); + expect(url).toBe( + 'https://k8s.foo.com/#/deployment/bar/foobar?namespace=bar', + ); + }); + it('should return an url on the service', () => { + const url = formatClusterLink({ + dashboardUrl: 'https://k8s.foo.com/', + dashboardApp: 'standard', + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Service', + }); + expect(url).toBe( + 'https://k8s.foo.com/#/service/bar/foobar?namespace=bar', + ); + }); + }); + }); +}); diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.ts b/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.ts new file mode 100644 index 0000000000..f04ec1daea --- /dev/null +++ b/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defaultFormatterName, clusterLinksFormatters } from './formatters'; + +export function formatClusterLink(options: { + dashboardUrl?: string; + dashboardApp?: string; + object: any; + kind: string; +}) { + if (!options.dashboardUrl) { + return undefined; + } + if (!options.object) { + return options.dashboardUrl; + } + const app = options.dashboardApp || defaultFormatterName; + const formatter = clusterLinksFormatters[app]; + if (!formatter) { + throw new Error(`Could not find Kubernetes dashboard app named '${app}'`); + } + const url = formatter({ + dashboardUrl: new URL(options.dashboardUrl), + object: options.object, + kind: options.kind, + }); + // Note that we can't rely on 'url.href' since it will put the search before the hash + // and this won't be properly recognized by SPAs such as Angular in the standard dashboard. + // Note also that pathname, hash and search will be properly url encoded. + return `${url.origin}${url.pathname}${url.hash}${url.search}`; +} diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/aks.test.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/aks.test.ts new file mode 100644 index 0000000000..381df0dc28 --- /dev/null +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/aks.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { aksFormatter } from './aks'; + +describe('clusterLinks - aks formatter', () => { + it('should return an url on the workloads when there is a namespace only', () => { + expect(() => + aksFormatter({ + dashboardUrl: new URL('https://k8s.foo.com'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }), + ).toThrowError('AKS formatter is not yet implemented'); + }); +}); diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/aks.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/aks.ts new file mode 100644 index 0000000000..ef221b604d --- /dev/null +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/aks.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ClusterLinksFormatterOptions } from '../../../types/types'; + +export function aksFormatter(_options: ClusterLinksFormatterOptions): URL { + throw new Error('AKS formatter is not yet implemented'); +} diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/eks.test.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/eks.test.ts new file mode 100644 index 0000000000..a1a5252bc4 --- /dev/null +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/eks.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { eksFormatter } from './eks'; + +describe('clusterLinks - aks formatter', () => { + it('should return an url on the workloads when there is a namespace only', () => { + expect(() => + eksFormatter({ + dashboardUrl: new URL('https://k8s.foo.com'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }), + ).toThrowError('EKS formatter is not yet implemented'); + }); +}); diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/eks.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/eks.ts new file mode 100644 index 0000000000..a9fb2f265b --- /dev/null +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/eks.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ClusterLinksFormatterOptions } from '../../../types/types'; + +export function eksFormatter(_options: ClusterLinksFormatterOptions): URL { + throw new Error('EKS formatter is not yet implemented'); +} diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/gke.test.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/gke.test.ts new file mode 100644 index 0000000000..56e8b510d3 --- /dev/null +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/gke.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { gkeFormatter } from './gke'; + +describe('clusterLinks - aks formatter', () => { + it('should return an url on the workloads when there is a namespace only', () => { + expect(() => + gkeFormatter({ + dashboardUrl: new URL('https://k8s.foo.com'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }), + ).toThrowError('GKE formatter is not yet implemented'); + }); +}); diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/gke.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/gke.ts new file mode 100644 index 0000000000..858b872389 --- /dev/null +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/gke.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ClusterLinksFormatterOptions } from '../../../types/types'; + +export function gkeFormatter(_options: ClusterLinksFormatterOptions): URL { + throw new Error('GKE formatter is not yet implemented'); +} diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/index.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/index.ts new file mode 100644 index 0000000000..f0530f1f7b --- /dev/null +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ClusterLinksFormatter } from '../../../types/types'; +import { standardFormatter } from './standard'; +import { rancherFormatter } from './rancher'; +import { openshiftFormatter } from './openshift'; + +export const clusterLinksFormatters: Record = { + standard: standardFormatter, + rancher: rancherFormatter, + openshift: openshiftFormatter, +}; +export const defaultFormatterName = 'standard'; diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.test.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.test.ts new file mode 100644 index 0000000000..f9a70d8d9a --- /dev/null +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { openshiftFormatter } from './openshift'; + +describe('clusterLinks - aks formatter', () => { + it('should return an url on the workloads when there is a namespace only', () => { + expect(() => + openshiftFormatter({ + dashboardUrl: new URL('https://k8s.foo.com'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }), + ).toThrowError('OpenShift formatter is not yet implemented'); + }); +}); diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.ts new file mode 100644 index 0000000000..acab9ec1cb --- /dev/null +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ClusterLinksFormatterOptions } from '../../../types/types'; + +export function openshiftFormatter( + _options: ClusterLinksFormatterOptions, +): URL { + throw new Error('OpenShift formatter is not yet implemented'); +} diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.test.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.test.ts new file mode 100644 index 0000000000..61de1a0a4e --- /dev/null +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { rancherFormatter } from './rancher'; + +describe('clusterLinks - aks formatter', () => { + it('should return an url on the workloads when there is a namespace only', () => { + expect(() => + rancherFormatter({ + dashboardUrl: new URL('https://k8s.foo.com'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }), + ).toThrowError('Rancher formatter is not yet implemented'); + }); +}); diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.ts new file mode 100644 index 0000000000..962f494408 --- /dev/null +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ClusterLinksFormatterOptions } from '../../../types/types'; + +export function rancherFormatter(_options: ClusterLinksFormatterOptions): URL { + throw new Error('Rancher formatter is not yet implemented'); +} diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.test.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.test.ts new file mode 100644 index 0000000000..76bbf5643d --- /dev/null +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.test.ts @@ -0,0 +1,115 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { standardFormatter } from './standard'; + +function formatUrl(url: URL) { + // Note that we can't rely on 'url.href' since it will put the search before the hash + // and this won't be properly recognized by SPAs such as Angular in the standard dashboard. + // Note also that pathname, hash and search will be properly url encoded. + return `${url.origin}${url.pathname}${url.hash}${url.search}`; +} + +describe('clusterLinks - standard formatter', () => { + it('should return an url on the workloads when there is a namespace only', () => { + const url = standardFormatter({ + dashboardUrl: new URL('https://k8s.foo.com'), + object: { + metadata: { + namespace: 'bar', + }, + }, + kind: 'foo', + }); + expect(formatUrl(url)).toBe( + 'https://k8s.foo.com/#/workloads?namespace=bar', + ); + }); + it('should return an url on the workloads when the kind is not recognizeed', () => { + const url = standardFormatter({ + dashboardUrl: new URL('https://k8s.foo.com'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'UnknownKind', + }); + expect(formatUrl(url)).toBe( + 'https://k8s.foo.com/#/workloads?namespace=bar', + ); + }); + it('should return an url on the deployment', () => { + const url = standardFormatter({ + dashboardUrl: new URL('https://k8s.foo.com/'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }); + expect(formatUrl(url)).toBe( + 'https://k8s.foo.com/#/deployment/bar/foobar?namespace=bar', + ); + }); + it('should return an url on the service', () => { + const url = standardFormatter({ + dashboardUrl: new URL('https://k8s.foo.com/'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Service', + }); + expect(formatUrl(url)).toBe( + 'https://k8s.foo.com/#/service/bar/foobar?namespace=bar', + ); + }); + it('should return an url on the ingress', () => { + const url = standardFormatter({ + dashboardUrl: new URL('https://k8s.foo.com/'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Ingress', + }); + expect(formatUrl(url)).toBe( + 'https://k8s.foo.com/#/ingress/bar/foobar?namespace=bar', + ); + }); + it('should return an url on the deployment for a hpa', () => { + const url = standardFormatter({ + dashboardUrl: new URL('https://k8s.foo.com/'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'HorizontalPodAutoscaler', + }); + expect(formatUrl(url)).toBe( + 'https://k8s.foo.com/#/deployment/bar/foobar?namespace=bar', + ); + }); +}); diff --git a/plugins/kubernetes/src/utils/clusterLinks.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.ts similarity index 62% rename from plugins/kubernetes/src/utils/clusterLinks.ts rename to plugins/kubernetes/src/utils/clusterLinks/formatters/standard.ts index 53f771a1b6..06a8f9e0a8 100644 --- a/plugins/kubernetes/src/utils/clusterLinks.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { ClusterLinksFormatterOptions } from '../../../types/types'; const KindMappings: Record = { deployment: 'deployment', @@ -21,30 +22,18 @@ const KindMappings: Record = { horizontalpodautoscaler: 'deployment', }; -export function formatClusterLink(options: { - dashboardUrl?: string; - object: any; - kind: string; -}) { - if (!options.dashboardUrl) { - return undefined; - } - if (!options.object) { - return options.dashboardUrl; - } - const host = options.dashboardUrl.endsWith('/') - ? options.dashboardUrl - : `${options.dashboardUrl}/`; +export function standardFormatter(options: ClusterLinksFormatterOptions) { + const result = new URL(options.dashboardUrl.href); const name = options.object.metadata?.name; const namespace = options.object.metadata?.namespace; const validKind = KindMappings[options.kind.toLocaleLowerCase()]; - if (validKind && name && namespace) { - return `${host}#/${encodeURIComponent(validKind)}/${encodeURIComponent( - namespace, - )}/${encodeURIComponent(name)}?namespace=${encodeURIComponent(namespace)}`; - } if (namespace) { - return `${host}#/workloads?namespace=${encodeURIComponent(namespace)}`; + result.searchParams.set('namespace', namespace); } - return options.dashboardUrl; + if (validKind && name && namespace) { + result.hash = `/${validKind}/${namespace}/${name}`; + } else if (namespace) { + result.hash = '/workloads'; + } + return result; } diff --git a/plugins/kubernetes/src/utils/clusterLinks/index.ts b/plugins/kubernetes/src/utils/clusterLinks/index.ts new file mode 100644 index 0000000000..0a2a32cdbf --- /dev/null +++ b/plugins/kubernetes/src/utils/clusterLinks/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { formatClusterLink } from './formatClusterLink'; +export { clusterLinksFormatters } from './formatters'; diff --git a/plugins/kubernetes/src/utils/index.ts b/plugins/kubernetes/src/utils/index.ts new file mode 100644 index 0000000000..d47afaf668 --- /dev/null +++ b/plugins/kubernetes/src/utils/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './clusterLinks';