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