github.com/jfrog/jfrog-cli-core/v2@v2.52.0/pipelines/commands/status.go (about)

     1  package commands
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"github.com/gookit/color"
     7  	"github.com/jfrog/jfrog-cli-core/v2/pipelines/manager"
     8  	"github.com/jfrog/jfrog-cli-core/v2/pipelines/status"
     9  	"github.com/jfrog/jfrog-cli-core/v2/utils/config"
    10  	"github.com/jfrog/jfrog-client-go/pipelines"
    11  	"github.com/jfrog/jfrog-client-go/pipelines/services"
    12  	"github.com/jfrog/jfrog-client-go/utils"
    13  	"github.com/jfrog/jfrog-client-go/utils/log"
    14  	"golang.org/x/exp/slices"
    15  	"time"
    16  )
    17  
    18  type StatusCommand struct {
    19  	// Server details with pipelines server URl and authentication
    20  	serverDetails *config.ServerDetails
    21  	// Branch name for applying filter on pipeline statuses
    22  	branch string
    23  	// Pipeline name to apply filter on pipeline statuses
    24  	pipelineName string
    25  	// Notify used for determining for continuous monitoring of status and notifying
    26  	notify        bool
    27  	isMultiBranch bool
    28  }
    29  
    30  const (
    31  	PipelineName                      = "PipelineName :"
    32  	Branch                            = "Branch :"
    33  	Run                               = "Run :"
    34  	Duration                          = "Duration :"
    35  	StatusLabel                       = "Status :"
    36  	MaxRetries                        = 1500
    37  	MinimumIntervalRetriesInMilliSecs = 5000
    38  )
    39  
    40  func NewStatusCommand() *StatusCommand {
    41  	return &StatusCommand{}
    42  }
    43  
    44  func (sc *StatusCommand) ServerDetails() (*config.ServerDetails, error) {
    45  	return sc.serverDetails, nil
    46  }
    47  
    48  func (sc *StatusCommand) SetServerDetails(serverDetails *config.ServerDetails) *StatusCommand {
    49  	sc.serverDetails = serverDetails
    50  	return sc
    51  }
    52  
    53  func (sc *StatusCommand) CommandName() string {
    54  	return "pl_status"
    55  }
    56  
    57  func (sc *StatusCommand) SetBranch(br string) *StatusCommand {
    58  	sc.branch = br
    59  	return sc
    60  }
    61  
    62  func (sc *StatusCommand) SetPipeline(pl string) *StatusCommand {
    63  	sc.pipelineName = pl
    64  	return sc
    65  }
    66  
    67  func (sc *StatusCommand) SetNotify(nf bool) *StatusCommand {
    68  	sc.notify = nf
    69  	return sc
    70  }
    71  
    72  func (sc *StatusCommand) SetMultiBranch(multiBranch bool) *StatusCommand {
    73  	sc.isMultiBranch = multiBranch
    74  	return sc
    75  }
    76  
    77  func (sc *StatusCommand) Run() error {
    78  	// Create service manager to fetch run status
    79  	serviceManager, err := manager.CreateServiceManager(sc.serverDetails)
    80  	if err != nil {
    81  		return err
    82  	}
    83  
    84  	// Get pipeline status using branch name, pipelines name and whether it is multi-branch
    85  	matchingPipes, err := serviceManager.GetPipelineRunStatusByBranch(sc.branch, sc.pipelineName, sc.isMultiBranch)
    86  	if err != nil {
    87  		return err
    88  	}
    89  	var res string
    90  	for i := range matchingPipes.Pipelines {
    91  		pipe := matchingPipes.Pipelines[i]
    92  		// Eliminate pipelines which have not been run
    93  		if pipe.LatestRunID != 0 {
    94  			// When notification option is selected use this flow to notify
    95  			if sc.pipelineName != "" && sc.notify {
    96  				err := monitorStatusAndNotify(context.Background(), serviceManager, sc.branch, sc.pipelineName, sc.isMultiBranch)
    97  				if err != nil {
    98  					return err
    99  				}
   100  			} else {
   101  				respStatus, colorCode, duration := getPipelineStatusAndColorCode(&pipe)
   102  				res += colorCode.Sprintf("\n%s %s\n%14s %s\n%14s %d \n%14s %s \n%14s %s\n", PipelineName,
   103  					pipe.Name, Branch, pipe.PipelineSourceBranch, Run, pipe.Run.RunNumber, Duration,
   104  					duration, StatusLabel, string(respStatus))
   105  			}
   106  		}
   107  	}
   108  	log.Output(res)
   109  	return nil
   110  }
   111  
   112  // getPipelineStatusAndColorCode from received pipeline statusCode
   113  // return PipelineStatus - pipeline status string conversion from statusCode
   114  // colorCode - color to be used for text formatting
   115  // duration - duration for the pipeline in seconds
   116  func getPipelineStatusAndColorCode(pipeline *services.Pipelines) (pipelineStatus status.PipelineStatus, colorCode color.Color, duration string) {
   117  	pipelineStatus = status.GetPipelineStatus(pipeline.Run.StatusCode)
   118  	colorCode = status.GetStatusColorCode(pipelineStatus)
   119  	durationSeconds := pipeline.Run.DurationSeconds
   120  	if durationSeconds == 0 {
   121  		// Calculate the duration time by differentiating created time from the current time
   122  		durationSeconds = int(time.Now().Unix() - pipeline.Run.CreatedAt.Unix())
   123  	}
   124  
   125  	return pipelineStatus, colorCode, convertSecToDay(durationSeconds)
   126  }
   127  
   128  // ConvertSecToDay converts seconds passed as integer to Days, Hours, Minutes, Seconds
   129  // Duration in D H M S format for example 124 seconds to "0D 0H 2M 4S"
   130  func convertSecToDay(sec int) string {
   131  	log.Debug("Duration time in seconds:", sec)
   132  	day := sec / (24 * 3600)
   133  
   134  	sec %= 24 * 3600
   135  	hour := sec / 3600
   136  
   137  	sec %= 3600
   138  	minutes := sec / 60
   139  
   140  	sec %= 60
   141  	seconds := sec
   142  
   143  	return fmt.Sprintf("%dD %dH %dM %dS", day, hour, minutes, seconds)
   144  }
   145  
   146  // monitorStatusAndNotify monitors for status change and
   147  // sends notification if there is a change identified in the pipeline run status
   148  func monitorStatusAndNotify(ctx context.Context, pipelinesMgr *pipelines.PipelinesServicesManager, branch string, pipName string, isMultiBranch bool) error {
   149  	var previousStatus string
   150  
   151  	retryExecutor := utils.RetryExecutor{
   152  		Context:                  ctx,
   153  		MaxRetries:               MaxRetries,
   154  		RetriesIntervalMilliSecs: MinimumIntervalRetriesInMilliSecs,
   155  		ExecutionHandler: func() (shouldRetry bool, err error) {
   156  			pipelineStatus, err := pipelinesMgr.GetPipelineRunStatusByBranch(branch, pipName, isMultiBranch)
   157  			if err != nil {
   158  				// Pipelines is expected to be available. Any error is not expected and no need to retry.
   159  				return false, err
   160  			}
   161  			pipeline := pipelineStatus.Pipelines[0]
   162  			currentStatus, colorCode, duration := getPipelineStatusAndColorCode(&pipeline)
   163  			if pipelineStatusChanged(string(currentStatus), previousStatus) {
   164  				changedPipelineStatus := colorCode.Sprintf("\n%s %s\n%14s %s\n%14s %d \n%14s %s \n%14s %s\n", PipelineName,
   165  					pipeline.Name, Branch, pipeline.PipelineSourceBranch, Run, pipeline.Run.RunNumber, Duration,
   166  					duration, StatusLabel, string(currentStatus))
   167  				log.Output(changedPipelineStatus)
   168  				if pipelineRunEnded(string(currentStatus)) {
   169  					return false, nil
   170  				}
   171  			}
   172  			previousStatus = string(currentStatus)
   173  			// Should retry even though successful since retry mechanism is trying to fetch pipeline status continuously
   174  			return true, nil
   175  		},
   176  	}
   177  
   178  	return retryExecutor.Execute()
   179  }
   180  
   181  // pipelineStatusChanged returns true if the current pipeline status is different from the previous one.
   182  // Return false otherwise.
   183  func pipelineStatusChanged(currentStatus, previousState string) bool {
   184  	log.Debug("Previous status: %s current status: %s", previousState, currentStatus)
   185  	return previousState != currentStatus
   186  }
   187  
   188  // pipelineRunEnded if pipeline status is one of
   189  // CANCELLED, FAILED, SUCCESS, ERROR, TIMEOUT pipeline run
   190  // life is considered to be done.
   191  func pipelineRunEnded(pipStatus string) bool {
   192  	pipRunEndLife := []string{string(status.SUCCESS), string(status.FAILURE), string(status.ERROR), string(status.CANCELLED), string(status.TIMEOUT)}
   193  	return slices.Contains(pipRunEndLife, pipStatus)
   194  }