Skip to content

Commit 925d052

Browse files
committed
Display status information for Deployments in Application Explorer tree #3823
Fixes: #3823 Signed-off-by: Victor Rubezhny <vrubezhny@redhat.com>
1 parent c81a62f commit 925d052

File tree

4 files changed

+139
-9
lines changed

4 files changed

+139
-9
lines changed
12.5 KB
Loading
13 KB
Loading
12.5 KB
Loading

src/explorer.ts

Lines changed: 139 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ export interface DeploymentPodObject extends KubernetesObject {
5757
},
5858
}
5959

60+
export interface DeploymentPodErrors {
61+
inCrashLoopBackOff: boolean,
62+
messages: string[]
63+
}
64+
6065
type PackageJSON = {
6166
version: string;
6267
bugs: string;
@@ -270,24 +275,149 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
270275
collapsibleState: TreeItemCollapsibleState.Collapsed
271276
}
272277
}
273-
const routeURL = await Oc.Instance.getRouteURL(element.metadata.name);
274-
return {
278+
return this.getDeploymentItem(element);
279+
}
280+
return {
281+
label: 'Unknown element'
282+
}
283+
}
284+
285+
private getDeploymentIconSuffix(pods: DeploymentPodObject[]): string {
286+
// Find all not 'Running' pods
287+
const notRunning = pods.filter((pod) => pod.status && pod.status.phase !== 'Running');
288+
if (notRunning.length === 0) {
289+
return '-green'; // All running - return 'green'
290+
}
291+
// Find any 'Failed' or 'Unknown' pod - if any return error ('red')
292+
const failed = notRunning.find((pod) => pod.status &&
293+
(pod.status.phase === 'Failed' || pod.status.phase === 'Unknown'));
294+
if (failed) {
295+
return '-red'; // At least one failed or unknown - return 'red'
296+
}
297+
// Find any 'Pending' pod - if any return pending ('yellow')
298+
const pending = notRunning.find((pod) => pod.status && pod.status.phase === 'Pending');
299+
return pending ? '-yellow' : '';
300+
}
301+
302+
/*
303+
* Search for 'CrashLoopBackOff` in Pod's opbect:
304+
* status:
305+
* containerStatuses:
306+
* state:
307+
* waiting:
308+
* message: |
309+
* container create failed: time="2024-05-28T10:30:28Z" level=error msg="runc create failed: unable to start container process: exec: \"asdf\": executable file not found in $PATH"
310+
* reason: CreateContainerError
311+
*/
312+
private detectCrashLoopBackOff(pods): DeploymentPodErrors {
313+
let inCrashLoopBackOff = false;
314+
const messages: string[] = [];
315+
316+
// Search for Pod continers' errors
317+
pods.forEach((pod) => {
318+
pod.status?.containerStatuses &&
319+
pod.status.containerStatuses.forEach((cs) => {
320+
if (cs.state?.waiting) {
321+
const reason = cs.state.waiting.reason;
322+
const message = cs.state.waiting.message;
323+
324+
inCrashLoopBackOff = inCrashLoopBackOff || reason === 'CrashLoopBackOff';
325+
326+
const msg = `${reason}: ${message ? message.trim(): 'No valuable message'}`;
327+
// Skip duplicates and show not more than 10 errors
328+
if (messages.length <= 10 && !(messages.find((m) => m === msg))) {
329+
messages.push(msg);
330+
}
331+
}
332+
});
333+
});
334+
335+
return {
336+
inCrashLoopBackOff,
337+
messages
338+
}
339+
}
340+
341+
private collectDeploymentErrors(deployment): string[] {
342+
const messages: string[] = [];
343+
deployment.status.conditions.filter((c) => c.status === 'False')
344+
.forEach((c) => {
345+
const message = `${c.reason}: ${c.message ? c.message.trim(): 'No valuable message'}`;
346+
347+
// Skip duplicates and show not more than 10 errors
348+
if (messages.length <= 10 && !(messages.find((m) => m === message))) {
349+
messages.push(message);
350+
}
351+
});
352+
353+
return messages;
354+
}
355+
356+
private async getDeploymentItem(element): Promise<TreeItem> {
357+
const shouldHaveReplicas = element.spec.replicas > 0;
358+
const desiredReplicas = element.spec.replicas ? element.spec.replicas : 0;
359+
const actualReplicas = element.status.replicas ? element.status.replicas : 0;
360+
const readyReplicas = element.status.readyReplicas ? element.status.readyReplicas : 0;
361+
const availableReplicas = element.status.availableReplicas ? element.status.availableReplicas : 0;
362+
const unavailableReplicas = element.status.unavailableReplicas ? element.status.unavailableReplicas : 0;
363+
364+
let pods: DeploymentPodObject[] = [];
365+
if (shouldHaveReplicas) {
366+
try {
367+
pods = await this.getPods(element);
368+
} catch {
369+
// ignore
370+
}
371+
}
372+
373+
// Look into Pod containers' states for any 'CrashLoopBackOff` status
374+
const podErrors = this.detectCrashLoopBackOff(pods);
375+
let podsMessages = '';
376+
podErrors.messages.forEach((m) => podsMessages = podsMessages.concat(`\n\t${m}`));
377+
378+
// We get Deployment's 'CrashLoopBackOff` status and error messages
379+
const deploymentErrors = this.collectDeploymentErrors(element);
380+
381+
let errorMessages = '';
382+
deploymentErrors.forEach((m) => errorMessages = errorMessages.concat(`\n\t${m}`));
383+
384+
// const inCrashLoopBackOff = element.status.conditions.find((condition) => condition.status === 'False' && condition.reason === 'CrashLoopBackOff');
385+
let description = `${this.makeCaps(element.kind)}`;
386+
let tooltip = description;
387+
if (element.kind === 'Deployment') {
388+
description = `${description} (${availableReplicas}/${desiredReplicas})`
389+
tooltip = `${tooltip}: ${element.metadata.name}\n`.concat(
390+
`Desired Replicas: ${desiredReplicas}\n`,
391+
`Actual Replicas: ${actualReplicas}\n`,
392+
'Of wich:\n',
393+
`\tReady Replicas: ${readyReplicas}\n`,
394+
`\tAvailable Replicas: ${availableReplicas}\n`,
395+
`\tUnavailable Replicas: ${unavailableReplicas}\n`,
396+
`---\nCrashLoopBackOff detected: ${podErrors.inCrashLoopBackOff ? 'Yes' : 'No'}\n`,
397+
podsMessages.length > 0 ? `---\nPod Container Failures:${podsMessages}\n` : '',
398+
errorMessages.length > 0 ? `---\nDeployment Failures:${errorMessages}\n` : ''
399+
);
400+
}
401+
const iconSuffix = !shouldHaveReplicas ? '' :
402+
podErrors.inCrashLoopBackOff ? '-red' : this.getDeploymentIconSuffix(pods);
403+
const iconPath = element.kind === 'Deployment' || element.kind === 'DeploymentConfig' ?
404+
path.resolve(__dirname, `../../images/context/component-node${iconSuffix}.png`)
405+
: undefined;
406+
407+
const routeURL = await Oc.Instance.getRouteURL(element.metadata.name);
408+
return {
275409
contextValue: `openshift.k8sObject.${element.kind}${routeURL ? '.route' : ''}`,
276410
label: element.metadata.name,
277-
description: `${element.kind.substring(0, 1).toLocaleUpperCase()}${element.kind.substring(1)}`,
411+
description,
412+
tooltip,
278413
collapsibleState: element.kind === 'Deployment' ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None,
279-
iconPath: element.kind === 'Deployment' || element.kind === 'DeploymentConfig' ? path.resolve(__dirname, '../../images/context/component-node.png') : undefined,
414+
iconPath,
280415
command: {
281416
title: 'Load',
282417
command: 'openshift.resource.load',
283418
arguments: [element]
284419
}
285420
};
286-
287-
}
288-
return {
289-
label: 'Unknown element'
290-
}
291421
}
292422

293423
private makeCaps(kind: string): string {

0 commit comments

Comments
 (0)