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"