Merge pull request #29570 from yolossn/headlamp-formatter
feat(kubernetes): add headlamp dashboard formatter
This commit is contained in:
@@ -283,6 +283,12 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider {
|
||||
// @public (undocumented)
|
||||
export const GroupedResponsesContext: Context<GroupedResponses>;
|
||||
|
||||
// @public (undocumented)
|
||||
export class HeadlampClusterLinksFormatter implements ClusterLinksFormatter {
|
||||
// (undocumented)
|
||||
formatClusterLink(options: ClusterLinksFormatterOptions): Promise<URL>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const HorizontalPodAutoscalerDrawer: (props: {
|
||||
hpa: V2HorizontalPodAutoscaler;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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 { HeadlampClusterLinksFormatter } from './HeadlampClusterLinksFormatter';
|
||||
import { ClusterLinksFormatterOptions } from '../../types';
|
||||
|
||||
describe('HeadlampClusterLinksFormatter', () => {
|
||||
let formatter: HeadlampClusterLinksFormatter;
|
||||
|
||||
beforeEach(() => {
|
||||
formatter = new HeadlampClusterLinksFormatter();
|
||||
// Mock window.location.origin
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { origin: 'http://localhost:3000' },
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('formats internal dashboard link correctly', async () => {
|
||||
const options: ClusterLinksFormatterOptions = {
|
||||
dashboardParameters: {
|
||||
internal: true,
|
||||
headlampRoute: '/headlamp',
|
||||
clusterName: 'test-cluster',
|
||||
},
|
||||
object: { metadata: { name: 'test-pod', namespace: 'default' } },
|
||||
kind: 'Pod',
|
||||
};
|
||||
|
||||
const result = await formatter.formatClusterLink(options);
|
||||
expect(result.toString()).toBe(
|
||||
'http://localhost:3000/headlamp?to=%2Fc%2Ftest-cluster%2Fpods%2Fdefault%2Ftest-pod',
|
||||
);
|
||||
});
|
||||
|
||||
it('formats external dashboard link correctly', async () => {
|
||||
const options: ClusterLinksFormatterOptions = {
|
||||
dashboardUrl: new URL('https://headlamp.com'),
|
||||
object: { metadata: { name: 'test-deployment', namespace: 'default' } },
|
||||
kind: 'Deployment',
|
||||
};
|
||||
|
||||
const result = await formatter.formatClusterLink(options);
|
||||
expect(result.toString()).toBe(
|
||||
'https://headlamp.com/?to=%2Fc%2Fdefault%2Fdeployments%2Fdefault%2Ftest-deployment',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error when dashboard URL is missing for external dashboard', async () => {
|
||||
const options: ClusterLinksFormatterOptions = {
|
||||
object: { metadata: { name: 'test-service', namespace: 'default' } },
|
||||
kind: 'Service',
|
||||
};
|
||||
|
||||
await expect(formatter.formatClusterLink(options)).rejects.toThrow(
|
||||
'Dashboard URL is required or dashboardInternal must be true',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for unsupported kind', async () => {
|
||||
const options: ClusterLinksFormatterOptions = {
|
||||
dashboardParameters: { internal: true },
|
||||
object: { metadata: { name: 'test-unknown', namespace: 'default' } },
|
||||
kind: 'UnknownKind',
|
||||
};
|
||||
|
||||
await expect(formatter.formatClusterLink(options)).rejects.toThrow(
|
||||
'Could not find path for kind: UnknownKind',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
/** @public */
|
||||
export class HeadlampClusterLinksFormatter implements ClusterLinksFormatter {
|
||||
async formatClusterLink(options: ClusterLinksFormatterOptions): Promise<URL> {
|
||||
const { dashboardUrl, dashboardParameters, object, kind } = options;
|
||||
|
||||
if (!dashboardUrl && !dashboardParameters?.internal) {
|
||||
throw new Error(
|
||||
'Dashboard URL is required or dashboardInternal must be true',
|
||||
);
|
||||
}
|
||||
|
||||
const clusterName =
|
||||
(dashboardParameters?.clusterName as string) ?? 'default';
|
||||
const path = this.getHeadlampPath(kind, object, clusterName);
|
||||
if (!path) {
|
||||
throw new Error(`Could not find path for kind: ${kind}`);
|
||||
}
|
||||
|
||||
let baseUrl: URL;
|
||||
|
||||
if (dashboardParameters?.internal) {
|
||||
baseUrl = new URL(window.location.origin);
|
||||
baseUrl.pathname =
|
||||
(dashboardParameters.headlampRoute as string) || '/headlamp';
|
||||
} else {
|
||||
if (!dashboardUrl?.href) {
|
||||
throw new Error(
|
||||
'Dashboard URL is required when not using internal dashboard',
|
||||
);
|
||||
}
|
||||
baseUrl = new URL(dashboardUrl.href);
|
||||
}
|
||||
|
||||
baseUrl.searchParams.set('to', path);
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
private readonly NAMESPACED_RESOURCES = new Set([
|
||||
'pod',
|
||||
'deployment',
|
||||
'replicaset',
|
||||
'statefulset',
|
||||
'daemonset',
|
||||
'job',
|
||||
'cronjob',
|
||||
'service',
|
||||
'ingress',
|
||||
'configmap',
|
||||
'secret',
|
||||
'serviceaccount',
|
||||
'role',
|
||||
'rolebinding',
|
||||
'networkpolicy',
|
||||
'horizontalpodautoscaler',
|
||||
'poddisruptionbudget',
|
||||
'persistentvolumeclaim',
|
||||
]);
|
||||
|
||||
private getHeadlampPath(
|
||||
kind: string,
|
||||
object: {
|
||||
metadata?: {
|
||||
name?: string;
|
||||
namespace?: string;
|
||||
};
|
||||
},
|
||||
clusterName: string,
|
||||
): string {
|
||||
const lowercaseKind = kind.toLocaleLowerCase('en-US');
|
||||
const { name } = object.metadata ?? {};
|
||||
let { namespace } = object.metadata ?? {};
|
||||
|
||||
if (!name) {
|
||||
throw new Error(`Resource name is required for kind: ${kind}`);
|
||||
}
|
||||
|
||||
// Add namespace validation
|
||||
if (this.NAMESPACED_RESOURCES.has(lowercaseKind) && !namespace) {
|
||||
throw new Error(`Namespace is required for namespaced resource: ${kind}`);
|
||||
}
|
||||
if (!namespace) {
|
||||
namespace = 'default';
|
||||
}
|
||||
|
||||
const pathMap: Record<string, string> = {
|
||||
namespace: `/c/${clusterName}/namespaces/${name}`,
|
||||
node: `/c/${clusterName}/nodes/${name}`,
|
||||
persistentvolume: `/c/${clusterName}/storage/persistentvolumes/${name}`,
|
||||
persistentvolumeclaim: `/c/${clusterName}/storage/persistentvolumeclaims/${namespace}/${name}`,
|
||||
pod: `/c/${clusterName}/pods/${namespace}/${name}`,
|
||||
deployment: `/c/${clusterName}/deployments/${namespace}/${name}`,
|
||||
replicaset: `/c/${clusterName}/replicasets/${namespace}/${name}`,
|
||||
statefulset: `/c/${clusterName}/statefulsets/${namespace}/${name}`,
|
||||
daemonset: `/c/${clusterName}/daemonsets/${namespace}/${name}`,
|
||||
job: `/c/${clusterName}/jobs/${namespace}/${name}`,
|
||||
cronjob: `/c/${clusterName}/cronjobs/${namespace}/${name}`,
|
||||
service: `/c/${clusterName}/services/${namespace}/${name}`,
|
||||
ingress: `/c/${clusterName}/ingresses/${namespace}/${name}`,
|
||||
configmap: `/c/${clusterName}/configmaps/${namespace}/${name}`,
|
||||
secret: `/c/${clusterName}/secrets/${namespace}/${name}`,
|
||||
serviceaccount: `/c/${clusterName}/serviceaccounts/${namespace}/${name}`,
|
||||
role: `/c/${clusterName}/roles/${namespace}/${name}`,
|
||||
rolebinding: `/c/${clusterName}/rolebindings/${namespace}/${name}`,
|
||||
clusterrole: `/c/${clusterName}/clusterroles/${name}`,
|
||||
clusterrolebinding: `/c/${clusterName}/clusterrolebindings/${name}`,
|
||||
storageclass: `/c/${clusterName}/storage/storageclasses/${name}`,
|
||||
networkpolicy: `/c/${clusterName}/networkpolicies/${namespace}/${name}`,
|
||||
horizontalpodautoscaler: `/c/${clusterName}/horizontalpodautoscalers/${namespace}/${name}`,
|
||||
poddisruptionbudget: `/c/${clusterName}/poddisruptionbudgets/${namespace}/${name}`,
|
||||
customresourcedefinition: `/c/${clusterName}/customresourcedefinitions/${name}`,
|
||||
};
|
||||
|
||||
return pathMap[lowercaseKind] ?? '';
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { GkeClusterLinksFormatter } from './GkeClusterLinksFormatter';
|
||||
import { StandardClusterLinksFormatter } from './StandardClusterLinksFormatter';
|
||||
import { OpenshiftClusterLinksFormatter } from './OpenshiftClusterLinksFormatter';
|
||||
import { RancherClusterLinksFormatter } from './RancherClusterLinksFormatter';
|
||||
import { HeadlampClusterLinksFormatter } from './HeadlampClusterLinksFormatter';
|
||||
import { ProfileInfoApi } from '@backstage/core-plugin-api';
|
||||
|
||||
export {
|
||||
@@ -30,6 +31,7 @@ export {
|
||||
GkeClusterLinksFormatter,
|
||||
OpenshiftClusterLinksFormatter,
|
||||
RancherClusterLinksFormatter,
|
||||
HeadlampClusterLinksFormatter,
|
||||
};
|
||||
|
||||
/** @public */
|
||||
@@ -46,5 +48,6 @@ export function getDefaultFormatters(deps: {
|
||||
gke: new GkeClusterLinksFormatter(deps.googleAuthApi),
|
||||
openshift: new OpenshiftClusterLinksFormatter(),
|
||||
rancher: new RancherClusterLinksFormatter(),
|
||||
headlamp: new HeadlampClusterLinksFormatter(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user