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