@@ -57,6 +57,11 @@ export interface DeploymentPodObject extends KubernetesObject {
5757 } ,
5858}
5959
60+ export interface DeploymentPodErrors {
61+ inCrashLoopBackOff : boolean ,
62+ messages : string [ ]
63+ }
64+
6065type 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