github.com/hooklift/terraform@v0.11.0-beta1.0.20171117000744-6786c1361ffe/command/apply.go (about) 1 package command 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "os" 8 "sort" 9 "strings" 10 11 "github.com/hashicorp/errwrap" 12 "github.com/hashicorp/go-getter" 13 "github.com/hashicorp/terraform/backend" 14 "github.com/hashicorp/terraform/config" 15 "github.com/hashicorp/terraform/config/module" 16 "github.com/hashicorp/terraform/terraform" 17 ) 18 19 // ApplyCommand is a Command implementation that applies a Terraform 20 // configuration and actually builds or changes infrastructure. 21 type ApplyCommand struct { 22 Meta 23 24 // If true, then this apply command will become the "destroy" 25 // command. It is just like apply but only processes a destroy. 26 Destroy bool 27 28 // When this channel is closed, the apply will be cancelled. 29 ShutdownCh <-chan struct{} 30 } 31 32 func (c *ApplyCommand) Run(args []string) int { 33 var destroyForce, refresh, autoApprove bool 34 args, err := c.Meta.process(args, true) 35 if err != nil { 36 return 1 37 } 38 39 cmdName := "apply" 40 if c.Destroy { 41 cmdName = "destroy" 42 } 43 44 cmdFlags := c.Meta.flagSet(cmdName) 45 if c.Destroy { 46 cmdFlags.BoolVar(&destroyForce, "force", false, "force") 47 } 48 cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") 49 if !c.Destroy { 50 cmdFlags.BoolVar(&autoApprove, "auto-approve", false, "skip interactive approval of plan before applying") 51 } 52 cmdFlags.IntVar( 53 &c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism") 54 cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") 55 cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") 56 cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") 57 cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") 58 cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") 59 cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } 60 if err := cmdFlags.Parse(args); err != nil { 61 return 1 62 } 63 64 // Get the args. The "maybeInit" flag tracks whether we may need to 65 // initialize the configuration from a remote path. This is true as long 66 // as we have an argument. 67 args = cmdFlags.Args() 68 maybeInit := len(args) == 1 69 configPath, err := ModulePath(args) 70 if err != nil { 71 c.Ui.Error(err.Error()) 72 return 1 73 } 74 75 // Check for user-supplied plugin path 76 if c.pluginPath, err = c.loadPluginPath(); err != nil { 77 c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err)) 78 return 1 79 } 80 81 if !c.Destroy && maybeInit { 82 // We need the pwd for the getter operation below 83 pwd, err := os.Getwd() 84 if err != nil { 85 c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) 86 return 1 87 } 88 89 // Do a detect to determine if we need to do an init + apply. 90 if detected, err := getter.Detect(configPath, pwd, getter.Detectors); err != nil { 91 c.Ui.Error(fmt.Sprintf( 92 "Invalid path: %s", err)) 93 return 1 94 } else if !strings.HasPrefix(detected, "file") { 95 // If this isn't a file URL then we're doing an init + 96 // apply. 97 var init InitCommand 98 init.Meta = c.Meta 99 if code := init.Run([]string{detected}); code != 0 { 100 return code 101 } 102 103 // Change the config path to be the cwd 104 configPath = pwd 105 } 106 } 107 108 // Check if the path is a plan 109 plan, err := c.Plan(configPath) 110 if err != nil { 111 c.Ui.Error(err.Error()) 112 return 1 113 } 114 if c.Destroy && plan != nil { 115 c.Ui.Error(fmt.Sprintf( 116 "Destroy can't be called with a plan file.")) 117 return 1 118 } 119 if plan != nil { 120 // Reset the config path for backend loading 121 configPath = "" 122 } 123 124 // Load the module if we don't have one yet (not running from plan) 125 var mod *module.Tree 126 if plan == nil { 127 mod, err = c.Module(configPath) 128 if err != nil { 129 err = errwrap.Wrapf("Failed to load root config module: {{err}}", err) 130 c.showDiagnostics(err) 131 return 1 132 } 133 } 134 135 /* 136 terraform.SetDebugInfo(DefaultDataDir) 137 138 // Check for the legacy graph 139 if experiment.Enabled(experiment.X_legacyGraph) { 140 c.Ui.Output(c.Colorize().Color( 141 "[reset][bold][yellow]" + 142 "Legacy graph enabled! This will use the graph from Terraform 0.7.x\n" + 143 "to execute this operation. This will be removed in the future so\n" + 144 "please report any issues causing you to use this to the Terraform\n" + 145 "project.\n\n")) 146 } 147 */ 148 149 var conf *config.Config 150 if mod != nil { 151 conf = mod.Config() 152 } 153 154 // Load the backend 155 b, err := c.Backend(&BackendOpts{ 156 Config: conf, 157 Plan: plan, 158 }) 159 if err != nil { 160 c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) 161 return 1 162 } 163 164 // Build the operation 165 opReq := c.Operation() 166 opReq.Destroy = c.Destroy 167 opReq.Module = mod 168 opReq.Plan = plan 169 opReq.PlanRefresh = refresh 170 opReq.Type = backend.OperationTypeApply 171 opReq.AutoApprove = autoApprove 172 opReq.DestroyForce = destroyForce 173 174 // Perform the operation 175 ctx, ctxCancel := context.WithCancel(context.Background()) 176 defer ctxCancel() 177 op, err := b.Operation(ctx, opReq) 178 if err != nil { 179 c.Ui.Error(fmt.Sprintf("Error starting operation: %s", err)) 180 return 1 181 } 182 183 // Wait for the operation to complete or an interrupt to occur 184 select { 185 case <-c.ShutdownCh: 186 // Cancel our context so we can start gracefully exiting 187 ctxCancel() 188 189 // Notify the user 190 c.Ui.Output(outputInterrupt) 191 192 // Still get the result, since there is still one 193 select { 194 case <-c.ShutdownCh: 195 c.Ui.Error( 196 "Two interrupts received. Exiting immediately. Note that data\n" + 197 "loss may have occurred.") 198 return 1 199 case <-op.Done(): 200 } 201 case <-op.Done(): 202 if err := op.Err; err != nil { 203 c.showDiagnostics(err) 204 return 1 205 } 206 } 207 208 if !c.Destroy { 209 // Get the right module that we used. If we ran a plan, then use 210 // that module. 211 if plan != nil { 212 mod = plan.Module 213 } 214 215 if outputs := outputsAsString(op.State, terraform.RootModulePath, mod.Config().Outputs, true); outputs != "" { 216 c.Ui.Output(c.Colorize().Color(outputs)) 217 } 218 } 219 220 return 0 221 } 222 223 func (c *ApplyCommand) Help() string { 224 if c.Destroy { 225 return c.helpDestroy() 226 } 227 228 return c.helpApply() 229 } 230 231 func (c *ApplyCommand) Synopsis() string { 232 if c.Destroy { 233 return "Destroy Terraform-managed infrastructure" 234 } 235 236 return "Builds or changes infrastructure" 237 } 238 239 func (c *ApplyCommand) helpApply() string { 240 helpText := ` 241 Usage: terraform apply [options] [DIR-OR-PLAN] 242 243 Builds or changes infrastructure according to Terraform configuration 244 files in DIR. 245 246 By default, apply scans the current directory for the configuration 247 and applies the changes appropriately. However, a path to another 248 configuration or an execution plan can be provided. Execution plans can be 249 used to only execute a pre-determined set of actions. 250 251 Options: 252 253 -backup=path Path to backup the existing state file before 254 modifying. Defaults to the "-state-out" path with 255 ".backup" extension. Set to "-" to disable backup. 256 257 -lock=true Lock the state file when locking is supported. 258 259 -lock-timeout=0s Duration to retry a state lock. 260 261 -auto-approve Skip interactive approval of plan before applying. 262 263 -input=true Ask for input for variables if not directly set. 264 265 -no-color If specified, output won't contain any color. 266 267 -parallelism=n Limit the number of parallel resource operations. 268 Defaults to 10. 269 270 -refresh=true Update state prior to checking for differences. This 271 has no effect if a plan file is given to apply. 272 273 -state=path Path to read and save state (unless state-out 274 is specified). Defaults to "terraform.tfstate". 275 276 -state-out=path Path to write state to that is different than 277 "-state". This can be used to preserve the old 278 state. 279 280 -target=resource Resource to target. Operation will be limited to this 281 resource and its dependencies. This flag can be used 282 multiple times. 283 284 -var 'foo=bar' Set a variable in the Terraform configuration. This 285 flag can be set multiple times. 286 287 -var-file=foo Set variables in the Terraform configuration from 288 a file. If "terraform.tfvars" or any ".auto.tfvars" 289 files are present, they will be automatically loaded. 290 291 292 ` 293 return strings.TrimSpace(helpText) 294 } 295 296 func (c *ApplyCommand) helpDestroy() string { 297 helpText := ` 298 Usage: terraform destroy [options] [DIR] 299 300 Destroy Terraform-managed infrastructure. 301 302 Options: 303 304 -backup=path Path to backup the existing state file before 305 modifying. Defaults to the "-state-out" path with 306 ".backup" extension. Set to "-" to disable backup. 307 308 -force Don't ask for input for destroy confirmation. 309 310 -lock=true Lock the state file when locking is supported. 311 312 -lock-timeout=0s Duration to retry a state lock. 313 314 -no-color If specified, output won't contain any color. 315 316 -parallelism=n Limit the number of concurrent operations. 317 Defaults to 10. 318 319 -refresh=true Update state prior to checking for differences. This 320 has no effect if a plan file is given to apply. 321 322 -state=path Path to read and save state (unless state-out 323 is specified). Defaults to "terraform.tfstate". 324 325 -state-out=path Path to write state to that is different than 326 "-state". This can be used to preserve the old 327 state. 328 329 -target=resource Resource to target. Operation will be limited to this 330 resource and its dependencies. This flag can be used 331 multiple times. 332 333 -var 'foo=bar' Set a variable in the Terraform configuration. This 334 flag can be set multiple times. 335 336 -var-file=foo Set variables in the Terraform configuration from 337 a file. If "terraform.tfvars" or any ".auto.tfvars" 338 files are present, they will be automatically loaded. 339 340 341 ` 342 return strings.TrimSpace(helpText) 343 } 344 345 func outputsAsString(state *terraform.State, modPath []string, schema []*config.Output, includeHeader bool) string { 346 if state == nil { 347 return "" 348 } 349 350 ms := state.ModuleByPath(modPath) 351 if ms == nil { 352 return "" 353 } 354 355 outputs := ms.Outputs 356 outputBuf := new(bytes.Buffer) 357 if len(outputs) > 0 { 358 schemaMap := make(map[string]*config.Output) 359 if schema != nil { 360 for _, s := range schema { 361 schemaMap[s.Name] = s 362 } 363 } 364 365 if includeHeader { 366 outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n") 367 } 368 369 // Output the outputs in alphabetical order 370 keyLen := 0 371 ks := make([]string, 0, len(outputs)) 372 for key, _ := range outputs { 373 ks = append(ks, key) 374 if len(key) > keyLen { 375 keyLen = len(key) 376 } 377 } 378 sort.Strings(ks) 379 380 for _, k := range ks { 381 schema, ok := schemaMap[k] 382 if ok && schema.Sensitive { 383 outputBuf.WriteString(fmt.Sprintf("%s = <sensitive>\n", k)) 384 continue 385 } 386 387 v := outputs[k] 388 switch typedV := v.Value.(type) { 389 case string: 390 outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, typedV)) 391 case []interface{}: 392 outputBuf.WriteString(formatListOutput("", k, typedV)) 393 outputBuf.WriteString("\n") 394 case map[string]interface{}: 395 outputBuf.WriteString(formatMapOutput("", k, typedV)) 396 outputBuf.WriteString("\n") 397 } 398 } 399 } 400 401 return strings.TrimSpace(outputBuf.String()) 402 } 403 404 const outputInterrupt = `Interrupt received. 405 Please wait for Terraform to exit or data loss may occur. 406 Gracefully shutting down...`