github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/command/apply.go (about) 1 package command 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "sort" 8 "strings" 9 10 "github.com/hashicorp/go-getter" 11 "github.com/hashicorp/terraform/addrs" 12 "github.com/hashicorp/terraform/backend" 13 "github.com/hashicorp/terraform/configs/hcl2shim" 14 "github.com/hashicorp/terraform/repl" 15 "github.com/hashicorp/terraform/states" 16 "github.com/hashicorp/terraform/tfdiags" 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 29 func (c *ApplyCommand) Run(args []string) int { 30 var destroyForce, refresh, autoApprove bool 31 args, err := c.Meta.process(args, true) 32 if err != nil { 33 return 1 34 } 35 36 cmdName := "apply" 37 if c.Destroy { 38 cmdName = "destroy" 39 } 40 41 cmdFlags := c.Meta.extendedFlagSet(cmdName) 42 cmdFlags.BoolVar(&autoApprove, "auto-approve", false, "skip interactive approval of plan before applying") 43 if c.Destroy { 44 cmdFlags.BoolVar(&destroyForce, "force", false, "deprecated: same as auto-approve") 45 } 46 cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") 47 cmdFlags.IntVar(&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism") 48 cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") 49 cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") 50 cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") 51 cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") 52 cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") 53 cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } 54 if err := cmdFlags.Parse(args); err != nil { 55 return 1 56 } 57 58 var diags tfdiags.Diagnostics 59 60 // Get the args. The "maybeInit" flag tracks whether we may need to 61 // initialize the configuration from a remote path. This is true as long 62 // as we have an argument. 63 args = cmdFlags.Args() 64 maybeInit := len(args) == 1 65 configPath, err := ModulePath(args) 66 if err != nil { 67 c.Ui.Error(err.Error()) 68 return 1 69 } 70 71 // Check for user-supplied plugin path 72 if c.pluginPath, err = c.loadPluginPath(); err != nil { 73 c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err)) 74 return 1 75 } 76 77 if !c.Destroy && maybeInit { 78 // We need the pwd for the getter operation below 79 pwd, err := os.Getwd() 80 if err != nil { 81 c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) 82 return 1 83 } 84 85 // Do a detect to determine if we need to do an init + apply. 86 if detected, err := getter.Detect(configPath, pwd, getter.Detectors); err != nil { 87 c.Ui.Error(fmt.Sprintf("Invalid path: %s", err)) 88 return 1 89 } else if !strings.HasPrefix(detected, "file") { 90 // If this isn't a file URL then we're doing an init + 91 // apply. 92 var init InitCommand 93 init.Meta = c.Meta 94 if code := init.Run([]string{detected}); code != 0 { 95 return code 96 } 97 98 // Change the config path to be the cwd 99 configPath = pwd 100 } 101 } 102 103 // Check if the path is a plan 104 planFile, err := c.PlanFile(configPath) 105 if err != nil { 106 c.Ui.Error(err.Error()) 107 return 1 108 } 109 if c.Destroy && planFile != nil { 110 c.Ui.Error(fmt.Sprintf("Destroy can't be called with a plan file.")) 111 return 1 112 } 113 if planFile != nil { 114 // Reset the config path for backend loading 115 configPath = "" 116 117 if !c.variableArgs.Empty() { 118 diags = diags.Append(tfdiags.Sourceless( 119 tfdiags.Error, 120 "Can't set variables when applying a saved plan", 121 "The -var and -var-file options cannot be used when applying a saved plan file, because a saved plan includes the variable values that were set when it was created.", 122 )) 123 c.showDiagnostics(diags) 124 return 1 125 } 126 } 127 128 // Load the backend 129 var be backend.Enhanced 130 var beDiags tfdiags.Diagnostics 131 if planFile == nil { 132 backendConfig, configDiags := c.loadBackendConfig(configPath) 133 diags = diags.Append(configDiags) 134 if configDiags.HasErrors() { 135 c.showDiagnostics(diags) 136 return 1 137 } 138 139 be, beDiags = c.Backend(&BackendOpts{ 140 Config: backendConfig, 141 }) 142 } else { 143 plan, err := planFile.ReadPlan() 144 if err != nil { 145 diags = diags.Append(tfdiags.Sourceless( 146 tfdiags.Error, 147 "Failed to read plan from plan file", 148 fmt.Sprintf("Cannot read the plan from the given plan file: %s.", err), 149 )) 150 c.showDiagnostics(diags) 151 return 1 152 } 153 if plan.Backend.Config == nil { 154 // Should never happen; always indicates a bug in the creation of the plan file 155 diags = diags.Append(tfdiags.Sourceless( 156 tfdiags.Error, 157 "Failed to read plan from plan file", 158 fmt.Sprintf("The given plan file does not have a valid backend configuration. This is a bug in the Terraform command that generated this plan file."), 159 )) 160 c.showDiagnostics(diags) 161 return 1 162 } 163 be, beDiags = c.BackendForPlan(plan.Backend) 164 } 165 diags = diags.Append(beDiags) 166 if beDiags.HasErrors() { 167 c.showDiagnostics(diags) 168 return 1 169 } 170 171 // Before we delegate to the backend, we'll print any warning diagnostics 172 // we've accumulated here, since the backend will start fresh with its own 173 // diagnostics. 174 c.showDiagnostics(diags) 175 diags = nil 176 177 // Build the operation 178 opReq := c.Operation(be) 179 opReq.AutoApprove = autoApprove 180 opReq.ConfigDir = configPath 181 opReq.Destroy = c.Destroy 182 opReq.DestroyForce = destroyForce 183 opReq.PlanFile = planFile 184 opReq.PlanRefresh = refresh 185 opReq.Type = backend.OperationTypeApply 186 187 opReq.ConfigLoader, err = c.initConfigLoader() 188 if err != nil { 189 c.showDiagnostics(err) 190 return 1 191 } 192 193 { 194 var moreDiags tfdiags.Diagnostics 195 opReq.Variables, moreDiags = c.collectVariableValues() 196 diags = diags.Append(moreDiags) 197 if moreDiags.HasErrors() { 198 c.showDiagnostics(diags) 199 return 1 200 } 201 } 202 203 op, err := c.RunOperation(be, opReq) 204 if err != nil { 205 c.showDiagnostics(err) 206 return 1 207 } 208 if op.Result != backend.OperationSuccess { 209 return op.Result.ExitStatus() 210 } 211 212 if !c.Destroy { 213 if outputs := outputsAsString(op.State, addrs.RootModuleInstance, true); outputs != "" { 214 c.Ui.Output(c.Colorize().Color(outputs)) 215 } 216 } 217 218 return op.Result.ExitStatus() 219 } 220 221 func (c *ApplyCommand) Help() string { 222 if c.Destroy { 223 return c.helpDestroy() 224 } 225 226 return c.helpApply() 227 } 228 229 func (c *ApplyCommand) Synopsis() string { 230 if c.Destroy { 231 return "Destroy Terraform-managed infrastructure" 232 } 233 234 return "Builds or changes infrastructure" 235 } 236 237 func (c *ApplyCommand) helpApply() string { 238 helpText := ` 239 Usage: terraform apply [options] [DIR-OR-PLAN] 240 241 Builds or changes infrastructure according to Terraform configuration 242 files in DIR. 243 244 By default, apply scans the current directory for the configuration 245 and applies the changes appropriately. However, a path to another 246 configuration or an execution plan can be provided. Execution plans can be 247 used to only execute a pre-determined set of actions. 248 249 Options: 250 251 -auto-approve Skip interactive approval of plan before applying. 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 -compact-warnings If Terraform produces any warnings that are not 258 accompanied by errors, show them in a more compact 259 form that includes only the summary messages. 260 261 -lock=true Lock the state file when locking is supported. 262 263 -lock-timeout=0s Duration to retry a state lock. 264 265 -input=true Ask for input for variables if not directly set. 266 267 -no-color If specified, output won't contain any color. 268 269 -parallelism=n Limit the number of parallel resource operations. 270 Defaults to 10. 271 272 -refresh=true Update state prior to checking for differences. This 273 has no effect if a plan file is given to apply. 274 275 -state=path Path to read and save state (unless state-out 276 is specified). Defaults to "terraform.tfstate". 277 278 -state-out=path Path to write state to that is different than 279 "-state". This can be used to preserve the old 280 state. 281 282 -target=resource Resource to target. Operation will be limited to this 283 resource and its dependencies. This flag can be used 284 multiple times. 285 286 -var 'foo=bar' Set a variable in the Terraform configuration. This 287 flag can be set multiple times. 288 289 -var-file=foo Set variables in the Terraform configuration from 290 a file. If "terraform.tfvars" or any ".auto.tfvars" 291 files are present, they will be automatically loaded. 292 293 294 ` 295 return strings.TrimSpace(helpText) 296 } 297 298 func (c *ApplyCommand) helpDestroy() string { 299 helpText := ` 300 Usage: terraform destroy [options] [DIR] 301 302 Destroy Terraform-managed infrastructure. 303 304 Options: 305 306 -backup=path Path to backup the existing state file before 307 modifying. Defaults to the "-state-out" path with 308 ".backup" extension. Set to "-" to disable backup. 309 310 -auto-approve Skip interactive approval before destroying. 311 312 -force Deprecated: same as auto-approve. 313 314 -lock=true Lock the state file when locking is supported. 315 316 -lock-timeout=0s Duration to retry a state lock. 317 318 -no-color If specified, output won't contain any color. 319 320 -parallelism=n Limit the number of concurrent operations. 321 Defaults to 10. 322 323 -refresh=true Update state prior to checking for differences. This 324 has no effect if a plan file is given to apply. 325 326 -state=path Path to read and save state (unless state-out 327 is specified). Defaults to "terraform.tfstate". 328 329 -state-out=path Path to write state to that is different than 330 "-state". This can be used to preserve the old 331 state. 332 333 -target=resource Resource to target. Operation will be limited to this 334 resource and its dependencies. This flag can be used 335 multiple times. 336 337 -var 'foo=bar' Set a variable in the Terraform configuration. This 338 flag can be set multiple times. 339 340 -var-file=foo Set variables in the Terraform configuration from 341 a file. If "terraform.tfvars" or any ".auto.tfvars" 342 files are present, they will be automatically loaded. 343 344 345 ` 346 return strings.TrimSpace(helpText) 347 } 348 349 func outputsAsString(state *states.State, modPath addrs.ModuleInstance, includeHeader bool) string { 350 if state == nil { 351 return "" 352 } 353 354 ms := state.Module(modPath) 355 if ms == nil { 356 return "" 357 } 358 359 outputs := ms.OutputValues 360 outputBuf := new(bytes.Buffer) 361 if len(outputs) > 0 { 362 if includeHeader { 363 outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n") 364 } 365 366 // Output the outputs in alphabetical order 367 keyLen := 0 368 ks := make([]string, 0, len(outputs)) 369 for key, _ := range outputs { 370 ks = append(ks, key) 371 if len(key) > keyLen { 372 keyLen = len(key) 373 } 374 } 375 sort.Strings(ks) 376 377 for _, k := range ks { 378 v := outputs[k] 379 if v.Sensitive { 380 outputBuf.WriteString(fmt.Sprintf("%s = <sensitive>\n", k)) 381 continue 382 } 383 384 // Our formatter still wants an old-style raw interface{} value, so 385 // for now we'll just shim it. 386 // FIXME: Port the formatter to work with cty.Value directly. 387 legacyVal := hcl2shim.ConfigValueFromHCL2(v.Value) 388 result, err := repl.FormatResult(legacyVal) 389 if err != nil { 390 // We can't really return errors from here, so we'll just have 391 // to stub this out. This shouldn't happen in practice anyway. 392 result = "<error during formatting>" 393 } 394 395 outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, result)) 396 } 397 } 398 399 return strings.TrimSpace(outputBuf.String()) 400 } 401 402 const outputInterrupt = `Interrupt received. 403 Please wait for Terraform to exit or data loss may occur. 404 Gracefully shutting down...`