diff --git a/.changeset/eight-pumas-rule.md b/.changeset/eight-pumas-rule.md new file mode 100644 index 0000000000..166ebd8bd3 --- /dev/null +++ b/.changeset/eight-pumas-rule.md @@ -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`. diff --git a/.changeset/forty-eagles-kick.md b/.changeset/forty-eagles-kick.md new file mode 100644 index 0000000000..4d701a729b --- /dev/null +++ b/.changeset/forty-eagles-kick.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-react': patch +--- + +Pod dialogs display cluster title when specified. diff --git a/.changeset/fresh-paws-raise.md b/.changeset/fresh-paws-raise.md new file mode 100644 index 0000000000..61712db1e8 --- /dev/null +++ b/.changeset/fresh-paws-raise.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-react': patch +--- + +The `ErrorPanel` component will display the `title` field (when specified) for +clusters with errors. diff --git a/.changeset/happy-peas-hunt.md b/.changeset/happy-peas-hunt.md new file mode 100644 index 0000000000..8fbac9c33a --- /dev/null +++ b/.changeset/happy-peas-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +The `/clusters` API now surfaces cluster titles. diff --git a/.changeset/healthy-bees-deliver.md b/.changeset/healthy-bees-deliver.md new file mode 100644 index 0000000000..80da17dc0b --- /dev/null +++ b/.changeset/healthy-bees-deliver.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-common': patch +--- + +The `ClusterAttributes` type now includes the cluster title. diff --git a/.changeset/healthy-goats-jump.md b/.changeset/healthy-goats-jump.md new file mode 100644 index 0000000000..e5b43d3f8c --- /dev/null +++ b/.changeset/healthy-goats-jump.md @@ -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. diff --git a/.changeset/new-plants-rhyme.md b/.changeset/new-plants-rhyme.md new file mode 100644 index 0000000000..c901754069 --- /dev/null +++ b/.changeset/new-plants-rhyme.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Responses from the `/api/kubernetes/services/:serviceId` endpoint now include the cluster title. diff --git a/.changeset/quick-gifts-leave.md b/.changeset/quick-gifts-leave.md new file mode 100644 index 0000000000..2f5f2a4635 --- /dev/null +++ b/.changeset/quick-gifts-leave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-react': patch +--- + +The `Cluster` component now renders the cluster's title, if specified. diff --git a/.changeset/sixty-bags-fry.md b/.changeset/sixty-bags-fry.md new file mode 100644 index 0000000000..139e31c6a4 --- /dev/null +++ b/.changeset/sixty-bags-fry.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-node': patch +--- + +The `ClusterDetails` type now has a `title` field, which should be a +human-readable name. diff --git a/.changeset/thin-turtles-float.md b/.changeset/thin-turtles-float.md new file mode 100644 index 0000000000..7416093e17 --- /dev/null +++ b/.changeset/thin-turtles-float.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Clusters in the catalog can now specify a human-readable title via `metadata.title`. diff --git a/.changeset/tidy-peaches-nail.md b/.changeset/tidy-peaches-nail.md new file mode 100644 index 0000000000..d044a6bfaa --- /dev/null +++ b/.changeset/tidy-peaches-nail.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Clusters in the app-config can now specify a `title` property for human readability. diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index 9bb4a52cf4..525e24d251 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -92,6 +92,7 @@ Deutsche dev devops devs +dialogs discoverability Discoverability dls diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 331883be35..ff3a42c7ed 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -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 diff --git a/plugins/kubernetes-backend/config.d.ts b/plugins/kubernetes-backend/config.d.ts index de7f79b66c..777a3392db 100644 --- a/plugins/kubernetes-backend/config.d.ts +++ b/plugins/kubernetes-backend/config.d.ts @@ -43,6 +43,8 @@ export interface Config { url: string; /** @visibility frontend */ name: string; + /** @visibility frontend */ + title?: string; /** @visibility secret */ serviceAccountToken?: string; /** @visibility frontend */ diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts index ad09f52f0a..d6bc64221d 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts @@ -43,6 +43,7 @@ const mockCatalogApi = { 'kubernetes.io/dashboard-app': 'my-app', }, name: 'owned', + title: 'title', namespace: 'default', }, }, diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts index 9be4cd39cc..a2c5f0376a 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts @@ -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], diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index d36f10bb13..3f014b2869 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -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: [ diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index 6fd5f3b1cb..6403ec89a7 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -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, diff --git a/plugins/kubernetes-backend/src/cluster-locator/__snapshots__/CatalogClusterLocator.test.ts.snap b/plugins/kubernetes-backend/src/cluster-locator/__snapshots__/CatalogClusterLocator.test.ts.snap index b3731f1c08..f8b3e71515 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/__snapshots__/CatalogClusterLocator.test.ts.snap +++ b/plugins/kubernetes-backend/src/cluster-locator/__snapshots__/CatalogClusterLocator.test.ts.snap @@ -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", } `; diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 5d19c6d94d..295d11570f 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -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', () => { diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 8cc9b8a8b6..fec4bc5cc0 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -381,6 +381,7 @@ export class KubernetesBuilder { return { name: cd.name, + title: cd.title, dashboardUrl: cd.dashboardUrl, authProvider, ...(oidcTokenProvider && { oidcTokenProvider }), diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index a50d0b37a2..5c4534d61b 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -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 = { '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({ items: [ { cluster: { name: 'test-cluster', + title: 'cluster-title', }, errors: [], podMetrics: [POD_METRICS_FIXTURE], diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index 7a8ff1bf2d..4df60bc8da 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -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, diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index e4dbf79f4a..c1d399b196 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -112,6 +112,7 @@ export interface ClusterAttributes { dashboardParameters?: JsonObject; dashboardUrl?: string; name: string; + title?: string; } // @public (undocumented) diff --git a/plugins/kubernetes-common/src/types.ts b/plugins/kubernetes-common/src/types.ts index c9a4ce4c5a..cd316af089 100644 --- a/plugins/kubernetes-common/src/types.ts +++ b/plugins/kubernetes-common/src/types.ts @@ -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 diff --git a/plugins/kubernetes-node/api-report.md b/plugins/kubernetes-node/api-report.md index 0521ed72aa..509416fa4a 100644 --- a/plugins/kubernetes-node/api-report.md +++ b/plugins/kubernetes-node/api-report.md @@ -51,6 +51,7 @@ export interface ClusterDetails { skipMetricsLookup?: boolean; // (undocumented) skipTLSVerify?: boolean; + title?: string; // (undocumented) url: string; } diff --git a/plugins/kubernetes-node/src/types/types.ts b/plugins/kubernetes-node/src/types/types.ts index 2881338868..665b06178a 100644 --- a/plugins/kubernetes-node/src/types/types.ts +++ b/plugins/kubernetes-node/src/types/types.ts @@ -67,9 +67,13 @@ export type AuthMetadata = Record; */ 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; diff --git a/plugins/kubernetes-react/api-report.md b/plugins/kubernetes-react/api-report.md index 8a0ec4de8f..ede2c9ddb7 100644 --- a/plugins/kubernetes-react/api-report.md +++ b/plugins/kubernetes-react/api-report.md @@ -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) diff --git a/plugins/kubernetes-react/src/components/Cluster/Cluster.test.tsx b/plugins/kubernetes-react/src/components/Cluster/Cluster.test.tsx index 482a386efb..2814606dd6 100644 --- a/plugins/kubernetes-react/src/components/Cluster/Cluster.test.tsx +++ b/plugins/kubernetes-react/src/components/Cluster/Cluster.test.tsx @@ -56,4 +56,25 @@ describe('Cluster', () => { expect(screen.getByText('cluster-1')).toBeInTheDocument(); expect(screen.getByText('10 pods')).toBeInTheDocument(); }); + + it('renders title', async () => { + await renderInTestApp( + (), + } as any)} + />, + ); + + expect(screen.getByText('Cluster Number One')).toBeInTheDocument(); + }); }); diff --git a/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx b/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx index 42f1aca3a0..f2108ac1b7 100644 --- a/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx +++ b/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx @@ -132,7 +132,9 @@ export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => { }> diff --git a/plugins/kubernetes-react/src/components/ErrorPanel/ErrorPanel.test.tsx b/plugins/kubernetes-react/src/components/ErrorPanel/ErrorPanel.test.tsx index 5c7b10697c..3917cf4513 100644 --- a/plugins/kubernetes-react/src/components/ErrorPanel/ErrorPanel.test.tsx +++ b/plugins/kubernetes-react/src/components/ErrorPanel/ErrorPanel.test.tsx @@ -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", diff --git a/plugins/kubernetes-react/src/components/ErrorPanel/ErrorPanel.tsx b/plugins/kubernetes-react/src/components/ErrorPanel/ErrorPanel.tsx index b02f4c18dc..7109849b8e 100644 --- a/plugins/kubernetes-react/src/components/ErrorPanel/ErrorPanel.tsx +++ b/plugins/kubernetes-react/src/components/ErrorPanel/ErrorPanel.tsx @@ -25,7 +25,9 @@ const clustersWithErrorsToErrorMessage = ( return clustersWithErrors.map((c, i) => { return (
- {`Cluster: ${c.cluster.name}`} + {`Cluster: ${ + c.cluster.title || c.cluster.name + }`} {c.errors.map((e, j) => { return ( diff --git a/plugins/kubernetes-react/src/components/ErrorReporting/ErrorReporting.test.tsx b/plugins/kubernetes-react/src/components/ErrorReporting/ErrorReporting.test.tsx new file mode 100644 index 0000000000..6da259d7fa --- /dev/null +++ b/plugins/kubernetes-react/src/components/ErrorReporting/ErrorReporting.test.tsx @@ -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( + ([ + [ + '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( + ([ + [ + '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( + ([ + [ + '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(); + }); +}); diff --git a/plugins/kubernetes-react/src/components/ErrorReporting/ErrorReporting.tsx b/plugins/kubernetes-react/src/components/ErrorReporting/ErrorReporting.tsx index 64345662ed..5bf6cff50e 100644 --- a/plugins/kubernetes-react/src/components/ErrorReporting/ErrorReporting.tsx +++ b/plugins/kubernetes-react/src/components/ErrorReporting/ErrorReporting.tsx @@ -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[] = [ { 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[] = [ ]; 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, })); }) diff --git a/plugins/kubernetes-react/src/components/PodExecTerminal/PodExecTerminal.test.tsx b/plugins/kubernetes-react/src/components/PodExecTerminal/PodExecTerminal.test.tsx index 18e40bc241..4aa6112436 100644 --- a/plugins/kubernetes-react/src/components/PodExecTerminal/PodExecTerminal.test.tsx +++ b/plugins/kubernetes-react/src/components/PodExecTerminal/PodExecTerminal.test.tsx @@ -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( { await renderInTestApp( { - 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 + }`} > diff --git a/plugins/kubernetes-react/src/components/Pods/ErrorList/ErrorList.test.tsx b/plugins/kubernetes-react/src/components/Pods/ErrorList/ErrorList.test.tsx index 2423654cc3..998ef4b4d0 100644 --- a/plugins/kubernetes-react/src/components/Pods/ErrorList/ErrorList.test.tsx +++ b/plugins/kubernetes-react/src/components/Pods/ErrorList/ErrorList.test.tsx @@ -26,7 +26,7 @@ describe('ErrorList', () => { { diff --git a/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.tsx b/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.tsx index c691cc67fd..d0f3e74c14 100644 --- a/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.tsx +++ b/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.tsx @@ -119,7 +119,7 @@ export const FixDialog: React.FC = ({ containerScope={{ podName: pod.metadata?.name ?? 'unknown', podNamespace: pod.metadata?.namespace ?? 'unknown', - clusterName: clusterName, + cluster: { name: clusterName }, containerName: pf.container, }} /> diff --git a/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.test.tsx b/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.test.tsx index b7729f8b5f..6d64b8ad5b 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.test.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.test.tsx @@ -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: { diff --git a/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx b/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx index 592a93e3ea..746abbd668 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx @@ -233,7 +233,7 @@ export const ContainerCard: React.FC = ({ /> {isPodExecTerminalEnabled && ( { {...({ open: true, podAndErrors: { - clusterName: 'some-cluster-1', + cluster: { name: 'some-cluster-1' }, pod: { metadata: { name: 'some-pod', diff --git a/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.tsx b/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.tsx index 1b98237d5a..7c08f027f5 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.tsx @@ -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 ( { podName: podAndErrors.pod.metadata?.name ?? 'unknown', podNamespace: podAndErrors.pod.metadata?.namespace ?? 'unknown', - clusterName: podAndErrors.clusterName, + cluster: podAndErrors.cluster, }} containerSpec={containerSpec} containerStatus={containerStatus} diff --git a/plugins/kubernetes-react/src/components/Pods/PodLogs/PodLogsDialog.tsx b/plugins/kubernetes-react/src/components/Pods/PodLogs/PodLogsDialog.tsx index 3ffd4d8fa0..b8b2cbdf42 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodLogs/PodLogsDialog.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodLogs/PodLogsDialog.tsx @@ -42,7 +42,11 @@ export const PodLogsDialog = ({ containerScope }: PodLogsDialogProps) => { buttonIcon={} 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 + }`} > diff --git a/plugins/kubernetes-react/src/components/Pods/PodLogs/types.ts b/plugins/kubernetes-react/src/components/Pods/PodLogs/types.ts index 9ad551771e..c9f687d9d5 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodLogs/types.ts +++ b/plugins/kubernetes-react/src/components/Pods/PodLogs/types.ts @@ -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; } /** diff --git a/plugins/kubernetes-react/src/components/Pods/PodLogs/usePodLogs.ts b/plugins/kubernetes-react/src/components/Pods/PodLogs/usePodLogs.ts index 26764fc64c..98ed6c3177 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodLogs/usePodLogs.ts +++ b/plugins/kubernetes-react/src/components/Pods/PodLogs/usePodLogs.ts @@ -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)]); diff --git a/plugins/kubernetes-react/src/components/Pods/PodsTable.tsx b/plugins/kubernetes-react/src/components/Pods/PodsTable.tsx index d83aebb210..d9a7657367 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodsTable.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodsTable.tsx @@ -80,7 +80,6 @@ const READY: TableColumn[] = [ ]; 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 }) => { diff --git a/plugins/kubernetes-react/src/components/Pods/types.ts b/plugins/kubernetes-react/src/components/Pods/types.ts index 847e6d63bc..ee3ab5153a 100644 --- a/plugins/kubernetes-react/src/components/Pods/types.ts +++ b/plugins/kubernetes-react/src/components/Pods/types.ts @@ -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[]; } diff --git a/plugins/kubernetes/src/KubernetesContent.tsx b/plugins/kubernetes/src/KubernetesContent.tsx index 9c266b1854..ed00cb9a13 100644 --- a/plugins/kubernetes/src/KubernetesContent.tsx +++ b/plugins/kubernetes/src/KubernetesContent.tsx @@ -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 && ( - + Your Clusters