Merge pull request #6782 from livetocode/feature/k8s-dashboard-links

provide access to the Kubernetes dashboard when viewing a specific resource
This commit is contained in:
Fredrik Adelöw
2021-09-20 14:17:30 +02:00
committed by GitHub
35 changed files with 972 additions and 40 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-kubernetes': patch
'@backstage/plugin-kubernetes-backend': patch
'@backstage/plugin-kubernetes-common': patch
---
Provide access to the Kubernetes dashboard when viewing a specific resource
+37
View File
@@ -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,41 @@ kubectl -n <NAMESPACE> get secret $(kubectl -n <NAMESPACE> get sa <SERVICE_ACCOU
| base64 --decode
```
##### `clusters.\*.dashboardUrl` (optional)
Specifies the link to the Kubernetes dashboard managing this cluster.
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.
##### `clusters.\*.dashboardApp` (optional)
Specifies the app that provides the Kubernetes dashboard.
This will be used for formatting links to kubernetes objects inside the
dashboard.
The supported dashboards are: `standard`, `rancher`, `openshift`, `gke`, `aks`,
`eks` However, not all of them are implemented yet, so please contribute!
Note that it will default to the regular dashboard provided by the Kubernetes
project (`standard`), that can run in any Kubernetes cluster.
Note that you can add your own formatter by registering it to the
`clusterLinksFormatters` dictionary, in the app project.
Example:
```ts
import { clusterLinksFormatters } from '@backstage/plugin-kubernetes';
clusterLinksFormatters.myDashboard = (options) => ...;
```
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
+5 -3
View File
@@ -3,10 +3,11 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ClusterObjects } from '@backstage/plugin-kubernetes-common';
import { Config } from '@backstage/config';
import express from 'express';
import { FetchResponse } from '@backstage/plugin-kubernetes-common';
import { KubernetesFetchError } from '@backstage/plugin-kubernetes-common';
import type { FetchResponse } from '@backstage/plugin-kubernetes-common';
import type { KubernetesFetchError } from '@backstage/plugin-kubernetes-common';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
import { Logger as Logger_2 } from 'winston';
@@ -26,7 +27,8 @@ export interface AWSClusterDetails extends ClusterDetails {
export interface ClusterDetails {
// (undocumented)
authProvider: string;
// (undocumented)
dashboardApp?: string;
dashboardUrl?: string;
name: string;
// (undocumented)
serviceAccountToken?: string | undefined;
@@ -66,6 +66,7 @@ describe('ConfigClusterLocator', () => {
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
skipTLSVerify: false,
dashboardUrl: 'https://k8s.foo.com',
},
{
name: 'cluster2',
@@ -83,6 +84,7 @@ describe('ConfigClusterLocator', () => {
expect(result).toStrictEqual([
{
name: 'cluster1',
dashboardUrl: 'https://k8s.foo.com',
serviceAccountToken: 'token',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
@@ -30,13 +30,21 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier {
return new ConfigClusterLocator(
config.getConfigArray('clusters').map(c => {
const authProvider = c.getString('authProvider');
const clusterDetails = {
const clusterDetails: ClusterDetails = {
name: c.getString('name'),
url: c.getString('url'),
serviceAccountToken: c.getOptionalString('serviceAccountToken'),
skipTLSVerify: c.getOptionalBoolean('skipTLSVerify') ?? false,
authProvider: authProvider,
};
const dashboardUrl = c.getOptionalString('dashboardUrl');
if (dashboardUrl) {
clusterDetails.dashboardUrl = dashboardUrl;
}
const dashboardApp = c.getOptionalString('dashboardApp');
if (dashboardApp) {
clusterDetails.dashboardApp = dashboardApp;
}
switch (authProvider) {
case 'google': {
@@ -211,6 +211,7 @@ describe('handleGetKubernetesObjectsForService', () => {
{
name: 'test-cluster',
authProvider: 'serviceAccount',
dashboardUrl: 'https://k8s.foo.coom',
},
{
name: 'other-cluster',
@@ -260,6 +261,7 @@ describe('handleGetKubernetesObjectsForService', () => {
items: [
{
cluster: {
dashboardUrl: 'https://k8s.foo.coom',
name: 'test-cluster',
},
errors: [],
@@ -22,7 +22,10 @@ import {
KubernetesObjectTypes,
KubernetesServiceLocator,
} from '../types/types';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
import {
ClusterObjects,
KubernetesRequestBody,
} from '@backstage/plugin-kubernetes-common';
import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types';
import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator';
@@ -111,13 +114,20 @@ export class KubernetesFanOutHandler {
customResources: this.customResources,
})
.then(result => {
return {
const objects: ClusterObjects = {
cluster: {
name: clusterDetailsItem.name,
},
resources: result.responses,
errors: result.errors,
};
if (clusterDetailsItem.dashboardUrl) {
objects.cluster.dashboardUrl = clusterDetailsItem.dashboardUrl;
}
if (clusterDetailsItem.dashboardApp) {
objects.cluster.dashboardApp = clusterDetailsItem.dashboardApp;
}
return objects;
});
}),
).then(r => ({
@@ -86,6 +86,7 @@ export const makeRouter = (
res.json({
items: clusterDetails.map(cd => ({
name: cd.name,
dashboardUrl: cd.dashboardUrl,
authProvider: cd.authProvider,
})),
});
@@ -75,11 +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 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.
* @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 clusterLinksFormatters dictionary.
* @defaultValue standard
* @see dashboardUrl
* @example
* ```ts
* import { clusterLinksFormatters } from '@backstage/plugin-kubernetes';
* clusterLinksFormatters.myDashboard = (options) => ...;
* ```
*/
dashboardApp?: string;
}
export interface GKEClusterDetails extends ClusterDetails {}
+10 -3
View File
@@ -17,14 +17,21 @@ import { V1Service } from '@kubernetes/client-node';
// @public (undocumented)
export type AuthProviderType = 'google' | 'serviceAccount' | 'aws';
// Warning: (ae-missing-release-tag) "ClusterAttributes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface ClusterAttributes {
dashboardApp?: string;
dashboardUrl?: string;
name: string;
}
// Warning: (ae-missing-release-tag) "ClusterObjects" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface ClusterObjects {
// (undocumented)
cluster: {
name: string;
};
cluster: ClusterAttributes;
// (undocumented)
errors: KubernetesFetchError[];
// (undocumented)
+33 -1
View File
@@ -32,8 +32,40 @@ export interface KubernetesRequestBody {
entity: Entity;
}
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 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.
* @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 clusterLinksFormatters dictionary.
* @defaultValue standard
* @see dashboardUrl
* @example
* ```ts
* import { clusterLinksFormatters } from '@backstage/plugin-kubernetes';
* clusterLinksFormatters.myDashboard = (options) => ...;
* ```
*/
dashboardApp?: string;
}
export interface ClusterObjects {
cluster: { name: string };
cluster: ClusterAttributes;
resources: FetchResponse[];
errors: KubernetesFetchError[];
}
+14
View File
@@ -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<string, ClusterLinksFormatter>;
// 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,14 @@ export const EntityKubernetesContent: (_props: {
entity?: Entity | undefined;
}) => JSX.Element;
// Warning: (ae-forgotten-export) The symbol "FormatClusterLinkOptions" needs to be exported by the entry point index.d.ts
// 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: FormatClusterLinkOptions,
): 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)
//
@@ -36,6 +36,7 @@ import { ServicesAccordions } from '../ServicesAccordions';
import { CustomResources } from '../CustomResources';
import EmptyStateImage from '../../assets/emptystate.svg';
import {
ClusterContext,
GroupedResponsesContext,
PodNamesWithErrorsContext,
useKubernetesObjects,
@@ -119,35 +120,37 @@ type ClusterProps = {
const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => {
const groupedResponses = groupResponses(clusterObjects.resources);
return (
<GroupedResponsesContext.Provider value={groupedResponses}>
<PodNamesWithErrorsContext.Provider value={podsWithErrors}>
<Accordion TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<ClusterSummary
clusterName={clusterObjects.cluster.name}
totalNumberOfPods={groupedResponses.pods.length}
numberOfPodsWithErrors={podsWithErrors.size}
/>
</AccordionSummary>
<AccordionDetails>
<Grid container direction="column">
<Grid item>
<CustomResources />
<ClusterContext.Provider value={clusterObjects.cluster}>
<GroupedResponsesContext.Provider value={groupedResponses}>
<PodNamesWithErrorsContext.Provider value={podsWithErrors}>
<Accordion TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<ClusterSummary
clusterName={clusterObjects.cluster.name}
totalNumberOfPods={groupedResponses.pods.length}
numberOfPodsWithErrors={podsWithErrors.size}
/>
</AccordionSummary>
<AccordionDetails>
<Grid container direction="column">
<Grid item>
<CustomResources />
</Grid>
<Grid item>
<DeploymentsAccordions />
</Grid>
<Grid item>
<IngressesAccordions />
</Grid>
<Grid item>
<ServicesAccordions />
</Grid>
</Grid>
<Grid item>
<DeploymentsAccordions />
</Grid>
<Grid item>
<IngressesAccordions />
</Grid>
<Grid item>
<ServicesAccordions />
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
</PodNamesWithErrorsContext.Provider>
</GroupedResponsesContext.Provider>
</AccordionDetails>
</Accordion>
</PodNamesWithErrorsContext.Provider>
</GroupedResponsesContext.Provider>
</ClusterContext.Provider>
);
};
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { ChangeEvent, useState } from 'react';
import React, { ChangeEvent, useContext, useState } from 'react';
import {
Button,
Typography,
@@ -28,13 +28,20 @@ import {
Grid,
} from '@material-ui/core';
import Close from '@material-ui/icons/Close';
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
import { V1ObjectMeta } from '@kubernetes/client-node';
import { withStyles } from '@material-ui/core/styles';
import jsYaml from 'js-yaml';
import {
Button as BackstageButton,
CodeSnippet,
StructuredMetadataTable,
WarningPanel,
} from '@backstage/core-components';
import { ClusterContext } from '../../hooks';
import { formatClusterLink } from '../../utils/clusterLinks';
import { ClusterAttributes } from '@backstage/plugin-kubernetes-common';
import { FormatClusterLinkOptions } from '../../utils/clusterLinks/formatClusterLink';
const useDrawerStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -53,10 +60,14 @@ const useDrawerContentStyles = makeStyles((_: Theme) =>
flexDirection: 'row',
justifyContent: 'space-between',
},
errorMessage: {
marginTop: '1em',
marginBottom: '1em',
},
options: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
justifyContent: 'space-between',
},
icon: {
fontSize: 20,
@@ -76,6 +87,27 @@ const PodDrawerButton = withStyles({
},
})(Button);
type ErrorPanelProps = {
cluster: ClusterAttributes;
errorMessage?: string;
children?: React.ReactNode;
};
export const ErrorPanel = ({ cluster, errorMessage }: ErrorPanelProps) => (
<WarningPanel
title="There was a problem formatting the link to the Kubernetes dashboard"
message={`Could not format the link to the dashboard of your cluster named '${
cluster.name
}'. Its dashboardApp property has been set to '${
cluster.dashboardApp || 'standard'
}.'`}
>
{errorMessage && (
<Typography variant="body2">Errors: {errorMessage}</Typography>
)}
</WarningPanel>
);
interface KubernetesDrawerable {
metadata?: V1ObjectMeta;
}
@@ -96,6 +128,20 @@ function replaceNullsWithUndefined(someObj: any) {
return JSON.parse(JSON.stringify(someObj, replacer));
}
function tryFormatClusterLink(options: FormatClusterLinkOptions) {
try {
return {
clusterLink: formatClusterLink(options),
errorMessage: '',
};
} catch (err) {
return {
clusterLink: '',
errorMessage: err.message || err.toString(),
};
}
}
const KubernetesDrawerContent = <T extends KubernetesDrawerable>({
toggleDrawer,
object,
@@ -105,6 +151,13 @@ const KubernetesDrawerContent = <T extends KubernetesDrawerable>({
const [isYaml, setIsYaml] = useState<boolean>(false);
const classes = useDrawerContentStyles();
const cluster = useContext(ClusterContext);
const { clusterLink, errorMessage } = tryFormatClusterLink({
dashboardUrl: cluster.dashboardUrl,
dashboardApp: cluster.dashboardApp,
object,
kind,
});
return (
<>
@@ -135,7 +188,25 @@ const KubernetesDrawerContent = <T extends KubernetesDrawerable>({
<Close className={classes.icon} />
</IconButton>
</div>
{errorMessage && (
<div className={classes.errorMessage}>
<ErrorPanel cluster={cluster} errorMessage={errorMessage} />
</div>
)}
<div className={classes.options}>
<div>
{clusterLink && (
<BackstageButton
variant="outlined"
color="primary"
size="small"
to={clusterLink}
endIcon={<OpenInNewIcon />}
>
Open Kubernetes Dashboard
</BackstageButton>
)}
</div>
<FormControlLabel
control={
<Switch
+21
View File
@@ -0,0 +1,21 @@
/*
* 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 React from 'react';
import { ClusterAttributes } from '@backstage/plugin-kubernetes-common';
export const ClusterContext = React.createContext<ClusterAttributes>({
name: '',
});
+1
View File
@@ -17,3 +17,4 @@
export * from './useKubernetesObjects';
export * from './PodNamesWithErrors';
export * from './GroupedResponses';
export * from './Cluster';
+1
View File
@@ -27,3 +27,4 @@ export {
} from './plugin';
export { Router } from './Router';
export * from './kubernetes-auth-provider';
export * from './utils/clusterLinks';
+10
View File
@@ -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;
@@ -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',
);
});
});
});
});
@@ -0,0 +1,47 @@
/*
* 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 type FormatClusterLinkOptions = {
dashboardUrl?: string;
dashboardApp?: string;
object: any;
kind: string;
};
export function formatClusterLink(options: FormatClusterLinkOptions) {
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}`;
}
@@ -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. Please, contribute!');
});
});
@@ -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. Please, contribute!');
}
@@ -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 - EKS 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. Please, contribute!');
});
});
@@ -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. Please, contribute!');
}
@@ -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 - GKE 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. Please, contribute!');
});
});
@@ -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. Please, contribute!');
}
@@ -0,0 +1,32 @@
/*
* 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';
import { aksFormatter } from './aks';
import { eksFormatter } from './eks';
import { gkeFormatter } from './gke';
export const clusterLinksFormatters: Record<string, ClusterLinksFormatter> = {
standard: standardFormatter,
rancher: rancherFormatter,
openshift: openshiftFormatter,
aks: aksFormatter,
eks: eksFormatter,
gke: gkeFormatter,
};
export const defaultFormatterName = 'standard';
@@ -0,0 +1,35 @@
/*
* 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 - OpenShift 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. Please, contribute!',
);
});
});
@@ -0,0 +1,24 @@
/*
* 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. Please, contribute!',
);
}
@@ -0,0 +1,35 @@
/*
* 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 - Rancher 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. Please, contribute!',
);
});
});
@@ -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 rancherFormatter(_options: ClusterLinksFormatterOptions): URL {
throw new Error(
'Rancher formatter is not yet implemented. Please, contribute!',
);
}
@@ -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',
);
});
});
@@ -0,0 +1,39 @@
/*
* 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';
const kindMappings: Record<string, string> = {
deployment: 'deployment',
ingress: 'ingress',
service: 'service',
horizontalpodautoscaler: 'deployment',
};
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('en-US')];
if (namespace) {
result.searchParams.set('namespace', namespace);
}
if (validKind && name && namespace) {
result.hash = `/${validKind}/${namespace}/${name}`;
} else if (namespace) {
result.hash = '/workloads';
}
return result;
}
@@ -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';
+16
View File
@@ -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';