github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/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/cycloidio/terraform/backend" 11 "github.com/cycloidio/terraform/plans" 12 "github.com/cycloidio/terraform/terraform" 13 "github.com/cycloidio/terraform/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 { 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 { 121 // If we don't need to ask for confirmation, insert a blank 122 // line to separate the ouputs. 123 if b.CLI != nil { 124 b.CLI.Output("") 125 } 126 } 127 128 if !op.AutoApprove && err != errRunApproved { 129 if err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{}); err != nil { 130 return r, generalError("Failed to approve the apply command", err) 131 } 132 } 133 134 r, err = b.waitForRun(stopCtx, cancelCtx, op, "apply", r, w) 135 if err != nil { 136 return r, err 137 } 138 139 logs, err := b.client.Applies.Logs(stopCtx, r.Apply.ID) 140 if err != nil { 141 return r, generalError("Failed to retrieve logs", err) 142 } 143 reader := bufio.NewReaderSize(logs, 64*1024) 144 145 if b.CLI != nil { 146 skip := 0 147 for next := true; next; { 148 var l, line []byte 149 150 for isPrefix := true; isPrefix; { 151 l, isPrefix, err = reader.ReadLine() 152 if err != nil { 153 if err != io.EOF { 154 return r, generalError("Failed to read logs", err) 155 } 156 next = false 157 } 158 line = append(line, l...) 159 } 160 161 // Skip the first 3 lines to prevent duplicate output. 162 if skip < 3 { 163 skip++ 164 continue 165 } 166 167 if next || len(line) > 0 { 168 b.CLI.Output(b.Colorize().Color(string(line))) 169 } 170 } 171 } 172 173 return r, nil 174 } 175 176 const applyDefaultHeader = ` 177 [reset][yellow]Running apply in Terraform Cloud. Output will stream here. Pressing Ctrl-C 178 will cancel the remote apply if it's still pending. If the apply started it 179 will stop streaming the logs, but will not stop the apply running remotely.[reset] 180 181 Preparing the remote apply... 182 `