diff --git a/app/kubernetes/views/applications/logs/logsController.js b/app/kubernetes/views/applications/logs/logsController.js
index 66601d98b..37cae6cad 100644
--- a/app/kubernetes/views/applications/logs/logsController.js
+++ b/app/kubernetes/views/applications/logs/logsController.js
@@ -77,6 +77,7 @@ class KubernetesApplicationLogsController {
await this.getApplicationLogsAsync();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve application logs');
+ this.stopRepeater();
} finally {
this.state.viewReady = true;
}
diff --git a/app/kubernetes/views/stacks/logs/logsController.js b/app/kubernetes/views/stacks/logs/logsController.js
index 536ea2ae4..d4e1b5ba7 100644
--- a/app/kubernetes/views/stacks/logs/logsController.js
+++ b/app/kubernetes/views/stacks/logs/logsController.js
@@ -104,6 +104,7 @@ class KubernetesStackLogsController {
await this.getStackLogsAsync();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve stack logs');
+ this.stopRepeater();
} finally {
this.state.viewReady = true;
}
diff --git a/app/react/components/datatables/index.ts b/app/react/components/datatables/index.ts
index 809efc23b..3ab420889 100644
--- a/app/react/components/datatables/index.ts
+++ b/app/react/components/datatables/index.ts
@@ -11,3 +11,4 @@ export { TableHeaderRow } from './TableHeaderRow';
export { TableRow } from './TableRow';
export { TableContent } from './TableContent';
export { TableFooter } from './TableFooter';
+export { TableSettingsMenuAutoRefresh } from './TableSettingsMenuAutoRefresh';
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.tsx
index b365c5851..7b40b4852 100644
--- a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.tsx
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.tsx
@@ -1,14 +1,18 @@
import { Server } from 'lucide-react';
import { useCurrentStateAndParams } from '@uirouter/react';
import { useMemo } from 'react';
-import { ContainerStatus, Pod } from 'kubernetes-types/core/v1';
+import { Pod } from 'kubernetes-types/core/v1';
import { IndexOptional } from '@/react/kubernetes/configs/types';
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { useEnvironment } from '@/react/portainer/environments/queries';
-import { Datatable } from '@@/datatables';
+import {
+ Datatable,
+ TableSettingsMenu,
+ TableSettingsMenuAutoRefresh,
+} from '@@/datatables';
import { useTableState } from '@@/datatables/useTableState';
import { useApplication } from '../../queries/useApplication';
@@ -16,6 +20,7 @@ import { useApplicationPods } from '../../queries/useApplicationPods';
import { ContainerRowData } from './types';
import { getColumns } from './columns';
+import { computeContainerStatus } from './computeContainerStatus';
const storageKey = 'k8sContainersDatatable';
const settingsStore = createStore(storageKey);
@@ -36,13 +41,19 @@ export function ApplicationContainersDatatable() {
environmentId,
namespace,
name,
- resourceType
+ resourceType,
+ {
+ autoRefreshRate: tableState.autoRefreshRate * 1000,
+ }
);
const podsQuery = useApplicationPods(
environmentId,
namespace,
name,
- applicationQuery.data
+ applicationQuery.data,
+ {
+ autoRefreshRate: tableState.autoRefreshRate * 1000,
+ }
);
const appContainers = useContainersRowData(podsQuery.data);
@@ -61,6 +72,14 @@ export function ApplicationContainersDatatable() {
getRowId={(row) => row.podName} // use pod name because it's unique (name is not unique)
disableSelect
data-cy="k8s-application-containers-datatable"
+ renderTableSettings={() => (
+
+ tableState.setAutoRefreshRate(value)}
+ />
+
+ )}
/>
);
}
@@ -73,8 +92,14 @@ function useContainersRowData(pods?: Pod[]): ContainerRowData[] {
() =>
pods?.flatMap((pod) => {
const containers = [
- ...(pod.spec?.containers || []),
- ...(pod.spec?.initContainers || []),
+ ...(pod.spec?.containers?.map((c) => ({ ...c, isInit: false })) ||
+ []),
+ ...(pod.spec?.initContainers?.map((c) => ({
+ ...c,
+ isInit: true,
+ // https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/#sidecar-containers-and-pod-lifecycle
+ isSidecar: c.restartPolicy === 'Always',
+ })) || []),
];
return containers.map((container) => ({
...container,
@@ -84,7 +109,8 @@ function useContainersRowData(pods?: Pod[]): ContainerRowData[] {
creationDate: pod.status?.startTime ?? '',
status: computeContainerStatus(
container.name,
- pod.status?.containerStatuses
+ pod.status?.containerStatuses,
+ pod.status?.initContainerStatuses
),
}));
}) || [],
@@ -92,21 +118,3 @@ function useContainersRowData(pods?: Pod[]): ContainerRowData[] {
) || []
);
}
-
-function computeContainerStatus(
- containerName: string,
- statuses?: ContainerStatus[]
-) {
- const status = statuses?.find((status) => status.name === containerName);
- if (!status) {
- return 'Terminated';
- }
- const { state } = status;
- if (state?.waiting) {
- return 'Waiting';
- }
- if (!state?.running) {
- return 'Terminated';
- }
- return 'Running';
-}
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/actions.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/actions.tsx
index d3b06ca52..c875db4a6 100644
--- a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/actions.tsx
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/actions.tsx
@@ -13,27 +13,30 @@ export function getActions(isServerMetricsEnabled: boolean) {
enableSorting: false,
cell: ({ row: { original: container } }) => (
- {container.status === 'Running' && isServerMetricsEnabled && (
+ {container.status.status.includes('Running') &&
+ isServerMetricsEnabled && (
+
+
+ Stats
+
+ )}
+ {container.status.hasLogs !== false && (
-
- Stats
+
+ Logs
)}
-
-
- Logs
-
- {container.status === 'Running' && (
+ {container.status.status.includes('Running') && (
(
+
+ {container.name}
+
+
+ ),
});
+
+function ContainerTypeBadge({ container }: { container: ContainerRowData }) {
+ if (container.isSidecar) {
+ return (
+
+ Sidecar
+
+
+ Sidecar containers
+ {' '}
+ run continuously alongside the main application, starting before
+ other containers.
+ >
+ }
+ />
+
+ );
+ }
+
+ if (container.isInit) {
+ return (
+
+ Init
+
+
+ Init containers
+ {' '}
+ run and complete before the main application containers start.
+ >
+ }
+ />
+
+ );
+ }
+
+ return null;
+}
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/status.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/status.tsx
index d181f8377..7a47e3026 100644
--- a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/status.tsx
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/status.tsx
@@ -1,6 +1,9 @@
import { CellContext } from '@tanstack/react-table';
-import { Badge, BadgeType } from '@@/Badge';
+import { pluralize } from '@/react/common/string-utils';
+
+import { Badge } from '@@/Badge';
+import { Tooltip } from '@@/Tip/Tooltip';
import { ContainerRowData } from '../types';
@@ -11,19 +14,24 @@ export const status = columnHelper.accessor('status', {
cell: StatusCell,
});
-function StatusCell({ getValue }: CellContext) {
- return {getValue()};
-}
+function StatusCell({
+ getValue,
+}: CellContext) {
+ const statusData = getValue();
-function getContainerStatusType(status: string): BadgeType {
- switch (status.toLowerCase()) {
- case 'running':
- return 'success';
- case 'waiting':
- return 'warn';
- case 'terminated':
- return 'info';
- default:
- return 'danger';
- }
+ return (
+
+
+
+ {statusData.status}
+ {statusData.restartCount &&
+ ` (Restarted ${statusData.restartCount} ${pluralize(
+ statusData.restartCount,
+ 'time'
+ )})`}
+
+
+ {statusData.message && }
+
+ );
}
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/computeContainerStatus.test.ts b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/computeContainerStatus.test.ts
new file mode 100644
index 000000000..4cc56809e
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/computeContainerStatus.test.ts
@@ -0,0 +1,483 @@
+import { ContainerStatus } from 'kubernetes-types/core/v1';
+
+import { computeContainerStatus } from './computeContainerStatus';
+
+// Helper to create a base ContainerStatus with required properties
+function createContainerStatus(
+ overrides: Partial
+): ContainerStatus {
+ return {
+ name: 'test-container',
+ ready: false,
+ restartCount: 0,
+ image: 'test-image:latest',
+ imageID: 'sha256:test123',
+ ...overrides,
+ };
+}
+
+describe('computeContainerStatus', () => {
+ describe('Critical Container States', () => {
+ test('ImagePullBackOff should return danger type with no logs', () => {
+ const containerStatus = createContainerStatus({
+ state: {
+ waiting: {
+ reason: 'ImagePullBackOff',
+ message: 'Failed to pull image',
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.status).toBe('ImagePullBackOff');
+ expect(result.type).toBe('danger');
+ expect(result.hasLogs).toBe(false);
+ });
+
+ test('ImagePullBackOff with containerID should return danger type with logs', () => {
+ const containerStatus = createContainerStatus({
+ containerID: 'docker://abc123def456',
+ state: {
+ waiting: {
+ reason: 'ImagePullBackOff',
+ message: 'Failed to pull image',
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.status).toBe('ImagePullBackOff');
+ expect(result.type).toBe('danger');
+ expect(result.hasLogs).toBe(true);
+ });
+
+ test('CrashLoopBackOff should return danger type with logs if container started', () => {
+ const containerStatus = createContainerStatus({
+ restartCount: 5,
+ state: {
+ waiting: {
+ reason: 'CrashLoopBackOff',
+ message: 'Back-off restarting failed container',
+ },
+ },
+ lastState: {
+ terminated: {
+ startedAt: '2023-01-01T10:00:00Z',
+ exitCode: 1,
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.status).toBe('CrashLoopBackOff');
+ expect(result.type).toBe('danger');
+ expect(result.hasLogs).toBe(true);
+ });
+
+ test('CrashLoopBackOff with only containerID should return danger type with logs', () => {
+ const containerStatus = createContainerStatus({
+ containerID: 'docker://crashed123',
+ restartCount: 3,
+ state: {
+ waiting: {
+ reason: 'CrashLoopBackOff',
+ message: 'Back-off restarting failed container',
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.status).toBe('CrashLoopBackOff');
+ expect(result.type).toBe('danger');
+ expect(result.hasLogs).toBe(true);
+ });
+
+ test('Running and ready should return success type with logs', () => {
+ const containerStatus = createContainerStatus({
+ ready: true,
+ state: {
+ running: {
+ startedAt: '2023-01-01T10:00:00Z',
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.status).toBe('Running');
+ expect(result.type).toBe('success');
+ expect(result.hasLogs).toBe(true);
+ });
+
+ test('Running but not ready should return warn type with logs', () => {
+ const containerStatus = createContainerStatus({
+ state: {
+ running: {
+ startedAt: '2023-01-01T10:00:00Z',
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.status).toBe('Running (not ready)');
+ expect(result.type).toBe('warn');
+ expect(result.hasLogs).toBe(true);
+ });
+
+ test('OOMKilled should return danger type with logs', () => {
+ const containerStatus = createContainerStatus({
+ restartCount: 2,
+ state: {
+ terminated: {
+ reason: 'OOMKilled',
+ exitCode: 137,
+ startedAt: '2023-01-01T10:00:00Z',
+ finishedAt: '2023-01-01T10:05:00Z',
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.status).toBe('OOMKilled');
+ expect(result.type).toBe('danger');
+ expect(result.hasLogs).toBe(true);
+ });
+
+ test('Completed successfully should return success type with logs', () => {
+ const containerStatus = createContainerStatus({
+ state: {
+ terminated: {
+ reason: 'Completed',
+ exitCode: 0,
+ startedAt: '2023-01-01T10:00:00Z',
+ finishedAt: '2023-01-01T10:05:00Z',
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.status).toBe('Completed');
+ expect(result.type).toBe('success');
+ expect(result.hasLogs).toBe(true);
+ });
+
+ test('ContainerCreating should return info type with no logs', () => {
+ const containerStatus = createContainerStatus({
+ state: {
+ waiting: {
+ reason: 'ContainerCreating',
+ message: 'Container is being created',
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.status).toBe('ContainerCreating');
+ expect(result.type).toBe('info');
+ expect(result.hasLogs).toBe(false);
+ });
+
+ test('ContainerCreating with containerID should return info type with logs', () => {
+ const containerStatus = createContainerStatus({
+ containerID: 'docker://creating123',
+ state: {
+ waiting: {
+ reason: 'ContainerCreating',
+ message: 'Container is being created',
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.status).toBe('ContainerCreating');
+ expect(result.type).toBe('info');
+ expect(result.hasLogs).toBe(true);
+ });
+
+ test('PodInitializing should return info type with prefixed status', () => {
+ const containerStatus = createContainerStatus({
+ state: {
+ waiting: {
+ reason: 'PodInitializing',
+ message: 'Waiting for init containers',
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.status).toBe('Waiting (PodInitializing)');
+ expect(result.type).toBe('info');
+ expect(result.hasLogs).toBe(false);
+ });
+
+ test('Container not found should return unknown with muted type', () => {
+ const result = computeContainerStatus('nonexistent-container', []);
+
+ expect(result.status).toBe('Unknown');
+ expect(result.type).toBe('muted');
+ expect(result.hasLogs).toBeUndefined();
+ });
+
+ test('Container with no state should return unknown with muted type', () => {
+ const containerStatus = createContainerStatus({
+ state: {},
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.status).toBe('Unknown');
+ expect(result.type).toBe('muted');
+ expect(result.hasLogs).toBe(false);
+ });
+
+ test('Container with no state but with containerID should have logs available', () => {
+ const containerStatus = createContainerStatus({
+ containerID: 'docker://unknown123',
+ state: {},
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.status).toBe('Unknown');
+ expect(result.type).toBe('muted');
+ expect(result.hasLogs).toBe(true);
+ });
+
+ test('Sidecar container should be handled like regular container', () => {
+ const containerStatus = createContainerStatus({
+ ready: true,
+ state: {
+ running: {
+ startedAt: '2023-01-01T10:00:00Z',
+ },
+ },
+ });
+
+ const result = computeContainerStatus(
+ 'test-container',
+ [],
+ [containerStatus]
+ ); // Sidecar containers are found in initContainerStatuses
+
+ expect(result.status).toBe('Running');
+ expect(result.type).toBe('success');
+ expect(result.hasLogs).toBe(true);
+ });
+ });
+
+ describe('Container Log Availability Tests', () => {
+ test('Container with running state should have logs', () => {
+ const containerStatus = createContainerStatus({
+ state: {
+ running: {
+ startedAt: '2023-01-01T10:00:00Z',
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.hasLogs).toBe(true);
+ });
+
+ test('Container with terminated state should have logs', () => {
+ const containerStatus = createContainerStatus({
+ state: {
+ terminated: {
+ startedAt: '2023-01-01T10:00:00Z',
+ exitCode: 0,
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.hasLogs).toBe(true);
+ });
+
+ test('Container with lastState running should have logs', () => {
+ const containerStatus = createContainerStatus({
+ state: {
+ waiting: {
+ reason: 'CrashLoopBackOff',
+ },
+ },
+ lastState: {
+ running: {
+ startedAt: '2023-01-01T10:00:00Z',
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.hasLogs).toBe(true);
+ });
+
+ test('Container with lastState terminated should have logs', () => {
+ const containerStatus = createContainerStatus({
+ state: {
+ waiting: {
+ reason: 'CrashLoopBackOff',
+ },
+ },
+ lastState: {
+ terminated: {
+ startedAt: '2023-01-01T10:00:00Z',
+ exitCode: 1,
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.hasLogs).toBe(true);
+ });
+
+ test('Container with only containerID should have logs', () => {
+ const containerStatus = createContainerStatus({
+ containerID: 'docker://abc123def456',
+ state: {
+ waiting: {
+ reason: 'Unknown',
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.hasLogs).toBe(true);
+ });
+
+ test('Container with empty containerID should not have logs', () => {
+ const containerStatus = createContainerStatus({
+ containerID: '',
+ state: {
+ waiting: {
+ reason: 'ImagePullBackOff',
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.hasLogs).toBe(false);
+ });
+
+ test('Container without containerID or start times should not have logs', () => {
+ const containerStatus = createContainerStatus({
+ state: {
+ waiting: {
+ reason: 'ImagePullBackOff',
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.hasLogs).toBe(false);
+ });
+
+ test('Container with terminated state but no startedAt should rely on containerID', () => {
+ const containerStatus = createContainerStatus({
+ containerID: 'docker://terminated123',
+ state: {
+ terminated: {
+ exitCode: 0,
+ // No startedAt field
+ },
+ },
+ });
+
+ const result = computeContainerStatus('test-container', [
+ containerStatus,
+ ]);
+
+ expect(result.hasLogs).toBe(true);
+ });
+
+ test('Multiple containers with different log availability', () => {
+ const containerWithLogs = createContainerStatus({
+ name: 'container-with-logs',
+ containerID: 'docker://logs123',
+ state: {
+ running: {
+ startedAt: '2023-01-01T10:00:00Z',
+ },
+ },
+ });
+
+ const containerWithoutLogs = createContainerStatus({
+ name: 'container-without-logs',
+ state: {
+ waiting: {
+ reason: 'ImagePullBackOff',
+ },
+ },
+ });
+
+ const resultWithLogs = computeContainerStatus('container-with-logs', [
+ containerWithLogs,
+ containerWithoutLogs,
+ ]);
+
+ const resultWithoutLogs = computeContainerStatus(
+ 'container-without-logs',
+ [containerWithLogs, containerWithoutLogs]
+ );
+
+ expect(resultWithLogs.hasLogs).toBe(true);
+ expect(resultWithoutLogs.hasLogs).toBe(false);
+ });
+ });
+});
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/computeContainerStatus.ts b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/computeContainerStatus.ts
new file mode 100644
index 000000000..b5ca69073
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/computeContainerStatus.ts
@@ -0,0 +1,360 @@
+import { ContainerStatus } from 'kubernetes-types/core/v1';
+
+import { ContainerRowData } from './types';
+
+/**
+ * Compute the status of a container, with translated messages.
+ *
+ * The cases are hardcoded, because there is not a single source that enumerates
+ * all the possible states.
+ * @param containerName - The name of the container
+ * @param containerStatuses - The statuses of the container
+ * @param initContainerStatuses - The statuses of the init container
+ * @returns The status of the container
+ */
+export function computeContainerStatus(
+ containerName: string,
+ containerStatuses?: ContainerStatus[],
+ initContainerStatuses?: ContainerStatus[]
+): ContainerRowData['status'] {
+ // Choose the correct status array based on container type
+ const statuses = [
+ ...(containerStatuses || []),
+ ...(initContainerStatuses || []),
+ ];
+ const status = statuses?.find((status) => status.name === containerName);
+
+ if (!status) {
+ return {
+ status: 'Unknown',
+ type: 'muted',
+ message: 'Container status information is not available',
+ };
+ }
+
+ const hasLogs = hasContainerEverStarted(status);
+ const { state, restartCount = 0 } = status;
+
+ // Handle waiting state with more specific reasons
+ if (state?.waiting) {
+ const { reason, message } = state.waiting;
+ if (reason) {
+ // Return specific waiting reasons that match kubectl output
+ switch (reason) {
+ case 'ImagePullBackOff':
+ case 'ErrImagePull':
+ case 'ImageInspectError':
+ case 'ErrImageNeverPull':
+ return {
+ status: reason,
+ type: 'danger',
+ message:
+ message ||
+ 'Failed to pull container image. Check image name, registry access, and network connectivity.',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'danger')
+ ? restartCount
+ : undefined,
+ };
+ case 'ContainerCreating':
+ return {
+ status: reason,
+ type: 'info',
+ message:
+ message ||
+ 'Container is being created. This may take a few moments.',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'info')
+ ? restartCount
+ : undefined,
+ };
+ case 'PodInitializing':
+ return {
+ status: `Waiting (${reason})`,
+ type: 'info',
+ message:
+ message ||
+ 'Waiting for init containers to complete. Wait a few moments or check the logs of any init containers that failed to complete.',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'info')
+ ? restartCount
+ : undefined,
+ };
+ case 'CreateContainerConfigError':
+ case 'CreateContainerError':
+ return {
+ status: reason,
+ type: 'danger',
+ message:
+ message ||
+ 'Failed to create container. Check resource limits, security contexts, and volume mounts.',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'danger')
+ ? restartCount
+ : undefined,
+ };
+ case 'InvalidImageName':
+ return {
+ status: reason,
+ type: 'danger',
+ message:
+ message ||
+ 'The specified container image name is invalid or malformed.',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'danger')
+ ? restartCount
+ : undefined,
+ };
+ case 'CrashLoopBackOff':
+ return {
+ status: reason,
+ type: 'danger',
+ message: `Container keeps crashing after startup. Check application logs and startup configuration. ${
+ message ? `Details: '${message}'` : ''
+ }`,
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'danger')
+ ? restartCount
+ : undefined,
+ };
+ case 'RunContainerError':
+ return {
+ status: reason,
+ type: 'danger',
+ message:
+ message ||
+ 'Failed to start container process. Check command, arguments, and environment variables.',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'danger')
+ ? restartCount
+ : undefined,
+ };
+ case 'KillContainerError':
+ return {
+ status: reason,
+ type: 'danger',
+ message:
+ message ||
+ 'Failed to stop container gracefully. Container may be unresponsive.',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'danger')
+ ? restartCount
+ : undefined,
+ };
+ case 'VerifyNonRootError':
+ return {
+ status: reason,
+ type: 'danger',
+ message:
+ message ||
+ 'Container is trying to run as root but security policy requires non-root execution.',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'danger')
+ ? restartCount
+ : undefined,
+ };
+ case 'ConfigError':
+ return {
+ status: reason,
+ type: 'danger',
+ message:
+ message ||
+ 'Container configuration is invalid. Check resource requirements and security settings.',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'danger')
+ ? restartCount
+ : undefined,
+ };
+ default:
+ return {
+ status: reason,
+ type: 'muted',
+ message: message || `Container is waiting: ${reason}`,
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'muted')
+ ? restartCount
+ : undefined,
+ };
+ }
+ }
+ return {
+ status: 'Waiting',
+ type: 'muted',
+ message: message || 'Container is waiting to be scheduled or started.',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'muted')
+ ? restartCount
+ : undefined,
+ };
+ }
+
+ // Handle terminated state
+ if (state?.terminated) {
+ const { exitCode = 0, reason, message } = state.terminated;
+
+ if (reason) {
+ switch (reason) {
+ case 'Error':
+ return {
+ status: 'Error',
+ type: 'danger',
+ message: `Container exited with code ${exitCode}${
+ restartCount > 0 ? ` (restarted ${restartCount} times)` : ''
+ }. ${message || 'Check application logs for error details.'}`,
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'danger')
+ ? restartCount
+ : undefined,
+ };
+ case 'Completed':
+ return {
+ status: 'Completed',
+ type: 'success',
+ message,
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'success')
+ ? restartCount
+ : undefined,
+ };
+ case 'OOMKilled':
+ return {
+ status: 'OOMKilled',
+ type: 'danger',
+ message:
+ message ||
+ 'Container was killed due to out-of-memory. Consider increasing memory limits.',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'danger')
+ ? restartCount
+ : undefined,
+ };
+ case 'DeadlineExceeded':
+ return {
+ status: 'DeadlineExceeded',
+ type: 'danger',
+ message:
+ message ||
+ 'Container was terminated because it exceeded the active deadline.',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'danger')
+ ? restartCount
+ : undefined,
+ };
+ case 'Evicted':
+ return {
+ status: 'Evicted',
+ type: 'warn',
+ message:
+ message ||
+ 'Container was evicted due to resource pressure on the node.',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'warn')
+ ? restartCount
+ : undefined,
+ };
+ case 'NodeLost':
+ return {
+ status: 'NodeLost',
+ type: 'danger',
+ message:
+ message || 'Container was lost when the node became unreachable.',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'danger')
+ ? restartCount
+ : undefined,
+ };
+ default:
+ return {
+ status: reason,
+ type: 'muted',
+ message: `Container terminated: ${reason}${
+ restartCount > 0 ? ` (restarted ${restartCount} times)` : ''
+ }. ${message || ''}`,
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'muted')
+ ? restartCount
+ : undefined,
+ };
+ }
+ }
+
+ if (exitCode === 0) {
+ return {
+ status: 'Completed',
+ type: 'success',
+ message,
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'success')
+ ? restartCount
+ : undefined,
+ };
+ }
+
+ return {
+ status: 'Error',
+ type: 'danger',
+ message: `Container exited with code ${exitCode}${
+ restartCount > 0 ? ` (restarted ${restartCount} times)` : ''
+ }. ${message || 'Check application logs for error details.'}`,
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'danger')
+ ? restartCount
+ : undefined,
+ };
+ }
+
+ // Handle running state
+ if (state?.running) {
+ // Check if container is ready
+ if (status.ready === false) {
+ return {
+ status: 'Running (not ready)',
+ type: 'warn',
+ message:
+ 'Container is running but not ready. Check readiness probe configuration.',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'warn')
+ ? restartCount
+ : undefined,
+ };
+ }
+ return {
+ status: 'Running',
+ type: 'success',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'success')
+ ? restartCount
+ : undefined,
+ };
+ }
+
+ // Fallback
+ return {
+ status: 'Unknown',
+ type: 'muted',
+ message:
+ 'Container state cannot be determined. Status information may be incomplete.',
+ hasLogs,
+ restartCount: shouldShowRestartCount(restartCount, 'muted')
+ ? restartCount
+ : undefined,
+ };
+}
+
+// Helper function to determine if restart count should be shown
+function shouldShowRestartCount(
+ restartCount: number,
+ type: ContainerRowData['status']['type']
+) {
+ return restartCount >= 1 && (type === 'danger' || type === 'warn');
+}
+
+function hasContainerEverStarted(status: ContainerStatus): boolean {
+ return (
+ !!status.state?.running?.startedAt ||
+ !!status.state?.terminated?.startedAt ||
+ !!status.lastState?.running?.startedAt ||
+ !!status.lastState?.terminated?.startedAt ||
+ !!status.containerID
+ );
+}
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/types.ts b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/types.ts
index 7981ab187..bdd229f89 100644
--- a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/types.ts
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/types.ts
@@ -1,9 +1,20 @@
import { Container } from 'kubernetes-types/core/v1';
+import { BadgeType } from '@@/Badge';
+
export interface ContainerRowData extends Container {
podName: string;
nodeName: string;
podIp: string;
creationDate: string;
- status: string;
+ status: {
+ status: string;
+ type: BadgeType;
+ message?: string;
+ hasLogs?: boolean;
+ startedAt?: string;
+ restartCount?: number;
+ };
+ isInit?: boolean;
+ isSidecar?: boolean;
}
diff --git a/package.json b/package.json
index 9a250005a..8cab91593 100644
--- a/package.json
+++ b/package.json
@@ -199,7 +199,7 @@
"html-loader": "^0.5.5",
"html-webpack-plugin": "^5.5.3",
"husky": "^8.0.0",
- "kubernetes-types": "^1.26.0",
+ "kubernetes-types": "^1.30.0",
"lint-staged": "^14.0.1",
"lodash-webpack-plugin": "^0.11.6",
"mini-css-extract-plugin": "^2.7.6",
diff --git a/yarn.lock b/yarn.lock
index 3bdc96e8a..3e826163f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12868,10 +12868,10 @@ klona@^2.0.6:
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22"
integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==
-kubernetes-types@^1.26.0:
- version "1.26.0"
- resolved "https://registry.yarnpkg.com/kubernetes-types/-/kubernetes-types-1.26.0.tgz#47b7db20eb084931cfebf67937cc6b9091dc3da3"
- integrity sha512-jv0XaTIGW/p18jaiKRD85hLTYWx0yEj+cb6PDX3GdNa3dWoRxnD4Gv7+bE6C/ehcsp2skcdy34vT25jbPofDIQ==
+kubernetes-types@^1.30.0:
+ version "1.30.0"
+ resolved "https://registry.yarnpkg.com/kubernetes-types/-/kubernetes-types-1.30.0.tgz#f686cacb08ffc5f7e89254899c2153c723420116"
+ integrity sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==
kuler@^2.0.0:
version "2.0.0"