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:
parent
eaa2be017d
commit
55cc250d2e
12 changed files with 996 additions and 61 deletions
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue