From 280ec10c18ed2db70a95ca7325df457b63d8b6d0 Mon Sep 17 00:00:00 2001 From: Matthew Clarke Date: Fri, 28 Apr 2023 10:34:33 -0400 Subject: [PATCH] feat: add Kubernetes pod logs component (#17120) * refactor: rename kubernetes structured metadata table Signed-off-by: Matthew Clarke * feat: add pod logs components Signed-off-by: Matthew Clarke * refactor: more generic proxy token generating Signed-off-by: Matthew Clarke * tests Signed-off-by: Matthew Clarke * chore: changeset Signed-off-by: Matthew Clarke * fix: tsc fix Signed-off-by: Matthew Clarke * docs: api report Signed-off-by: Matthew Clarke * fix: remove event/errors mention Signed-off-by: Matthew Clarke * feat: pending pod conditions Signed-off-by: Matthew Clarke * fix: missed file Signed-off-by: Matthew Clarke * fix: merge master Signed-off-by: Matthew Clarke * fix: remove unneeded code Signed-off-by: Matthew Clarke * feat: add proxy client Signed-off-by: Matthew Clarke * docs: breaking change Signed-off-by: Matthew Clarke * Update plugins/kubernetes/src/api/types.ts Co-authored-by: Patrik Oldsberg Signed-off-by: Matthew Clarke * Update plugins/kubernetes/src/api/types.ts Co-authored-by: Patrik Oldsberg Signed-off-by: Matthew Clarke * fix: PR feedback Signed-off-by: Matthew Clarke * fix: test Signed-off-by: Matthew Clarke * fix: PR feedback Signed-off-by: Matthew Clarke * fix: PR feedback Signed-off-by: Matthew Clarke * fix: todo Signed-off-by: Matthew Clarke * fix: api reports Signed-off-by: Matthew Clarke --------- Signed-off-by: Matthew Clarke Signed-off-by: Matthew Clarke Co-authored-by: Patrik Oldsberg --- .changeset/polite-dingos-train.md | 33 ++ plugins/kubernetes/api-report.md | 99 ++++- plugins/kubernetes/dev/index.tsx | 9 + plugins/kubernetes/package.json | 1 + .../src/api/KubernetesProxyClient.test.ts | 54 +++ .../src/api/KubernetesProxyClient.ts | 69 ++++ plugins/kubernetes/src/api/index.ts | 5 +- plugins/kubernetes/src/api/types.ts | 13 + .../CronJobsAccordions/CronJobsDrawer.tsx | 6 +- .../ArgoRollouts/RolloutDrawer.tsx | 6 +- .../DefaultCustomResourceDrawer.tsx | 6 +- .../DeploymentDrawer.tsx | 6 +- .../HorizontalPodAutoscalerDrawer.tsx | 6 +- .../IngressesAccordions/IngressDrawer.tsx | 6 +- .../components/JobsAccordions/JobsDrawer.tsx | 6 +- .../KubernetesDrawer/KubernetesDrawer.tsx | 385 ++++++------------ ...ubernetesStructuredMetadataTableDrawer.tsx | 298 ++++++++++++++ .../KubernetesDrawer/ManifestYaml.tsx | 60 +++ .../src/components/KubernetesDrawer/index.ts | 1 + .../src/components/Pods/PodDrawer.test.tsx | 99 ----- .../src/components/Pods/PodDrawer.tsx | 66 --- .../Pods/PodDrawer/ContainerCard.test.tsx | 137 +++++++ .../Pods/PodDrawer/ContainerCard.tsx | 173 ++++++++ .../Pods/PodDrawer/PendingPodContent.test.tsx | 185 +++++++++ .../Pods/PodDrawer/PendingPodContent.tsx | 94 +++++ .../Pods/PodDrawer/PodDrawer.test.tsx | 67 +++ .../components/Pods/PodDrawer/PodDrawer.tsx | 124 ++++++ .../src/components/Pods/PodDrawer/index.ts | 16 + .../src/components/Pods/PodLogs/PodLogs.tsx | 55 +++ .../components/Pods/PodLogs/PodLogsDialog.tsx | 90 ++++ .../src/components/Pods/PodLogs/index.ts | 19 + .../src/components/Pods/PodLogs/types.ts | 25 ++ .../src/components/Pods/PodLogs/usePodLogs.ts | 36 ++ .../src/components/Pods/PodsTable.tsx | 45 +- .../kubernetes/src/components/Pods/types.ts | 22 + .../ServicesAccordions/ServiceDrawer.tsx | 6 +- .../StatefulSetDrawer.tsx | 6 +- plugins/kubernetes/src/components/index.ts | 2 +- plugins/kubernetes/src/plugin.ts | 13 +- yarn.lock | 1 + 40 files changed, 1860 insertions(+), 490 deletions(-) create mode 100644 .changeset/polite-dingos-train.md create mode 100644 plugins/kubernetes/src/api/KubernetesProxyClient.test.ts create mode 100644 plugins/kubernetes/src/api/KubernetesProxyClient.ts create mode 100644 plugins/kubernetes/src/components/KubernetesDrawer/KubernetesStructuredMetadataTableDrawer.tsx create mode 100644 plugins/kubernetes/src/components/KubernetesDrawer/ManifestYaml.tsx delete mode 100644 plugins/kubernetes/src/components/Pods/PodDrawer.test.tsx delete mode 100644 plugins/kubernetes/src/components/Pods/PodDrawer.tsx create mode 100644 plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.test.tsx create mode 100644 plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.tsx create mode 100644 plugins/kubernetes/src/components/Pods/PodDrawer/PendingPodContent.test.tsx create mode 100644 plugins/kubernetes/src/components/Pods/PodDrawer/PendingPodContent.tsx create mode 100644 plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.test.tsx create mode 100644 plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.tsx create mode 100644 plugins/kubernetes/src/components/Pods/PodDrawer/index.ts create mode 100644 plugins/kubernetes/src/components/Pods/PodLogs/PodLogs.tsx create mode 100644 plugins/kubernetes/src/components/Pods/PodLogs/PodLogsDialog.tsx create mode 100644 plugins/kubernetes/src/components/Pods/PodLogs/index.ts create mode 100644 plugins/kubernetes/src/components/Pods/PodLogs/types.ts create mode 100644 plugins/kubernetes/src/components/Pods/PodLogs/usePodLogs.ts create mode 100644 plugins/kubernetes/src/components/Pods/types.ts diff --git a/.changeset/polite-dingos-train.md b/.changeset/polite-dingos-train.md new file mode 100644 index 0000000000..49ac56a000 --- /dev/null +++ b/.changeset/polite-dingos-train.md @@ -0,0 +1,33 @@ +--- +'@backstage/plugin-kubernetes': minor +--- + +Added Pod logs components for Kubernetes plugin + +**BREAKING**: `kubernetesProxyApi` for custom plugins built with components from the Kubernetes plugin apis, `kubernetesProxyApi` should be added to the plugin's API list. + +``` +... +export const kubernetesPlugin = createPlugin({ + id: 'kubernetes', + apis: [ +... + createApiFactory({ + api: kubernetesProxyApiRef, + deps: { + kubernetesApi: kubernetesApiRef, + }, + factory: ({ kubernetesApi }) => + new KubernetesProxyClient({ + kubernetesApi, + }), + }), +``` + +**BREAKING**: `KubernetesDrawer` is now called `KubernetesStructuredMetadataTableDrawer` so that we can do more than just show `StructuredMetadataTable` + +`import { KubernetesDrawer } from "@backstage/plugin-kubernetes"` + +should now be: + +`import { KubernetesStructuredMetadataTableDrawer } from "@backstage/plugin-kubernetes"` diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index 3814bcb4a5..60f5d3fc99 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -15,11 +15,13 @@ import { CustomResourceMatcher } from '@backstage/plugin-kubernetes-common'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; +import { IObjectMeta } from '@kubernetes-models/apimachinery/apis/meta/v1/ObjectMeta'; import type { 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'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; +import { Pod } from 'kubernetes-models/v1'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { V1ConfigMap } from '@kubernetes/client-node'; @@ -360,19 +362,28 @@ export const KubernetesContent: ({ refreshIntervalMs, }: KubernetesContentProps) => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "KubernetesDrawerable" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "KubernetesDrawerProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "KubernetesDrawer" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const KubernetesDrawer: ({ - object, - renderObject, - kind, - buttonVariant, - expanded, +export const KubernetesDrawer: ({ + open, + label, + drawerContentsHeader, + kubernetesObject, children, -}: KubernetesDrawerProps) => JSX.Element; +}: KubernetesDrawerProps) => JSX.Element; + +// Warning: (ae-forgotten-export) The symbol "KubernetesDrawerContentProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "KubernetesDrawerContent" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const KubernetesDrawerContent: ({ + children, + header, + kubernetesObject, + close, +}: KubernetesDrawerContentProps) => JSX.Element; // Warning: (ae-missing-release-tag) "KubernetesObjects" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -399,11 +410,75 @@ const kubernetesPlugin: BackstagePlugin< export { kubernetesPlugin }; export { kubernetesPlugin as plugin }; +// Warning: (ae-missing-release-tag) "KubernetesProxyApi" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const PodDrawer: (props: { - pod: V1Pod; - expanded?: boolean; -}) => JSX.Element; +export interface KubernetesProxyApi { + // (undocumented) + getPodLogs(request: { + podName: string; + namespace: string; + clusterName: string; + containerName: string; + }): Promise<{ + text: string; + }>; +} + +// Warning: (ae-missing-release-tag) "kubernetesProxyApiRef" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const kubernetesProxyApiRef: ApiRef; + +// @public +export class KubernetesProxyClient { + constructor(options: { kubernetesApi: KubernetesApi }); + // (undocumented) + getPodLogs({ + podName, + namespace, + clusterName, + containerName, + }: { + podName: string; + namespace: string; + clusterName: string; + containerName: string; + }): Promise<{ + text: string; + }>; +} + +// Warning: (ae-forgotten-export) The symbol "KubernetesDrawerable" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "KubernetesStructuredMetadataTableDrawerProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "KubernetesStructuredMetadataTableDrawer" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const KubernetesStructuredMetadataTableDrawer: < + T extends KubernetesDrawerable, +>({ + object, + renderObject, + kind, + buttonVariant, + expanded, + children, +}: KubernetesStructuredMetadataTableDrawerProps) => JSX.Element; + +// Warning: (ae-forgotten-export) The symbol "ErrorPanelProps_2" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "LinkErrorPanel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const LinkErrorPanel: ({ + cluster, + errorMessage, +}: ErrorPanelProps_2) => JSX.Element; + +// Warning: (ae-forgotten-export) The symbol "PodDrawerProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "PodDrawer" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const PodDrawer: ({ podAndErrors, open }: PodDrawerProps) => JSX.Element; // Warning: (ae-missing-release-tag) "PodNamesWithErrorsContext" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/kubernetes/dev/index.tsx b/plugins/kubernetes/dev/index.tsx index 5780d649fb..520f8802a9 100644 --- a/plugins/kubernetes/dev/index.tsx +++ b/plugins/kubernetes/dev/index.tsx @@ -61,6 +61,15 @@ class MockKubernetesClient implements KubernetesApi { ({ type: type.toLocaleLowerCase('en-US'), resources } as FetchResponse), ); } + async getPodLogs(_request: { + podName: string; + namespace: string; + clusterName: string; + containerName: string; + token: string; + }): Promise { + return await 'some logs'; + } async getWorkloadsByEntity( _request: WorkloadsByEntityRequest, ): Promise { diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 00b8703c90..e99ca889e3 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -41,6 +41,7 @@ "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/theme": "workspace:^", + "@kubernetes-models/apimachinery": "^1.1.0", "@kubernetes/client-node": "0.18.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", diff --git a/plugins/kubernetes/src/api/KubernetesProxyClient.test.ts b/plugins/kubernetes/src/api/KubernetesProxyClient.test.ts new file mode 100644 index 0000000000..6db1e459be --- /dev/null +++ b/plugins/kubernetes/src/api/KubernetesProxyClient.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { KubernetesProxyClient } from './KubernetesProxyClient'; + +describe('KubernetesProxyClient', () => { + let proxy: KubernetesProxyClient; + const callProxyMock = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + proxy = new KubernetesProxyClient({ + kubernetesApi: { + proxy: callProxyMock, + } as any, + }); + }); + it('/logs returns log text', async () => { + const request = { + podName: 'some-pod', + namespace: 'some-namespace', + clusterName: 'some-cluster', + containerName: 'some-container', + }; + + callProxyMock.mockResolvedValue({ + text: jest.fn().mockResolvedValue('Hello World'), + ok: true, + }); + + const response = await proxy.getPodLogs(request); + await expect(response).toStrictEqual({ text: 'Hello World' }); + expect(callProxyMock).toHaveBeenCalledWith({ + clusterName: 'some-cluster', + init: { + method: 'GET', + }, + path: '/api/v1/namespaces/some-namespace/pods/some-pod/log?container=some-container', + }); + }); +}); diff --git a/plugins/kubernetes/src/api/KubernetesProxyClient.ts b/plugins/kubernetes/src/api/KubernetesProxyClient.ts new file mode 100644 index 0000000000..8fd4674ddd --- /dev/null +++ b/plugins/kubernetes/src/api/KubernetesProxyClient.ts @@ -0,0 +1,69 @@ +/* + * 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 { KubernetesApi } from './types'; + +/** + * A client for common requests through the proxy endpoint of the kubernetes backend plugin. + * + * @public + */ +export class KubernetesProxyClient { + private readonly kubernetesApi: KubernetesApi; + + constructor(options: { kubernetesApi: KubernetesApi }) { + this.kubernetesApi = options.kubernetesApi; + } + + private async handleText(response: Response): Promise { + if (!response.ok) { + const payload = await response.text(); + let message; + switch (response.status) { + default: + message = `Proxy request failed with ${response.status} ${response.statusText}, ${payload}`; + } + throw new Error(message); + } + + return await response.text(); + } + + async getPodLogs({ + podName, + namespace, + clusterName, + containerName, + }: { + podName: string; + namespace: string; + clusterName: string; + containerName: string; + }): Promise<{ text: string }> { + const params = new URLSearchParams({ + container: containerName, + }); + return await this.kubernetesApi + .proxy({ + clusterName: clusterName, + path: `/api/v1/namespaces/${namespace}/pods/${podName}/log?${params.toString()}`, + init: { + method: 'GET', + }, + }) + .then(response => this.handleText(response)) + .then(text => ({ text })); + } +} diff --git a/plugins/kubernetes/src/api/index.ts b/plugins/kubernetes/src/api/index.ts index c21a258279..ec8843937a 100644 --- a/plugins/kubernetes/src/api/index.ts +++ b/plugins/kubernetes/src/api/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ -export { kubernetesApiRef } from './types'; -export type { KubernetesApi } from './types'; +export { kubernetesApiRef, kubernetesProxyApiRef } from './types'; +export type { KubernetesApi, KubernetesProxyApi } from './types'; export { KubernetesBackendClient } from './KubernetesBackendClient'; +export { KubernetesProxyClient } from './KubernetesProxyClient'; diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts index 02f2fd3521..5dec189982 100644 --- a/plugins/kubernetes/src/api/types.ts +++ b/plugins/kubernetes/src/api/types.ts @@ -26,6 +26,10 @@ export const kubernetesApiRef = createApiRef({ id: 'plugin.kubernetes.service', }); +export const kubernetesProxyApiRef = createApiRef({ + id: 'plugin.kubernetes.proxy-service', +}); + export interface KubernetesApi { getObjectsByEntity( requestBody: KubernetesRequestBody, @@ -49,3 +53,12 @@ export interface KubernetesApi { init?: RequestInit; }): Promise; } + +export interface KubernetesProxyApi { + getPodLogs(request: { + podName: string; + namespace: string; + clusterName: string; + containerName: string; + }): Promise<{ text: string }>; +} diff --git a/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.tsx b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.tsx index 0dc53396bb..45242af307 100644 --- a/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.tsx +++ b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; import { V1CronJob } from '@kubernetes/client-node'; -import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; +import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer'; import { Typography, Grid, Chip } from '@material-ui/core'; export const CronJobDrawer = ({ @@ -27,7 +27,7 @@ export const CronJobDrawer = ({ }) => { const namespace = cronJob.metadata?.namespace; return ( - )} - + ); }; diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/RolloutDrawer.tsx b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/RolloutDrawer.tsx index f1c4fe7f7e..150f3bc186 100644 --- a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/RolloutDrawer.tsx +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/RolloutDrawer.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { KubernetesDrawer } from '../../KubernetesDrawer/KubernetesDrawer'; +import { KubernetesStructuredMetadataTableDrawer } from '../../KubernetesDrawer'; import { Typography, Grid } from '@material-ui/core'; export const RolloutDrawer = ({ @@ -26,7 +26,7 @@ export const RolloutDrawer = ({ expanded?: boolean; }) => { return ( - - + ); }; diff --git a/plugins/kubernetes/src/components/CustomResources/DefaultCustomResourceDrawer.tsx b/plugins/kubernetes/src/components/CustomResources/DefaultCustomResourceDrawer.tsx index 5b968f72a5..82dcd0746f 100644 --- a/plugins/kubernetes/src/components/CustomResources/DefaultCustomResourceDrawer.tsx +++ b/plugins/kubernetes/src/components/CustomResources/DefaultCustomResourceDrawer.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; +import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer'; import { Typography, Grid } from '@material-ui/core'; const capitalize = (str: string) => @@ -33,7 +33,7 @@ export const DefaultCustomResourceDrawer = ({ const capitalizedName = capitalize(customResourceName); return ( - - + ); }; diff --git a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx index 8be5ee1e46..0fc8463f73 100644 --- a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { V1Deployment } from '@kubernetes/client-node'; -import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; +import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer'; import { renderCondition } from '../../utils/pod'; import { Typography, Grid, Chip } from '@material-ui/core'; @@ -29,7 +29,7 @@ export const DeploymentDrawer = ({ }) => { const namespace = deployment.metadata?.namespace; return ( - )} - + ); }; diff --git a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.tsx b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.tsx index ca7abe07aa..dba942155c 100644 --- a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.tsx +++ b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { V1HorizontalPodAutoscaler } from '@kubernetes/client-node'; -import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; +import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer'; /** @public */ export const HorizontalPodAutoscalerDrawer = (props: { @@ -27,7 +27,7 @@ export const HorizontalPodAutoscalerDrawer = (props: { const { hpa, expanded, children } = props; return ( - {children} - + ); }; diff --git a/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx b/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx index ea79c1e4dc..5881f6d6c4 100644 --- a/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx +++ b/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { V1Ingress } from '@kubernetes/client-node'; -import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; +import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer'; import { Typography, Grid } from '@material-ui/core'; export const IngressDrawer = ({ @@ -27,7 +27,7 @@ export const IngressDrawer = ({ expanded?: boolean; }) => { return ( - - + ); }; diff --git a/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.tsx b/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.tsx index b91fb15541..c60153e77f 100644 --- a/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.tsx +++ b/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; import { V1Job } from '@kubernetes/client-node'; -import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; +import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer'; import { Typography, Grid } from '@material-ui/core'; export const JobDrawer = ({ @@ -26,7 +26,7 @@ export const JobDrawer = ({ expanded?: boolean; }) => { return ( - - + ); }; diff --git a/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx b/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx index 9be30debf3..18ef5d9505 100644 --- a/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx +++ b/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * 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. @@ -13,36 +13,115 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import React, { ChangeEvent, useState } from 'react'; -import React, { ChangeEvent, useContext, useState } from 'react'; +import { IObjectMeta } from '@kubernetes-models/apimachinery/apis/meta/v1/ObjectMeta'; import { - Button, - Typography, - makeStyles, - IconButton, createStyles, - Theme, Drawer, - Switch, - FormControlLabel, - FormGroup, + makeStyles, + Theme, Grid, + IconButton, + Switch, + Typography, + Button, + withStyles, + FormControlLabel, } from '@material-ui/core'; -import Close from '@material-ui/icons/Close'; -import OpenInNewIcon from '@material-ui/icons/OpenInNew'; -import { V1ObjectMeta } from '@kubernetes/client-node'; -import { withStyles } from '@material-ui/core/styles'; -import jsYaml from 'js-yaml'; -import { - LinkButton as BackstageButton, - CodeSnippet, - StructuredMetadataTable, - 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 CloseIcon from '@material-ui/icons/Close'; +import { ManifestYaml } from './ManifestYaml'; + +const useDrawerContentStyles = makeStyles((_theme: Theme) => + createStyles({ + header: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + }, + content: { + height: '80%', + }, + icon: { + fontSize: 20, + }, + }), +); + +interface KubernetesObject { + kind: string; + metadata?: IObjectMeta; +} + +interface KubernetesDrawerContentProps { + close: () => void; + kubernetesObject: KubernetesObject; + header?: React.ReactNode; + children?: React.ReactNode; +} + +export const KubernetesDrawerContent = ({ + children, + header, + kubernetesObject, + close, +}: KubernetesDrawerContentProps) => { + const classes = useDrawerContentStyles(); + const [isYaml, setIsYaml] = useState(false); + + return ( + <> +
+ + + + {kubernetesObject.metadata?.name} + + + + close()} + color="inherit" + > + + + + + {header} + + + { + setIsYaml(event.target.checked); + }} + name="YAML" + /> + } + label="YAML" + /> + + +
+
+ {isYaml && } + {!isYaml && children} +
+ + ); +}; + +interface KubernetesDrawerProps { + open?: boolean; + kubernetesObject: KubernetesObject; + label: React.ReactNode; + drawerContentsHeader?: React.ReactNode; + children?: React.ReactNode; +} const useDrawerStyles = makeStyles((theme: Theme) => createStyles({ @@ -54,32 +133,7 @@ const useDrawerStyles = makeStyles((theme: Theme) => }), ); -const useDrawerContentStyles = makeStyles((_: Theme) => - createStyles({ - header: { - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - }, - errorMessage: { - marginTop: '1em', - marginBottom: '1em', - }, - options: { - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - }, - icon: { - fontSize: 20, - }, - content: { - height: '80%', - }, - }), -); - -const PodDrawerButton = withStyles({ +const DrawerButton = withStyles({ root: { padding: '6px 5px', }, @@ -88,203 +142,15 @@ const PodDrawerButton = withStyles({ }, })(Button); -type ErrorPanelProps = { - cluster: ClusterAttributes; - errorMessage?: string; - children?: React.ReactNode; -}; - -export const ErrorPanel = ({ cluster, errorMessage }: ErrorPanelProps) => ( - - {errorMessage && ( - Errors: {errorMessage} - )} - -); - -interface KubernetesDrawerable { - metadata?: V1ObjectMeta; -} - -interface KubernetesDrawerContentProps { - toggleDrawer: (e: ChangeEvent<{}>, isOpen: boolean) => void; - object: T; - renderObject: (obj: T) => object; - kind: string; -} - -function replaceNullsWithUndefined(someObj: any) { - const replacer = (_: any, value: any) => - String(value) === 'null' || String(value) === 'undefined' - ? undefined - : value; - - 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 KubernetesDrawerContent = ({ - toggleDrawer, - object, - renderObject, - kind, -}: KubernetesDrawerContentProps) => { - const [isYaml, setIsYaml] = useState(false); - - // Toggle whether the Kubernetes resource managed fields should be shown in - // the YAML display. This toggle is only available when the YAML is being - // shown because managed fields are never visible in the structured display. - const [managedFields, setManagedFields] = useState(false); - - const classes = useDrawerContentStyles(); - const cluster = useContext(ClusterContext); - const { clusterLink, errorMessage } = tryFormatClusterLink({ - dashboardUrl: cluster.dashboardUrl, - dashboardApp: cluster.dashboardApp, - dashboardParameters: cluster.dashboardParameters, - object, - kind, - }); - - return ( - <> -
- - - - {object.metadata?.name ?? 'unknown name'} - - - - - {kind} - - - - toggleDrawer(e, false)} - color="inherit" - > - - -
- {errorMessage && ( -
- -
- )} -
-
- {clusterLink && ( - } - > - Open Kubernetes Dashboard - - )} -
- - { - setIsYaml(event.target.checked); - }} - name="YAML" - /> - } - label="YAML" - /> - { - if (isYaml) { - setManagedFields(event.target.checked); - } - }} - name="Managed Fields" - /> - } - label="Managed Fields" - /> - -
-
- {isYaml && ( - { - if (!managedFields) { - return key === 'managedFields' ? undefined : value; - } - return value; - }, - })} - /> - )} - {!isYaml && ( - - )} -
- - ); -}; -interface KubernetesDrawerProps { - object: T; - renderObject: (obj: T) => object; - buttonVariant?: 'h5' | 'subtitle2'; - kind: string; - expanded?: boolean; - children?: React.ReactNode; -} - -export const KubernetesDrawer = ({ - object, - renderObject, - kind, - buttonVariant = 'subtitle2', - expanded = false, +export const KubernetesDrawer = ({ + open, + label, + drawerContentsHeader, + kubernetesObject, children, -}: KubernetesDrawerProps) => { - const [isOpen, setIsOpen] = useState(expanded); +}: KubernetesDrawerProps) => { const classes = useDrawerStyles(); + const [isOpen, setIsOpen] = useState(open ?? false); const toggleDrawer = (e: ChangeEvent<{}>, newValue: boolean) => { e.stopPropagation(); @@ -293,18 +159,7 @@ export const KubernetesDrawer = ({ return ( <> - toggleDrawer(e, true)} - onFocus={event => event.stopPropagation()} - > - {children === undefined ? ( - - {object.metadata?.name ?? 'unknown object'} - - ) : ( - children - )} - + setIsOpen(true)}>{label} ({ onClose={(e: any) => toggleDrawer(e, false)} onClick={event => event.stopPropagation()} > - + {isOpen && ( + setIsOpen(false)} + /> + )} ); diff --git a/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesStructuredMetadataTableDrawer.tsx b/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesStructuredMetadataTableDrawer.tsx new file mode 100644 index 0000000000..8f28549f83 --- /dev/null +++ b/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesStructuredMetadataTableDrawer.tsx @@ -0,0 +1,298 @@ +/* + * 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 React, { ChangeEvent, useContext, useState } from 'react'; +import { + Button, + Typography, + makeStyles, + IconButton, + createStyles, + Theme, + Drawer, + Switch, + FormControlLabel, + Grid, +} from '@material-ui/core'; +import Close from '@material-ui/icons/Close'; +import OpenInNewIcon from '@material-ui/icons/OpenInNew'; +import { V1ObjectMeta } from '@kubernetes/client-node'; +import { withStyles } from '@material-ui/core/styles'; +import { + LinkButton as BackstageButton, + StructuredMetadataTable, + 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'; + +const useDrawerStyles = makeStyles((theme: Theme) => + createStyles({ + paper: { + width: '50%', + justifyContent: 'space-between', + padding: theme.spacing(2.5), + }, + }), +); + +const useDrawerContentStyles = makeStyles((_: Theme) => + createStyles({ + header: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + }, + errorMessage: { + marginTop: '1em', + marginBottom: '1em', + }, + options: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + }, + icon: { + fontSize: 20, + }, + content: { + height: '80%', + }, + }), +); + +const PodDrawerButton = withStyles({ + root: { + padding: '6px 5px', + }, + label: { + textTransform: 'none', + }, +})(Button); + +type ErrorPanelProps = { + cluster: ClusterAttributes; + errorMessage?: string; + children?: React.ReactNode; +}; + +export const LinkErrorPanel = ({ cluster, errorMessage }: ErrorPanelProps) => ( + + {errorMessage && ( + Errors: {errorMessage} + )} + +); + +interface KubernetesDrawerable { + metadata?: V1ObjectMeta; +} + +interface KubernetesStructuredMetadataTableDrawerContentProps< + T extends KubernetesDrawerable, +> { + toggleDrawer: (e: ChangeEvent<{}>, isOpen: boolean) => void; + object: T; + renderObject: (obj: T) => object; + kind: string; +} + +function replaceNullsWithUndefined(someObj: any) { + const replacer = (_: any, value: any) => + String(value) === 'null' || String(value) === 'undefined' + ? undefined + : value; + + 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, +>({ + toggleDrawer, + object, + renderObject, + kind, +}: KubernetesStructuredMetadataTableDrawerContentProps) => { + const [isYaml, setIsYaml] = useState(false); + + const classes = useDrawerContentStyles(); + const cluster = useContext(ClusterContext); + const { clusterLink, errorMessage } = tryFormatClusterLink({ + dashboardUrl: cluster.dashboardUrl, + dashboardApp: cluster.dashboardApp, + dashboardParameters: cluster.dashboardParameters, + object, + kind, + }); + + return ( + <> +
+ + + + {object.metadata?.name ?? 'unknown name'} + + + + toggleDrawer(e, false)} + color="inherit" + > + + + + + + {kind} + + + + { + setIsYaml(event.target.checked); + }} + name="YAML" + /> + } + label="YAML" + /> + + +
+ {errorMessage && ( +
+ +
+ )} +
+
+ {clusterLink && ( + } + > + Open Kubernetes Dashboard + + )} +
+
+
+ {isYaml && } + {!isYaml && ( + + )} +
+ + ); +}; +interface KubernetesStructuredMetadataTableDrawerProps< + T extends KubernetesDrawerable, +> { + object: T; + renderObject: (obj: T) => object; + buttonVariant?: 'h5' | 'subtitle2'; + kind: string; + expanded?: boolean; + children?: React.ReactNode; +} + +export const KubernetesStructuredMetadataTableDrawer = < + T extends KubernetesDrawerable, +>({ + object, + renderObject, + kind, + buttonVariant = 'subtitle2', + expanded = false, + children, +}: KubernetesStructuredMetadataTableDrawerProps) => { + const [isOpen, setIsOpen] = useState(expanded); + const classes = useDrawerStyles(); + + const toggleDrawer = (e: ChangeEvent<{}>, newValue: boolean) => { + e.stopPropagation(); + setIsOpen(newValue); + }; + + return ( + <> + toggleDrawer(e, true)} + onFocus={event => event.stopPropagation()} + > + {children === undefined ? ( + + {object.metadata?.name ?? 'unknown object'} + + ) : ( + children + )} + + toggleDrawer(e, false)} + onClick={event => event.stopPropagation()} + > + + + + ); +}; diff --git a/plugins/kubernetes/src/components/KubernetesDrawer/ManifestYaml.tsx b/plugins/kubernetes/src/components/KubernetesDrawer/ManifestYaml.tsx new file mode 100644 index 0000000000..8f0c38afde --- /dev/null +++ b/plugins/kubernetes/src/components/KubernetesDrawer/ManifestYaml.tsx @@ -0,0 +1,60 @@ +/* + * 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 { CodeSnippet } from '@backstage/core-components'; +import { FormControlLabel, Switch } from '@material-ui/core'; +import jsyaml from 'js-yaml'; +import React, { useState } from 'react'; + +export interface ManifestYamlProps { + object: object; +} + +export const ManifestYaml = ({ object }: ManifestYamlProps) => { + // Toggle whether the Kubernetes resource managed fields should be shown in + // the YAML display. This toggle is only available when the YAML is being + // shown because managed fields are never visible in the structured display. + const [managedFields, setManagedFields] = useState(false); + return ( + <> + { + setManagedFields(event.target.checked); + }} + name="Managed Fields" + /> + } + label="Managed Fields" + /> + { + if (!managedFields) { + return key === 'managedFields' ? undefined : value; + } + return value; + }, + })} + /> + + ); +}; diff --git a/plugins/kubernetes/src/components/KubernetesDrawer/index.ts b/plugins/kubernetes/src/components/KubernetesDrawer/index.ts index ecd4b1b653..a0108cbda6 100644 --- a/plugins/kubernetes/src/components/KubernetesDrawer/index.ts +++ b/plugins/kubernetes/src/components/KubernetesDrawer/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ +export * from './KubernetesStructuredMetadataTableDrawer'; export * from './KubernetesDrawer'; diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer.test.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer.test.tsx deleted file mode 100644 index b11093250d..0000000000 --- a/plugins/kubernetes/src/components/Pods/PodDrawer.test.tsx +++ /dev/null @@ -1,99 +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 React from 'react'; -import { render } from '@testing-library/react'; -import * as pod from './__fixtures__/pod.json'; -import * as crashingPod from './__fixtures__/crashing-pod.json'; -import { textContentMatcher, wrapInTestApp } from '@backstage/test-utils'; -import { PodDrawer } from './PodDrawer'; - -describe('PodDrawer', () => { - it('should render pod', async () => { - const { getByText, getAllByText } = render( - wrapInTestApp(), - ); - - expect(getAllByText('dice-roller-6c8646bfd-2m5hv')).toHaveLength(2); - expect(getByText('Pod')).toBeInTheDocument(); - expect(getByText('YAML')).toBeInTheDocument(); - expect(getByText('Images')).toBeInTheDocument(); - expect(getByText('nginx=nginx:1.14.2')).toBeInTheDocument(); - expect(getByText('Phase')).toBeInTheDocument(); - expect(getByText('Running')).toBeInTheDocument(); - expect(getAllByText('Containers Ready')).toHaveLength(2); - expect(getByText('1/1')).toBeInTheDocument(); - expect(getByText('Total Restarts')).toBeInTheDocument(); - expect(getByText('0')).toBeInTheDocument(); - expect(getByText('Container Statuses')).toBeInTheDocument(); - expect(getByText('OK')).toBeInTheDocument(); - expect(getByText('Initialized')).toBeInTheDocument(); - expect(getByText('Ready')).toBeInTheDocument(); - expect(getByText('Pod Scheduled')).toBeInTheDocument(); - expect(getAllByText('True')).toHaveLength(4); - expect(getByText('Exposed Ports')).toBeInTheDocument(); - expect(getByText('Nginx:')).toBeInTheDocument(); - expect( - getByText(textContentMatcher('Container Port: 80')), - ).toBeInTheDocument(); - expect(getByText(textContentMatcher('Protocol: TCP'))).toBeInTheDocument(); - }); - it('should render crashing pod', async () => { - const { getByText, getAllByText } = render( - wrapInTestApp(), - ); - - expect(getAllByText('dice-roller-canary-7d64cd756c-55rfq')).toHaveLength(2); - expect(getByText('Pod')).toBeInTheDocument(); - expect(getByText('YAML')).toBeInTheDocument(); - expect(getByText('Images')).toBeInTheDocument(); - expect(getByText('nginx=nginx:1.14.2')).toBeInTheDocument(); - expect(getByText('other-side-car=nginx:1.14.2')).toBeInTheDocument(); - expect(getByText('side-car=nginx:1.14.2')).toBeInTheDocument(); - expect(getByText('Phase')).toBeInTheDocument(); - expect(getByText('Running')).toBeInTheDocument(); - expect(getAllByText('Containers Ready')).toHaveLength(2); - expect(getByText('1/3')).toBeInTheDocument(); - expect(getByText('Total Restarts')).toBeInTheDocument(); - expect(getByText('76')).toBeInTheDocument(); - expect(getByText('Container Statuses')).toBeInTheDocument(); - expect(getByText('Container: side-car')).toBeInTheDocument(); - expect(getByText('Container: other-side-car')).toBeInTheDocument(); - expect(getAllByText('CrashLoopBackOff')).toHaveLength(2); - expect(getByText('Initialized')).toBeInTheDocument(); - expect(getByText('Ready')).toBeInTheDocument(); - expect(getByText('Pod Scheduled')).toBeInTheDocument(); - expect(getAllByText('True')).toHaveLength(2); - expect(getAllByText('False')).toHaveLength(2); - expect( - getAllByText('containers with unready status: [side-car other-side-car]'), - ).toHaveLength(2); - expect(getByText('Exposed Ports')).toBeInTheDocument(); - expect(getAllByText(textContentMatcher('Protocol: TCP'))).toHaveLength(3); - expect(getByText('Nginx:')).toBeInTheDocument(); - expect( - getByText(textContentMatcher('Container Port: 80')), - ).toBeInTheDocument(); - expect(getByText('Side Car:')).toBeInTheDocument(); - expect( - getByText(textContentMatcher('Container Port: 81')), - ).toBeInTheDocument(); - expect(getByText('Other Side Car:')).toBeInTheDocument(); - expect( - getByText(textContentMatcher('Container Port: 82')), - ).toBeInTheDocument(); - }); -}); diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer.tsx deleted file mode 100644 index fa775a89c5..0000000000 --- a/plugins/kubernetes/src/components/Pods/PodDrawer.tsx +++ /dev/null @@ -1,66 +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 React from 'react'; -import { V1Pod } from '@kubernetes/client-node'; -import { - containersReady, - containerStatuses, - totalRestarts, - imageChips, - renderCondition, -} from '../../utils/pod'; -import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; - -/** @public */ -export const PodDrawer = (props: { pod: V1Pod; expanded?: boolean }) => { - const { pod, expanded } = props; - - return ( - { - const phase = podObject.status?.phase ?? 'unknown'; - - const ports = - podObject.spec?.containers?.map(c => { - return { - [c.name]: c.ports, - }; - }) ?? 'N/A'; - - const conditions = (podObject.status?.conditions ?? []) - .map(renderCondition) - .reduce((accum, next) => { - accum[next[0]] = next[1]; - return accum; - }, {} as { [key: string]: React.ReactNode }); - - return { - images: imageChips(podObject), - phase: phase, - 'Containers Ready': containersReady(podObject), - 'Total Restarts': totalRestarts(podObject), - 'Container Statuses': containerStatuses(podObject), - ...conditions, - 'Exposed ports': ports, - }; - }} - /> - ); -}; diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.test.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.test.tsx new file mode 100644 index 0000000000..7bc7c989ed --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.test.tsx @@ -0,0 +1,137 @@ +/* + * 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 React from 'react'; + +import { render } from '@testing-library/react'; + +import '@testing-library/jest-dom'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { ContainerCard } from './ContainerCard'; +import { DateTime } from 'luxon'; + +const now = DateTime.now(); +const oneHourAgo = now.minus({ hours: 1 }).toISO(); +const twoHoursAgo = now.minus({ hours: 2 }).toISO(); + +describe('ContainerCard', () => { + it('show healthy when all checks pass', async () => { + const { getByText, queryByText, getAllByText } = render( + wrapInTestApp( + , + ), + ); + expect(getByText('Started: 1 hour ago')).toBeInTheDocument(); + expect(getByText('Status: Running')).toBeInTheDocument(); + expect(getByText('some-name')).toBeInTheDocument(); + expect(getByText('gcr.io/some-proj/some-image')).toBeInTheDocument(); + expect(getAllByText('✅')).toHaveLength(5); + expect(queryByText('❌')).toBeNull(); + }); + it('show unhealthy when all checks fail', async () => { + const { getByText, queryByText, getAllByText } = render( + wrapInTestApp( + , + ), + ); + expect(getByText('some-name')).toBeInTheDocument(); + expect(getByText('gcr.io/some-proj/some-image')).toBeInTheDocument(); + expect(getAllByText('❌')).toHaveLength(5); + expect(queryByText('✅')).toBeNull(); + }); + it('show correct checks for completed container', async () => { + const { getByText, queryByText, getAllByText } = render( + wrapInTestApp( + , + ), + ); + expect(getByText('some-name')).toBeInTheDocument(); + expect(getByText('gcr.io/some-proj/some-image')).toBeInTheDocument(); + expect(getByText('Started: 2 hours ago')).toBeInTheDocument(); + expect(getByText('Completed: 1 hour ago')).toBeInTheDocument(); + expect( + getByText('Execution time: 1 hour, 0 minutes, 0 seconds'), + ).toBeInTheDocument(); + expect(getByText('Status: Completed')).toBeInTheDocument(); + expect(getAllByText('✅')).toHaveLength(2); + expect(queryByText('❌')).toBeNull(); + }); +}); diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.tsx new file mode 100644 index 0000000000..eac64ae741 --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.tsx @@ -0,0 +1,173 @@ +/* + * 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 React from 'react'; + +import { IContainer, IContainerStatus } from 'kubernetes-models/v1'; +import { + Card, + CardActions, + CardContent, + CardHeader, + Grid, + Typography, +} from '@material-ui/core'; + +import { DateTime } from 'luxon'; + +import { PodScope, PodLogsDialog } from '../PodLogs'; +import { StructuredMetadataTable } from '@backstage/core-components'; + +const getContainerHealthChecks = ( + containerSpec: IContainer, + containerStatus: IContainerStatus, +): { [key: string]: boolean } => { + if (containerStatus.state?.terminated?.reason === 'Completed') { + return { + 'not waiting to start': containerStatus.state?.waiting === undefined, + 'no restarts': containerStatus.restartCount === 0, + }; + } + return { + 'not waiting to start': containerStatus.state?.waiting === undefined, + started: !!containerStatus.started, + ready: containerStatus.ready, + 'no restarts': containerStatus.restartCount === 0, + 'readiness probe set': + containerSpec && containerSpec?.readinessProbe !== undefined, + }; +}; + +const getCurrentState = (containerStatus: IContainerStatus): string => { + return ( + containerStatus.state?.waiting?.reason || + containerStatus.state?.terminated?.reason || + (containerStatus.state?.running !== undefined ? 'Running' : 'Unknown') + ); +}; + +const getStartedAtTime = ( + containerStatus: IContainerStatus, +): string | undefined => { + return ( + containerStatus.state?.running?.startedAt || + containerStatus.state?.terminated?.startedAt + ); +}; + +interface ContainerDatetimeProps { + prefix: string; + dateTime: string; +} + +const ContainerDatetime = ({ prefix, dateTime }: ContainerDatetimeProps) => { + return ( + + {prefix}:{' '} + {DateTime.fromISO(dateTime).toRelative({ + locale: 'en', + })} + + ); +}; + +export interface ContainerCardProps { + podScope: PodScope; + containerSpec?: IContainer; + containerStatus: IContainerStatus; +} + +export const ContainerCard: React.FC = ({ + podScope, + containerSpec, + containerStatus, +}: ContainerCardProps) => { + // This should never be undefined + if (containerSpec === undefined) { + return error reading pod from cluster; + } + const containerStartedTime = getStartedAtTime(containerStatus); + const containerFinishedTime = containerStatus.state?.terminated?.finishedAt; + + return ( + + + + + + {containerStartedTime && ( + + )} + {containerFinishedTime && ( + + )} + {containerStartedTime && containerFinishedTime && ( + + Execution time:{' '} + {DateTime.fromISO(containerFinishedTime) + .diff(DateTime.fromISO(containerStartedTime), [ + 'hours', + 'minutes', + 'seconds', + ]) + .toHuman()} + + )} + + + + Status: {getCurrentState(containerStatus)} + + + {containerStatus.restartCount > 0 && ( + + + Restarts: {containerStatus.restartCount} + + + )} + + Container health + + + + + + + + + + + ); +}; diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/PendingPodContent.test.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer/PendingPodContent.test.tsx new file mode 100644 index 0000000000..e4d042217b --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/PodDrawer/PendingPodContent.test.tsx @@ -0,0 +1,185 @@ +/* + * 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 React from 'react'; + +import { render } from '@testing-library/react'; + +import '@testing-library/jest-dom'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { PendingPodContent } from './PendingPodContent'; +import { IPodCondition } from 'kubernetes-models/v1'; +import { DateTime } from 'luxon'; + +const podWithConditions = (conditions: IPodCondition[]): any => { + return { + metadata: { + name: 'ok-pod', + }, + spec: { + containers: [ + { + name: 'some-container', + }, + ], + }, + status: { + podIP: '127.0.0.1', + conditions: conditions, + }, + }; +}; + +describe('PendingPodContent', () => { + it('show startup conditions - all healthy', async () => { + const oneDayAgo = DateTime.now().minus({ days: 1 }).toISO(); + const { getByText, queryByLabelText, queryAllByLabelText } = render( + wrapInTestApp( + , + ), + ); + expect(getByText('Pod is Pending. Conditions:')).toBeInTheDocument(); + + expect(getByText('Initialized - (1 day ago)')).toBeInTheDocument(); + expect(getByText('PodScheduled - (1 day ago)')).toBeInTheDocument(); + expect(getByText('ContainersReady - (1 day ago)')).toBeInTheDocument(); + expect(getByText('Ready - (1 day ago)')).toBeInTheDocument(); + + expect(queryAllByLabelText('Status ok')).toHaveLength(4); + expect(queryByLabelText('Status warning')).not.toBeInTheDocument(); + expect(queryByLabelText('Status error')).not.toBeInTheDocument(); + }); + it('show startup conditions - all fail', async () => { + const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO(); + const { getByText, queryByLabelText, queryAllByLabelText } = render( + wrapInTestApp( + , + ), + ); + expect(getByText('Pod is Pending. Conditions:')).toBeInTheDocument(); + + expect( + getByText( + 'Initialized - (InitializedFailureReason 1 hour ago) - reason why Initialized failed', + ), + ).toBeInTheDocument(); + expect( + getByText( + 'PodScheduled - (PodScheduledFailureReason 1 hour ago) - reason why PodScheduled failed', + ), + ).toBeInTheDocument(); + expect( + getByText( + 'ContainersReady - (ContainersReadyFailureReason 1 hour ago) - reason why ContainersReady failed', + ), + ).toBeInTheDocument(); + expect( + getByText( + 'Ready - (ReadyFailureReason 1 hour ago) - reason why Ready failed', + ), + ).toBeInTheDocument(); + + expect(queryByLabelText('Status ok')).not.toBeInTheDocument(); + expect(queryByLabelText('Status warning')).not.toBeInTheDocument(); + expect(queryAllByLabelText('Status error')).toHaveLength(4); + }); + it('show startup conditions - show unknown', async () => { + const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO(); + const { getByText, queryByLabelText, getByLabelText } = render( + wrapInTestApp( + , + ), + ); + expect(getByText('Pod is Pending. Conditions:')).toBeInTheDocument(); + + expect( + getByText('Initialized - (1 hour ago) dont know what is happening'), + ).toBeInTheDocument(); + + expect(queryByLabelText('Status ok')).not.toBeInTheDocument(); + expect(getByLabelText('Status warning')).toBeInTheDocument(); + expect(queryByLabelText('Status error')).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/PendingPodContent.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer/PendingPodContent.tsx new file mode 100644 index 0000000000..ef6851b146 --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/PodDrawer/PendingPodContent.tsx @@ -0,0 +1,94 @@ +/* + * 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 React from 'react'; +import { Grid, List, ListItem, Typography } from '@material-ui/core'; +import { IPodCondition, Pod } from 'kubernetes-models/v1'; +import { + StatusError, + StatusOK, + StatusWarning, +} from '@backstage/core-components'; +import { DateTime } from 'luxon'; + +interface PodConditionProps { + condition: IPodCondition; +} + +export const PodCondition = ({ condition }: PodConditionProps) => { + return ( + <> + {condition.status === 'False' && ( + + {condition.type} - ({condition.reason}{' '} + {condition.lastTransitionTime && + DateTime.fromISO(condition.lastTransitionTime).toRelative({ + locale: 'en', + })} + ) - {condition.message}{' '} + + )} + {condition.status === 'True' && ( + + {condition.type} - ( + {condition.lastTransitionTime && + DateTime.fromISO(condition.lastTransitionTime).toRelative({ + locale: 'en', + })} + ) + + )} + {condition.status === 'Unknown' && ( + + {condition.type} - ( + {condition.lastTransitionTime && + DateTime.fromISO(condition.lastTransitionTime).toRelative({ + locale: 'en', + })} + ) {condition.message} + + )} + + ); +}; + +interface PendingPodContentProps { + pod: Pod; +} + +export const PendingPodContent = ({ pod }: PendingPodContentProps) => { + // TODO add PodHasNetwork when it's out of alpha + // https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions + const startupConditions = [ + pod.status?.conditions?.find(c => c.type === 'PodScheduled'), + pod.status?.conditions?.find(c => c.type === 'Initialized'), + pod.status?.conditions?.find(c => c.type === 'ContainersReady'), + pod.status?.conditions?.find(c => c.type === 'Ready'), + ].filter((c): c is IPodCondition => !!c); // filter out undefined + return ( + + + Pod is Pending. Conditions: + + {startupConditions.map(c => ( + + + + ))} + + + + ); +}; diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.test.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.test.tsx new file mode 100644 index 0000000000..fa4f9f7168 --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.test.tsx @@ -0,0 +1,67 @@ +/* + * 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 React from 'react'; + +import { render } from '@testing-library/react'; +import { wrapInTestApp } from '@backstage/test-utils'; +import '@testing-library/jest-dom'; + +import { PodDrawer } from '.'; + +describe('PodDrawer', () => { + it('Should show title and container names', async () => { + const { getAllByText, getByText } = render( + wrapInTestApp( + , + ), + ); + + expect(getAllByText('ok-pod')).toHaveLength(2); + expect(getByText('Pod (127.0.0.1)')).toBeInTheDocument(); + expect(getByText('YAML')).toBeInTheDocument(); + expect(getByText('Containers')).toBeInTheDocument(); + expect(getByText('some-container')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.tsx new file mode 100644 index 0000000000..a78f08683c --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.tsx @@ -0,0 +1,124 @@ +/* + * 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 React from 'react'; + +import { ItemCardGrid } from '@backstage/core-components'; +import { + createStyles, + makeStyles, + Theme, + Grid, + Typography, +} from '@material-ui/core'; + +import { Pod } from 'kubernetes-models/v1'; + +import { ContainerCard } from './ContainerCard'; + +import { PodAndErrors } from '../types'; +import { KubernetesDrawer } from '../../KubernetesDrawer'; +import { PendingPodContent } from './PendingPodContent'; + +const useDrawerContentStyles = makeStyles((_theme: Theme) => + createStyles({ + header: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + }, + content: { + height: '80%', + }, + icon: { + fontSize: 20, + }, + podoklist: { + width: '100%', + maxWidth: 360, + maxHeight: 360, + }, + }), +); + +function getContainerSpecByName(pod: Pod, containerName: string) { + return pod.spec?.containers.find(c => c.name === containerName); +} +interface PodDrawerProps { + open?: boolean; + podAndErrors: PodAndErrors; +} + +export const PodDrawer = ({ podAndErrors, open }: PodDrawerProps) => { + const classes = useDrawerContentStyles(); + + return ( + + Pod{' '} + {podAndErrors.pod.status?.podIP && + `(${podAndErrors.pod.status?.podIP})`} + + } + kubernetesObject={podAndErrors.pod} + label={ + + {podAndErrors.pod.metadata?.name ?? 'unknown'} + + } + > +
+ {podAndErrors.pod.status?.phase === 'Pending' && ( + + )} + {podAndErrors.pod.status?.containerStatuses?.length && ( + + + Containers + + + + {podAndErrors.pod.status?.containerStatuses?.map( + (containerStatus, i) => { + const containerSpec = getContainerSpecByName( + podAndErrors.pod, + containerStatus.name, + ); + return ( + + ); + }, + )} + + + + )} +
+
+ ); +}; diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/index.ts b/plugins/kubernetes/src/components/Pods/PodDrawer/index.ts new file mode 100644 index 0000000000..17d50ddfcd --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/PodDrawer/index.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +export { PodDrawer } from './PodDrawer'; diff --git a/plugins/kubernetes/src/components/Pods/PodLogs/PodLogs.tsx b/plugins/kubernetes/src/components/Pods/PodLogs/PodLogs.tsx new file mode 100644 index 0000000000..5cff023772 --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/PodLogs/PodLogs.tsx @@ -0,0 +1,55 @@ +/* + * 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 React from 'react'; + +import { DismissableBanner, LogViewer } from '@backstage/core-components'; +import { Paper } from '@material-ui/core'; +import { Skeleton } from '@material-ui/lab'; + +import { ContainerScope } from './types'; +import { usePodLogs } from './usePodLogs'; + +interface PodLogsProps { + podScope: ContainerScope; +} + +export const PodLogs: React.FC = ({ podScope }: PodLogsProps) => { + const { value, error, loading } = usePodLogs({ + podScope: podScope, + }); + + return ( + <> + {error && ( + + )} + + {loading && } + {!loading && value !== undefined && } + + + ); +}; diff --git a/plugins/kubernetes/src/components/Pods/PodLogs/PodLogsDialog.tsx b/plugins/kubernetes/src/components/Pods/PodLogs/PodLogsDialog.tsx new file mode 100644 index 0000000000..05d980b26a --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/PodLogs/PodLogsDialog.tsx @@ -0,0 +1,90 @@ +/* + * 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 React, { useState } from 'react'; + +import { + Button, + createStyles, + Dialog, + DialogContent, + DialogTitle, + IconButton, + makeStyles, + Theme, +} from '@material-ui/core'; +import SubjectIcon from '@material-ui/icons/Subject'; + +import CloseIcon from '@material-ui/icons/Close'; + +import { PodLogs } from './PodLogs'; +import { ContainerScope } from './types'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + closeButton: { + position: 'absolute', + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, + }), +); + +interface PodLogsDialogProps { + podScope: ContainerScope; +} + +export const PodLogsDialog = ({ podScope }: PodLogsDialogProps) => { + const classes = useStyles(); + const [open, setOpen] = useState(false); + + const openDialog = () => { + setOpen(true); + }; + + const closeDialog = () => { + setOpen(false); + }; + return ( + <> + + + {podScope.podName} - {podScope.containerName} logs on cluster{' '} + {podScope.clusterName} + + + + + + + + + + + ); +}; diff --git a/plugins/kubernetes/src/components/Pods/PodLogs/index.ts b/plugins/kubernetes/src/components/Pods/PodLogs/index.ts new file mode 100644 index 0000000000..ca5c1dde11 --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/PodLogs/index.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ +export * from './PodLogs'; +export * from './PodLogsDialog'; +export * from './usePodLogs'; +export * from './types'; diff --git a/plugins/kubernetes/src/components/Pods/PodLogs/types.ts b/plugins/kubernetes/src/components/Pods/PodLogs/types.ts new file mode 100644 index 0000000000..fcc1c40194 --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/PodLogs/types.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +export interface PodScope { + podName: string; + podNamespace: string; + clusterName: string; +} + +export interface ContainerScope extends PodScope { + containerName: string; +} diff --git a/plugins/kubernetes/src/components/Pods/PodLogs/usePodLogs.ts b/plugins/kubernetes/src/components/Pods/PodLogs/usePodLogs.ts new file mode 100644 index 0000000000..9f3af5cb3b --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/PodLogs/usePodLogs.ts @@ -0,0 +1,36 @@ +/* + * 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 { kubernetesProxyApiRef } from '@backstage/plugin-kubernetes'; +import useAsync from 'react-use/lib/useAsync'; + +import { ContainerScope } from './types'; +import { useApi } from '@backstage/core-plugin-api'; + +interface PodLogsOptions { + podScope: ContainerScope; +} + +export const usePodLogs = ({ podScope }: PodLogsOptions) => { + const kubernetesProxyApi = useApi(kubernetesProxyApiRef); + return useAsync(async () => { + return await kubernetesProxyApi.getPodLogs({ + podName: podScope.podName, + namespace: podScope.podNamespace, + containerName: podScope.containerName, + clusterName: podScope.clusterName, + }); + }, [JSON.stringify(podScope)]); +}; diff --git a/plugins/kubernetes/src/components/Pods/PodsTable.tsx b/plugins/kubernetes/src/components/Pods/PodsTable.tsx index a6e4b33020..4b7b4b93ed 100644 --- a/plugins/kubernetes/src/components/Pods/PodsTable.tsx +++ b/plugins/kubernetes/src/components/Pods/PodsTable.tsx @@ -26,6 +26,7 @@ import { } from '../../utils/pod'; import { Table, TableColumn } from '@backstage/core-components'; import { PodNamesWithMetricsContext } from '../../hooks/PodNamesWithMetrics'; +import { ClusterContext } from '../../hooks/Cluster'; export const READY_COLUMNS: PodColumns = 'READY'; export const RESOURCE_COLUMNS: PodColumns = 'RESOURCE'; @@ -37,23 +38,6 @@ type PodsTablesProps = { children?: React.ReactNode; }; -const DEFAULT_COLUMNS: TableColumn[] = [ - { - title: 'name', - highlight: true, - render: (pod: V1Pod) => , - }, - { - title: 'phase', - render: (pod: V1Pod) => pod.status?.phase ?? 'unknown', - width: 'auto', - }, - { - title: 'status', - render: containerStatuses, - }, -]; - const READY: TableColumn[] = [ { title: 'containers ready', @@ -72,7 +56,32 @@ const READY: TableColumn[] = [ export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => { const podNamesWithMetrics = useContext(PodNamesWithMetricsContext); - const columns: TableColumn[] = [...DEFAULT_COLUMNS]; + const cluster = useContext(ClusterContext); + const defaultColumns: TableColumn[] = [ + { + title: 'name', + highlight: true, + render: (pod: V1Pod) => ( + + ), + }, + { + title: 'phase', + render: (pod: V1Pod) => pod.status?.phase ?? 'unknown', + width: 'auto', + }, + { + title: 'status', + render: containerStatuses, + }, + ]; + const columns: TableColumn[] = [...defaultColumns]; if (extraColumns.includes(READY_COLUMNS)) { columns.push(...READY); diff --git a/plugins/kubernetes/src/components/Pods/types.ts b/plugins/kubernetes/src/components/Pods/types.ts new file mode 100644 index 0000000000..195f0b9135 --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/types.ts @@ -0,0 +1,22 @@ +/* + * 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 { Pod } from 'kubernetes-models/v1'; + +export interface PodAndErrors { + clusterName: string; + pod: Pod; + errors: any[]; +} diff --git a/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx b/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx index a088c4792d..9588cd00ed 100644 --- a/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx +++ b/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { V1Service } from '@kubernetes/client-node'; -import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; +import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer'; import { Typography, Grid } from '@material-ui/core'; export const ServiceDrawer = ({ @@ -27,7 +27,7 @@ export const ServiceDrawer = ({ expanded?: boolean; }) => { return ( - - + ); }; diff --git a/plugins/kubernetes/src/components/StatefulSetsAccordions/StatefulSetDrawer.tsx b/plugins/kubernetes/src/components/StatefulSetsAccordions/StatefulSetDrawer.tsx index e83c890426..e07a5f426c 100644 --- a/plugins/kubernetes/src/components/StatefulSetsAccordions/StatefulSetDrawer.tsx +++ b/plugins/kubernetes/src/components/StatefulSetsAccordions/StatefulSetDrawer.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { V1StatefulSet } from '@kubernetes/client-node'; -import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; +import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer'; import { renderCondition } from '../../utils/pod'; import { Typography, Grid, Chip } from '@material-ui/core'; @@ -29,7 +29,7 @@ export const StatefulSetDrawer = ({ }) => { const namespace = statefulset.metadata?.namespace; return ( - )} - + ); }; diff --git a/plugins/kubernetes/src/components/index.ts b/plugins/kubernetes/src/components/index.ts index 72a3cd3ee1..11550e767b 100644 --- a/plugins/kubernetes/src/components/index.ts +++ b/plugins/kubernetes/src/components/index.ts @@ -22,7 +22,7 @@ export * from './ErrorReporting'; export * from './HorizontalPodAutoscalers'; export * from './IngressesAccordions'; export * from './JobsAccordions'; -export { KubernetesDrawer } from './KubernetesDrawer'; +export * from './KubernetesDrawer'; export * from './Pods'; export * from './ServicesAccordions'; export * from './KubernetesContent'; diff --git a/plugins/kubernetes/src/plugin.ts b/plugins/kubernetes/src/plugin.ts index cb85cf06fb..e10b4856b7 100644 --- a/plugins/kubernetes/src/plugin.ts +++ b/plugins/kubernetes/src/plugin.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { KubernetesBackendClient } from './api/KubernetesBackendClient'; -import { kubernetesApiRef } from './api/types'; +import { kubernetesApiRef, kubernetesProxyApiRef } from './api/types'; import { kubernetesAuthProvidersApiRef } from './kubernetes-auth-provider/types'; import { KubernetesAuthProviders } from './kubernetes-auth-provider/KubernetesAuthProviders'; import { @@ -30,6 +30,7 @@ import { oneloginAuthApiRef, createRoutableExtension, } from '@backstage/core-plugin-api'; +import { KubernetesProxyClient } from './api'; export const rootCatalogKubernetesRouteRef = createRouteRef({ id: 'kubernetes', @@ -52,6 +53,16 @@ export const kubernetesPlugin = createPlugin({ kubernetesAuthProvidersApi, }), }), + createApiFactory({ + api: kubernetesProxyApiRef, + deps: { + kubernetesApi: kubernetesApiRef, + }, + factory: ({ kubernetesApi }) => + new KubernetesProxyClient({ + kubernetesApi, + }), + }), createApiFactory({ api: kubernetesAuthProvidersApiRef, deps: { diff --git a/yarn.lock b/yarn.lock index 92bcd59055..c0b5189db6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7245,6 +7245,7 @@ __metadata: "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" + "@kubernetes-models/apimachinery": ^1.1.0 "@kubernetes/client-node": 0.18.1 "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1