mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-22 19:56:01 +00:00
kubernetes(status): make degraded k8s states visible and filterable
The canonical Kubernetes adapter reports every unhealthy-but-alive state (cordoned node, not-ready container, pending pod, partially available controller) as `warning`, but the platform page's filter triad only accepted degraded/paused, so those rows matched none of the Healthy / Degraded / Offline chips and were findable only under All. Flagged by the v5-parity audit: v5's Unhealthy filter classified all of these. - Accept `warning` into the page's degraded filter bucket. - Declare `unschedulable` on ResourceKubernetesMeta (the adapter has projected it all along) and map ready-but-cordoned nodes to an amber Unschedulable indicator instead of green Ready. NotReady still wins. - Render the mapped status label in the pods table status column. The raw phase reads Running while a container is in CrashLoopBackOff; the reason was previously visible only as the dot's hover title. - Add `warning` to the frontend ResourceStatus union to match what the backend emits.
This commit is contained in:
@@ -53,9 +53,6 @@ const podScope = (resource: Resource): string => {
|
||||
return cluster || 'Cluster';
|
||||
};
|
||||
|
||||
const podPhase = (resource: Resource): string =>
|
||||
textValue(resource.kubernetes?.podPhase || resource.kubernetes?.phase);
|
||||
|
||||
const podContainers = (resource: Resource): ResourceKubernetesPodContainerStatus[] =>
|
||||
resource.kubernetes?.podContainers ?? [];
|
||||
|
||||
@@ -171,7 +168,7 @@ export const KubernetesPodsTable: Component<{
|
||||
Node
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[8%]`}>
|
||||
Phase
|
||||
Status
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[7%]`}
|
||||
@@ -207,7 +204,6 @@ export const KubernetesPodsTable: Component<{
|
||||
const name = () => podName(resource);
|
||||
const scope = () => podScope(resource);
|
||||
const node = () => textValue(resource.kubernetes?.nodeName);
|
||||
const phase = () => podPhase(resource);
|
||||
const owner = () => ownerValue(resource);
|
||||
const image = () => imageValue(resource);
|
||||
const age = () => ageValue(resource);
|
||||
@@ -255,7 +251,12 @@ export const KubernetesPodsTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
{phase()}
|
||||
{/* The mapped label carries the container failure reason
|
||||
(CrashLoopBackOff, ImagePullBackOff, ...) where the raw
|
||||
phase would still read "Running". */}
|
||||
<span class="block max-w-full truncate" title={indicator().label}>
|
||||
{indicator().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('KubernetesPodsTable', () => {
|
||||
expect(screen.getByText('Pod')).toBeInTheDocument();
|
||||
expect(screen.getByText('Scope')).toBeInTheDocument();
|
||||
expect(screen.getByText('Node')).toBeInTheDocument();
|
||||
expect(screen.getByText('Phase')).toBeInTheDocument();
|
||||
expect(screen.getByText('Status')).toBeInTheDocument();
|
||||
expect(screen.getByText('Ready')).toBeInTheDocument();
|
||||
expect(screen.getByText('Restarts')).toBeInTheDocument();
|
||||
expect(screen.getByText('Owner')).toBeInTheDocument();
|
||||
@@ -83,7 +83,9 @@ describe('KubernetesPodsTable', () => {
|
||||
expect(screen.getByText('checkout-api-6c746d5bcf-c7z2p')).toBeInTheDocument();
|
||||
expect(screen.getByText('prod-euw1/services')).toBeInTheDocument();
|
||||
expect(screen.getByText('prod-euw1-k8s-02')).toBeInTheDocument();
|
||||
expect(screen.getByText('Running')).toBeInTheDocument();
|
||||
// Raw phase is Running, but the metrics-sidecar container is not ready;
|
||||
// the status column shows the mapped label, not the phase.
|
||||
expect(screen.getByText('Not ready')).toBeInTheDocument();
|
||||
expect(screen.getByText('1/2')).toBeInTheDocument();
|
||||
expect(screen.getByText('3')).toBeInTheDocument();
|
||||
expect(screen.getByText('Deployment/checkout-api')).toBeInTheDocument();
|
||||
@@ -136,8 +138,17 @@ describe('KubernetesPodsTable', () => {
|
||||
(row) => row.getAttribute('data-kubernetes-pod-row'),
|
||||
);
|
||||
expect(rows).toEqual(['crashing-pod', 'not-ready-pod', 'happy-pod']);
|
||||
expect(screen.getByTitle('CrashLoopBackOff')).toHaveClass('bg-red-500');
|
||||
expect(screen.getByTitle('Not ready')).toHaveClass('bg-amber-500');
|
||||
expect(screen.getByTitle('Running')).toHaveClass('bg-emerald-500');
|
||||
// The failure reason is visible cell text, not just a dot tooltip.
|
||||
expect(screen.getByText('CrashLoopBackOff')).toBeInTheDocument();
|
||||
expect(screen.getByText('Not ready')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getAllByTitle('CrashLoopBackOff').some((el) => el.classList.contains('bg-red-500')),
|
||||
).toBe(true);
|
||||
expect(
|
||||
screen.getAllByTitle('Not ready').some((el) => el.classList.contains('bg-amber-500')),
|
||||
).toBe(true);
|
||||
expect(
|
||||
screen.getAllByTitle('Running').some((el) => el.classList.contains('bg-emerald-500')),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -365,6 +365,32 @@ describe('kubernetesPageModel', () => {
|
||||
).toEqual({ variant: 'success', label: 'Ready' });
|
||||
});
|
||||
|
||||
it('returns warning for a cordoned node even though it reports ready', () => {
|
||||
expect(
|
||||
mapKubernetesNodeStatus(
|
||||
makeResource({
|
||||
id: 'cordoned',
|
||||
type: 'k8s-node',
|
||||
status: 'warning',
|
||||
kubernetes: { ready: true, unschedulable: true },
|
||||
}),
|
||||
),
|
||||
).toEqual({ variant: 'warning', label: 'Unschedulable' });
|
||||
});
|
||||
|
||||
it('keeps NotReady as the dominant state when a cordoned node is also down', () => {
|
||||
expect(
|
||||
mapKubernetesNodeStatus(
|
||||
makeResource({
|
||||
id: 'cordoned-down',
|
||||
type: 'k8s-node',
|
||||
status: 'offline',
|
||||
kubernetes: { ready: false, unschedulable: true },
|
||||
}),
|
||||
),
|
||||
).toEqual({ variant: 'danger', label: 'NotReady' });
|
||||
});
|
||||
|
||||
it('falls back to resource.status when ready is undefined', () => {
|
||||
expect(
|
||||
mapKubernetesNodeStatus(makeResource({ id: 'fallback-ok', type: 'k8s-node', status: 'online' })),
|
||||
@@ -758,6 +784,27 @@ describe('kubernetesPageModel', () => {
|
||||
]);
|
||||
expect(filterKubernetesResources(rows, 'v1.30', 'online')).toEqual([]);
|
||||
});
|
||||
|
||||
it('buckets the backend `warning` status into the degraded filter', () => {
|
||||
// The Kubernetes adapter emits `warning` (never `degraded`) for
|
||||
// unhealthy-but-alive rows; the Degraded chip must match them.
|
||||
const warningRows: Resource[] = [
|
||||
makeResource({
|
||||
id: 'pod-crashloop',
|
||||
type: 'pod',
|
||||
status: 'warning',
|
||||
kubernetes: { namespace: 'payments', podName: 'checkout-api-abc' },
|
||||
}),
|
||||
makeResource({ id: 'pod-healthy', type: 'pod', status: 'online' }),
|
||||
];
|
||||
expect(filterKubernetesResources(warningRows, '', 'degraded').map((r) => r.id)).toEqual([
|
||||
'pod-crashloop',
|
||||
]);
|
||||
expect(filterKubernetesResources(warningRows, '', 'online').map((r) => r.id)).toEqual([
|
||||
'pod-healthy',
|
||||
]);
|
||||
expect(filterKubernetesResources(warningRows, '', 'offline')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapKubernetesIncidentSeverity', () => {
|
||||
|
||||
@@ -132,6 +132,11 @@ export function mapKubernetesPodStatus(resource: Resource): StatusIndicator {
|
||||
export function mapKubernetesNodeStatus(resource: Resource): StatusIndicator {
|
||||
const ready = resource.kubernetes?.ready;
|
||||
if (ready === false) return { variant: 'danger', label: 'NotReady' };
|
||||
// A cordoned node still reports ready=true; check unschedulable before
|
||||
// declaring the row healthy or the cordon is invisible on the page.
|
||||
if (resource.kubernetes?.unschedulable === true) {
|
||||
return { variant: 'warning', label: 'Unschedulable' };
|
||||
}
|
||||
if (ready === true) return { variant: 'success', label: 'Ready' };
|
||||
|
||||
const status = normalizeKubernetesToken(resource.status);
|
||||
@@ -438,7 +443,11 @@ export function filterKubernetesIncidents(
|
||||
export type KubernetesResourceStatusFilter = 'all' | 'online' | 'degraded' | 'offline';
|
||||
|
||||
const ONLINE_RESOURCE_STATUSES = new Set<string>(['online', 'running']);
|
||||
const DEGRADED_RESOURCE_STATUSES = new Set<string>(['degraded', 'paused']);
|
||||
// The canonical Kubernetes adapter reports every unhealthy-but-alive state
|
||||
// (cordoned node, not-ready container, pending pod, partially available
|
||||
// controller) as `warning`, so the degraded bucket must accept it or those
|
||||
// rows match none of the Healthy / Degraded / Offline filter chips.
|
||||
const DEGRADED_RESOURCE_STATUSES = new Set<string>(['degraded', 'paused', 'warning']);
|
||||
const OFFLINE_RESOURCE_STATUSES = new Set<string>(['offline', 'stopped']);
|
||||
|
||||
const mapResourceStatusToTriad = (
|
||||
|
||||
@@ -135,6 +135,7 @@ describe('useResources - Resource Filtering Logic', () => {
|
||||
running: 0,
|
||||
stopped: 0,
|
||||
degraded: 0,
|
||||
warning: 0,
|
||||
paused: 0,
|
||||
unknown: 0,
|
||||
};
|
||||
|
||||
@@ -143,6 +143,7 @@ export function useResources(storeOverride?: ResourceStoreLike): UseResourcesRet
|
||||
running: 0,
|
||||
stopped: 0,
|
||||
degraded: 0,
|
||||
warning: 0,
|
||||
paused: 0,
|
||||
unknown: 0,
|
||||
};
|
||||
|
||||
@@ -95,13 +95,16 @@ export type SourceType =
|
||||
| 'agent' // Data pushed from agent
|
||||
| 'hybrid'; // Both sources, agent preferred
|
||||
|
||||
// Resource status - operational state
|
||||
// Resource status - operational state. `warning` is what the canonical
|
||||
// Kubernetes adapter emits for unhealthy-but-alive rows (cordoned nodes,
|
||||
// not-ready containers, pending pods, partially available controllers).
|
||||
export type ResourceStatus =
|
||||
| 'online'
|
||||
| 'offline'
|
||||
| 'running'
|
||||
| 'stopped'
|
||||
| 'degraded'
|
||||
| 'warning'
|
||||
| 'paused'
|
||||
| 'unknown';
|
||||
|
||||
@@ -1108,6 +1111,7 @@ export interface ResourceKubernetesMeta {
|
||||
kernelVersion?: string;
|
||||
roles?: string[];
|
||||
ready?: boolean;
|
||||
unschedulable?: boolean;
|
||||
capacityCpuCores?: number;
|
||||
allocatableCpuCores?: number;
|
||||
capacityMemoryBytes?: number;
|
||||
|
||||
Reference in New Issue
Block a user