Merge pull request #22497 from jamieklassen/cluster-title
'title' field for Kubernetes clusters
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-react': minor
|
||||
---
|
||||
|
||||
**BREAKING** The `PodScope`, `PodAndErrors`, and `PodExecTerminalProps` types no
|
||||
longer have a `clusterName` field; instead they now have the field `cluster`
|
||||
which contains the full `ClusterAttributes`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-react': patch
|
||||
---
|
||||
|
||||
Pod dialogs display cluster title when specified.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-react': patch
|
||||
---
|
||||
|
||||
The `ErrorPanel` component will display the `title` field (when specified) for
|
||||
clusters with errors.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-backend': patch
|
||||
---
|
||||
|
||||
The `/clusters` API now surfaces cluster titles.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-common': patch
|
||||
---
|
||||
|
||||
The `ClusterAttributes` type now includes the cluster title.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes': patch
|
||||
'@backstage/plugin-kubernetes-react': patch
|
||||
---
|
||||
|
||||
The `ErrorReporting` component's cluster column now displays cluster titles when
|
||||
specified.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-backend': patch
|
||||
---
|
||||
|
||||
Responses from the `/api/kubernetes/services/:serviceId` endpoint now include the cluster title.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-react': patch
|
||||
---
|
||||
|
||||
The `Cluster` component now renders the cluster's title, if specified.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-node': patch
|
||||
---
|
||||
|
||||
The `ClusterDetails` type now has a `title` field, which should be a
|
||||
human-readable name.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-backend': patch
|
||||
---
|
||||
|
||||
Clusters in the catalog can now specify a human-readable title via `metadata.title`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-backend': patch
|
||||
---
|
||||
|
||||
Clusters in the app-config can now specify a `title` property for human readability.
|
||||
@@ -92,6 +92,7 @@ Deutsche
|
||||
dev
|
||||
devops
|
||||
devs
|
||||
dialogs
|
||||
discoverability
|
||||
Discoverability
|
||||
dls
|
||||
|
||||
@@ -38,6 +38,7 @@ kubernetes:
|
||||
plural: 'rollouts'
|
||||
- url: http://127.0.0.2:9999
|
||||
name: aws-cluster-1
|
||||
title: 'My AWS Cluster Number One'
|
||||
authProvider: 'aws'
|
||||
- type: 'gke'
|
||||
projectId: 'gke-clusters'
|
||||
@@ -151,6 +152,11 @@ The base URL to the Kubernetes control plane. Can be found by using the
|
||||
A name to represent this cluster, this must be unique within the `clusters`
|
||||
array. Users will see this value in the Software Catalog Kubernetes plugin.
|
||||
|
||||
##### `clusters.\*.title`
|
||||
|
||||
A human-readable name for the cluster. This value will override the `name` field
|
||||
for the purposes of display in the catalog.
|
||||
|
||||
##### `clusters.\*.authProvider`
|
||||
|
||||
This determines how the Kubernetes client authenticates with the Kubernetes
|
||||
|
||||
+2
@@ -43,6 +43,8 @@ export interface Config {
|
||||
url: string;
|
||||
/** @visibility frontend */
|
||||
name: string;
|
||||
/** @visibility frontend */
|
||||
title?: string;
|
||||
/** @visibility secret */
|
||||
serviceAccountToken?: string;
|
||||
/** @visibility frontend */
|
||||
|
||||
@@ -43,6 +43,7 @@ const mockCatalogApi = {
|
||||
'kubernetes.io/dashboard-app': 'my-app',
|
||||
},
|
||||
name: 'owned',
|
||||
title: 'title',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -63,6 +63,7 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier {
|
||||
const annotations = entity.metadata.annotations!;
|
||||
const clusterDetails: ClusterDetails = {
|
||||
name: entity.metadata.name,
|
||||
title: entity.metadata.title,
|
||||
url: annotations[ANNOTATION_KUBERNETES_API_SERVER],
|
||||
authMetadata: annotations,
|
||||
caData: annotations[ANNOTATION_KUBERNETES_API_SERVER_CA],
|
||||
|
||||
@@ -21,8 +21,8 @@ import {
|
||||
ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE,
|
||||
ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
import { ClusterDetails } from '@backstage/plugin-kubernetes-node';
|
||||
import { ConfigClusterLocator } from './ConfigClusterLocator';
|
||||
import { ClusterDetails } from '../types/types';
|
||||
import { AuthenticationStrategy } from '../auth';
|
||||
|
||||
describe('ConfigClusterLocator', () => {
|
||||
@@ -78,6 +78,28 @@ describe('ConfigClusterLocator', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('reads `title` property', async () => {
|
||||
const sut = ConfigClusterLocator.fromConfig(
|
||||
new ConfigReader({
|
||||
clusters: [
|
||||
{
|
||||
name: 'cluster-name',
|
||||
title: 'cluster-title',
|
||||
url: 'url',
|
||||
authMetadata: { 'kubernetes.io/auth-provider': 'serviceAccount' },
|
||||
},
|
||||
],
|
||||
}),
|
||||
authStrategy,
|
||||
);
|
||||
|
||||
const result = await sut.getClusters();
|
||||
|
||||
expect(result).toEqual([
|
||||
expect.objectContaining({ title: 'cluster-title' }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('two clusters returns two cluster details', async () => {
|
||||
const config: Config = new ConfigReader({
|
||||
clusters: [
|
||||
|
||||
@@ -56,8 +56,10 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier {
|
||||
`'authMetadata.${ANNOTATION_KUBERNETES_AUTH_PROVIDER}' parameter`,
|
||||
);
|
||||
}
|
||||
const title = c.getOptionalString('title');
|
||||
const clusterDetails: ClusterDetails = {
|
||||
name,
|
||||
...(title && { title }),
|
||||
url: c.getString('url'),
|
||||
skipTLSVerify: c.getOptionalBoolean('skipTLSVerify') ?? false,
|
||||
skipMetricsLookup: c.getOptionalBoolean('skipMetricsLookup') ?? false,
|
||||
|
||||
+2
@@ -19,6 +19,7 @@ exports[`CatalogClusterLocator returns the aws cluster details provided by annot
|
||||
"name": "owned",
|
||||
"skipMetricsLookup": false,
|
||||
"skipTLSVerify": false,
|
||||
"title": undefined,
|
||||
"url": "https://apiserver.com",
|
||||
}
|
||||
`;
|
||||
@@ -42,6 +43,7 @@ exports[`CatalogClusterLocator returns the cluster details provided by annotatio
|
||||
"name": "owned",
|
||||
"skipMetricsLookup": true,
|
||||
"skipTLSVerify": true,
|
||||
"title": "title",
|
||||
"url": "https://apiserver.com",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -278,6 +278,32 @@ describe('API integration tests', () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('surfaces cluster title', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
minimalValidConfigService,
|
||||
import('@backstage/plugin-kubernetes-backend/alpha'),
|
||||
withClusters([
|
||||
{
|
||||
name: 'cluster-name',
|
||||
title: 'cluster-title',
|
||||
url: 'url',
|
||||
authMetadata: {
|
||||
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount',
|
||||
},
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
app = server;
|
||||
|
||||
const response = await request(app).get('/api/kubernetes/clusters');
|
||||
|
||||
expect(response.body).toEqual({
|
||||
items: [expect.objectContaining({ title: 'cluster-title' })],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('post /services/:serviceId', () => {
|
||||
|
||||
@@ -381,6 +381,7 @@ export class KubernetesBuilder {
|
||||
|
||||
return {
|
||||
name: cd.name,
|
||||
title: cd.title,
|
||||
dashboardUrl: cd.dashboardUrl,
|
||||
authProvider,
|
||||
...(oidcTokenProvider && { oidcTokenProvider }),
|
||||
|
||||
@@ -29,6 +29,7 @@ import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import {
|
||||
FetchResponse,
|
||||
KubernetesRequestAuth,
|
||||
ObjectsByEntityResponse,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
@@ -104,7 +105,7 @@ describe('KubernetesFanOutHandler', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const mockClusterResourceMap = {
|
||||
const mockClusterResourceMap: Record<string, FetchResponse[]> = {
|
||||
'test-cluster': [
|
||||
{
|
||||
resources: [
|
||||
@@ -358,17 +359,16 @@ describe('KubernetesFanOutHandler', () => {
|
||||
|
||||
describe('getKubernetesObjectsByEntity', () => {
|
||||
it('retrieve objects for one cluster', async () => {
|
||||
getClustersByEntity.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
clusters: [
|
||||
{
|
||||
name: 'test-cluster',
|
||||
url: '',
|
||||
authMetadata: {},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
getClustersByEntity.mockResolvedValue({
|
||||
clusters: [
|
||||
{
|
||||
name: 'test-cluster',
|
||||
title: 'cluster-title',
|
||||
url: '',
|
||||
authMetadata: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
sut = getKubernetesFanOutHandler([]);
|
||||
|
||||
@@ -386,11 +386,12 @@ describe('KubernetesFanOutHandler', () => {
|
||||
new Set(['ns-test-component-test-cluster']),
|
||||
expect.anything(),
|
||||
);
|
||||
expect(result).toStrictEqual({
|
||||
expect(result).toStrictEqual<ObjectsByEntityResponse>({
|
||||
items: [
|
||||
{
|
||||
cluster: {
|
||||
name: 'test-cluster',
|
||||
title: 'cluster-title',
|
||||
},
|
||||
errors: [],
|
||||
podMetrics: [POD_METRICS_FIXTURE],
|
||||
|
||||
@@ -335,6 +335,7 @@ export class KubernetesFanOutHandler {
|
||||
const objects: ClusterObjects = {
|
||||
cluster: {
|
||||
name: clusterDetails.name,
|
||||
...(clusterDetails.title && { title: clusterDetails.title }),
|
||||
},
|
||||
podMetrics: toClientSafePodMetrics(metrics),
|
||||
resources: result.responses,
|
||||
|
||||
@@ -112,6 +112,7 @@ export interface ClusterAttributes {
|
||||
dashboardParameters?: JsonObject;
|
||||
dashboardUrl?: string;
|
||||
name: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -67,9 +67,13 @@ export interface KubernetesRequestBody {
|
||||
/** @public */
|
||||
export interface ClusterAttributes {
|
||||
/**
|
||||
* Specifies the name of the Kubernetes cluster.
|
||||
* Name of the Kubernetes cluster; used as an internal identifier.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Human-readable name for the cluster, to be dispayed in UIs.
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* Specifies the link to the Kubernetes dashboard managing this cluster.
|
||||
* @remarks
|
||||
|
||||
@@ -51,6 +51,7 @@ export interface ClusterDetails {
|
||||
skipMetricsLookup?: boolean;
|
||||
// (undocumented)
|
||||
skipTLSVerify?: boolean;
|
||||
title?: string;
|
||||
// (undocumented)
|
||||
url: string;
|
||||
}
|
||||
|
||||
@@ -67,9 +67,13 @@ export type AuthMetadata = Record<string, string>;
|
||||
*/
|
||||
export interface ClusterDetails {
|
||||
/**
|
||||
* Specifies the name of the Kubernetes cluster.
|
||||
* Name of the Kubernetes cluster; used as an internal identifier.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Human-readable name for the cluster, to be dispayed in UIs.
|
||||
*/
|
||||
title?: string;
|
||||
url: string;
|
||||
authMetadata: AuthMetadata;
|
||||
skipTLSVerify?: boolean;
|
||||
|
||||
@@ -176,11 +176,13 @@ export type ErrorPanelProps = {
|
||||
// @public (undocumented)
|
||||
export const ErrorReporting: ({
|
||||
detectedErrors,
|
||||
clusters,
|
||||
}: ErrorReportingProps) => React_3.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export type ErrorReportingProps = {
|
||||
detectedErrors: DetectedErrorsByCluster;
|
||||
clusters: ClusterAttributes[];
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -653,7 +655,7 @@ export interface PendingPodContentProps {
|
||||
// @public
|
||||
export interface PodAndErrors {
|
||||
// (undocumented)
|
||||
clusterName: string;
|
||||
cluster: ClusterAttributes;
|
||||
// (undocumented)
|
||||
errors: DetectedError[];
|
||||
// (undocumented)
|
||||
@@ -690,7 +692,7 @@ export const PodExecTerminalDialog: (
|
||||
// @public
|
||||
export interface PodExecTerminalProps {
|
||||
// (undocumented)
|
||||
clusterName: string;
|
||||
cluster: ClusterAttributes;
|
||||
// (undocumented)
|
||||
containerName: string;
|
||||
// (undocumented)
|
||||
@@ -748,7 +750,7 @@ export const PodNamesWithMetricsContext: React_2.Context<
|
||||
// @public
|
||||
export interface PodScope {
|
||||
// (undocumented)
|
||||
clusterName: string;
|
||||
cluster: ClusterAttributes;
|
||||
// (undocumented)
|
||||
podName: string;
|
||||
// (undocumented)
|
||||
|
||||
@@ -56,4 +56,25 @@ describe('Cluster', () => {
|
||||
expect(screen.getByText('cluster-1')).toBeInTheDocument();
|
||||
expect(screen.getByText('10 pods')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders title', async () => {
|
||||
await renderInTestApp(
|
||||
<Cluster
|
||||
{...({
|
||||
clusterObjects: {
|
||||
cluster: {
|
||||
name: 'cluster-1',
|
||||
title: 'Cluster Number One',
|
||||
},
|
||||
resources: [],
|
||||
podMetrics: [],
|
||||
errors: [],
|
||||
},
|
||||
podsWithErrors: new Set<string>(),
|
||||
} as any)}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Cluster Number One')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -132,7 +132,9 @@ export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => {
|
||||
<Accordion TransitionProps={{ unmountOnExit: true }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<ClusterSummary
|
||||
clusterName={clusterObjects.cluster.name}
|
||||
clusterName={
|
||||
clusterObjects.cluster.title || clusterObjects.cluster.name
|
||||
}
|
||||
totalNumberOfPods={groupedResponses.pods.length}
|
||||
numberOfPodsWithErrors={podsWithErrors.size}
|
||||
/>
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('ErrorPanel', () => {
|
||||
entityName="THIS_ENTITY"
|
||||
clustersWithErrors={[
|
||||
{
|
||||
cluster: { name: 'THIS_CLUSTER' },
|
||||
cluster: { name: 'THIS_CLUSTER', title: 'This Fine Cluster' },
|
||||
resources: [],
|
||||
podMetrics: [],
|
||||
errors: [
|
||||
@@ -50,7 +50,7 @@ describe('ErrorPanel', () => {
|
||||
|
||||
// message
|
||||
expect(screen.getByText('Errors:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Cluster: THIS_CLUSTER')).toBeInTheDocument();
|
||||
expect(screen.getByText('Cluster: This Fine Cluster')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Error fetching Kubernetes resource: 'some/resource', error: SYSTEM_ERROR, status code: 500",
|
||||
|
||||
@@ -25,7 +25,9 @@ const clustersWithErrorsToErrorMessage = (
|
||||
return clustersWithErrors.map((c, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<Typography variant="body2">{`Cluster: ${c.cluster.name}`}</Typography>
|
||||
<Typography variant="body2">{`Cluster: ${
|
||||
c.cluster.title || c.cluster.name
|
||||
}`}</Typography>
|
||||
{c.errors.map((e, j) => {
|
||||
return (
|
||||
<Typography variant="body2" key={j}>
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { DetectedError } from '@backstage/plugin-kubernetes-common';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { MatcherFunction, screen } from '@testing-library/react';
|
||||
import { ErrorReporting } from './ErrorReporting';
|
||||
|
||||
describe('ErrorReporting', () => {
|
||||
const matchTextContent =
|
||||
(text: string): MatcherFunction =>
|
||||
(_, node) =>
|
||||
node?.textContent?.includes(text) ?? false;
|
||||
|
||||
it('sorts errors by severity', async () => {
|
||||
await renderInTestApp(
|
||||
<ErrorReporting
|
||||
detectedErrors={
|
||||
new Map<string, DetectedError[]>([
|
||||
[
|
||||
'cluster',
|
||||
[
|
||||
{
|
||||
type: 'readiness-probe-taking-too-long',
|
||||
message:
|
||||
'The container my-container failed to start properly, but is not crashing',
|
||||
severity: 4,
|
||||
proposedFix: undefined,
|
||||
sourceRef: {
|
||||
name: 'my-pod',
|
||||
namespace: 'default',
|
||||
kind: 'Pod',
|
||||
apiGroup: 'v1',
|
||||
},
|
||||
occurrenceCount: 1,
|
||||
},
|
||||
{
|
||||
type: 'condition-message-present',
|
||||
message: 'some condition message',
|
||||
severity: 6,
|
||||
sourceRef: {
|
||||
name: 'my-deployment',
|
||||
namespace: 'default',
|
||||
kind: 'Deployment',
|
||||
apiGroup: 'apps/v1',
|
||||
},
|
||||
occurrenceCount: 1,
|
||||
},
|
||||
],
|
||||
],
|
||||
])
|
||||
}
|
||||
clusters={[{ name: 'cluster' }]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getAllByRole('row')).toEqual([
|
||||
expect.anything(),
|
||||
screen.getByText(matchTextContent('some condition message'), {
|
||||
selector: 'tr',
|
||||
}),
|
||||
screen.getByText(
|
||||
matchTextContent(
|
||||
'The container my-container failed to start properly, but is not crashing',
|
||||
),
|
||||
{ selector: 'tr' },
|
||||
),
|
||||
expect.anything(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('displays cluster name', async () => {
|
||||
await renderInTestApp(
|
||||
<ErrorReporting
|
||||
detectedErrors={
|
||||
new Map<string, DetectedError[]>([
|
||||
[
|
||||
'my-cluster',
|
||||
[
|
||||
{
|
||||
type: 'condition-message-present',
|
||||
message: 'some condition message',
|
||||
severity: 6,
|
||||
sourceRef: {
|
||||
name: 'my-deployment',
|
||||
namespace: 'default',
|
||||
kind: 'Deployment',
|
||||
apiGroup: 'apps/v1',
|
||||
},
|
||||
occurrenceCount: 1,
|
||||
},
|
||||
],
|
||||
],
|
||||
])
|
||||
}
|
||||
clusters={[{ name: 'my-cluster' }]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole('cell', { name: 'my-cluster' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays cluster title when specified', async () => {
|
||||
await renderInTestApp(
|
||||
<ErrorReporting
|
||||
detectedErrors={
|
||||
new Map<string, DetectedError[]>([
|
||||
[
|
||||
'my-cluster',
|
||||
[
|
||||
{
|
||||
type: 'condition-message-present',
|
||||
message: 'some condition message',
|
||||
severity: 6,
|
||||
sourceRef: {
|
||||
name: 'my-deployment',
|
||||
namespace: 'default',
|
||||
kind: 'Deployment',
|
||||
apiGroup: 'apps/v1',
|
||||
},
|
||||
occurrenceCount: 1,
|
||||
},
|
||||
],
|
||||
],
|
||||
])
|
||||
}
|
||||
clusters={[{ name: 'my-cluster', title: 'cluster-title' }]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole('cell', { name: 'cluster-title' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ClusterAttributes,
|
||||
DetectedError,
|
||||
DetectedErrorsByCluster,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
@@ -27,13 +28,14 @@ import { Table, TableColumn } from '@backstage/core-components';
|
||||
*/
|
||||
export type ErrorReportingProps = {
|
||||
detectedErrors: DetectedErrorsByCluster;
|
||||
clusters: ClusterAttributes[];
|
||||
};
|
||||
|
||||
const columns: TableColumn<Row>[] = [
|
||||
{
|
||||
title: 'cluster',
|
||||
width: '10%',
|
||||
render: (row: Row) => row.clusterName,
|
||||
render: (row: Row) => row.cluster.title || row.cluster.name,
|
||||
},
|
||||
{
|
||||
title: 'namespace',
|
||||
@@ -60,7 +62,7 @@ const columns: TableColumn<Row>[] = [
|
||||
];
|
||||
|
||||
interface Row {
|
||||
clusterName: string;
|
||||
cluster: ClusterAttributes;
|
||||
error: DetectedError;
|
||||
}
|
||||
|
||||
@@ -78,11 +80,14 @@ const sortBySeverity = (a: Row, b: Row) => {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const ErrorReporting = ({ detectedErrors }: ErrorReportingProps) => {
|
||||
export const ErrorReporting = ({
|
||||
detectedErrors,
|
||||
clusters,
|
||||
}: ErrorReportingProps) => {
|
||||
const errors = Array.from(detectedErrors.entries())
|
||||
.flatMap(([clusterName, resourceErrors]) => {
|
||||
return resourceErrors.map(e => ({
|
||||
clusterName,
|
||||
cluster: clusters.find(c => c.name === clusterName)!,
|
||||
error: e,
|
||||
}));
|
||||
})
|
||||
|
||||
@@ -32,7 +32,7 @@ global.TextEncoder = require('util').TextEncoder;
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
describe('PodExecTerminal', () => {
|
||||
const clusterName = 'cluster1';
|
||||
const cluster = { name: 'cluster1' };
|
||||
const containerName = 'container2';
|
||||
const podName = 'pod1';
|
||||
const podNamespace = 'podNamespace';
|
||||
@@ -45,7 +45,7 @@ describe('PodExecTerminal', () => {
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[discoveryApiRef, mockDiscoveryApi]]}>
|
||||
<PodExecTerminal
|
||||
clusterName={clusterName}
|
||||
cluster={cluster}
|
||||
containerName={containerName}
|
||||
podName={podName}
|
||||
podNamespace={podNamespace}
|
||||
@@ -68,7 +68,7 @@ describe('PodExecTerminal', () => {
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[discoveryApiRef, mockDiscoveryApi]]}>
|
||||
<PodExecTerminal
|
||||
clusterName={clusterName}
|
||||
cluster={cluster}
|
||||
containerName={containerName}
|
||||
podName={podName}
|
||||
podNamespace={podNamespace}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import 'xterm/css/xterm.css';
|
||||
|
||||
import { discoveryApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { ClusterAttributes } from '@backstage/plugin-kubernetes-common';
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Terminal } from 'xterm';
|
||||
@@ -29,7 +30,7 @@ import { PodExecTerminalAttachAddon } from './PodExecTerminalAttachAddon';
|
||||
* @public
|
||||
*/
|
||||
export interface PodExecTerminalProps {
|
||||
clusterName: string;
|
||||
cluster: ClusterAttributes;
|
||||
containerName: string;
|
||||
podName: string;
|
||||
podNamespace: string;
|
||||
|
||||
@@ -27,7 +27,7 @@ import { PodExecTerminal, PodExecTerminalProps } from './PodExecTerminal';
|
||||
* @public
|
||||
*/
|
||||
export const PodExecTerminalDialog = (props: PodExecTerminalProps) => {
|
||||
const { clusterName, containerName, podName } = props;
|
||||
const { cluster, containerName, podName } = props;
|
||||
|
||||
const isPodExecTerminalSupported = useIsPodExecTerminalSupported();
|
||||
|
||||
@@ -42,7 +42,9 @@ export const PodExecTerminalDialog = (props: PodExecTerminalProps) => {
|
||||
isPodExecTerminalSupported.loading ||
|
||||
!isPodExecTerminalSupported.value
|
||||
}
|
||||
title={`${podName} - ${containerName} terminal shell on cluster ${clusterName}`}
|
||||
title={`${podName} - ${containerName} terminal shell on cluster ${
|
||||
cluster.title || cluster.name
|
||||
}`}
|
||||
>
|
||||
<PodExecTerminal {...props} />
|
||||
</KubernetesDialog>
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('ErrorList', () => {
|
||||
<ErrorList
|
||||
podAndErrors={[
|
||||
{
|
||||
clusterName: 'some-cluster',
|
||||
cluster: { name: 'some-cluster' },
|
||||
pod: {
|
||||
metadata: {
|
||||
name: 'some-pod',
|
||||
|
||||
@@ -82,7 +82,7 @@ export const ErrorList = ({ podAndErrors }: ErrorListProps) => {
|
||||
<FixDialog
|
||||
pod={onlyPodWithErrors.pod}
|
||||
error={error}
|
||||
clusterName={onlyPodWithErrors.clusterName}
|
||||
clusterName={onlyPodWithErrors.cluster.name}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -119,7 +119,7 @@ export const FixDialog: React.FC<FixDialogProps> = ({
|
||||
containerScope={{
|
||||
podName: pod.metadata?.name ?? 'unknown',
|
||||
podNamespace: pod.metadata?.namespace ?? 'unknown',
|
||||
clusterName: clusterName,
|
||||
cluster: { name: clusterName },
|
||||
containerName: pf.container,
|
||||
}}
|
||||
/>
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ describe('ContainerCard', () => {
|
||||
podScope: {
|
||||
name: 'some-name',
|
||||
namespace: 'some-namespace',
|
||||
clusterName: 'some-cluster',
|
||||
cluster: { name: 'some-cluster' },
|
||||
},
|
||||
containerSpec: {
|
||||
readinessProbe: {},
|
||||
@@ -71,7 +71,7 @@ describe('ContainerCard', () => {
|
||||
podScope: {
|
||||
podName: 'some-name',
|
||||
podNamespace: 'some-namespace',
|
||||
clusterName: 'some-cluster',
|
||||
cluster: { name: 'some-cluster' },
|
||||
},
|
||||
containerSpec: {},
|
||||
containerStatus: {
|
||||
@@ -100,7 +100,7 @@ describe('ContainerCard', () => {
|
||||
podScope: {
|
||||
podName: 'some-name',
|
||||
podNamespace: 'some-namespace',
|
||||
clusterName: 'some-cluster',
|
||||
cluster: { name: 'some-cluster' },
|
||||
},
|
||||
containerSpec: {},
|
||||
containerStatus: {
|
||||
|
||||
@@ -233,7 +233,7 @@ export const ContainerCard: React.FC<ContainerCardProps> = ({
|
||||
/>
|
||||
{isPodExecTerminalEnabled && (
|
||||
<PodExecTerminalDialog
|
||||
clusterName={podScope.clusterName}
|
||||
cluster={podScope.cluster}
|
||||
containerName={containerStatus.name}
|
||||
podName={podScope.podName}
|
||||
podNamespace={podScope.podNamespace}
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('PodDrawer', () => {
|
||||
{...({
|
||||
open: true,
|
||||
podAndErrors: {
|
||||
clusterName: 'some-cluster-1',
|
||||
cluster: { name: 'some-cluster-1' },
|
||||
pod: {
|
||||
metadata: {
|
||||
name: 'some-pod',
|
||||
|
||||
@@ -79,7 +79,7 @@ export interface PodDrawerProps {
|
||||
*/
|
||||
export const PodDrawer = ({ podAndErrors, open }: PodDrawerProps) => {
|
||||
const classes = useDrawerContentStyles();
|
||||
const podMetrics = usePodMetrics(podAndErrors.clusterName, podAndErrors.pod);
|
||||
const podMetrics = usePodMetrics(podAndErrors.cluster.name, podAndErrors.pod);
|
||||
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
@@ -161,7 +161,7 @@ export const PodDrawer = ({ podAndErrors, open }: PodDrawerProps) => {
|
||||
podName: podAndErrors.pod.metadata?.name ?? 'unknown',
|
||||
podNamespace:
|
||||
podAndErrors.pod.metadata?.namespace ?? 'unknown',
|
||||
clusterName: podAndErrors.clusterName,
|
||||
cluster: podAndErrors.cluster,
|
||||
}}
|
||||
containerSpec={containerSpec}
|
||||
containerStatus={containerStatus}
|
||||
|
||||
@@ -42,7 +42,11 @@ export const PodLogsDialog = ({ containerScope }: PodLogsDialogProps) => {
|
||||
buttonIcon={<SubjectIcon />}
|
||||
buttonText="Logs"
|
||||
disabled={false}
|
||||
title={`${containerScope.podName} - ${containerScope.containerName} logs on cluster ${containerScope.clusterName}`}
|
||||
title={`${containerScope.podName} - ${
|
||||
containerScope.containerName
|
||||
} logs on cluster ${
|
||||
containerScope.cluster.title || containerScope.cluster.name
|
||||
}`}
|
||||
>
|
||||
<PodLogs containerScope={containerScope} />
|
||||
</KubernetesDialog>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { ClusterAttributes } from '@backstage/plugin-kubernetes-common';
|
||||
|
||||
/**
|
||||
* Contains the details needed to make a log request to Kubernetes, except the container name
|
||||
@@ -22,7 +23,7 @@
|
||||
export interface PodScope {
|
||||
podName: string;
|
||||
podNamespace: string;
|
||||
clusterName: string;
|
||||
cluster: ClusterAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,7 +41,7 @@ export const usePodLogs = ({ containerScope, previous }: PodLogsOptions) => {
|
||||
podName: containerScope.podName,
|
||||
namespace: containerScope.podNamespace,
|
||||
containerName: containerScope.containerName,
|
||||
clusterName: containerScope.clusterName,
|
||||
clusterName: containerScope.cluster.name,
|
||||
previous,
|
||||
});
|
||||
}, [JSON.stringify(containerScope)]);
|
||||
|
||||
+1
-2
@@ -80,7 +80,6 @@ const READY: TableColumn<Pod>[] = [
|
||||
];
|
||||
|
||||
const PodDrawerTrigger = ({ pod }: { pod: Pod }) => {
|
||||
const cluster = useContext(ClusterContext);
|
||||
const errors = useMatchingErrors({
|
||||
kind: 'Pod',
|
||||
apiVersion: 'v1',
|
||||
@@ -90,7 +89,7 @@ const PodDrawerTrigger = ({ pod }: { pod: Pod }) => {
|
||||
<PodDrawer
|
||||
podAndErrors={{
|
||||
pod: pod as any,
|
||||
clusterName: cluster.name,
|
||||
cluster: useContext(ClusterContext),
|
||||
errors: errors,
|
||||
}}
|
||||
/>
|
||||
|
||||
+6
-3
@@ -14,15 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Pod } from 'kubernetes-models/v1';
|
||||
import { DetectedError } from '@backstage/plugin-kubernetes-common';
|
||||
import {
|
||||
ClusterAttributes,
|
||||
DetectedError,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
|
||||
/**
|
||||
* Wraps a pod with the associated detected errors and cluster name
|
||||
* Wraps a pod with the associated detected errors and cluster
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PodAndErrors {
|
||||
clusterName: string;
|
||||
cluster: ClusterAttributes;
|
||||
pod: Pod;
|
||||
errors: DetectedError[];
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ export const KubernetesContent = ({
|
||||
refreshIntervalMs,
|
||||
);
|
||||
|
||||
const clusters = kubernetesObjects?.items.map(item => item.cluster) ?? [];
|
||||
|
||||
const clustersWithErrors =
|
||||
kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? [];
|
||||
|
||||
@@ -93,7 +95,10 @@ export const KubernetesContent = ({
|
||||
{kubernetesObjects && (
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<ErrorReporting detectedErrors={detectedErrors} />
|
||||
<ErrorReporting
|
||||
detectedErrors={detectedErrors}
|
||||
clusters={clusters}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="h3">Your Clusters</Typography>
|
||||
|
||||
Reference in New Issue
Block a user