github.com/opentofu/opentofu@v1.7.1/internal/cloud/backend_taskStage_taskResults.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package cloud
     7  
     8  import (
     9  	"fmt"
    10  	"strings"
    11  
    12  	"github.com/hashicorp/go-tfe"
    13  )
    14  
    15  type taskResultSummary struct {
    16  	unreachable     bool
    17  	pending         int
    18  	failed          int
    19  	failedMandatory int
    20  	passed          int
    21  }
    22  
    23  type taskResultSummarizer struct {
    24  	finished bool
    25  	cloud    *Cloud
    26  	counter  int
    27  }
    28  
    29  func newTaskResultSummarizer(b *Cloud, ts *tfe.TaskStage) taskStageSummarizer {
    30  	if len(ts.TaskResults) == 0 {
    31  		return nil
    32  	}
    33  	return &taskResultSummarizer{
    34  		finished: false,
    35  		cloud:    b,
    36  	}
    37  }
    38  
    39  func (trs *taskResultSummarizer) Summarize(context *IntegrationContext, output IntegrationOutputWriter, ts *tfe.TaskStage) (bool, *string, error) {
    40  	if trs.finished {
    41  		return false, nil, nil
    42  	}
    43  	trs.counter++
    44  
    45  	counts := summarizeTaskResults(ts.TaskResults)
    46  
    47  	if counts.pending != 0 {
    48  		pendingMessage := "%d tasks still pending, %d passed, %d failed ... "
    49  		message := fmt.Sprintf(pendingMessage, counts.pending, counts.passed, counts.failed)
    50  		return true, &message, nil
    51  	}
    52  	if counts.unreachable {
    53  		output.Output("Skipping task results.")
    54  		output.End()
    55  		return false, nil, nil
    56  	}
    57  
    58  	// Print out the summary
    59  	trs.runTasksWithTaskResults(output, ts.TaskResults, counts)
    60  
    61  	// Mark as finished
    62  	trs.finished = true
    63  
    64  	return false, nil, nil
    65  }
    66  
    67  func summarizeTaskResults(taskResults []*tfe.TaskResult) *taskResultSummary {
    68  	var pendingCount, errCount, errMandatoryCount, passedCount int
    69  	for _, task := range taskResults {
    70  		if task.Status == tfe.TaskUnreachable {
    71  			return &taskResultSummary{
    72  				unreachable: true,
    73  			}
    74  		} else if task.Status == tfe.TaskRunning || task.Status == tfe.TaskPending {
    75  			pendingCount++
    76  		} else if task.Status == tfe.TaskPassed {
    77  			passedCount++
    78  		} else {
    79  			// Everything else is a failure
    80  			errCount++
    81  			if task.WorkspaceTaskEnforcementLevel == tfe.Mandatory {
    82  				errMandatoryCount++
    83  			}
    84  		}
    85  	}
    86  
    87  	return &taskResultSummary{
    88  		unreachable:     false,
    89  		pending:         pendingCount,
    90  		failed:          errCount,
    91  		failedMandatory: errMandatoryCount,
    92  		passed:          passedCount,
    93  	}
    94  }
    95  
    96  func (trs *taskResultSummarizer) runTasksWithTaskResults(output IntegrationOutputWriter, taskResults []*tfe.TaskResult, count *taskResultSummary) {
    97  	// Track the first task name that is a mandatory enforcement level breach.
    98  	var firstMandatoryTaskFailed *string = nil
    99  
   100  	if trs.counter == 0 {
   101  		output.Output(fmt.Sprintf("All tasks completed! %d passed, %d failed", count.passed, count.failed))
   102  	} else {
   103  		output.OutputElapsed(fmt.Sprintf("All tasks completed! %d passed, %d failed", count.passed, count.failed), 50)
   104  	}
   105  
   106  	output.Output("")
   107  
   108  	for _, t := range taskResults {
   109  		capitalizedStatus := string(t.Status)
   110  		capitalizedStatus = strings.ToUpper(capitalizedStatus[:1]) + capitalizedStatus[1:]
   111  
   112  		status := "[green]" + capitalizedStatus
   113  		if t.Status != "passed" {
   114  			level := string(t.WorkspaceTaskEnforcementLevel)
   115  			level = strings.ToUpper(level[:1]) + level[1:]
   116  			status = fmt.Sprintf("[red]%s (%s)", capitalizedStatus, level)
   117  
   118  			if t.WorkspaceTaskEnforcementLevel == "mandatory" && firstMandatoryTaskFailed == nil {
   119  				firstMandatoryTaskFailed = &t.TaskName
   120  			}
   121  		}
   122  
   123  		title := fmt.Sprintf(`%s ⸺   %s`, t.TaskName, status)
   124  		output.SubOutput(title)
   125  
   126  		if len(t.Message) > 0 {
   127  			output.SubOutput(fmt.Sprintf("[dim]%s", t.Message))
   128  		}
   129  		if len(t.URL) > 0 {
   130  			output.SubOutput(fmt.Sprintf("[dim]Details: %s", t.URL))
   131  		}
   132  		output.SubOutput("")
   133  	}
   134  
   135  	// If a mandatory enforcement level is breached, return an error.
   136  	var overall string = "[green]Passed"
   137  	if firstMandatoryTaskFailed != nil {
   138  		overall = "[red]Failed"
   139  		if count.failedMandatory > 1 {
   140  			output.Output(fmt.Sprintf("[reset][bold][red]Error:[reset][bold]the run failed because %d mandatory tasks are required to succeed", count.failedMandatory))
   141  		} else {
   142  			output.Output(fmt.Sprintf("[reset][bold][red]Error: [reset][bold]the run failed because the run task, %s, is required to succeed", *firstMandatoryTaskFailed))
   143  		}
   144  	} else if count.failed > 0 { // we have failures but none of them mandatory
   145  		overall = "[green]Passed with advisory failures"
   146  	}
   147  
   148  	output.SubOutput("")
   149  	output.SubOutput("[bold]Overall Result: " + overall)
   150  
   151  	output.End()
   152  }