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 }