Skip to content

Commit dfc5efd

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 c217985 commit dfc5efd

File tree

4 files changed

+130
-9
lines changed

4 files changed

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

src/explorer.ts

Lines changed: 130 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,140 @@ 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 === undefined) {
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+
const messages: string[] = [];
314+
315+
// Search for 'CrashLoopBackOff' Pod containers' state
316+
const podsInCLBO = pods.filter((pod) => pod.status?.containerStatuses?.state?.waiting?.reason === 'CrashLoopBackOff');
317+
318+
// Search for Pod continers' errors
319+
pods.forEach((pod) => {
320+
pod.status?.containerStatuses &&
321+
pod.status.containerStatuses.forEach((cs) => {
322+
if (cs.state?.waiting) {
323+
const reason = cs.state.waiting.reason;
324+
const message = cs.state.waiting.message;
325+
const msg = `${reason}: ${message ? message.trim(): 'No any valuable message'}`;
326+
// Skip duplicates and show not more than 10 errors
327+
if (messages.length <= 10 && !(messages.find((m) => m === msg))) {
328+
messages.push(msg);
329+
}
330+
}
331+
});
332+
});
333+
334+
return {
335+
inCrashLoopBackOff: (podsInCLBO.length > 0),
336+
messages
337+
}
338+
}
339+
340+
private collectDeploymentErrors(deployment): string[] {
341+
const messages: string[] = [];
342+
deployment.status.conditions.filter((c) => c.status === 'False')
343+
.forEach((c) => {
344+
const message = `${c.reason}: ${c.message ? c.message.trim(): 'No any valuable message'}`;
345+
346+
// Skip duplicates and show not more than 10 errors
347+
if (messages.length <= 10 && !(messages.find((m) => m === message))) {
348+
messages.push(message);
349+
}
350+
});
351+
352+
return messages;
353+
}
354+
355+
private async getDeploymentItem(element): Promise<TreeItem> {
356+
const shouldHaveReplicas = element.spec.replicas > 0;
357+
const availableReplicas = element.status.availableReplicas ? element.status.availableReplicas :
358+
element.spec.replicas ? element.spec.replicas : 0;
359+
const replicas = element.status.replicas ? element.status.replicas : 0;
360+
let pods: DeploymentPodObject[] = [];
361+
if (shouldHaveReplicas) {
362+
try {
363+
pods = await this.getPods(element);
364+
} catch {
365+
// ignore
366+
}
367+
}
368+
369+
// Look into Pod containers' states for any 'CrashLoopBackOff` status
370+
const podErrors = this.detectCrashLoopBackOff(pods);
371+
let clboMessages = '';
372+
podErrors.messages.forEach((m) => clboMessages = clboMessages.concat(`\n\t${m}`));
373+
374+
// We get Deployment's 'CrashLoopBackOff` status and error messages
375+
const deploymentErrors = this.collectDeploymentErrors(element);
376+
377+
let errorMessages = '';
378+
deploymentErrors.forEach((m) => errorMessages = errorMessages.concat(`\n\t${m}`));
379+
380+
// const inCrashLoopBackOff = element.status.conditions.find((condition) => condition.status === 'False' && condition.reason === 'CrashLoopBackOff');
381+
let description = `${this.makeCaps(element.kind)}`;
382+
let tooltip = description;
383+
if (element.kind === 'Deployment') {
384+
description = `${description} (${replicas}/${availableReplicas})`
385+
tooltip = `${tooltip}: ${element.metadata.name}\n`.concat(
386+
`Available replicas: ${availableReplicas}\n`,
387+
`Replicas: ${replicas}\n`,
388+
`CrashLoopBackOff detected: ${podErrors.inCrashLoopBackOff ? `Yes` : 'No'}\n`,
389+
clboMessages.length > 0 ? `\nPod Container Failures:${clboMessages}\n` : '',
390+
errorMessages.length > 0 ? `\nDeployment Failures:${errorMessages}\n` : ''
391+
);
392+
}
393+
const iconSuffix = !shouldHaveReplicas ? '' : this.getDeploymentIconSuffix(pods);
394+
const iconPath = element.kind === 'Deployment' || element.kind === 'DeploymentConfig' ?
395+
path.resolve(__dirname, `../../images/context/component-node${iconSuffix}.png`)
396+
: undefined;
397+
398+
const routeURL = await Oc.Instance.getRouteURL(element.metadata.name);
399+
return {
275400
contextValue: `openshift.k8sObject.${element.kind}${routeURL ? '.route' : ''}`,
276401
label: element.metadata.name,
277-
description: `${element.kind.substring(0, 1).toLocaleUpperCase()}${element.kind.substring(1)}`,
402+
description,
403+
tooltip,
278404
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,
405+
iconPath,
280406
command: {
281407
title: 'Load',
282408
command: 'openshift.resource.load',
283409
arguments: [element]
284410
}
285411
};
286-
287-
}
288-
return {
289-
label: 'Unknown element'
290-
}
291412
}
292413

293414
private makeCaps(kind: string): string {

0 commit comments

Comments
 (0)