github.com/svenhamers/terraform@v0.11.12-beta1/backend/remote/backend_plan.go (about) 1 package remote 2 3 import ( 4 "bufio" 5 "context" 6 "errors" 7 "fmt" 8 "io/ioutil" 9 "log" 10 "os" 11 "path/filepath" 12 "strings" 13 "syscall" 14 "time" 15 16 tfe "github.com/hashicorp/go-tfe" 17 "github.com/hashicorp/terraform/backend" 18 ) 19 20 func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) { 21 log.Printf("[INFO] backend/remote: starting Plan operation") 22 23 if !w.Permissions.CanQueueRun { 24 return nil, fmt.Errorf(strings.TrimSpace(fmt.Sprintf(planErrNoQueueRunRights))) 25 } 26 27 if op.ModuleDepth != defaultModuleDepth { 28 return nil, fmt.Errorf(strings.TrimSpace(planErrModuleDepthNotSupported)) 29 } 30 31 if op.Parallelism != defaultParallelism { 32 return nil, fmt.Errorf(strings.TrimSpace(planErrParallelismNotSupported)) 33 } 34 35 if op.Plan != nil { 36 return nil, fmt.Errorf(strings.TrimSpace(planErrPlanNotSupported)) 37 } 38 39 if op.PlanOutPath != "" { 40 return nil, fmt.Errorf(strings.TrimSpace(planErrOutPathNotSupported)) 41 } 42 43 if !op.PlanRefresh { 44 return nil, fmt.Errorf(strings.TrimSpace(planErrNoRefreshNotSupported)) 45 } 46 47 if op.Targets != nil { 48 return nil, fmt.Errorf(strings.TrimSpace(planErrTargetsNotSupported)) 49 } 50 51 if op.Variables != nil { 52 return nil, fmt.Errorf(strings.TrimSpace( 53 fmt.Sprintf(planErrVariablesNotSupported, b.hostname, b.organization, op.Workspace))) 54 } 55 56 if (op.Module == nil || op.Module.Config().Dir == "") && !op.Destroy { 57 return nil, fmt.Errorf(strings.TrimSpace(planErrNoConfig)) 58 } 59 60 return b.plan(stopCtx, cancelCtx, op, w) 61 } 62 63 func (b *Remote) plan(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) { 64 if b.CLI != nil { 65 header := planDefaultHeader 66 if op.Type == backend.OperationTypeApply { 67 header = applyDefaultHeader 68 } 69 b.CLI.Output(b.Colorize().Color(strings.TrimSpace(header) + "\n")) 70 } 71 72 configOptions := tfe.ConfigurationVersionCreateOptions{ 73 AutoQueueRuns: tfe.Bool(false), 74 Speculative: tfe.Bool(op.Type == backend.OperationTypePlan), 75 } 76 77 cv, err := b.client.ConfigurationVersions.Create(stopCtx, w.ID, configOptions) 78 if err != nil { 79 return nil, generalError("error creating configuration version", err) 80 } 81 82 var configDir string 83 if op.Module != nil && op.Module.Config().Dir != "" { 84 // Make sure to take the working directory into account by removing 85 // the working directory from the current path. This will result in 86 // a path that points to the expected root of the workspace. 87 configDir = filepath.Clean(strings.TrimSuffix( 88 filepath.Clean(op.Module.Config().Dir), 89 filepath.Clean(w.WorkingDirectory), 90 )) 91 } else { 92 // We did a check earlier to make sure we either have a config dir, 93 // or the plan is run with -destroy. So this else clause will only 94 // be executed when we are destroying and doesn't need the config. 95 configDir, err = ioutil.TempDir("", "tf") 96 if err != nil { 97 return nil, generalError("error creating temporary directory", err) 98 } 99 defer os.RemoveAll(configDir) 100 101 // Make sure the configured working directory exists. 102 err = os.MkdirAll(filepath.Join(configDir, w.WorkingDirectory), 0700) 103 if err != nil { 104 return nil, generalError( 105 "error creating temporary working directory", err) 106 } 107 } 108 109 err = b.client.ConfigurationVersions.Upload(stopCtx, cv.UploadURL, configDir) 110 if err != nil { 111 return nil, generalError("error uploading configuration files", err) 112 } 113 114 uploaded := false 115 for i := 0; i < 60 && !uploaded; i++ { 116 select { 117 case <-stopCtx.Done(): 118 return nil, context.Canceled 119 case <-cancelCtx.Done(): 120 return nil, context.Canceled 121 case <-time.After(500 * time.Millisecond): 122 cv, err = b.client.ConfigurationVersions.Read(stopCtx, cv.ID) 123 if err != nil { 124 return nil, generalError("error retrieving configuration version", err) 125 } 126 127 if cv.Status == tfe.ConfigurationUploaded { 128 uploaded = true 129 } 130 } 131 } 132 133 if !uploaded { 134 return nil, generalError( 135 "error uploading configuration files", errors.New("operation timed out")) 136 } 137 138 runOptions := tfe.RunCreateOptions{ 139 IsDestroy: tfe.Bool(op.Destroy), 140 Message: tfe.String("Queued manually using Terraform"), 141 ConfigurationVersion: cv, 142 Workspace: w, 143 } 144 145 r, err := b.client.Runs.Create(stopCtx, runOptions) 146 if err != nil { 147 return r, generalError("error creating run", err) 148 } 149 150 // When the lock timeout is set, 151 if op.StateLockTimeout > 0 { 152 go func() { 153 select { 154 case <-stopCtx.Done(): 155 return 156 case <-cancelCtx.Done(): 157 return 158 case <-time.After(op.StateLockTimeout): 159 // Retrieve the run to get its current status. 160 r, err := b.client.Runs.Read(cancelCtx, r.ID) 161 if err != nil { 162 log.Printf("[ERROR] error reading run: %v", err) 163 return 164 } 165 166 if r.Status == tfe.RunPending && r.Actions.IsCancelable { 167 if b.CLI != nil { 168 b.CLI.Output(b.Colorize().Color(strings.TrimSpace(lockTimeoutErr))) 169 } 170 171 // We abuse the auto aprove flag to indicate that we do not 172 // want to ask if the remote operation should be canceled. 173 op.AutoApprove = true 174 175 p, err := os.FindProcess(os.Getpid()) 176 if err != nil { 177 log.Printf("[ERROR] error searching process ID: %v", err) 178 return 179 } 180 p.Signal(syscall.SIGINT) 181 } 182 } 183 }() 184 } 185 186 if b.CLI != nil { 187 b.CLI.Output(b.Colorize().Color(strings.TrimSpace(fmt.Sprintf( 188 runHeader, b.hostname, b.organization, op.Workspace, r.ID)) + "\n")) 189 } 190 191 r, err = b.waitForRun(stopCtx, cancelCtx, op, "plan", r, w) 192 if err != nil { 193 return r, err 194 } 195 196 logs, err := b.client.Plans.Logs(stopCtx, r.Plan.ID) 197 if err != nil { 198 return r, generalError("error retrieving logs", err) 199 } 200 scanner := bufio.NewScanner(logs) 201 202 for scanner.Scan() { 203 if b.CLI != nil { 204 b.CLI.Output(b.Colorize().Color(scanner.Text())) 205 } 206 } 207 if err := scanner.Err(); err != nil { 208 return r, generalError("error reading logs", err) 209 } 210 211 // Retrieve the run to get its current status. 212 r, err = b.client.Runs.Read(stopCtx, r.ID) 213 if err != nil { 214 return r, generalError("error retrieving run", err) 215 } 216 217 // Return if the run errored. We return without an error, even 218 // if the run errored, as the error is already displayed by the 219 // output of the remote run. 220 if r.Status == tfe.RunErrored { 221 return r, nil 222 } 223 224 // Check any configured sentinel policies. 225 if len(r.PolicyChecks) > 0 { 226 err = b.checkPolicy(stopCtx, cancelCtx, op, r) 227 if err != nil { 228 return r, err 229 } 230 } 231 232 return r, nil 233 } 234 235 const planErrNoQueueRunRights = ` 236 Insufficient rights to generate a plan! 237 238 [reset][yellow]The provided credentials have insufficient rights to generate a plan. In order 239 to generate plans, at least plan permissions on the workspace are required.[reset] 240 ` 241 242 const planErrModuleDepthNotSupported = ` 243 Custom module depths are currently not supported! 244 245 The "remote" backend does not support setting a custom module 246 depth at this time. 247 ` 248 249 const planErrParallelismNotSupported = ` 250 Custom parallelism values are currently not supported! 251 252 The "remote" backend does not support setting a custom parallelism 253 value at this time. 254 ` 255 256 const planErrPlanNotSupported = ` 257 Displaying a saved plan is currently not supported! 258 259 The "remote" backend currently requires configuration to be present and 260 does not accept an existing saved plan as an argument at this time. 261 ` 262 263 const planErrOutPathNotSupported = ` 264 Saving a generated plan is currently not supported! 265 266 The "remote" backend does not support saving the generated execution 267 plan locally at this time. 268 ` 269 270 const planErrNoRefreshNotSupported = ` 271 Planning without refresh is currently not supported! 272 273 Currently the "remote" backend will always do an in-memory refresh of 274 the Terraform state prior to generating the plan. 275 ` 276 277 const planErrTargetsNotSupported = ` 278 Resource targeting is currently not supported! 279 280 The "remote" backend does not support resource targeting at this time. 281 ` 282 283 const planErrVariablesNotSupported = ` 284 Run variables are currently not supported! 285 286 The "remote" backend does not support setting run variables at this time. 287 Currently the only to way to pass variables to the remote backend is by 288 creating a '*.auto.tfvars' variables file. This file will automatically 289 be loaded by the "remote" backend when the workspace is configured to use 290 Terraform v0.10.0 or later. 291 292 Additionally you can also set variables on the workspace in the web UI: 293 https://%s/app/%s/%s/variables 294 ` 295 296 const planErrNoConfig = ` 297 No configuration files found! 298 299 Plan requires configuration to be present. Planning without a configuration 300 would mark everything for destruction, which is normally not what is desired. 301 If you would like to destroy everything, please run plan with the "-destroy" 302 flag or create a single empty configuration file. Otherwise, please create 303 a Terraform configuration file in the path being executed and try again. 304 ` 305 306 const planDefaultHeader = ` 307 [reset][yellow]Running plan in the remote backend. Output will stream here. Pressing Ctrl-C 308 will stop streaming the logs, but will not stop the plan running remotely.[reset] 309 310 Preparing the remote plan... 311 ` 312 313 const runHeader = ` 314 [reset][yellow]To view this run in a browser, visit: 315 https://%s/app/%s/%s/runs/%s[reset] 316 ` 317 318 // The newline in this error is to make it look good in the CLI! 319 const lockTimeoutErr = ` 320 [reset][red]Lock timeout exceeded, sending interrupt to cancel the remote operation. 321 [reset] 322 `