1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-23 15:29:42 +02:00

fix(pods): represent pod container statuses correctly [r8s-416] (#910)

This commit is contained in:
Ali 2025-07-21 15:05:08 +12:00 committed by GitHub
parent eaa2be017d
commit 55cc250d2e
12 changed files with 996 additions and 61 deletions

View file

@ -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={() => (
<TableSettingsMenu>
<TableSettingsMenuAutoRefresh
value={tableState.autoRefreshRate}
onChange={(value) => tableState.setAutoRefreshRate(value)}
/>
</TableSettingsMenu>
)}
/>
);
}
@ -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';
}

View file

@ -13,27 +13,30 @@ export function getActions(isServerMetricsEnabled: boolean) {
enableSorting: false,
cell: ({ row: { original: container } }) => (
<div className="flex gap-x-2">
{container.status === 'Running' && isServerMetricsEnabled && (
{container.status.status.includes('Running') &&
isServerMetricsEnabled && (
<Link
className="flex items-center gap-1"
to="kubernetes.applications.application.stats"
params={{ pod: container.podName, container: container.name }}
data-cy={`application-container-stats-${container.name}`}
>
<Icon icon={BarChart} />
Stats
</Link>
)}
{container.status.hasLogs !== false && (
<Link
className="flex items-center gap-1"
to="kubernetes.applications.application.stats"
to="kubernetes.applications.application.logs"
params={{ pod: container.podName, container: container.name }}
data-cy={`application-container-stats-${container.name}`}
data-cy={`application-container-logs-${container.name}`}
>
<Icon icon={BarChart} />
Stats
<Icon icon={FileText} />
Logs
</Link>
)}
<Link
className="flex items-center gap-1"
to="kubernetes.applications.application.logs"
params={{ pod: container.podName, container: container.name }}
data-cy={`application-container-logs-${container.name}`}
>
<Icon icon={FileText} />
Logs
</Link>
{container.status === 'Running' && (
{container.status.status.includes('Running') && (
<Authorized authorizations="K8sApplicationConsoleRW">
<Link
className="flex items-center gap-1"

View file

@ -1,6 +1,65 @@
import { Badge } from '@@/Badge';
import { Tooltip } from '@@/Tip/Tooltip';
import { ExternalLink } from '@@/ExternalLink';
import { ContainerRowData } from '../types';
import { columnHelper } from './helper';
export const name = columnHelper.accessor('name', {
header: 'Name',
id: 'name',
cell: ({ row: { original: container } }) => (
<div className="flex justify-between gap-2">
<span>{container.name}</span>
<ContainerTypeBadge container={container} />
</div>
),
});
function ContainerTypeBadge({ container }: { container: ContainerRowData }) {
if (container.isSidecar) {
return (
<Badge type="info">
Sidecar
<Tooltip
message={
<>
<ExternalLink
to="https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/"
data-cy="sidecar-link"
>
Sidecar containers
</ExternalLink>{' '}
run continuously alongside the main application, starting before
other containers.
</>
}
/>
</Badge>
);
}
if (container.isInit) {
return (
<Badge type="info">
Init
<Tooltip
message={
<>
<ExternalLink
to="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/"
data-cy="init-link"
>
Init containers
</ExternalLink>{' '}
run and complete before the main application containers start.
</>
}
/>
</Badge>
);
}
return null;
}

View file

@ -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<ContainerRowData, string>) {
return <Badge type={getContainerStatusType(getValue())}>{getValue()}</Badge>;
}
function StatusCell({
getValue,
}: CellContext<ContainerRowData, ContainerRowData['status']>) {
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 (
<Badge type={statusData.type}>
<div className="flex items-center gap-1">
<span>
{statusData.status}
{statusData.restartCount &&
` (Restarted ${statusData.restartCount} ${pluralize(
statusData.restartCount,
'time'
)})`}
</span>
</div>
{statusData.message && <Tooltip message={statusData.message} />}
</Badge>
);
}

View file

@ -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>
): 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);
});
});
});

View file

@ -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
);
}

View file

@ -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;
}