github.com/opentofu/opentofu@v1.7.1/internal/cloud/backend_taskStages.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  	"context"
    10  	"fmt"
    11  	"strings"
    12  
    13  	"github.com/hashicorp/go-multierror"
    14  	tfe "github.com/hashicorp/go-tfe"
    15  	"github.com/opentofu/opentofu/internal/tofu"
    16  )
    17  
    18  type taskStages map[tfe.Stage]*tfe.TaskStage
    19  
    20  const (
    21  	taskStageBackoffMin = 4000.0
    22  	taskStageBackoffMax = 12000.0
    23  )
    24  
    25  const taskStageHeader = `
    26  To view this run in a browser, visit:
    27  https://%s/app/%s/%s/runs/%s
    28  `
    29  
    30  type taskStageSummarizer interface {
    31  	// Summarize takes an IntegrationContext, IntegrationOutputWriter for
    32  	// writing output and a pointer to a tfe.TaskStage object as arguments.
    33  	// This function summarizes and outputs the results of the task stage.
    34  	// It returns a boolean which signifies whether we should continue polling
    35  	// for results, an optional message string to print while it is polling
    36  	// and an error if any.
    37  	Summarize(*IntegrationContext, IntegrationOutputWriter, *tfe.TaskStage) (bool, *string, error)
    38  }
    39  
    40  func (b *Cloud) runTaskStages(ctx context.Context, client *tfe.Client, runId string) (taskStages, error) {
    41  	taskStages := make(taskStages, 0)
    42  	result, err := client.Runs.ReadWithOptions(ctx, runId, &tfe.RunReadOptions{
    43  		Include: []tfe.RunIncludeOpt{tfe.RunTaskStages},
    44  	})
    45  	if err == nil {
    46  		for _, t := range result.TaskStages {
    47  			if t != nil {
    48  				taskStages[t.Stage] = t
    49  			}
    50  		}
    51  	} else {
    52  		// This error would be expected for older versions of TFE that do not allow
    53  		// fetching task_stages.
    54  		if !strings.HasSuffix(err.Error(), "Invalid include parameter") {
    55  			return taskStages, generalError("Failed to retrieve run", err)
    56  		}
    57  	}
    58  
    59  	return taskStages, nil
    60  }
    61  
    62  func (b *Cloud) getTaskStageWithAllOptions(ctx *IntegrationContext, stageID string) (*tfe.TaskStage, error) {
    63  	options := tfe.TaskStageReadOptions{
    64  		Include: []tfe.TaskStageIncludeOpt{tfe.TaskStageTaskResults, tfe.PolicyEvaluationsTaskResults},
    65  	}
    66  	stage, err := b.client.TaskStages.Read(ctx.StopContext, stageID, &options)
    67  	if err != nil {
    68  		return nil, generalError("Failed to retrieve task stage", err)
    69  	} else {
    70  		return stage, nil
    71  	}
    72  }
    73  
    74  func (b *Cloud) runTaskStage(ctx *IntegrationContext, output IntegrationOutputWriter, stageID string) error {
    75  	var errs *multierror.Error
    76  
    77  	// Create our summarizers
    78  	summarizers := make([]taskStageSummarizer, 0)
    79  	ts, err := b.getTaskStageWithAllOptions(ctx, stageID)
    80  	if err != nil {
    81  		return err
    82  	}
    83  
    84  	if s := newTaskResultSummarizer(b, ts); s != nil {
    85  		summarizers = append(summarizers, s)
    86  	}
    87  
    88  	if s := newPolicyEvaluationSummarizer(b, ts); s != nil {
    89  		summarizers = append(summarizers, s)
    90  	}
    91  
    92  	return ctx.Poll(taskStageBackoffMin, taskStageBackoffMax, func(i int) (bool, error) {
    93  		options := tfe.TaskStageReadOptions{
    94  			Include: []tfe.TaskStageIncludeOpt{tfe.TaskStageTaskResults, tfe.PolicyEvaluationsTaskResults},
    95  		}
    96  		stage, err := b.client.TaskStages.Read(ctx.StopContext, stageID, &options)
    97  		if err != nil {
    98  			return false, generalError("Failed to retrieve task stage", err)
    99  		}
   100  
   101  		switch stage.Status {
   102  		case tfe.TaskStagePending:
   103  			// Waiting for it to start
   104  			return true, nil
   105  		case tfe.TaskStageRunning:
   106  			if _, e := processSummarizers(ctx, output, stage, summarizers, errs); e != nil {
   107  				errs = e
   108  			}
   109  			// not a terminal status so we continue to poll
   110  			return true, nil
   111  		// Note: Terminal statuses need to print out one last time just in case
   112  		case tfe.TaskStagePassed:
   113  			ok, e := processSummarizers(ctx, output, stage, summarizers, errs)
   114  			if e != nil {
   115  				errs = e
   116  			}
   117  			if ok {
   118  				return true, nil
   119  			}
   120  		case tfe.TaskStageCanceled, tfe.TaskStageErrored, tfe.TaskStageFailed:
   121  			ok, e := processSummarizers(ctx, output, stage, summarizers, errs)
   122  			if e != nil {
   123  				errs = e
   124  			}
   125  			if ok {
   126  				return true, nil
   127  			}
   128  			return false, fmt.Errorf("Task Stage %s.", stage.Status)
   129  		case tfe.TaskStageAwaitingOverride:
   130  			ok, e := processSummarizers(ctx, output, stage, summarizers, errs)
   131  			if e != nil {
   132  				errs = e
   133  			}
   134  			if ok {
   135  				return true, nil
   136  			}
   137  			cont, err := b.processStageOverrides(ctx, output, stage.ID)
   138  			if err != nil {
   139  				errs = multierror.Append(errs, err)
   140  			} else {
   141  				return cont, nil
   142  			}
   143  		case tfe.TaskStageUnreachable:
   144  			return false, nil
   145  		default:
   146  			return false, fmt.Errorf("Invalid Task stage status: %s ", stage.Status)
   147  		}
   148  		return false, errs.ErrorOrNil()
   149  	})
   150  }
   151  
   152  func processSummarizers(ctx *IntegrationContext, output IntegrationOutputWriter, stage *tfe.TaskStage, summarizers []taskStageSummarizer, errs *multierror.Error) (bool, *multierror.Error) {
   153  	for _, s := range summarizers {
   154  		cont, msg, err := s.Summarize(ctx, output, stage)
   155  		if err != nil {
   156  			errs = multierror.Append(errs, err)
   157  			break
   158  		}
   159  
   160  		if !cont {
   161  			continue
   162  		}
   163  
   164  		// cont is true and we must continue to poll
   165  		if msg != nil {
   166  			output.OutputElapsed(*msg, len(*msg)) // Up to 2 digits are allowed by the max message allocation
   167  		}
   168  		return true, nil
   169  	}
   170  	return false, errs
   171  }
   172  
   173  func (b *Cloud) processStageOverrides(context *IntegrationContext, output IntegrationOutputWriter, taskStageID string) (bool, error) {
   174  	opts := &tofu.InputOpts{
   175  		Id:          fmt.Sprintf("%c%c [bold]Override", Arrow, Arrow),
   176  		Query:       "\nDo you want to override the failed policy check?",
   177  		Description: "Only 'override' will be accepted to override.",
   178  	}
   179  	runUrl := fmt.Sprintf(taskStageHeader, b.hostname, b.organization, context.Op.Workspace, context.Run.ID)
   180  	err := b.confirm(context.StopContext, context.Op, opts, context.Run, "override")
   181  	if err != nil && err != errRunOverridden {
   182  		return false, fmt.Errorf("Failed to override: %w\n%s\n", err, runUrl)
   183  	}
   184  
   185  	if err != errRunOverridden {
   186  		if _, err = b.client.TaskStages.Override(context.StopContext, taskStageID, tfe.TaskStageOverrideOptions{}); err != nil {
   187  			return false, generalError(fmt.Sprintf("Failed to override policy check.\n%s", runUrl), err)
   188  		} else {
   189  			return true, nil
   190  		}
   191  	} else {
   192  		output.Output(fmt.Sprintf("The run needs to be manually overridden or discarded.\n%s\n", runUrl))
   193  	}
   194  	return false, nil
   195  }