[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:
@@ -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`
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+20
-19
@@ -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),
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+9
-5
@@ -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
-3
@@ -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!',
|
||||
);
|
||||
}
|
||||
}
|
||||
+34
-31
@@ -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;
|
||||
}
|
||||
}
|
||||
+25
-23
@@ -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);
|
||||
}
|
||||
}
|
||||
+21
-19
@@ -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);
|
||||
}
|
||||
}
|
||||
+27
-25
@@ -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(),
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
+23
-12
@@ -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();
|
||||
|
||||
+7
-4
@@ -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);
|
||||
|
||||
+20
-25
@@ -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);
|
||||
|
||||
+23
-12
@@ -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();
|
||||
|
||||
@@ -27,5 +27,4 @@ export * from './hooks';
|
||||
export * from './api';
|
||||
export * from './kubernetes-auth-provider';
|
||||
export * from './components';
|
||||
export * from './utils';
|
||||
export * from './types';
|
||||
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user