mirror of
https://github.com/portainer/portainer.git
synced 2025-07-30 02:39:41 +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
|
@ -77,6 +77,7 @@ class KubernetesApplicationLogsController {
|
||||||
await this.getApplicationLogsAsync();
|
await this.getApplicationLogsAsync();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve application logs');
|
this.Notifications.error('Failure', err, 'Unable to retrieve application logs');
|
||||||
|
this.stopRepeater();
|
||||||
} finally {
|
} finally {
|
||||||
this.state.viewReady = true;
|
this.state.viewReady = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,6 +104,7 @@ class KubernetesStackLogsController {
|
||||||
await this.getStackLogsAsync();
|
await this.getStackLogsAsync();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve stack logs');
|
this.Notifications.error('Failure', err, 'Unable to retrieve stack logs');
|
||||||
|
this.stopRepeater();
|
||||||
} finally {
|
} finally {
|
||||||
this.state.viewReady = true;
|
this.state.viewReady = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,3 +11,4 @@ export { TableHeaderRow } from './TableHeaderRow';
|
||||||
export { TableRow } from './TableRow';
|
export { TableRow } from './TableRow';
|
||||||
export { TableContent } from './TableContent';
|
export { TableContent } from './TableContent';
|
||||||
export { TableFooter } from './TableFooter';
|
export { TableFooter } from './TableFooter';
|
||||||
|
export { TableSettingsMenuAutoRefresh } from './TableSettingsMenuAutoRefresh';
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
import { Server } from 'lucide-react';
|
import { Server } from 'lucide-react';
|
||||||
import { useCurrentStateAndParams } from '@uirouter/react';
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
import { useMemo } from '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 { IndexOptional } from '@/react/kubernetes/configs/types';
|
||||||
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import { useEnvironment } from '@/react/portainer/environments/queries';
|
import { useEnvironment } from '@/react/portainer/environments/queries';
|
||||||
|
|
||||||
import { Datatable } from '@@/datatables';
|
import {
|
||||||
|
Datatable,
|
||||||
|
TableSettingsMenu,
|
||||||
|
TableSettingsMenuAutoRefresh,
|
||||||
|
} from '@@/datatables';
|
||||||
import { useTableState } from '@@/datatables/useTableState';
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
|
|
||||||
import { useApplication } from '../../queries/useApplication';
|
import { useApplication } from '../../queries/useApplication';
|
||||||
|
@ -16,6 +20,7 @@ import { useApplicationPods } from '../../queries/useApplicationPods';
|
||||||
|
|
||||||
import { ContainerRowData } from './types';
|
import { ContainerRowData } from './types';
|
||||||
import { getColumns } from './columns';
|
import { getColumns } from './columns';
|
||||||
|
import { computeContainerStatus } from './computeContainerStatus';
|
||||||
|
|
||||||
const storageKey = 'k8sContainersDatatable';
|
const storageKey = 'k8sContainersDatatable';
|
||||||
const settingsStore = createStore(storageKey);
|
const settingsStore = createStore(storageKey);
|
||||||
|
@ -36,13 +41,19 @@ export function ApplicationContainersDatatable() {
|
||||||
environmentId,
|
environmentId,
|
||||||
namespace,
|
namespace,
|
||||||
name,
|
name,
|
||||||
resourceType
|
resourceType,
|
||||||
|
{
|
||||||
|
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
const podsQuery = useApplicationPods(
|
const podsQuery = useApplicationPods(
|
||||||
environmentId,
|
environmentId,
|
||||||
namespace,
|
namespace,
|
||||||
name,
|
name,
|
||||||
applicationQuery.data
|
applicationQuery.data,
|
||||||
|
{
|
||||||
|
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
const appContainers = useContainersRowData(podsQuery.data);
|
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)
|
getRowId={(row) => row.podName} // use pod name because it's unique (name is not unique)
|
||||||
disableSelect
|
disableSelect
|
||||||
data-cy="k8s-application-containers-datatable"
|
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) => {
|
pods?.flatMap((pod) => {
|
||||||
const containers = [
|
const containers = [
|
||||||
...(pod.spec?.containers || []),
|
...(pod.spec?.containers?.map((c) => ({ ...c, isInit: false })) ||
|
||||||
...(pod.spec?.initContainers || []),
|
[]),
|
||||||
|
...(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) => ({
|
return containers.map((container) => ({
|
||||||
...container,
|
...container,
|
||||||
|
@ -84,7 +109,8 @@ function useContainersRowData(pods?: Pod[]): ContainerRowData[] {
|
||||||
creationDate: pod.status?.startTime ?? '',
|
creationDate: pod.status?.startTime ?? '',
|
||||||
status: computeContainerStatus(
|
status: computeContainerStatus(
|
||||||
container.name,
|
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,
|
enableSorting: false,
|
||||||
cell: ({ row: { original: container } }) => (
|
cell: ({ row: { original: container } }) => (
|
||||||
<div className="flex gap-x-2">
|
<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
|
<Link
|
||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
to="kubernetes.applications.application.stats"
|
to="kubernetes.applications.application.logs"
|
||||||
params={{ pod: container.podName, container: container.name }}
|
params={{ pod: container.podName, container: container.name }}
|
||||||
data-cy={`application-container-stats-${container.name}`}
|
data-cy={`application-container-logs-${container.name}`}
|
||||||
>
|
>
|
||||||
<Icon icon={BarChart} />
|
<Icon icon={FileText} />
|
||||||
Stats
|
Logs
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<Link
|
{container.status.status.includes('Running') && (
|
||||||
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' && (
|
|
||||||
<Authorized authorizations="K8sApplicationConsoleRW">
|
<Authorized authorizations="K8sApplicationConsoleRW">
|
||||||
<Link
|
<Link
|
||||||
className="flex items-center gap-1"
|
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';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
export const name = columnHelper.accessor('name', {
|
export const name = columnHelper.accessor('name', {
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
id: '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 { 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';
|
import { ContainerRowData } from '../types';
|
||||||
|
|
||||||
|
@ -11,19 +14,24 @@ export const status = columnHelper.accessor('status', {
|
||||||
cell: StatusCell,
|
cell: StatusCell,
|
||||||
});
|
});
|
||||||
|
|
||||||
function StatusCell({ getValue }: CellContext<ContainerRowData, string>) {
|
function StatusCell({
|
||||||
return <Badge type={getContainerStatusType(getValue())}>{getValue()}</Badge>;
|
getValue,
|
||||||
}
|
}: CellContext<ContainerRowData, ContainerRowData['status']>) {
|
||||||
|
const statusData = getValue();
|
||||||
|
|
||||||
function getContainerStatusType(status: string): BadgeType {
|
return (
|
||||||
switch (status.toLowerCase()) {
|
<Badge type={statusData.type}>
|
||||||
case 'running':
|
<div className="flex items-center gap-1">
|
||||||
return 'success';
|
<span>
|
||||||
case 'waiting':
|
{statusData.status}
|
||||||
return 'warn';
|
{statusData.restartCount &&
|
||||||
case 'terminated':
|
` (Restarted ${statusData.restartCount} ${pluralize(
|
||||||
return 'info';
|
statusData.restartCount,
|
||||||
default:
|
'time'
|
||||||
return 'danger';
|
)})`}
|
||||||
}
|
</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 { Container } from 'kubernetes-types/core/v1';
|
||||||
|
|
||||||
|
import { BadgeType } from '@@/Badge';
|
||||||
|
|
||||||
export interface ContainerRowData extends Container {
|
export interface ContainerRowData extends Container {
|
||||||
podName: string;
|
podName: string;
|
||||||
nodeName: string;
|
nodeName: string;
|
||||||
podIp: string;
|
podIp: string;
|
||||||
creationDate: string;
|
creationDate: string;
|
||||||
status: string;
|
status: {
|
||||||
|
status: string;
|
||||||
|
type: BadgeType;
|
||||||
|
message?: string;
|
||||||
|
hasLogs?: boolean;
|
||||||
|
startedAt?: string;
|
||||||
|
restartCount?: number;
|
||||||
|
};
|
||||||
|
isInit?: boolean;
|
||||||
|
isSidecar?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -199,7 +199,7 @@
|
||||||
"html-loader": "^0.5.5",
|
"html-loader": "^0.5.5",
|
||||||
"html-webpack-plugin": "^5.5.3",
|
"html-webpack-plugin": "^5.5.3",
|
||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
"kubernetes-types": "^1.26.0",
|
"kubernetes-types": "^1.30.0",
|
||||||
"lint-staged": "^14.0.1",
|
"lint-staged": "^14.0.1",
|
||||||
"lodash-webpack-plugin": "^0.11.6",
|
"lodash-webpack-plugin": "^0.11.6",
|
||||||
"mini-css-extract-plugin": "^2.7.6",
|
"mini-css-extract-plugin": "^2.7.6",
|
||||||
|
|
|
@ -12868,10 +12868,10 @@ klona@^2.0.6:
|
||||||
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22"
|
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22"
|
||||||
integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==
|
integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==
|
||||||
|
|
||||||
kubernetes-types@^1.26.0:
|
kubernetes-types@^1.30.0:
|
||||||
version "1.26.0"
|
version "1.30.0"
|
||||||
resolved "https://registry.yarnpkg.com/kubernetes-types/-/kubernetes-types-1.26.0.tgz#47b7db20eb084931cfebf67937cc6b9091dc3da3"
|
resolved "https://registry.yarnpkg.com/kubernetes-types/-/kubernetes-types-1.30.0.tgz#f686cacb08ffc5f7e89254899c2153c723420116"
|
||||||
integrity sha512-jv0XaTIGW/p18jaiKRD85hLTYWx0yEj+cb6PDX3GdNa3dWoRxnD4Gv7+bE6C/ehcsp2skcdy34vT25jbPofDIQ==
|
integrity sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==
|
||||||
|
|
||||||
kuler@^2.0.0:
|
kuler@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue