UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

205 lines (178 loc) 5.69 kB
import { K8s } from '@kinvolk/headlamp-plugin/lib'; export const PodResource = K8s.ResourceClasses.Pod; /** * Runtime issue extracted from a single Pod created by a Volcano Job. */ export interface JobPodIssue { /** Pod object that exposed the issue. */ pod: InstanceType<typeof PodResource>; /** Pod name shown in the UI. */ podName: string; /** Container name associated with the issue when one can be identified. */ containerName?: string; /** User-visible reason associated with the Pod issue. */ reason: string; /** Detailed message associated with the Pod issue. */ message?: string; } /** * Repeated Pod issues grouped by reason and message. */ export interface GroupedJobPodIssue { /** Pods that share the same grouped issue. */ pods: Array<{ /** Pod object that exposed the issue. */ pod: InstanceType<typeof PodResource>; /** Pod name shown in the grouped UI. */ podName: string; }>; /** Unique container names affected by the grouped issue. */ containerNames: string[]; /** Shared user-visible reason for the grouped issue. */ reason: string; /** Shared detailed message for the grouped issue. */ message?: string; } const nonActionablePodReasons = new Set([ 'Pending', 'ContainerCreating', 'PodInitializing', 'Running', 'Completed', 'Succeeded', 'Terminating', 'NotReady', 'Unknown', 'ContainersNotReady', 'Init:ContainerCreating', 'Init:PodInitializing', ]); /** * Checks whether an init-container reason only reflects startup progress. * * @param reason Pod status reason. * @returns `true` when the reason is an init progress marker like `Init:1/2`. */ function isInitProgressReason(reason: string) { return /^Init:\d+\/\d+$/.test(reason); } /** * Expands a reason into equivalent candidates that may appear on init container states. * * @param reason Pod status reason. * @returns Reason candidates used when matching container waiting or terminated states. */ function getReasonCandidates(reason: string) { if (reason.startsWith('Init:') && !isInitProgressReason(reason)) { return [reason, reason.slice('Init:'.length)]; } return [reason]; } /** * Reports whether a Pod reason should be surfaced as an actionable issue. * * @param reason Pod status reason. * @returns `true` when the reason represents a user-actionable runtime problem. */ function isActionablePodReason(reason?: string) { if (!reason) { return false; } if (nonActionablePodReasons.has(reason) || isInitProgressReason(reason)) { return false; } return true; } /** * Resolves the most specific container name and message for a Pod issue reason. * * @param pod Pod whose statuses are inspected. * @param reason User-visible Pod reason. * @param message Fallback Pod message from the detailed status helper. * @returns Container-specific issue details when a matching container state is found. */ function getIssueReasonDetails( pod: InstanceType<typeof PodResource>, reason: string, message?: string ) { const containerStatuses = [ ...(pod.status?.containerStatuses ?? []), ...(pod.status?.initContainerStatuses ?? []), ]; const reasonCandidates = getReasonCandidates(reason); const matchingContainer = containerStatuses.find(status => { return ( reasonCandidates.includes(status.state?.waiting?.reason || '') || reasonCandidates.includes(status.state?.terminated?.reason || '') ); }); if (!matchingContainer) { return { containerName: undefined, reason, message, }; } return { containerName: matchingContainer.name, reason, message: matchingContainer.state?.waiting?.message || matchingContainer.state?.terminated?.message || message, }; } /** * Extracts user-visible runtime issues from Pods created by a Volcano Job. * * @param pods Pods related to a Volcano Job. * @returns Pod issues derived from waiting and failed container states. */ export function getJobPodIssues(pods: InstanceType<typeof PodResource>[]): JobPodIssue[] { return pods.flatMap(pod => { const { reason, message } = pod.getDetailedStatus(); if (!isActionablePodReason(reason)) { return []; } const issueDetails = getIssueReasonDetails(pod, reason, message); return [ { pod, podName: pod.getName(), containerName: issueDetails.containerName, reason: issueDetails.reason, message: issueDetails.message, }, ]; }); } /** * Groups repeated Pod issues by reason and message to reduce duplicate output. * * @param podIssues Pod issues derived from Job-owned Pods. * @returns Grouped issues with Pod links and unique container names. */ export function groupJobPodIssues(podIssues: JobPodIssue[]): GroupedJobPodIssue[] { const groupedIssues = new Map<string, GroupedJobPodIssue>(); podIssues.forEach(issue => { const key = `${issue.reason} ${issue.message || ''}`; const existingIssue = groupedIssues.get(key); if (existingIssue) { if (!existingIssue.pods.some(pod => pod.podName === issue.podName)) { existingIssue.pods.push({ pod: issue.pod, podName: issue.podName }); } if (issue.containerName && !existingIssue.containerNames.includes(issue.containerName)) { existingIssue.containerNames.push(issue.containerName); } return; } groupedIssues.set(key, { pods: [{ pod: issue.pod, podName: issue.podName }], containerNames: issue.containerName ? [issue.containerName] : [], reason: issue.reason, message: issue.message, }); }); return Array.from(groupedIssues.values()); }