github.com/kevinklinger/open_terraform@v1.3.6/noninternal/cloud/backend_apply.go (about) 1 package cloud 2 3 import ( 4 "bufio" 5 "context" 6 "io" 7 "log" 8 9 tfe "github.com/hashicorp/go-tfe" 10 "github.com/kevinklinger/open_terraform/noninternal/backend" 11 "github.com/kevinklinger/open_terraform/noninternal/plans" 12 "github.com/kevinklinger/open_terraform/noninternal/terraform" 13 "github.com/kevinklinger/open_terraform/noninternal/tfdiags" 14 ) 15 16 func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) { 17 log.Printf("[INFO] cloud: starting Apply operation") 18 19 var diags tfdiags.Diagnostics 20 21 // We should remove the `CanUpdate` part of this test, but for now 22 // (to remain compatible with tfe.v2.1) we'll leave it in here. 23 if !w.Permissions.CanUpdate && !w.Permissions.CanQueueApply { 24 diags = diags.Append(tfdiags.Sourceless( 25 tfdiags.Error, 26 "Insufficient rights to apply changes", 27 "The provided credentials have insufficient rights to apply changes. In order "+ 28 "to apply changes at least write permissions on the workspace are required.", 29 )) 30 return nil, diags.Err() 31 } 32 33 if w.VCSRepo != nil { 34 diags = diags.Append(tfdiags.Sourceless( 35 tfdiags.Error, 36 "Apply not allowed for workspaces with a VCS connection", 37 "A workspace that is connected to a VCS requires the VCS-driven workflow "+ 38 "to ensure that the VCS remains the single source of truth.", 39 )) 40 return nil, diags.Err() 41 } 42 43 if b.ContextOpts != nil && b.ContextOpts.Parallelism != defaultParallelism { 44 diags = diags.Append(tfdiags.Sourceless( 45 tfdiags.Error, 46 "Custom parallelism values are currently not supported", 47 `Terraform Cloud does not support setting a custom parallelism `+ 48 `value at this time.`, 49 )) 50 } 51 52 if op.PlanFile != nil { 53 diags = diags.Append(tfdiags.Sourceless( 54 tfdiags.Error, 55 "Applying a saved plan is currently not supported", 56 `Terraform Cloud currently requires configuration to be present and `+ 57 `does not accept an existing saved plan as an argument at this time.`, 58 )) 59 } 60 61 if !op.HasConfig() && op.PlanMode != plans.DestroyMode { 62 diags = diags.Append(tfdiags.Sourceless( 63 tfdiags.Error, 64 "No configuration files found", 65 `Apply requires configuration to be present. Applying without a configuration `+ 66 `would mark everything for destruction, which is normally not what is desired. `+ 67 `If you would like to destroy everything, please run 'terraform destroy' which `+ 68 `does not require any configuration files.`, 69 )) 70 } 71 72 // Return if there are any errors. 73 if diags.HasErrors() { 74 return nil, diags.Err() 75 } 76 77 // Run the plan phase. 78 r, err := b.plan(stopCtx, cancelCtx, op, w) 79 if err != nil { 80 return r, err 81 } 82 83 // This check is also performed in the plan method to determine if 84 // the policies should be checked, but we need to check the values 85 // here again to determine if we are done and should return. 86 if !r.HasChanges || r.Status == tfe.RunCanceled || r.Status == tfe.RunErrored { 87 return r, nil 88 } 89 90 // Retrieve the run to get its current status. 91 r, err = b.client.Runs.Read(stopCtx, r.ID) 92 if err != nil { 93 return r, generalError("Failed to retrieve run", err) 94 } 95 96 // Return if the run cannot be confirmed. 97 if !op.AutoApprove && !r.Actions.IsConfirmable { 98 return r, nil 99 } 100 101 mustConfirm := (op.UIIn != nil && op.UIOut != nil) && !op.AutoApprove 102 103 if mustConfirm && b.input { 104 opts := &terraform.InputOpts{Id: "approve"} 105 106 if op.PlanMode == plans.DestroyMode { 107 opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?" 108 opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" + 109 "There is no undo. Only 'yes' will be accepted to confirm." 110 } else { 111 opts.Query = "\nDo you want to perform these actions in workspace \"" + op.Workspace + "\"?" 112 opts.Description = "Terraform will perform the actions described above.\n" + 113 "Only 'yes' will be accepted to approve." 114 } 115 116 err = b.confirm(stopCtx, op, opts, r, "yes") 117 if err != nil && err != errRunApproved { 118 return r, err 119 } 120 } else if mustConfirm && !b.input { 121 return r, errApplyNeedsUIConfirmation 122 } else { 123 // If we don't need to ask for confirmation, insert a blank 124 // line to separate the ouputs. 125 if b.CLI != nil { 126 b.CLI.Output("") 127 } 128 } 129 130 if !op.AutoApprove && err != errRunApproved { 131 if err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{}); err != nil { 132 return r, generalError("Failed to approve the apply command", err) 133 } 134 } 135 136 r, err = b.waitForRun(stopCtx, cancelCtx, op, "apply", r, w) 137 if err != nil { 138 return r, err 139 } 140 141 logs, err := b.client.Applies.Logs(stopCtx, r.Apply.ID) 142 if err != nil { 143 return r, generalError("Failed to retrieve logs", err) 144 } 145 reader := bufio.NewReaderSize(logs, 64*1024) 146 147 if b.CLI != nil { 148 skip := 0 149 for next := true; next; { 150 var l, line []byte 151 152 for isPrefix := true; isPrefix; { 153 l, isPrefix, err = reader.ReadLine() 154 if err != nil { 155 if err != io.EOF { 156 return r, generalError("Failed to read logs", err) 157 } 158 next = false 159 } 160 line = append(line, l...) 161 } 162 163 // Skip the first 3 lines to prevent duplicate output. 164 if skip < 3 { 165 skip++ 166 continue 167 } 168 169 if next || len(line) > 0 { 170 b.CLI.Output(b.Colorize().Color(string(line))) 171 } 172 } 173 } 174 175 return r, nil 176 } 177 178 const applyDefaultHeader = ` 179 [reset][yellow]Running apply in Terraform Cloud. Output will stream here. Pressing Ctrl-C 180 will cancel the remote apply if it's still pending. If the apply started it 181 will stop streaming the logs, but will not stop the apply running remotely.[reset] 182 183 Preparing the remote apply... 184 `