github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/remote/backend_apply.go (about) 1 package remote 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "io" 8 "log" 9 10 tfe "github.com/hashicorp/go-tfe" 11 version "github.com/hashicorp/go-version" 12 "github.com/hashicorp/terraform/backend" 13 "github.com/hashicorp/terraform/terraform" 14 "github.com/hashicorp/terraform/tfdiags" 15 ) 16 17 func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) { 18 log.Printf("[INFO] backend/remote: starting Apply operation") 19 20 var diags tfdiags.Diagnostics 21 22 // We should remove the `CanUpdate` part of this test, but for now 23 // (to remain compatible with tfe.v2.1) we'll leave it in here. 24 if !w.Permissions.CanUpdate && !w.Permissions.CanQueueApply { 25 diags = diags.Append(tfdiags.Sourceless( 26 tfdiags.Error, 27 "Insufficient rights to apply changes", 28 "The provided credentials have insufficient rights to apply changes. In order "+ 29 "to apply changes at least write permissions on the workspace are required.", 30 )) 31 return nil, diags.Err() 32 } 33 34 if w.VCSRepo != nil { 35 diags = diags.Append(tfdiags.Sourceless( 36 tfdiags.Error, 37 "Apply not allowed for workspaces with a VCS connection", 38 "A workspace that is connected to a VCS requires the VCS-driven workflow "+ 39 "to ensure that the VCS remains the single source of truth.", 40 )) 41 return nil, diags.Err() 42 } 43 44 if op.Parallelism != defaultParallelism { 45 diags = diags.Append(tfdiags.Sourceless( 46 tfdiags.Error, 47 "Custom parallelism values are currently not supported", 48 `The "remote" backend does not support setting a custom parallelism `+ 49 `value at this time.`, 50 )) 51 } 52 53 if op.PlanFile != nil { 54 diags = diags.Append(tfdiags.Sourceless( 55 tfdiags.Error, 56 "Applying a saved plan is currently not supported", 57 `The "remote" backend currently requires configuration to be present and `+ 58 `does not accept an existing saved plan as an argument at this time.`, 59 )) 60 } 61 62 if !op.PlanRefresh { 63 diags = diags.Append(tfdiags.Sourceless( 64 tfdiags.Error, 65 "Applying without refresh is currently not supported", 66 `Currently the "remote" backend will always do an in-memory refresh of `+ 67 `the Terraform state prior to generating the plan.`, 68 )) 69 } 70 71 if b.hasExplicitVariableValues(op) { 72 diags = diags.Append(tfdiags.Sourceless( 73 tfdiags.Error, 74 "Run variables are currently not supported", 75 fmt.Sprintf( 76 "The \"remote\" backend does not support setting run variables at this time. "+ 77 "Currently the only to way to pass variables to the remote backend is by "+ 78 "creating a '*.auto.tfvars' variables file. This file will automatically "+ 79 "be loaded by the \"remote\" backend when the workspace is configured to use "+ 80 "Terraform v0.10.0 or later.\n\nAdditionally you can also set variables on "+ 81 "the workspace in the web UI:\nhttps://%s/app/%s/%s/variables", 82 b.hostname, b.organization, op.Workspace, 83 ), 84 )) 85 } 86 87 if !op.HasConfig() && !op.Destroy { 88 diags = diags.Append(tfdiags.Sourceless( 89 tfdiags.Error, 90 "No configuration files found", 91 `Apply requires configuration to be present. Applying without a configuration `+ 92 `would mark everything for destruction, which is normally not what is desired. `+ 93 `If you would like to destroy everything, please run 'terraform destroy' which `+ 94 `does not require any configuration files.`, 95 )) 96 } 97 98 if len(op.Targets) != 0 { 99 // For API versions prior to 2.3, RemoteAPIVersion will return an empty string, 100 // so if there's an error when parsing the RemoteAPIVersion, it's handled as 101 // equivalent to an API version < 2.3. 102 currentAPIVersion, parseErr := version.NewVersion(b.client.RemoteAPIVersion()) 103 desiredAPIVersion, _ := version.NewVersion("2.3") 104 105 if parseErr != nil || currentAPIVersion.LessThan(desiredAPIVersion) { 106 diags = diags.Append(tfdiags.Sourceless( 107 tfdiags.Error, 108 "Resource targeting is not supported", 109 fmt.Sprintf( 110 `The host %s does not support the -target option for `+ 111 `remote plans.`, 112 b.hostname, 113 ), 114 )) 115 } 116 } 117 118 // Return if there are any errors. 119 if diags.HasErrors() { 120 return nil, diags.Err() 121 } 122 123 // Run the plan phase. 124 r, err := b.plan(stopCtx, cancelCtx, op, w) 125 if err != nil { 126 return r, err 127 } 128 129 // This check is also performed in the plan method to determine if 130 // the policies should be checked, but we need to check the values 131 // here again to determine if we are done and should return. 132 if !r.HasChanges || r.Status == tfe.RunCanceled || r.Status == tfe.RunErrored { 133 return r, nil 134 } 135 136 // Retrieve the run to get its current status. 137 r, err = b.client.Runs.Read(stopCtx, r.ID) 138 if err != nil { 139 return r, generalError("Failed to retrieve run", err) 140 } 141 142 // Return if the run cannot be confirmed. 143 if !w.AutoApply && !r.Actions.IsConfirmable { 144 return r, nil 145 } 146 147 // Since we already checked the permissions before creating the run 148 // this should never happen. But it doesn't hurt to keep this in as 149 // a safeguard for any unexpected situations. 150 if !w.AutoApply && !r.Permissions.CanApply { 151 // Make sure we discard the run if possible. 152 if r.Actions.IsDiscardable { 153 err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{}) 154 if err != nil { 155 if op.Destroy { 156 return r, generalError("Failed to discard destroy", err) 157 } 158 return r, generalError("Failed to discard apply", err) 159 } 160 } 161 diags = diags.Append(tfdiags.Sourceless( 162 tfdiags.Error, 163 "Insufficient rights to approve the pending changes", 164 fmt.Sprintf("There are pending changes, but the provided credentials have "+ 165 "insufficient rights to approve them. The run will be discarded to prevent "+ 166 "it from blocking the queue waiting for external approval. To queue a run "+ 167 "that can be approved by someone else, please use the 'Queue Plan' button in "+ 168 "the web UI:\nhttps://%s/app/%s/%s/runs", b.hostname, b.organization, op.Workspace), 169 )) 170 return r, diags.Err() 171 } 172 173 mustConfirm := (op.UIIn != nil && op.UIOut != nil) && 174 ((op.Destroy && (!op.DestroyForce && !op.AutoApprove)) || (!op.Destroy && !op.AutoApprove)) 175 176 if !w.AutoApply { 177 if mustConfirm { 178 opts := &terraform.InputOpts{Id: "approve"} 179 180 if op.Destroy { 181 opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?" 182 opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" + 183 "There is no undo. Only 'yes' will be accepted to confirm." 184 } else { 185 opts.Query = "\nDo you want to perform these actions in workspace \"" + op.Workspace + "\"?" 186 opts.Description = "Terraform will perform the actions described above.\n" + 187 "Only 'yes' will be accepted to approve." 188 } 189 190 err = b.confirm(stopCtx, op, opts, r, "yes") 191 if err != nil && err != errRunApproved { 192 return r, err 193 } 194 } 195 196 if err != errRunApproved { 197 if err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{}); err != nil { 198 return r, generalError("Failed to approve the apply command", err) 199 } 200 } 201 } 202 203 // If we don't need to ask for confirmation, insert a blank 204 // line to separate the ouputs. 205 if w.AutoApply || !mustConfirm { 206 if b.CLI != nil { 207 b.CLI.Output("") 208 } 209 } 210 211 r, err = b.waitForRun(stopCtx, cancelCtx, op, "apply", r, w) 212 if err != nil { 213 return r, err 214 } 215 216 logs, err := b.client.Applies.Logs(stopCtx, r.Apply.ID) 217 if err != nil { 218 return r, generalError("Failed to retrieve logs", err) 219 } 220 reader := bufio.NewReaderSize(logs, 64*1024) 221 222 if b.CLI != nil { 223 skip := 0 224 for next := true; next; { 225 var l, line []byte 226 227 for isPrefix := true; isPrefix; { 228 l, isPrefix, err = reader.ReadLine() 229 if err != nil { 230 if err != io.EOF { 231 return r, generalError("Failed to read logs", err) 232 } 233 next = false 234 } 235 line = append(line, l...) 236 } 237 238 // Skip the first 3 lines to prevent duplicate output. 239 if skip < 3 { 240 skip++ 241 continue 242 } 243 244 if next || len(line) > 0 { 245 b.CLI.Output(b.Colorize().Color(string(line))) 246 } 247 } 248 } 249 250 return r, nil 251 } 252 253 const applyDefaultHeader = ` 254 [reset][yellow]Running apply in the remote backend. Output will stream here. Pressing Ctrl-C 255 will cancel the remote apply if it's still pending. If the apply started it 256 will stop streaming the logs, but will not stop the apply running remotely.[reset] 257 258 Preparing the remote apply... 259 `