diff --git a/.changeset/wise-bats-repair.md b/.changeset/wise-bats-repair.md
new file mode 100644
index 0000000000..fc383eb11d
--- /dev/null
+++ b/.changeset/wise-bats-repair.md
@@ -0,0 +1,6 @@
+---
+'@backstage/plugin-kubernetes-backend': patch
+'@backstage/plugin-kubernetes': patch
+---
+
+Add proposed fix dialog for pod errors
diff --git a/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml b/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml
index a1a2e105b3..8f225e666a 100644
--- a/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml
+++ b/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml
@@ -32,6 +32,42 @@ spec:
ports:
- containerPort: 80
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: dice-roller-incorrect-probe
+ labels:
+ 'backstage.io/kubernetes-id': dice-roller
+spec:
+ selector:
+ matchLabels:
+ app: dice-roller-incorrect-probe
+ replicas: 2
+ template:
+ metadata:
+ labels:
+ app: dice-roller-incorrect-probe
+ 'backstage.io/kubernetes-id': dice-roller
+ spec:
+ containers:
+ - name: nginx
+ image: nginx:1.14.2
+ resources:
+ requests:
+ memory: '64Mi'
+ cpu: '50m'
+ limits:
+ memory: '128Mi'
+ cpu: '50m'
+ readinessProbe:
+ tcpSocket:
+ port: 8080
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ ports:
+ - containerPort: 80
+
---
apiVersion: apps/v1
kind: Deployment
diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md
index 823b9c89a2..e117e3627d 100644
--- a/plugins/kubernetes/api-report.md
+++ b/plugins/kubernetes/api-report.md
@@ -6,6 +6,7 @@
///
import { ApiRef } from '@backstage/core-plugin-api';
+import { AsyncState } from 'react-use/lib/useAsyncFn';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { ClientPodStatus } from '@backstage/plugin-kubernetes-common';
import { ClusterAttributes } from '@backstage/plugin-kubernetes-common';
@@ -14,6 +15,9 @@ import { CustomObjectsByEntityRequest } from '@backstage/plugin-kubernetes-commo
import { CustomResourceMatcher } from '@backstage/plugin-kubernetes-common';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
+import { Event as Event_2 } from 'kubernetes-models/v1';
+import { IContainer } from 'kubernetes-models/v1';
+import { IContainerStatus } from 'kubernetes-models/v1';
import { IdentityApi } from '@backstage/core-plugin-api';
import { IObjectMeta } from '@kubernetes-models/apimachinery/apis/meta/v1/ObjectMeta';
import type { JsonObject } from '@backstage/types';
@@ -21,8 +25,8 @@ 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 { Pod as Pod_2 } from 'kubernetes-models/v1/Pod';
+import { Pod } from 'kubernetes-models/v1/Pod';
+import { Pod as Pod_2 } from 'kubernetes-models/v1';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
import { V1ConfigMap } from '@kubernetes/client-node';
@@ -78,6 +82,25 @@ export interface ClusterLinksFormatterOptions {
// @public (undocumented)
export const clusterLinksFormatters: Record;
+// @public
+export const ContainerCard: React_2.FC;
+
+// @public
+export interface ContainerCardProps {
+ // (undocumented)
+ containerSpec?: IContainer;
+ // (undocumented)
+ containerStatus: IContainerStatus;
+ // (undocumented)
+ podScope: PodScope;
+}
+
+// @public
+export interface ContainerScope extends PodScope {
+ // (undocumented)
+ containerName: string;
+}
+
// Warning: (ae-forgotten-export) The symbol "CronJobsAccordionsProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "CronJobsAccordions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -113,7 +136,7 @@ export interface DetectedError {
// Warning: (ae-forgotten-export) The symbol "ProposedFix" needs to be exported by the entry point index.d.ts
//
// (undocumented)
- proposedFix: ProposedFix[];
+ proposedFix?: ProposedFix;
// (undocumented)
severity: ErrorSeverity;
// Warning: (ae-forgotten-export) The symbol "ResourceRef" needs to be exported by the entry point index.d.ts
@@ -144,6 +167,15 @@ export type EntityKubernetesContentProps = {
refreshIntervalMs?: number;
};
+// @public
+export const ErrorList: ({ podAndErrors }: ErrorListProps) => JSX.Element;
+
+// @public
+export interface ErrorListProps {
+ // (undocumented)
+ podAndErrors: PodAndErrors[];
+}
+
// Warning: (ae-forgotten-export) The symbol "ErrorPanelProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "ErrorPanel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -165,6 +197,65 @@ export const ErrorReporting: ({
// @public
export type ErrorSeverity = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
+// @public
+export const Events: ({
+ involvedObjectName,
+ namespace,
+ clusterName,
+ warningEventsOnly,
+}: EventsProps) => JSX.Element;
+
+// @public
+export const EventsContent: ({
+ events,
+ warningEventsOnly,
+}: EventsContentProps) => JSX.Element;
+
+// @public
+export interface EventsContentProps {
+ // (undocumented)
+ events: Event_2[];
+ // (undocumented)
+ warningEventsOnly?: boolean;
+}
+
+// @public
+export interface EventsOptions {
+ // (undocumented)
+ clusterName: string;
+ // (undocumented)
+ involvedObjectName: string;
+ // (undocumented)
+ namespace: string;
+}
+
+// @public
+export interface EventsProps {
+ // (undocumented)
+ clusterName: string;
+ // (undocumented)
+ involvedObjectName: string;
+ // (undocumented)
+ namespace: string;
+ // (undocumented)
+ warningEventsOnly?: boolean;
+}
+
+// @public
+export const FixDialog: React_2.FC;
+
+// @public
+export interface FixDialogProps {
+ // (undocumented)
+ clusterName: string;
+ // (undocumented)
+ error: DetectedError;
+ // (undocumented)
+ open?: boolean;
+ // (undocumented)
+ pod: Pod;
+}
+
// Warning: (ae-forgotten-export) The symbol "FormatClusterLinkOptions" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "formatClusterLink" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -416,12 +507,19 @@ export { kubernetesPlugin as plugin };
//
// @public (undocumented)
export interface KubernetesProxyApi {
+ // (undocumented)
+ getEventsByInvolvedObjectName(request: {
+ clusterName: string;
+ involvedObjectName: string;
+ namespace: string;
+ }): Promise;
// (undocumented)
getPodLogs(request: {
podName: string;
namespace: string;
clusterName: string;
containerName: string;
+ previous?: boolean;
}): Promise<{
text: string;
}>;
@@ -436,16 +534,28 @@ export const kubernetesProxyApiRef: ApiRef;
export class KubernetesProxyClient {
constructor(options: { kubernetesApi: KubernetesApi });
// (undocumented)
+ getEventsByInvolvedObjectName({
+ clusterName,
+ involvedObjectName,
+ namespace,
+ }: {
+ clusterName: string;
+ involvedObjectName: string;
+ namespace: string;
+ }): Promise;
+ // (undocumented)
getPodLogs({
podName,
namespace,
clusterName,
containerName,
+ previous,
}: {
podName: string;
namespace: string;
clusterName: string;
containerName: string;
+ previous?: boolean;
}): Promise<{
text: string;
}>;
@@ -476,12 +586,62 @@ export const LinkErrorPanel: ({
errorMessage,
}: ErrorPanelProps_2) => JSX.Element;
+// @public
+export const PendingPodContent: ({
+ pod,
+}: PendingPodContentProps) => JSX.Element;
+
+// @public
+export interface PendingPodContentProps {
+ // (undocumented)
+ pod: Pod_2;
+}
+
+// @public
+export interface PodAndErrors {
+ // (undocumented)
+ clusterName: string;
+ // (undocumented)
+ errors: DetectedError[];
+ // (undocumented)
+ pod: Pod_2;
+}
+
// 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)
+// @public
export const PodDrawer: ({ podAndErrors, open }: PodDrawerProps) => JSX.Element;
+// @public
+export const PodLogs: React_2.FC;
+
+// @public
+export const PodLogsDialog: ({
+ containerScope,
+}: PodLogsDialogProps) => JSX.Element;
+
+// @public
+export interface PodLogsDialogProps {
+ // (undocumented)
+ containerScope: ContainerScope;
+}
+
+// @public
+export interface PodLogsOptions {
+ // (undocumented)
+ containerScope: ContainerScope;
+ // (undocumented)
+ previous?: boolean;
+}
+
+// @public
+export interface PodLogsProps {
+ // (undocumented)
+ containerScope: ContainerScope;
+ // (undocumented)
+ previous?: boolean;
+}
+
// 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)
//
// @public (undocumented)
@@ -494,6 +654,16 @@ export const PodNamesWithMetricsContext: React_2.Context<
Map
>;
+// @public
+export interface PodScope {
+ // (undocumented)
+ clusterName: string;
+ // (undocumented)
+ podName: string;
+ // (undocumented)
+ podNamespace: string;
+}
+
// Warning: (ae-forgotten-export) The symbol "PodsTablesProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "PodsTable" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -533,6 +703,13 @@ export const useCustomResources: (
intervalMs?: number,
) => KubernetesObjects;
+// @public
+export const useEvents: ({
+ involvedObjectName,
+ namespace,
+ clusterName,
+}: EventsOptions) => AsyncState;
+
// Warning: (ae-missing-release-tag) "useKubernetesObjects" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -540,4 +717,12 @@ export const useKubernetesObjects: (
entity: Entity,
intervalMs?: number,
) => KubernetesObjects;
+
+// @public
+export const usePodLogs: ({
+ containerScope,
+ previous,
+}: PodLogsOptions) => AsyncState<{
+ text: string;
+}>;
```
diff --git a/plugins/kubernetes/src/api/KubernetesProxyClient.test.ts b/plugins/kubernetes/src/api/KubernetesProxyClient.test.ts
index 6db1e459be..6b0be6de6c 100644
--- a/plugins/kubernetes/src/api/KubernetesProxyClient.test.ts
+++ b/plugins/kubernetes/src/api/KubernetesProxyClient.test.ts
@@ -14,11 +14,13 @@
* limitations under the License.
*/
+import { DateTime } from 'luxon';
import { KubernetesProxyClient } from './KubernetesProxyClient';
describe('KubernetesProxyClient', () => {
let proxy: KubernetesProxyClient;
const callProxyMock = jest.fn();
+ const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO();
beforeEach(() => {
jest.resetAllMocks();
@@ -51,4 +53,72 @@ describe('KubernetesProxyClient', () => {
path: '/api/v1/namespaces/some-namespace/pods/some-pod/log?container=some-container',
});
});
+ it('/logs returns log text - crash logs', async () => {
+ const request = {
+ podName: 'some-pod',
+ namespace: 'some-namespace',
+ clusterName: 'some-cluster',
+ containerName: 'some-container',
+ previous: true,
+ };
+
+ 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&previous=',
+ });
+ });
+ it('/getEventsByInvolvedObjectName returns events', async () => {
+ const request = {
+ clusterName: 'some-cluster',
+ involvedObjectName: 'some-object',
+ namespace: 'some-namespace',
+ };
+ const events = [
+ {
+ type: 'Warning',
+ message: 'uh oh',
+ reason: 'something happened',
+ count: 23,
+ metadata: {
+ creationTimestamp: oneHourAgo,
+ },
+ },
+ {
+ type: 'Info',
+ message: 'hello there',
+ reason: 'something happened',
+ count: 52,
+ metadata: {
+ creationTimestamp: oneHourAgo,
+ },
+ },
+ ];
+
+ callProxyMock.mockResolvedValue({
+ json: jest.fn().mockResolvedValue({
+ items: events,
+ }),
+ ok: true,
+ });
+
+ const response = await proxy.getEventsByInvolvedObjectName(request);
+ await expect(response).toStrictEqual(events);
+ expect(callProxyMock).toHaveBeenCalledWith({
+ clusterName: 'some-cluster',
+ init: {
+ method: 'GET',
+ },
+ path: '/api/v1/namespaces/some-namespace/events?fieldSelector=involvedObject.name=some-object',
+ });
+ });
});
diff --git a/plugins/kubernetes/src/api/KubernetesProxyClient.ts b/plugins/kubernetes/src/api/KubernetesProxyClient.ts
index 8fd4674ddd..3703d8b180 100644
--- a/plugins/kubernetes/src/api/KubernetesProxyClient.ts
+++ b/plugins/kubernetes/src/api/KubernetesProxyClient.ts
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { KubernetesApi } from './types';
+import { Event } from 'kubernetes-models/v1';
/**
* A client for common requests through the proxy endpoint of the kubernetes backend plugin.
@@ -41,20 +42,63 @@ export class KubernetesProxyClient {
return await response.text();
}
+ private async handleJson(response: Response): Promise {
+ if (!response.ok) {
+ const payload = await response.text();
+ let message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
+ switch (response.status) {
+ case 404:
+ message = `Proxy request failed with ${response.status} ${response.statusText}, ${payload}`;
+ break;
+ default:
+ message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
+ }
+ throw new Error(message);
+ }
+
+ return await response.json();
+ }
+
+ async getEventsByInvolvedObjectName({
+ clusterName,
+ involvedObjectName,
+ namespace,
+ }: {
+ clusterName: string;
+ involvedObjectName: string;
+ namespace: string;
+ }): Promise {
+ return await this.kubernetesApi
+ .proxy({
+ clusterName,
+ path: `/api/v1/namespaces/${namespace}/events?fieldSelector=involvedObject.name=${involvedObjectName}`,
+ init: {
+ method: 'GET',
+ },
+ })
+ .then(response => this.handleJson(response))
+ .then(eventList => eventList.items);
+ }
+
async getPodLogs({
podName,
namespace,
clusterName,
containerName,
+ previous,
}: {
podName: string;
namespace: string;
clusterName: string;
containerName: string;
+ previous?: boolean;
}): Promise<{ text: string }> {
const params = new URLSearchParams({
container: containerName,
});
+ if (previous) {
+ params.append('previous', '');
+ }
return await this.kubernetesApi
.proxy({
clusterName: clusterName,
diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts
index 5dec189982..6065e47169 100644
--- a/plugins/kubernetes/src/api/types.ts
+++ b/plugins/kubernetes/src/api/types.ts
@@ -21,6 +21,7 @@ import {
CustomObjectsByEntityRequest,
} from '@backstage/plugin-kubernetes-common';
import { createApiRef } from '@backstage/core-plugin-api';
+import { Event } from 'kubernetes-models/v1';
export const kubernetesApiRef = createApiRef({
id: 'plugin.kubernetes.service',
@@ -60,5 +61,11 @@ export interface KubernetesProxyApi {
namespace: string;
clusterName: string;
containerName: string;
+ previous?: boolean;
}): Promise<{ text: string }>;
+ getEventsByInvolvedObjectName(request: {
+ clusterName: string;
+ involvedObjectName: string;
+ namespace: string;
+ }): Promise;
}
diff --git a/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.test.tsx b/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.test.tsx
index f28580b509..3ee755becb 100644
--- a/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.test.tsx
+++ b/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.test.tsx
@@ -45,15 +45,13 @@ describe('ErrorList', () => {
kind: 'Pod',
apiGroup: 'v1',
},
- proposedFix: [
- {
- type: 'logs',
- container: 'some-container',
- errorType: 'some error type',
- rootCauseExplanation: 'some root cause',
- possibleFixes: ['fix1', 'fix2'],
- },
- ],
+ proposedFix: {
+ type: 'logs',
+ container: 'some-container',
+ errorType: 'some error type',
+ rootCauseExplanation: 'some root cause',
+ actions: ['fix1', 'fix2'],
+ },
},
],
},
diff --git a/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.tsx b/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.tsx
index b7b3418889..4d1d7a3033 100644
--- a/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.tsx
+++ b/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.tsx
@@ -24,8 +24,10 @@ import {
makeStyles,
Theme,
Paper,
+ Grid,
} from '@material-ui/core';
import { PodAndErrors } from '../types';
+import { FixDialog } from '../FixDialog/FixDialog';
const useStyles = makeStyles((_theme: Theme) =>
createStyles({
@@ -38,10 +40,20 @@ const useStyles = makeStyles((_theme: Theme) =>
}),
);
-interface ErrorListProps {
+/**
+ * Props for ErrorList
+ *
+ * @public
+ */
+export interface ErrorListProps {
podAndErrors: PodAndErrors[];
}
+/**
+ * Shows a list of errors found on a Pod
+ *
+ * @public
+ */
export const ErrorList = ({ podAndErrors }: ErrorListProps) => {
const classes = useStyles();
return (
@@ -59,10 +71,21 @@ export const ErrorList = ({ podAndErrors }: ErrorListProps) => {
>
{i > 0 && }
-
+
+
+
+
+
+
+
+
);
diff --git a/plugins/kubernetes/src/components/Pods/Events/Events.test.tsx b/plugins/kubernetes/src/components/Pods/Events/Events.test.tsx
new file mode 100644
index 0000000000..8323666e66
--- /dev/null
+++ b/plugins/kubernetes/src/components/Pods/Events/Events.test.tsx
@@ -0,0 +1,96 @@
+/*
+ * 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 { EventsContent } from './Events';
+import { render } from '@testing-library/react';
+import { Event } from 'kubernetes-models/v1';
+import { DateTime } from 'luxon';
+
+describe('EventsContent', () => {
+ const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO();
+ it('should show info events', () => {
+ const { getByText } = render(
+ ,
+ );
+ expect(getByText('First event 1 hour ago (count: 52)')).toBeInTheDocument();
+ expect(getByText('something happened: hello there')).toBeInTheDocument();
+ });
+ it('should show warning events', () => {
+ const { getByText } = render(
+ ,
+ );
+ expect(getByText('First event 1 hour ago (count: 23)')).toBeInTheDocument();
+ expect(getByText('something happened: uh oh')).toBeInTheDocument();
+ });
+
+ it('should only show warning events when warningEventsOnly set', () => {
+ const { getByText, queryByText } = render(
+ ,
+ );
+ expect(queryByText('First event 1 hour ago (count: 52)')).toBeNull();
+ expect(queryByText('something happened: hello there')).toBeNull();
+ expect(getByText('First event 1 hour ago (count: 23)')).toBeInTheDocument();
+ expect(getByText('something happened: uh oh')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/kubernetes/src/components/Pods/Events/Events.tsx b/plugins/kubernetes/src/components/Pods/Events/Events.tsx
new file mode 100644
index 0000000000..9f888d182c
--- /dev/null
+++ b/plugins/kubernetes/src/components/Pods/Events/Events.tsx
@@ -0,0 +1,154 @@
+/*
+ * 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 {
+ Avatar,
+ Container,
+ Grid,
+ List,
+ ListItem,
+ ListItemAvatar,
+ ListItemText,
+ Tooltip,
+ Typography,
+} from '@material-ui/core';
+
+import InfoIcon from '@material-ui/icons/Info';
+import WarningIcon from '@material-ui/icons/Warning';
+import { DateTime } from 'luxon';
+
+import { useEvents } from './useEvents';
+import { Skeleton } from '@material-ui/lab';
+import { DismissableBanner } from '@backstage/core-components';
+import { Event } from 'kubernetes-models/v1';
+
+/**
+ * Props for Events
+ *
+ * @public
+ */
+export interface EventsContentProps {
+ warningEventsOnly?: boolean;
+ events: Event[];
+}
+
+const getAvatarByType = (type?: string) => {
+ return (
+
+ {type === 'Warning' ? : }
+
+ );
+};
+
+/**
+ * Shows given Kubernetes events
+ *
+ * @public
+ */
+export const EventsContent = ({
+ events,
+ warningEventsOnly,
+}: EventsContentProps) => {
+ if (events.length === 0) {
+ return No events found;
+ }
+
+ return (
+
+
+
+ {events
+ .filter(event => {
+ if (warningEventsOnly) {
+ return event.type === 'Warning';
+ }
+ return true;
+ })
+ .map(event => {
+ const timeAgo = event.metadata.creationTimestamp
+ ? DateTime.fromISO(event.metadata.creationTimestamp).toRelative(
+ {
+ locale: 'en',
+ },
+ )
+ : 'unknown';
+ return (
+
+
+ {getAvatarByType(event.type)}
+
+
+
+ );
+ })}
+
+
+
+ );
+};
+
+/**
+ * Props for Events
+ *
+ * @public
+ */
+export interface EventsProps {
+ involvedObjectName: string;
+ namespace: string;
+ clusterName: string;
+ warningEventsOnly?: boolean;
+}
+
+/**
+ * Retrieves and shows Kubernetes events for the given object
+ *
+ * @public
+ */
+export const Events = ({
+ involvedObjectName,
+ namespace,
+ clusterName,
+ warningEventsOnly,
+}: EventsProps) => {
+ const { value, error, loading } = useEvents({
+ involvedObjectName,
+ namespace,
+ clusterName,
+ });
+
+ return (
+ <>
+ {error && (
+
+ )}
+ {loading && }
+ {!loading && value !== undefined && (
+
+ )}
+ >
+ );
+};
diff --git a/plugins/kubernetes/src/components/Pods/Events/index.ts b/plugins/kubernetes/src/components/Pods/Events/index.ts
new file mode 100644
index 0000000000..da6c4102fa
--- /dev/null
+++ b/plugins/kubernetes/src/components/Pods/Events/index.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 './Events';
+export * from './useEvents';
diff --git a/plugins/kubernetes/src/components/Pods/Events/useEvents.test.tsx b/plugins/kubernetes/src/components/Pods/Events/useEvents.test.tsx
new file mode 100644
index 0000000000..15da221f64
--- /dev/null
+++ b/plugins/kubernetes/src/components/Pods/Events/useEvents.test.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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 { useApi } from '@backstage/core-plugin-api';
+import { renderHook } from '@testing-library/react-hooks';
+import { useEvents } from './useEvents';
+import { DateTime } from 'luxon';
+
+jest.mock('@backstage/core-plugin-api');
+
+jest.mock('@backstage/plugin-kubernetes', () => ({
+ kubernetesProxyApiRef: () => jest.fn(),
+}));
+
+const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO();
+
+const response = [
+ {
+ type: 'Info',
+ message: 'hello there',
+ reason: 'something happened',
+ count: 52,
+ metadata: {
+ creationTimestamp: oneHourAgo,
+ },
+ },
+] as any;
+
+describe('Events', () => {
+ const mockGetEventsByInvolvedObjectName = jest.fn();
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+ it('should fetch and show events', async () => {
+ (useApi as any).mockReturnValue({
+ getEventsByInvolvedObjectName:
+ mockGetEventsByInvolvedObjectName.mockResolvedValue(response),
+ });
+
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useEvents({
+ involvedObjectName: 'some-objecgt',
+ namespace: 'some-namespace',
+ clusterName: 'some-cluster',
+ }),
+ );
+
+ expect(result.current.loading).toEqual(true);
+
+ await waitForNextUpdate();
+
+ expect(result.current.error).toBeUndefined();
+ expect(result.current.loading).toEqual(false);
+ expect(result.current.value).toStrictEqual(response);
+ });
+});
diff --git a/plugins/kubernetes/src/components/Pods/Events/useEvents.ts b/plugins/kubernetes/src/components/Pods/Events/useEvents.ts
new file mode 100644
index 0000000000..441937507d
--- /dev/null
+++ b/plugins/kubernetes/src/components/Pods/Events/useEvents.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 { useApi } from '@backstage/core-plugin-api';
+import useAsync from 'react-use/lib/useAsync';
+import { kubernetesProxyApiRef } from '../../../api';
+
+/**
+ * Arguments for useEvents
+ *
+ * @public
+ */
+export interface EventsOptions {
+ involvedObjectName: string;
+ namespace: string;
+ clusterName: string;
+}
+
+/**
+ * Retrieves the events for the given object
+ *
+ * @public
+ */
+export const useEvents = ({
+ involvedObjectName,
+ namespace,
+ clusterName,
+}: EventsOptions) => {
+ const kubernetesProxyApi = useApi(kubernetesProxyApiRef);
+ return useAsync(async () => {
+ return await kubernetesProxyApi.getEventsByInvolvedObjectName({
+ involvedObjectName,
+ namespace,
+ clusterName,
+ });
+ }, [involvedObjectName, namespace, clusterName]);
+};
diff --git a/plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.test.tsx b/plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.test.tsx
new file mode 100644
index 0000000000..e779ab5f0d
--- /dev/null
+++ b/plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.test.tsx
@@ -0,0 +1,159 @@
+/*
+ * 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 { FixDialog } from './FixDialog';
+import { Pod } from 'kubernetes-models/v1/Pod';
+
+jest.mock('../Events', () => ({
+ Events: () => {
+ return ;
+ },
+}));
+
+jest.mock('../PodLogs', () => ({
+ PodLogs: () => {
+ return ;
+ },
+}));
+
+describe('FixDialog', () => {
+ it('docs link should render', () => {
+ const { getByText } = render(
+ ,
+ );
+ expect(getByText('Open docs')).toBeInTheDocument();
+ expect(getByText('some error message')).toBeInTheDocument();
+ expect(getByText('some-pod - some error type')).toBeInTheDocument();
+ expect(getByText('some root cause')).toBeInTheDocument();
+ expect(getByText('fix1')).toBeInTheDocument();
+ expect(getByText('fix2')).toBeInTheDocument();
+ });
+ it('events button should render', () => {
+ const { getByText } = render(
+ ,
+ );
+ expect(getByText('Events:')).toBeInTheDocument();
+ expect(getByText('some error message')).toBeInTheDocument();
+ expect(getByText('some-pod - some error type')).toBeInTheDocument();
+ expect(getByText('some root cause')).toBeInTheDocument();
+ expect(getByText('fix1')).toBeInTheDocument();
+ expect(getByText('fix2')).toBeInTheDocument();
+ });
+ it('Logs button should render', () => {
+ const { getByText } = render(
+ ,
+ );
+ expect(getByText('Crash logs:')).toBeInTheDocument();
+ expect(getByText('some error message')).toBeInTheDocument();
+ expect(getByText('some-pod - some error type')).toBeInTheDocument();
+ expect(getByText('some root cause')).toBeInTheDocument();
+ expect(getByText('fix1')).toBeInTheDocument();
+ expect(getByText('fix2')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.tsx b/plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.tsx
new file mode 100644
index 0000000000..63ce4af03b
--- /dev/null
+++ b/plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.tsx
@@ -0,0 +1,187 @@
+/*
+ * 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, Grid } from '@material-ui/core';
+import Dialog from '@material-ui/core/Dialog';
+import DialogActions from '@material-ui/core/DialogActions';
+import DialogContent from '@material-ui/core/DialogContent';
+import DialogTitle from '@material-ui/core/DialogTitle';
+import IconButton from '@material-ui/core/IconButton';
+import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
+import Typography from '@material-ui/core/Typography';
+import CloseIcon from '@material-ui/icons/Close';
+import HelpIcon from '@material-ui/icons/Help';
+import OpenInNewIcon from '@material-ui/icons/OpenInNew';
+
+import { Pod } from 'kubernetes-models/v1/Pod';
+import { DetectedError } from '../../../error-detection';
+import { PodLogs } from '../PodLogs';
+import { Events } from '../Events';
+import { LinkButton } from '@backstage/core-components';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ closeButton: {
+ position: 'absolute',
+ right: theme.spacing(1),
+ top: theme.spacing(1),
+ color: theme.palette.grey[500],
+ },
+ }),
+);
+
+/**
+ * Props for FixDialog
+ *
+ * @public
+ */
+export interface FixDialogProps {
+ open?: boolean;
+ clusterName: string;
+ pod: Pod;
+ error: DetectedError;
+}
+
+/**
+ * A dialog for fixing detected Kubernetes errors
+ *
+ * @public
+ */
+export const FixDialog: React.FC = ({
+ open,
+ pod,
+ error,
+ clusterName,
+}: FixDialogProps) => {
+ const [isOpen, setOpen] = useState(!!open);
+ const classes = useStyles();
+
+ const openDialog = () => {
+ setOpen(true);
+ };
+
+ const closeDialog = () => {
+ setOpen(false);
+ };
+
+ const pf = error.proposedFix;
+
+ const dialogContent = () => {
+ return (
+
+
+ Detected error:
+ {error.message}
+
+
+ Cause explanation:
+
+ {error.proposedFix?.rootCauseExplanation ?? 'unknown'}
+
+
+
+ Fix:
+
+
+ {(error.proposedFix?.actions ?? []).map((fix, i) => {
+ return (
+ -
+ {fix}
+
+ );
+ })}
+
+
+
+
+ {pf && pf.type === 'logs' && (
+ <>
+
+ Crash logs:
+
+
+
+
+ >
+ )}
+ {pf && pf.type === 'events' && (
+ <>
+
+ Events:
+
+
+
+
+ >
+ )}
+
+ );
+ };
+
+ return (
+ <>
+ }
+ >
+ Help
+
+
+ >
+ );
+};
diff --git a/plugins/kubernetes/src/components/Pods/FixDialog/index.ts b/plugins/kubernetes/src/components/Pods/FixDialog/index.ts
new file mode 100644
index 0000000000..35601f9bd6
--- /dev/null
+++ b/plugins/kubernetes/src/components/Pods/FixDialog/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 * from './FixDialog';
diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.tsx
index eac64ae741..970982d7a3 100644
--- a/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.tsx
+++ b/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.tsx
@@ -83,12 +83,22 @@ const ContainerDatetime = ({ prefix, dateTime }: ContainerDatetimeProps) => {
);
};
+/**
+ * Props for ContainerCard
+ *
+ * @public
+ */
export interface ContainerCardProps {
podScope: PodScope;
containerSpec?: IContainer;
containerStatus: IContainerStatus;
}
+/**
+ * Shows details about a container within a pod
+ *
+ * @public
+ */
export const ContainerCard: React.FC = ({
podScope,
containerSpec,
@@ -162,7 +172,7 @@ export const ContainerCard: React.FC = ({
{
+const PodCondition = ({ condition }: PodConditionProps) => {
return (
<>
{condition.status === 'False' && (
@@ -64,10 +64,20 @@ export const PodCondition = ({ condition }: PodConditionProps) => {
);
};
-interface PendingPodContentProps {
+/**
+ * Props for PendingPodContent
+ *
+ * @public
+ */
+export interface PendingPodContentProps {
pod: Pod;
}
+/**
+ * Shows details about pod's conditions as it starts
+ *
+ * @public
+ */
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
diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.test.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.test.tsx
index 64fd38c8d7..2b71976d8a 100644
--- a/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.test.tsx
+++ b/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.test.tsx
@@ -69,7 +69,7 @@ describe('PodDrawer', () => {
container: 'some-container',
errorType: 'some error type',
rootCauseExplanation: 'some root cause',
- possibleFixes: ['fix1', 'fix2'],
+ actions: ['fix1', 'fix2'],
},
],
},
diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.tsx
index 5e49410dd4..8e616df14e 100644
--- a/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.tsx
+++ b/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.tsx
@@ -58,11 +58,22 @@ const useDrawerContentStyles = makeStyles((_theme: Theme) =>
function getContainerSpecByName(pod: Pod, containerName: string) {
return pod.spec?.containers.find(c => c.name === containerName);
}
+
+/**
+ * Props for PodDrawer
+ *
+ * @public
+ */
interface PodDrawerProps {
open?: boolean;
podAndErrors: PodAndErrors;
}
+/**
+ * A Drawer for Kubernetes Pods
+ *
+ * @public
+ */
export const PodDrawer = ({ podAndErrors, open }: PodDrawerProps) => {
const classes = useDrawerContentStyles();
diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/index.ts b/plugins/kubernetes/src/components/Pods/PodDrawer/index.ts
index 17d50ddfcd..92bb254c48 100644
--- a/plugins/kubernetes/src/components/Pods/PodDrawer/index.ts
+++ b/plugins/kubernetes/src/components/Pods/PodDrawer/index.ts
@@ -13,4 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-export { PodDrawer } from './PodDrawer';
+export * from './PodDrawer';
+export * from './ContainerCard';
+export * from './PendingPodContent';
diff --git a/plugins/kubernetes/src/components/Pods/PodLogs/PodLogs.tsx b/plugins/kubernetes/src/components/Pods/PodLogs/PodLogs.tsx
index 5cff023772..de9341508e 100644
--- a/plugins/kubernetes/src/components/Pods/PodLogs/PodLogs.tsx
+++ b/plugins/kubernetes/src/components/Pods/PodLogs/PodLogs.tsx
@@ -15,20 +15,39 @@
*/
import React from 'react';
-import { DismissableBanner, LogViewer } from '@backstage/core-components';
+import {
+ DismissableBanner,
+ EmptyState,
+ 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;
+/**
+ * Props for PodLogs
+ *
+ * @public
+ */
+export interface PodLogsProps {
+ containerScope: ContainerScope;
+ previous?: boolean;
}
-export const PodLogs: React.FC = ({ podScope }: PodLogsProps) => {
+/**
+ * Shows the logs for the given pod
+ *
+ * @public
+ */
+export const PodLogs: React.FC = ({
+ containerScope,
+ previous,
+}: PodLogsProps) => {
const { value, error, loading } = usePodLogs({
- podScope: podScope,
+ containerScope,
+ previous,
});
return (
@@ -45,10 +64,20 @@ export const PodLogs: React.FC = ({ podScope }: PodLogsProps) => {
)}
{loading && }
- {!loading && value !== undefined && }
+ {!loading &&
+ value !== undefined &&
+ (value.text === '' ? (
+
+ ) : (
+
+ ))}
>
);
diff --git a/plugins/kubernetes/src/components/Pods/PodLogs/PodLogsDialog.tsx b/plugins/kubernetes/src/components/Pods/PodLogs/PodLogsDialog.tsx
index 05d980b26a..48fda2de2a 100644
--- a/plugins/kubernetes/src/components/Pods/PodLogs/PodLogsDialog.tsx
+++ b/plugins/kubernetes/src/components/Pods/PodLogs/PodLogsDialog.tsx
@@ -43,11 +43,21 @@ const useStyles = makeStyles((theme: Theme) =>
}),
);
-interface PodLogsDialogProps {
- podScope: ContainerScope;
+/**
+ * Props for PodLogsDialog
+ *
+ * @public
+ */
+export interface PodLogsDialogProps {
+ containerScope: ContainerScope;
}
-export const PodLogsDialog = ({ podScope }: PodLogsDialogProps) => {
+/**
+ * Shows the logs for the given pod in a Dialog
+ *
+ * @public
+ */
+export const PodLogsDialog = ({ containerScope }: PodLogsDialogProps) => {
const classes = useStyles();
const [open, setOpen] = useState(false);
@@ -62,8 +72,8 @@ export const PodLogsDialog = ({ podScope }: PodLogsDialogProps) => {
<>