[k8s] async formatClusterLink (#21535)

This is a refactor PR for formatClusterLink.
It rewrites it to be an API and allows async creation of cluster link.

Signed-off-by: Tomasz Szuba <tszuba@box.com>
This commit is contained in:
Tomasz Szuba
2023-11-30 20:37:14 +01:00
committed by GitHub
parent 950f8f5378
commit 899d71a37c
40 changed files with 882 additions and 768 deletions
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/plugin-kubernetes-react': minor
'@backstage/plugin-kubernetes': patch
---
Change `formatClusterLink` to be an API and make it async for further customization possibilities.
**BREAKING**
If you have a custom k8s page and used `formatClusterLink` directly, you need to migrate to new `kubernetesClusterLinkFormatterApiRef`
+75 -12
View File
@@ -22,7 +22,7 @@ import { IContainerStatus } from 'kubernetes-models/v1';
import { IdentityApi } from '@backstage/core-plugin-api';
import { IIoK8sApimachineryPkgApisMetaV1ObjectMeta } from '@kubernetes-models/apimachinery/apis/meta/v1/ObjectMeta';
import { IObjectMeta } from '@kubernetes-models/apimachinery/apis/meta/v1/ObjectMeta';
import type { JsonObject } from '@backstage/types';
import { 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';
@@ -38,6 +38,12 @@ import { V1ObjectMeta } from '@kubernetes/client-node';
import { V1Pod } from '@kubernetes/client-node';
import { WorkloadsByEntityRequest } from '@backstage/plugin-kubernetes-common';
// @public (undocumented)
export class AksClusterLinksFormatter implements ClusterLinksFormatter {
// (undocumented)
formatClusterLink(options: ClusterLinksFormatterOptions): Promise<URL>;
}
// @public (undocumented)
export class AksKubernetesAuthProvider implements KubernetesAuthProvider {
constructor(microsoftAuthApi: OAuthApi);
@@ -61,9 +67,10 @@ export const Cluster: ({
export const ClusterContext: React_2.Context<ClusterAttributes>;
// @public (undocumented)
export type ClusterLinksFormatter = (
options: ClusterLinksFormatterOptions,
) => URL;
export interface ClusterLinksFormatter {
// (undocumented)
formatClusterLink(options: ClusterLinksFormatterOptions): Promise<URL>;
}
// @public (undocumented)
export interface ClusterLinksFormatterOptions {
@@ -77,9 +84,6 @@ export interface ClusterLinksFormatterOptions {
object: any;
}
// @public (undocumented)
export const clusterLinksFormatters: Record<string, ClusterLinksFormatter>;
// @public
export type ClusterProps = {
clusterObjects: ClusterObjects;
@@ -125,9 +129,18 @@ export interface CustomResourcesProps {
children?: React_2.ReactNode;
}
// @public (undocumented)
export const DEFAULT_FORMATTER_NAME = 'standard';
// @public
export const DetectedErrorsContext: React_2.Context<DetectedError[]>;
// @public (undocumented)
export class EksClusterLinksFormatter implements ClusterLinksFormatter {
// (undocumented)
formatClusterLink(_options: ClusterLinksFormatterOptions): Promise<URL>;
}
// @public
export const ErrorList: ({
podAndErrors,
@@ -228,11 +241,6 @@ export interface FixDialogProps {
pod: Pod_2;
}
// @public (undocumented)
export function formatClusterLink(
options: FormatClusterLinkOptions,
): string | undefined;
// @public (undocumented)
export type FormatClusterLinkOptions = {
dashboardUrl?: string;
@@ -242,6 +250,18 @@ export type FormatClusterLinkOptions = {
kind: string;
};
// @public (undocumented)
export function getDefaultFormatters(_deps: {}): Record<
string,
ClusterLinksFormatter
>;
// @public (undocumented)
export class GkeClusterLinksFormatter implements ClusterLinksFormatter {
// (undocumented)
formatClusterLink(options: ClusterLinksFormatterOptions): Promise<URL>;
}
// @public (undocumented)
export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider {
constructor(authProvider: OAuthApi);
@@ -415,6 +435,31 @@ export class KubernetesBackendClient implements KubernetesApi {
}): Promise<Response>;
}
// @public (undocumented)
export class KubernetesClusterLinkFormatter
implements KubernetesClusterLinkFormatterApi
{
constructor(options: {
formatters: Record<string, ClusterLinksFormatter>;
defaultFormatterName: string;
});
// (undocumented)
formatClusterLink(
options: FormatClusterLinkOptions,
): Promise<string | undefined>;
}
// @public (undocumented)
export interface KubernetesClusterLinkFormatterApi {
// (undocumented)
formatClusterLink(
options: FormatClusterLinkOptions,
): Promise<string | undefined>;
}
// @public (undocumented)
export const kubernetesClusterLinkFormatterApiRef: ApiRef<KubernetesClusterLinkFormatterApi>;
// @public
export const KubernetesDrawer: ({
open,
@@ -587,6 +632,12 @@ export class OidcKubernetesAuthProvider implements KubernetesAuthProvider {
providerName: string;
}
// @public (undocumented)
export class OpenshiftClusterLinksFormatter {
// (undocumented)
formatClusterLink(options: ClusterLinksFormatterOptions): Promise<URL>;
}
// @public
export const PendingPodContent: ({
pod,
@@ -716,6 +767,12 @@ export type PodsTablesProps = {
children?: React_2.ReactNode;
};
// @public (undocumented)
export class RancherClusterLinksFormatter implements ClusterLinksFormatter {
// (undocumented)
formatClusterLink(options: ClusterLinksFormatterOptions): Promise<URL>;
}
// @public (undocumented)
export const READY_COLUMNS: PodColumns;
@@ -763,6 +820,12 @@ export const ServicesAccordions: ({}: ServicesAccordionsProps) => React_2.JSX.El
// @public (undocumented)
export type ServicesAccordionsProps = {};
// @public (undocumented)
export class StandardClusterLinksFormatter implements ClusterLinksFormatter {
// (undocumented)
formatClusterLink(options: ClusterLinksFormatterOptions): Promise<URL>;
}
// @public
export const useCustomResources: (
entity: Entity,
@@ -0,0 +1,76 @@
/*
* 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 { KubernetesClusterLinkFormatter } from './KubernetesClusterLinkFormatter';
const dashboardUrl = new URL('http://dashboard/url');
describe('KubernetesClusterLinkFormatter', () => {
const formatter = new KubernetesClusterLinkFormatter({
formatters: {
standard: { formatClusterLink: async _ => dashboardUrl },
},
defaultFormatterName: 'standard',
});
describe('formatter.formatClusterLink', () => {
it('should not return an url when there is no dashboard url', async () => {
const url = await formatter.formatClusterLink({
object: {},
kind: 'foo',
});
expect(url).toBeUndefined();
});
it('should return an url even when there is no object', async () => {
const url = await formatter.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', async () => {
await expect(
formatter.formatClusterLink({
dashboardUrl: 'https://k8s.foo.com',
dashboardApp: 'unknownapp',
object: {
metadata: {
name: 'foobar',
namespace: 'bar',
},
},
kind: 'Deployment',
}),
).rejects.toThrow(
"Could not find Kubernetes dashboard app named 'unknownapp'",
);
});
describe('default app', () => {
it('should use default app', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: 'https://k8s.foo.com/',
object: {
metadata: {
name: 'foobar',
namespace: 'bar',
},
},
kind: 'Deployment',
});
expect(url).toBe(dashboardUrl.toString());
});
});
});
});
@@ -0,0 +1,59 @@
/*
* Copyright 2023 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 {
KubernetesClusterLinkFormatterApi,
FormatClusterLinkOptions,
} from './types';
import { ClusterLinksFormatter } from '../types';
/** @public */
export class KubernetesClusterLinkFormatter
implements KubernetesClusterLinkFormatterApi
{
private readonly formatters: Record<string, ClusterLinksFormatter>;
private readonly defaultFormatterName: string;
constructor(options: {
formatters: Record<string, ClusterLinksFormatter>;
defaultFormatterName: string;
}) {
this.formatters = options.formatters;
this.defaultFormatterName = options.defaultFormatterName;
}
async formatClusterLink(options: FormatClusterLinkOptions) {
if (!options.dashboardUrl && !options.dashboardParameters) {
return undefined;
}
if (options.dashboardUrl && !options.object) {
return options.dashboardUrl;
}
const app = options.dashboardApp ?? this.defaultFormatterName;
const formatter = this.formatters[app];
if (!formatter) {
throw new Error(`Could not find Kubernetes dashboard app named '${app}'`);
}
const url = await formatter.formatClusterLink({
dashboardUrl: options.dashboardUrl
? new URL(options.dashboardUrl)
: undefined,
dashboardParameters: options.dashboardParameters,
object: options.object,
kind: options.kind,
});
return url.toString();
}
}
@@ -13,12 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { aksFormatter } from './aks';
import { AksClusterLinksFormatter } from './AksClusterLinksFormatter';
describe('clusterLinks - AKS formatter', () => {
it('should provide a dashboardParameters in the options', () => {
expect(() =>
aksFormatter({
const formatter = new AksClusterLinksFormatter();
it('should provide a dashboardParameters in the options', async () => {
await expect(() =>
formatter.formatClusterLink({
object: {
metadata: {
name: 'foobar',
@@ -27,11 +28,11 @@ describe('clusterLinks - AKS formatter', () => {
},
kind: 'Deployment',
}),
).toThrow('AKS dashboard requires a dashboardParameters option');
).rejects.toThrow('AKS dashboard requires a dashboardParameters option');
});
it('should provide a subscriptionId in the dashboardParameters options', () => {
expect(() =>
aksFormatter({
it('should provide a subscriptionId in the dashboardParameters options', async () => {
await expect(
formatter.formatClusterLink({
dashboardParameters: {
resourceGroup: 'rg-1',
clusterName: 'cluster-1',
@@ -44,13 +45,13 @@ describe('clusterLinks - AKS formatter', () => {
},
kind: 'Deployment',
}),
).toThrow(
).rejects.toThrow(
'AKS dashboard requires a "subscriptionId" of type string in the dashboardParameters option',
);
});
it('should provide a resourceGroup in the dashboardParameters options', () => {
expect(() =>
aksFormatter({
it('should provide a resourceGroup in the dashboardParameters options', async () => {
await expect(
formatter.formatClusterLink({
dashboardParameters: {
subscriptionId: '1234-GUID-5678',
clusterName: 'cluster-1',
@@ -63,13 +64,13 @@ describe('clusterLinks - AKS formatter', () => {
},
kind: 'Deployment',
}),
).toThrow(
).rejects.toThrow(
'AKS dashboard requires a "resourceGroup" of type string in the dashboardParameters option',
);
});
it('should provide a clusterName in the dashboardParameters options', () => {
expect(() =>
aksFormatter({
it('should provide a clusterName in the dashboardParameters options', async () => {
await expect(
formatter.formatClusterLink({
dashboardParameters: {
subscriptionId: '1234-GUID-5678',
resourceGroup: 'us-east1-c',
@@ -82,12 +83,12 @@ describe('clusterLinks - AKS formatter', () => {
},
kind: 'Deployment',
}),
).toThrow(
).rejects.toThrow(
'AKS dashboard requires a "clusterName" of type string in the dashboardParameters option',
);
});
it('should return an url on the cluster with object details', () => {
const url = aksFormatter({
it('should return an url on the cluster with object details', async () => {
const url = await formatter.formatClusterLink({
dashboardParameters: {
subscriptionId: '1234-GUID-5678',
resourceGroup: 'rg-1',
@@ -0,0 +1,59 @@
/*
* 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,
ClusterLinksFormatterOptions,
} from '../../types';
const basePath =
'https://portal.azure.com/#blade/Microsoft_Azure_ContainerService/AksK8ResourceMenuBlade/overview-Deployment/aksClusterId';
const requiredParams = ['subscriptionId', 'resourceGroup', 'clusterName'];
/** @public */
export class AksClusterLinksFormatter implements ClusterLinksFormatter {
async formatClusterLink(options: ClusterLinksFormatterOptions) {
if (!options.dashboardParameters) {
throw new Error('AKS dashboard requires a dashboardParameters option');
}
const args = options.dashboardParameters;
for (const param of requiredParams) {
if (typeof args[param] !== 'string') {
throw new Error(
`AKS dashboard requires a "${param}" of type string in the dashboardParameters option`,
);
}
}
const path = `/subscriptions/${args.subscriptionId}/resourceGroups/${args.resourceGroup}/providers/Microsoft.ContainerService/managedClusters/${args.clusterName}`;
const { name, namespace, uid } = options.object.metadata;
const { selector } = options.object.spec;
const params = {
kind: options.kind,
metadata: { name, namespace, uid },
spec: {
selector,
},
};
return new URL(
`${basePath}/${encodeURIComponent(path)}/resource/${encodeURIComponent(
JSON.stringify(params),
)}`,
);
}
}
@@ -13,12 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { eksFormatter } from './eks';
import { EksClusterLinksFormatter } from './EksClusterLinksFormatter';
describe('clusterLinks - EKS formatter', () => {
it('should return an url on the workloads when there is a namespace only', () => {
expect(() =>
eksFormatter({
const formatter = new EksClusterLinksFormatter();
it('should return an url on the workloads when there is a namespace only', async () => {
await expect(
formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com'),
object: {
metadata: {
@@ -28,6 +30,8 @@ describe('clusterLinks - EKS formatter', () => {
},
kind: 'Deployment',
}),
).toThrow('EKS formatter is not yet implemented. Please, contribute!');
).rejects.toThrow(
'EKS formatter is not yet implemented. Please, contribute!',
);
});
});
@@ -13,8 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ClusterLinksFormatterOptions } from '../../../types/types';
import {
ClusterLinksFormatter,
ClusterLinksFormatterOptions,
} from '../../types';
export function eksFormatter(_options: ClusterLinksFormatterOptions): URL {
throw new Error('EKS formatter is not yet implemented. Please, contribute!');
/** @public */
export class EksClusterLinksFormatter implements ClusterLinksFormatter {
async formatClusterLink(
_options: ClusterLinksFormatterOptions,
): Promise<URL> {
throw new Error(
'EKS formatter is not yet implemented. Please, contribute!',
);
}
}
@@ -13,12 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { gkeFormatter } from './gke';
import { GkeClusterLinksFormatter } from './GkeClusterLinksFormatter';
describe('clusterLinks - GKE formatter', () => {
it('should provide a dashboardParameters in the options', () => {
expect(() =>
gkeFormatter({
const formatter = new GkeClusterLinksFormatter();
it('should provide a dashboardParameters in the options', async () => {
await expect(
formatter.formatClusterLink({
object: {
metadata: {
name: 'foobar',
@@ -27,11 +30,11 @@ describe('clusterLinks - GKE formatter', () => {
},
kind: 'Deployment',
}),
).toThrow('GKE dashboard requires a dashboardParameters option');
).rejects.toThrow('GKE dashboard requires a dashboardParameters option');
});
it('should provide a projectId in the dashboardParameters options', () => {
expect(() =>
gkeFormatter({
it('should provide a projectId in the dashboardParameters options', async () => {
await expect(
formatter.formatClusterLink({
dashboardParameters: {
region: 'us-east1-c',
clusterName: 'cluster-1',
@@ -44,13 +47,13 @@ describe('clusterLinks - GKE formatter', () => {
},
kind: 'Deployment',
}),
).toThrow(
).rejects.toThrow(
'GKE dashboard requires a "projectId" of type string in the dashboardParameters option',
);
});
it('should provide a region in the dashboardParameters options', () => {
expect(() =>
gkeFormatter({
it('should provide a region in the dashboardParameters options', async () => {
await expect(
formatter.formatClusterLink({
dashboardParameters: {
projectId: 'foobar-333614',
clusterName: 'cluster-1',
@@ -63,13 +66,13 @@ describe('clusterLinks - GKE formatter', () => {
},
kind: 'Deployment',
}),
).toThrow(
).rejects.toThrow(
'GKE dashboard requires a "region" of type string in the dashboardParameters option',
);
});
it('should provide a clusterName in the dashboardParameters options', () => {
expect(() =>
gkeFormatter({
it('should provide a clusterName in the dashboardParameters options', async () => {
await expect(() =>
formatter.formatClusterLink({
dashboardParameters: {
projectId: 'foobar-333614',
region: 'us-east1-c',
@@ -82,12 +85,12 @@ describe('clusterLinks - GKE formatter', () => {
},
kind: 'Deployment',
}),
).toThrow(
).rejects.toThrow(
'GKE dashboard requires a "clusterName" of type string in the dashboardParameters option',
);
});
it('should return an url on the cluster when there is a namespace only', () => {
const url = gkeFormatter({
it('should return an url on the cluster when there is a namespace only', async () => {
const url = await formatter.formatClusterLink({
dashboardParameters: {
projectId: 'foobar-333614',
region: 'us-east1-c',
@@ -104,8 +107,8 @@ describe('clusterLinks - GKE formatter', () => {
'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({
it('should return an url on the cluster when the kind is not recognizeed', async () => {
const url = await formatter.formatClusterLink({
dashboardParameters: {
projectId: 'foobar-333614',
region: 'us-east1-c',
@@ -123,8 +126,8 @@ describe('clusterLinks - GKE formatter', () => {
'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({
it('should return an url on the deployment', async () => {
const url = await formatter.formatClusterLink({
dashboardParameters: {
projectId: 'foobar-333614',
region: 'us-east1-c',
@@ -143,8 +146,8 @@ describe('clusterLinks - GKE formatter', () => {
);
});
it('should return an url on the service', () => {
const url = gkeFormatter({
it('should return an url on the service', async () => {
const url = await formatter.formatClusterLink({
dashboardParameters: {
projectId: 'foobar-333614',
region: 'us-east1-c',
@@ -162,8 +165,8 @@ describe('clusterLinks - GKE formatter', () => {
'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({
it('should return an url on the pod', async () => {
const url = await formatter.formatClusterLink({
dashboardParameters: {
projectId: 'foobar-333614',
region: 'us-east1-c',
@@ -181,8 +184,8 @@ describe('clusterLinks - GKE formatter', () => {
'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({
it('should return an url on the ingress', async () => {
const url = await formatter.formatClusterLink({
dashboardParameters: {
projectId: 'foobar-333614',
region: 'us-east1-c',
@@ -200,8 +203,8 @@ describe('clusterLinks - GKE formatter', () => {
'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({
it('should return an url on the deployment for a hpa', async () => {
const url = await formatter.formatClusterLink({
dashboardParameters: {
projectId: 'foobar-333614',
region: 'us-east1-c',
@@ -0,0 +1,73 @@
/*
* 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,
ClusterLinksFormatterOptions,
} from '../../types';
const kindMappings: Record<string, string> = {
deployment: 'deployment',
pod: 'pod',
ingress: 'ingress',
service: 'service',
horizontalpodautoscaler: 'deployment',
};
/** @public */
export class GkeClusterLinksFormatter implements ClusterLinksFormatter {
async formatClusterLink(options: ClusterLinksFormatterOptions): Promise<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: string;
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;
}
}
@@ -13,12 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { openshiftFormatter } from './openshift';
import { OpenshiftClusterLinksFormatter } from './OpenshiftClusterLinksFormatter';
describe('clusterLinks - OpenShift formatter', () => {
it('should provide a dashboardUrl in the options', () => {
expect(() =>
openshiftFormatter({
const formatter = new OpenshiftClusterLinksFormatter();
it('should provide a dashboardUrl in the options', async () => {
await expect(() =>
formatter.formatClusterLink({
object: {
metadata: {
name: 'foobar',
@@ -27,10 +29,10 @@ describe('clusterLinks - OpenShift formatter', () => {
},
kind: 'Deployment',
}),
).toThrow('OpenShift dashboard requires a dashboardUrl option');
).rejects.toThrow('OpenShift dashboard requires a dashboardUrl option');
});
it('should return an url on the workloads when there is a namespace only', () => {
const url = openshiftFormatter({
it('should return an url on the workloads when there is a namespace only', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com'),
object: {
metadata: {
@@ -41,8 +43,8 @@ describe('clusterLinks - OpenShift formatter', () => {
});
expect(url.href).toBe('https://k8s.foo.com/k8s/cluster/projects/bar');
});
it('should return an url on the workloads when the kind is not recognizeed', () => {
const url = openshiftFormatter({
it('should return an url on the workloads when the kind is not recognizeed', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com'),
object: {
metadata: {
@@ -54,8 +56,8 @@ describe('clusterLinks - OpenShift formatter', () => {
});
expect(url.href).toBe('https://k8s.foo.com/k8s/cluster/projects/bar');
});
it('should return an url on the deployment', () => {
const url = openshiftFormatter({
it('should return an url on the deployment', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/'),
object: {
metadata: {
@@ -67,8 +69,8 @@ describe('clusterLinks - OpenShift formatter', () => {
});
expect(url.href).toBe('https://k8s.foo.com/k8s/ns/bar/deployments/foobar');
});
it('should return an url on the deployment and keep the path prefix 1', () => {
const url = openshiftFormatter({
it('should return an url on the deployment and keep the path prefix 1', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/some/prefix/'),
object: {
metadata: {
@@ -82,8 +84,8 @@ describe('clusterLinks - OpenShift formatter', () => {
'https://k8s.foo.com/some/prefix/k8s/ns/bar/deployments/foobar',
);
});
it('should return an url on the deployment and keep the path prefix 2', () => {
const url = openshiftFormatter({
it('should return an url on the deployment and keep the path prefix 2', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/some/prefix'),
object: {
metadata: {
@@ -97,8 +99,8 @@ describe('clusterLinks - OpenShift formatter', () => {
'https://k8s.foo.com/some/prefix/k8s/ns/bar/deployments/foobar',
);
});
it('should return an url on the service', () => {
const url = openshiftFormatter({
it('should return an url on the service', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/'),
object: {
metadata: {
@@ -110,8 +112,8 @@ describe('clusterLinks - OpenShift formatter', () => {
});
expect(url.href).toBe('https://k8s.foo.com/k8s/ns/bar/services/foobar');
});
it('should return an url on the ingress', () => {
const url = openshiftFormatter({
it('should return an url on the ingress', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/'),
object: {
metadata: {
@@ -123,8 +125,8 @@ describe('clusterLinks - OpenShift formatter', () => {
});
expect(url.href).toBe('https://k8s.foo.com/k8s/ns/bar/ingresses/foobar');
});
it('should return an url on the deployment for a hpa', () => {
const url = openshiftFormatter({
it('should return an url on the deployment for a hpa', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/'),
object: {
metadata: {
@@ -138,8 +140,8 @@ describe('clusterLinks - OpenShift formatter', () => {
'https://k8s.foo.com/k8s/ns/bar/horizontalpodautoscalers/foobar',
);
});
it('should return an url on the PV', () => {
const url = openshiftFormatter({
it('should return an url on the PV', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/'),
object: {
metadata: {
@@ -0,0 +1,60 @@
/*
* 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';
const kindMappings: Record<string, string> = {
deployment: 'deployments',
ingress: 'ingresses',
service: 'services',
horizontalpodautoscaler: 'horizontalpodautoscalers',
persistentvolume: 'persistentvolumes',
};
/** @public */
export class OpenshiftClusterLinksFormatter {
async formatClusterLink(options: ClusterLinksFormatterOptions): Promise<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(
options.object.metadata?.namespace ?? '',
);
const validKind = kindMappings[options.kind.toLocaleLowerCase('en-US')];
if (!basePath.pathname.endsWith('/')) {
// a dashboard url with a path should end with a slash otherwise
// the new combined URL will replace the last segment with the appended path!
// https://foobar.com/abc/def + k8s/cluster/projects/test --> https://foobar.com/abc/k8s/cluster/projects/test
// https://foobar.com/abc/def/ + k8s/cluster/projects/test --> https://foobar.com/abc/def/k8s/cluster/projects/test
basePath.pathname += '/';
}
let path = '';
if (namespace) {
if (name && validKind) {
path = `k8s/ns/${namespace}/${validKind}/${name}`;
} else {
path = `k8s/cluster/projects/${namespace}`;
}
} else if (validKind) {
path = `k8s/cluster/${validKind}`;
if (name) {
path += `/${name}`;
}
}
return new URL(path, basePath);
}
}
@@ -13,12 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { rancherFormatter } from './rancher';
import { RancherClusterLinksFormatter } from './RancherClusterLinksFormatter';
describe('clusterLinks - rancher formatter', () => {
it('should provide a dashboardUrl in the options', () => {
expect(() =>
rancherFormatter({
const formatter = new RancherClusterLinksFormatter();
it('should provide a dashboardUrl in the options', async () => {
await expect(() =>
formatter.formatClusterLink({
object: {
metadata: {
name: 'foobar',
@@ -27,10 +29,10 @@ describe('clusterLinks - rancher formatter', () => {
},
kind: 'Deployment',
}),
).toThrow('Rancher dashboard requires a dashboardUrl option');
).rejects.toThrow('Rancher dashboard requires a dashboardUrl option');
});
it('should return a url on the workloads when there is a namespace only', () => {
const url = rancherFormatter({
it('should return a url on the workloads when there is a namespace only', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com'),
object: {
metadata: {
@@ -41,8 +43,8 @@ describe('clusterLinks - rancher formatter', () => {
});
expect(url.href).toBe('https://k8s.foo.com/explorer/workload');
});
it('should return a url on the workloads when the kind is not recognized', () => {
const url = rancherFormatter({
it('should return a url on the workloads when the kind is not recognized', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com'),
object: {
metadata: {
@@ -54,8 +56,8 @@ describe('clusterLinks - rancher formatter', () => {
});
expect(url.href).toBe('https://k8s.foo.com/explorer/workload');
});
it('should return a url on the deployment', () => {
const url = rancherFormatter({
it('should return a url on the deployment', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/'),
object: {
metadata: {
@@ -69,8 +71,8 @@ describe('clusterLinks - rancher formatter', () => {
'https://k8s.foo.com/explorer/apps.deployment/bar/foobar',
);
});
it('should return a url on the service', () => {
const url = rancherFormatter({
it('should return a url on the service', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/'),
object: {
metadata: {
@@ -82,8 +84,8 @@ describe('clusterLinks - rancher formatter', () => {
});
expect(url.href).toBe('https://k8s.foo.com/explorer/service/bar/foobar');
});
it('should return a url on the ingress', () => {
const url = rancherFormatter({
it('should return a url on the ingress', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/'),
object: {
metadata: {
@@ -97,8 +99,8 @@ describe('clusterLinks - rancher formatter', () => {
'https://k8s.foo.com/explorer/networking.k8s.io.ingress/bar/foobar',
);
});
it('should return a url on the deployment for a hpa', () => {
const url = rancherFormatter({
it('should return a url on the deployment for a hpa', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/'),
object: {
metadata: {
@@ -112,8 +114,8 @@ describe('clusterLinks - rancher formatter', () => {
'https://k8s.foo.com/explorer/autoscaling.horizontalpodautoscaler/bar/foobar',
);
});
it('should support subpaths in dashboardUrl', () => {
const url = rancherFormatter({
it('should support subpaths in dashboardUrl', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/dashboard/c/c-28a4b/'),
object: {
metadata: {
@@ -0,0 +1,55 @@
/*
* 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,
ClusterLinksFormatterOptions,
} from '../../types';
const kindMappings: Record<string, string> = {
deployment: 'apps.deployment',
ingress: 'networking.k8s.io.ingress',
service: 'service',
horizontalpodautoscaler: 'autoscaling.horizontalpodautoscaler',
};
/** @public */
export class RancherClusterLinksFormatter implements ClusterLinksFormatter {
async formatClusterLink(options: ClusterLinksFormatterOptions): Promise<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(
options.object.metadata?.namespace ?? '',
);
const validKind = kindMappings[options.kind.toLocaleLowerCase('en-US')];
if (!basePath.pathname.endsWith('/')) {
// a dashboard url with a path should end with a slash otherwise
// the new combined URL will replace the last segment with the appended path!
// https://foobar.com/abc/def + explorer/service/test --> https://foobar.com/abc/explorer/service/test
// https://foobar.com/abc/def/ + explorer/service/test --> https://foobar.com/abc/def/explorer/service/test
basePath.pathname += '/';
}
let path = '';
if (validKind && name && namespace) {
path = `explorer/${validKind}/${namespace}/${name}`;
} else if (namespace) {
path = 'explorer/workload';
}
return new URL(path, basePath);
}
}
@@ -13,7 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { standardFormatter } from './standard';
import { StandardClusterLinksFormatter } from './StandardClusterLinksFormatter';
function formatUrl(url: URL) {
// Note that we can't rely on 'url.href' since it will put the search before the hash
@@ -23,9 +24,10 @@ function formatUrl(url: URL) {
}
describe('clusterLinks - standard formatter', () => {
it('should provide a dashboardUrl in the options', () => {
expect(() =>
standardFormatter({
const formatter = new StandardClusterLinksFormatter();
it('should provide a dashboardUrl in the options', async () => {
await expect(() =>
formatter.formatClusterLink({
object: {
metadata: {
name: 'foobar',
@@ -34,10 +36,10 @@ describe('clusterLinks - standard formatter', () => {
},
kind: 'Deployment',
}),
).toThrow('standard dashboard requires a dashboardUrl option');
).rejects.toThrow('standard dashboard requires a dashboardUrl option');
});
it('should return an url on the workloads when there is a namespace only', () => {
const url = standardFormatter({
it('should return an url on the workloads when there is a namespace only', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com'),
object: {
metadata: {
@@ -50,8 +52,8 @@ describe('clusterLinks - standard formatter', () => {
'https://k8s.foo.com/#/workloads?namespace=bar',
);
});
it('should return an url on the workloads when the kind is not recognizeed', () => {
const url = standardFormatter({
it('should return an url on the workloads when the kind is not recognizeed', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com'),
object: {
metadata: {
@@ -65,8 +67,8 @@ describe('clusterLinks - standard formatter', () => {
'https://k8s.foo.com/#/workloads?namespace=bar',
);
});
it('should return an url on the deployment', () => {
const url = standardFormatter({
it('should return an url on the deployment', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/'),
object: {
metadata: {
@@ -80,8 +82,8 @@ describe('clusterLinks - standard formatter', () => {
'https://k8s.foo.com/#/deployment/bar/foobar?namespace=bar',
);
});
it('should return an url on the pod', () => {
const url = standardFormatter({
it('should return an url on the pod', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/'),
object: {
metadata: {
@@ -95,8 +97,8 @@ describe('clusterLinks - standard formatter', () => {
'https://k8s.foo.com/#/pod/bar/foobar?namespace=bar',
);
});
it('should return an url on the deployment with a prefix 1', () => {
const url = standardFormatter({
it('should return an url on the deployment with a prefix 1', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/some/prefix'),
object: {
metadata: {
@@ -110,8 +112,8 @@ describe('clusterLinks - standard formatter', () => {
'https://k8s.foo.com/some/prefix/#/deployment/bar/foobar?namespace=bar',
);
});
it('should return an url on the deployment with a prefix 2', () => {
const url = standardFormatter({
it('should return an url on the deployment with a prefix 2', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/some/prefix/'),
object: {
metadata: {
@@ -125,8 +127,8 @@ describe('clusterLinks - standard formatter', () => {
'https://k8s.foo.com/some/prefix/#/deployment/bar/foobar?namespace=bar',
);
});
it('should return an url on the deployment properly url encoded', () => {
const url = standardFormatter({
it('should return an url on the deployment properly url encoded', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/'),
object: {
metadata: {
@@ -140,8 +142,8 @@ describe('clusterLinks - standard formatter', () => {
'https://k8s.foo.com/#/deployment/bar%20bar/foobar?namespace=bar%20bar',
);
});
it('should return an url on the service', () => {
const url = standardFormatter({
it('should return an url on the service', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/'),
object: {
metadata: {
@@ -155,8 +157,8 @@ describe('clusterLinks - standard formatter', () => {
'https://k8s.foo.com/#/service/bar/foobar?namespace=bar',
);
});
it('should return an url on the ingress', () => {
const url = standardFormatter({
it('should return an url on the ingress', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/'),
object: {
metadata: {
@@ -170,8 +172,8 @@ describe('clusterLinks - standard formatter', () => {
'https://k8s.foo.com/#/ingress/bar/foobar?namespace=bar',
);
});
it('should return an url on the deployment for a hpa', () => {
const url = standardFormatter({
it('should return an url on the deployment for a hpa', async () => {
const url = await formatter.formatClusterLink({
dashboardUrl: new URL('https://k8s.foo.com/'),
object: {
metadata: {
@@ -0,0 +1,56 @@
/*
* 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,
ClusterLinksFormatterOptions,
} from '../../types';
const kindMappings: Record<string, string> = {
deployment: 'deployment',
pod: 'pod',
ingress: 'ingress',
service: 'service',
horizontalpodautoscaler: 'deployment',
statefulset: 'statefulset',
};
/** @public */
export class StandardClusterLinksFormatter implements ClusterLinksFormatter {
async formatClusterLink(options: ClusterLinksFormatterOptions): Promise<URL> {
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(
options.object.metadata?.namespace ?? '',
);
const validKind = kindMappings[options.kind.toLocaleLowerCase('en-US')];
if (!result.pathname.endsWith('/')) {
result.pathname += '/';
}
if (validKind && name && namespace) {
result.hash = `/${validKind}/${namespace}/${name}`;
} else if (namespace) {
result.hash = '/workloads';
}
if (namespace) {
// Note that Angular SPA requires a hash and the query parameter should be part of it
result.hash += `?namespace=${namespace}`;
}
return result;
}
}
@@ -0,0 +1,50 @@
/*
* Copyright 2023 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 { AksClusterLinksFormatter } from './AksClusterLinksFormatter';
import { ClusterLinksFormatter } from '../../types';
import { EksClusterLinksFormatter } from './EksClusterLinksFormatter';
import { GkeClusterLinksFormatter } from './GkeClusterLinksFormatter';
import { StandardClusterLinksFormatter } from './StandardClusterLinksFormatter';
import { OpenshiftClusterLinksFormatter } from './OpenshiftClusterLinksFormatter';
import { RancherClusterLinksFormatter } from './RancherClusterLinksFormatter';
export {
StandardClusterLinksFormatter,
AksClusterLinksFormatter,
EksClusterLinksFormatter,
GkeClusterLinksFormatter,
OpenshiftClusterLinksFormatter,
RancherClusterLinksFormatter,
};
/** @public */
export const DEFAULT_FORMATTER_NAME = 'standard';
/** @public */
export function getDefaultFormatters(_deps: {}): Record<
string,
ClusterLinksFormatter
> {
return {
standard: new StandardClusterLinksFormatter(),
aks: new AksClusterLinksFormatter(),
eks: new EksClusterLinksFormatter(),
gke: new GkeClusterLinksFormatter(),
openshift: new OpenshiftClusterLinksFormatter(),
rancher: new RancherClusterLinksFormatter(),
};
}
+13 -2
View File
@@ -14,7 +14,18 @@
* limitations under the License.
*/
export { kubernetesApiRef, kubernetesProxyApiRef } from './types';
export type { KubernetesApi, KubernetesProxyApi } from './types';
export {
kubernetesApiRef,
kubernetesProxyApiRef,
kubernetesClusterLinkFormatterApiRef,
} from './types';
export type {
KubernetesApi,
KubernetesProxyApi,
FormatClusterLinkOptions,
KubernetesClusterLinkFormatterApi,
} from './types';
export { KubernetesBackendClient } from './KubernetesBackendClient';
export { KubernetesClusterLinkFormatter } from './KubernetesClusterLinkFormatter';
export { KubernetesProxyClient } from './KubernetesProxyClient';
export * from './formatters';
+25
View File
@@ -22,6 +22,7 @@ import {
} from '@backstage/plugin-kubernetes-common';
import { createApiRef } from '@backstage/core-plugin-api';
import { Event } from 'kubernetes-models/v1';
import { JsonObject } from '@backstage/types';
/** @public */
export const kubernetesApiRef = createApiRef<KubernetesApi>({
@@ -33,6 +34,12 @@ export const kubernetesProxyApiRef = createApiRef<KubernetesProxyApi>({
id: 'plugin.kubernetes.proxy-service',
});
/** @public */
export const kubernetesClusterLinkFormatterApiRef =
createApiRef<KubernetesClusterLinkFormatterApi>({
id: 'plugin.kubernetes.cluster-link-formatter-service',
});
/** @public */
export interface KubernetesApi {
getObjectsByEntity(
@@ -82,3 +89,21 @@ export interface KubernetesProxyApi {
namespace: string;
}): Promise<Event[]>;
}
/**
* @public
*/
export type FormatClusterLinkOptions = {
dashboardUrl?: string;
dashboardApp?: string;
dashboardParameters?: JsonObject;
object: any;
kind: string;
};
/** @public */
export interface KubernetesClusterLinkFormatterApi {
formatClusterLink(
options: FormatClusterLinkOptions,
): Promise<string | undefined>;
}
@@ -15,16 +15,20 @@
*/
import React from 'react';
import * as oneCronJobsFixture from '../../__fixtures__/1-cronjobs.json';
import { renderInTestApp } from '@backstage/test-utils';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { CronJobDrawer } from './CronJobsDrawer';
import { kubernetesClusterLinkFormatterApiRef } from '../../api';
describe('CronJobDrawer', () => {
it('should render cronJob drawer', async () => {
const { getByText, getAllByText } = await renderInTestApp(
<CronJobDrawer
cronJob={(oneCronJobsFixture as any).cronJobs[0]}
expanded
/>,
<TestApiProvider apis={[[kubernetesClusterLinkFormatterApiRef, {}]]}>
<CronJobDrawer
cronJob={(oneCronJobsFixture as any).cronJobs[0]}
expanded
/>
,
</TestApiProvider>,
);
expect(getAllByText('dice-roller-cronjob')).toHaveLength(2);
@@ -16,16 +16,24 @@
import React from 'react';
import * as deployments from '../../__fixtures__/2-deployments.json';
import { renderInTestApp, textContentMatcher } from '@backstage/test-utils';
import {
renderInTestApp,
TestApiProvider,
textContentMatcher,
} from '@backstage/test-utils';
import { DeploymentDrawer } from './DeploymentDrawer';
import { kubernetesClusterLinkFormatterApiRef } from '../../api';
describe('DeploymentDrawer', () => {
it('should render deployment drawer', async () => {
const { getByText, getAllByText } = await renderInTestApp(
<DeploymentDrawer
deployment={(deployments as any).deployments[0]}
expanded
/>,
<TestApiProvider apis={[[kubernetesClusterLinkFormatterApiRef, {}]]}>
<DeploymentDrawer
deployment={(deployments as any).deployments[0]}
expanded
/>
,
</TestApiProvider>,
);
expect(getAllByText('dice-roller')).toHaveLength(2);
@@ -53,13 +61,16 @@ describe('DeploymentDrawer', () => {
it('should render deployment drawer without namespace', async () => {
const deployment = (deployments as any).deployments[0];
const { queryByText } = await renderInTestApp(
<DeploymentDrawer
deployment={{
...deployment,
metadata: { ...deployment.metadata, namespace: undefined },
}}
expanded
/>,
<TestApiProvider apis={[[kubernetesClusterLinkFormatterApiRef, {}]]}>
<DeploymentDrawer
deployment={{
...deployment,
metadata: { ...deployment.metadata, namespace: undefined },
}}
expanded
/>
,
</TestApiProvider>,
);
expect(queryByText('namespace: default')).not.toBeInTheDocument();
@@ -17,15 +17,18 @@
import React from 'react';
import { screen } from '@testing-library/react';
import * as hpas from './__fixtures__/horizontalpodautoscalers.json';
import { renderInTestApp } from '@backstage/test-utils';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { HorizontalPodAutoscalerDrawer } from './HorizontalPodAutoscalerDrawer';
import { kubernetesClusterLinkFormatterApiRef } from '../../api';
describe('HorizontalPodAutoscalersDrawer', () => {
it('should render hpa drawer', async () => {
await renderInTestApp(
<HorizontalPodAutoscalerDrawer hpa={hpas[0] as any} expanded>
<h1>CHILD</h1>
</HorizontalPodAutoscalerDrawer>,
<TestApiProvider apis={[[kubernetesClusterLinkFormatterApiRef, {}]]}>
<HorizontalPodAutoscalerDrawer hpa={hpas[0] as any} expanded>
<h1>CHILD</h1>
</HorizontalPodAutoscalerDrawer>
</TestApiProvider>,
);
expect(screen.getByText('dice-roller')).toBeInTheDocument();
@@ -14,16 +14,23 @@
* limitations under the License.
*/
import { renderInTestApp, textContentMatcher } from '@backstage/test-utils';
import {
renderInTestApp,
TestApiProvider,
textContentMatcher,
} from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { IngressDrawer } from './IngressDrawer';
import * as ingresses from './__fixtures__/2-ingresses.json';
import { kubernetesClusterLinkFormatterApiRef } from '../../api';
describe('IngressDrawer', () => {
it('should render ingress drawer', async () => {
await renderInTestApp(
<IngressDrawer ingress={(ingresses as any).ingresses[0]} expanded />,
<TestApiProvider apis={[[kubernetesClusterLinkFormatterApiRef, {}]]}>
<IngressDrawer ingress={(ingresses as any).ingresses[0]} expanded />
</TestApiProvider>,
);
expect(screen.getAllByText('awesome-service')).toHaveLength(4);
@@ -15,13 +15,16 @@
*/
import React from 'react';
import * as oneCronJobsFixture from '../../__fixtures__/1-cronjobs.json';
import { renderInTestApp } from '@backstage/test-utils';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { JobDrawer } from './JobsDrawer';
import { kubernetesClusterLinkFormatterApiRef } from '../../api';
describe('JobDrawer', () => {
it('should render job drawer', async () => {
const { getByText, getAllByText } = await renderInTestApp(
<JobDrawer job={(oneCronJobsFixture as any).jobs[0]} expanded />,
<TestApiProvider apis={[[kubernetesClusterLinkFormatterApiRef, {}]]}>
<JobDrawer job={(oneCronJobsFixture as any).jobs[0]} expanded />,
</TestApiProvider>,
);
expect(getAllByText('dice-roller-cronjob-1637025000')).toHaveLength(2);
@@ -37,10 +37,11 @@ import {
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';
import { ManifestYaml } from './ManifestYaml';
import { useApi } from '@backstage/core-plugin-api';
import { kubernetesClusterLinkFormatterApiRef } from '../../api';
import useAsync from 'react-use/lib/useAsync';
const useDrawerStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -147,20 +148,6 @@ 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 KubernetesStructuredMetadataTableDrawerContent = <
T extends KubernetesDrawerable,
>({
@@ -171,15 +158,20 @@ const KubernetesStructuredMetadataTableDrawerContent = <
}: KubernetesStructuredMetadataTableDrawerContentProps<T>) => {
const [isYaml, setIsYaml] = useState<boolean>(false);
const formatter = useApi(kubernetesClusterLinkFormatterApiRef);
const classes = useDrawerContentStyles();
const cluster = useContext(ClusterContext);
const { clusterLink, errorMessage } = tryFormatClusterLink({
dashboardUrl: cluster.dashboardUrl,
dashboardApp: cluster.dashboardApp,
dashboardParameters: cluster.dashboardParameters,
object,
kind,
});
const { value: clusterLink, error } = useAsync(
async () =>
formatter.formatClusterLink({
dashboardUrl: cluster.dashboardUrl,
dashboardApp: cluster.dashboardApp,
dashboardParameters: cluster.dashboardParameters,
object,
kind,
}),
[cluster, object, kind, formatter],
);
return (
<>
@@ -221,9 +213,12 @@ const KubernetesStructuredMetadataTableDrawerContent = <
</Grid>
</Grid>
</div>
{errorMessage && (
{error && (
<div className={classes.errorMessage}>
<LinkErrorPanel cluster={cluster} errorMessage={errorMessage} />
<LinkErrorPanel
cluster={cluster}
errorMessage={error.message || error.toString()}
/>
</div>
)}
<div className={classes.options}>
@@ -17,13 +17,20 @@
import React from 'react';
import { screen } from '@testing-library/react';
import * as services from './__fixtures__/2-services.json';
import { textContentMatcher, renderInTestApp } from '@backstage/test-utils';
import {
textContentMatcher,
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
import { ServiceDrawer } from './ServiceDrawer';
import { kubernetesClusterLinkFormatterApiRef } from '../../api';
describe('ServiceDrawer', () => {
it('should render deployment drawer', async () => {
await renderInTestApp(
<ServiceDrawer service={(services as any).services[0]} expanded />,
<TestApiProvider apis={[[kubernetesClusterLinkFormatterApiRef, {}]]}>
<ServiceDrawer service={(services as any).services[0]} expanded />,
</TestApiProvider>,
);
expect(screen.getAllByText('awesome-service-grpc')).toHaveLength(2);
@@ -16,16 +16,24 @@
import React from 'react';
import * as statefulsets from '../../__fixtures__/2-statefulsets.json';
import { renderInTestApp, textContentMatcher } from '@backstage/test-utils';
import {
renderInTestApp,
TestApiProvider,
textContentMatcher,
} from '@backstage/test-utils';
import { StatefulSetDrawer } from './StatefulSetDrawer';
import { kubernetesClusterLinkFormatterApiRef } from '../../api';
describe('StatefulSetDrawer', () => {
it('should render statefulset drawer', async () => {
const { getByText, getAllByText } = await renderInTestApp(
<StatefulSetDrawer
statefulset={(statefulsets as any).statefulsets[0]}
expanded
/>,
<TestApiProvider apis={[[kubernetesClusterLinkFormatterApiRef, {}]]}>
<StatefulSetDrawer
statefulset={(statefulsets as any).statefulsets[0]}
expanded
/>
,
</TestApiProvider>,
);
expect(getAllByText('dice-roller')).toHaveLength(4);
@@ -55,13 +63,16 @@ describe('StatefulSetDrawer', () => {
it('should render statefulset drawer without namespace', async () => {
const statefulset = (statefulsets as any).statefulsets[0];
const { queryByText } = await renderInTestApp(
<StatefulSetDrawer
statefulset={{
...statefulset,
metadata: { ...statefulset.metadata, namespace: undefined },
}}
expanded
/>,
<TestApiProvider apis={[[kubernetesClusterLinkFormatterApiRef, {}]]}>
<StatefulSetDrawer
statefulset={{
...statefulset,
metadata: { ...statefulset.metadata, namespace: undefined },
}}
expanded
/>
,
</TestApiProvider>,
);
expect(queryByText('namespace: default')).not.toBeInTheDocument();
-1
View File
@@ -27,5 +27,4 @@ export * from './hooks';
export * from './api';
export * from './kubernetes-auth-provider';
export * from './components';
export * from './utils';
export * from './types';
+3 -3
View File
@@ -29,6 +29,6 @@ export interface ClusterLinksFormatterOptions {
/**
* @public
*/
export type ClusterLinksFormatter = (
options: ClusterLinksFormatterOptions,
) => URL;
export interface ClusterLinksFormatter {
formatClusterLink(options: ClusterLinksFormatterOptions): Promise<URL>;
}
@@ -1,158 +0,0 @@
/*
* 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',
}),
).toThrow("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',
);
});
});
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',
);
});
});
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',
);
});
});
});
});
@@ -1,55 +0,0 @@
/*
* 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 type { JsonObject } from '@backstage/types';
import { defaultFormatterName, clusterLinksFormatters } from './formatters';
/**
* @public
*/
export type FormatClusterLinkOptions = {
dashboardUrl?: string;
dashboardApp?: string;
dashboardParameters?: JsonObject;
object: any;
kind: string;
};
/**
* @public
*/
export function formatClusterLink(options: FormatClusterLinkOptions) {
if (!options.dashboardUrl && !options.dashboardParameters) {
return undefined;
}
if (options.dashboardUrl && !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: options.dashboardUrl
? new URL(options.dashboardUrl)
: undefined,
dashboardParameters: options.dashboardParameters,
object: options.object,
kind: options.kind,
});
return url.toString();
}
@@ -1,53 +0,0 @@
/*
* 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 basePath =
'https://portal.azure.com/#blade/Microsoft_Azure_ContainerService/AksK8ResourceMenuBlade/overview-Deployment/aksClusterId';
const requiredParams = ['subscriptionId', 'resourceGroup', 'clusterName'];
export function aksFormatter(options: ClusterLinksFormatterOptions): URL {
if (!options.dashboardParameters) {
throw new Error('AKS dashboard requires a dashboardParameters option');
}
const args = options.dashboardParameters;
for (const param of requiredParams) {
if (typeof args[param] !== 'string') {
throw new Error(
`AKS dashboard requires a "${param}" of type string in the dashboardParameters option`,
);
}
}
const path = `/subscriptions/${args.subscriptionId}/resourceGroups/${args.resourceGroup}/providers/Microsoft.ContainerService/managedClusters/${args.clusterName}`;
const { name, namespace, uid } = options.object.metadata;
const { selector } = options.object.spec;
const params = {
kind: options.kind,
metadata: { name, namespace, uid },
spec: {
selector,
},
};
return new URL(
`${basePath}/${encodeURIComponent(path)}/resource/${encodeURIComponent(
JSON.stringify(params),
)}`,
);
}
@@ -1,67 +0,0 @@
/*
* 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',
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;
}
@@ -1,35 +0,0 @@
/*
* 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';
/**
* @public
*/
export const clusterLinksFormatters: Record<string, ClusterLinksFormatter> = {
standard: standardFormatter,
rancher: rancherFormatter,
openshift: openshiftFormatter,
aks: aksFormatter,
eks: eksFormatter,
gke: gkeFormatter,
};
export const defaultFormatterName = 'standard';
@@ -1,57 +0,0 @@
/*
* 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: 'deployments',
ingress: 'ingresses',
service: 'services',
horizontalpodautoscaler: 'horizontalpodautoscalers',
persistentvolume: 'persistentvolumes',
};
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(
options.object.metadata?.namespace ?? '',
);
const validKind = kindMappings[options.kind.toLocaleLowerCase('en-US')];
if (!basePath.pathname.endsWith('/')) {
// a dashboard url with a path should end with a slash otherwise
// the new combined URL will replace the last segment with the appended path!
// https://foobar.com/abc/def + k8s/cluster/projects/test --> https://foobar.com/abc/k8s/cluster/projects/test
// https://foobar.com/abc/def/ + k8s/cluster/projects/test --> https://foobar.com/abc/def/k8s/cluster/projects/test
basePath.pathname += '/';
}
let path = '';
if (namespace) {
if (name && validKind) {
path = `k8s/ns/${namespace}/${validKind}/${name}`;
} else {
path = `k8s/cluster/projects/${namespace}`;
}
} else if (validKind) {
path = `k8s/cluster/${validKind}`;
if (name) {
path += `/${name}`;
}
}
return new URL(path, basePath);
}
@@ -1,49 +0,0 @@
/*
* 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: 'apps.deployment',
ingress: 'networking.k8s.io.ingress',
service: 'service',
horizontalpodautoscaler: 'autoscaling.horizontalpodautoscaler',
};
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(
options.object.metadata?.namespace ?? '',
);
const validKind = kindMappings[options.kind.toLocaleLowerCase('en-US')];
if (!basePath.pathname.endsWith('/')) {
// a dashboard url with a path should end with a slash otherwise
// the new combined URL will replace the last segment with the appended path!
// https://foobar.com/abc/def + explorer/service/test --> https://foobar.com/abc/explorer/service/test
// https://foobar.com/abc/def/ + explorer/service/test --> https://foobar.com/abc/def/explorer/service/test
basePath.pathname += '/';
}
let path = '';
if (validKind && name && namespace) {
path = `explorer/${validKind}/${namespace}/${name}`;
} else if (namespace) {
path = 'explorer/workload';
}
return new URL(path, basePath);
}
@@ -1,50 +0,0 @@
/*
* 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',
pod: 'pod',
ingress: 'ingress',
service: 'service',
horizontalpodautoscaler: 'deployment',
statefulset: 'statefulset',
};
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(
options.object.metadata?.namespace ?? '',
);
const validKind = kindMappings[options.kind.toLocaleLowerCase('en-US')];
if (!result.pathname.endsWith('/')) {
result.pathname += '/';
}
if (validKind && name && namespace) {
result.hash = `/${validKind}/${namespace}/${name}`;
} else if (namespace) {
result.hash = '/workloads';
}
if (namespace) {
// Note that Angular SPA requires a hash and the query parameter should be part of it
result.hash += `?namespace=${namespace}`;
}
return result;
}
@@ -1,21 +0,0 @@
/*
* 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,
type FormatClusterLinkOptions,
} from './formatClusterLink';
export { clusterLinksFormatters } from './formatters';
@@ -1,16 +0,0 @@
/*
* 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';
+15
View File
@@ -20,6 +20,10 @@ import {
kubernetesAuthProvidersApiRef,
KubernetesAuthProviders,
KubernetesProxyClient,
kubernetesClusterLinkFormatterApiRef,
getDefaultFormatters,
KubernetesClusterLinkFormatter,
DEFAULT_FORMATTER_NAME,
} from '@backstage/plugin-kubernetes-react';
import {
createApiFactory,
@@ -97,6 +101,17 @@ export const kubernetesPlugin = createPlugin({
});
},
}),
createApiFactory({
api: kubernetesClusterLinkFormatterApiRef,
deps: {},
factory: deps => {
const formatters = getDefaultFormatters(deps);
return new KubernetesClusterLinkFormatter({
formatters,
defaultFormatterName: DEFAULT_FORMATTER_NAME,
});
},
}),
],
routes: {
entityContent: rootCatalogKubernetesRouteRef,