github.com/muratcelep/terraform@v1.1.0-beta2-not-internal-4/not-internal/command/apply.go (about) 1 package command 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/muratcelep/terraform/not-internal/backend" 8 "github.com/muratcelep/terraform/not-internal/command/arguments" 9 "github.com/muratcelep/terraform/not-internal/command/views" 10 "github.com/muratcelep/terraform/not-internal/plans/planfile" 11 "github.com/muratcelep/terraform/not-internal/tfdiags" 12 ) 13 14 // ApplyCommand is a Command implementation that applies a Terraform 15 // configuration and actually builds or changes infrastructure. 16 type ApplyCommand struct { 17 Meta 18 19 // If true, then this apply command will become the "destroy" 20 // command. It is just like apply but only processes a destroy. 21 Destroy bool 22 } 23 24 func (c *ApplyCommand) Run(rawArgs []string) int { 25 var diags tfdiags.Diagnostics 26 27 // Parse and apply global view arguments 28 common, rawArgs := arguments.ParseView(rawArgs) 29 c.View.Configure(common) 30 31 // Propagate -no-color for legacy use of Ui. The remote backend and 32 // cloud package use this; it should be removed when/if they are 33 // migrated to views. 34 c.Meta.color = !common.NoColor 35 c.Meta.Color = c.Meta.color 36 37 // Parse and validate flags 38 var args *arguments.Apply 39 switch { 40 case c.Destroy: 41 args, diags = arguments.ParseApplyDestroy(rawArgs) 42 default: 43 args, diags = arguments.ParseApply(rawArgs) 44 } 45 46 // Instantiate the view, even if there are flag errors, so that we render 47 // diagnostics according to the desired view 48 view := views.NewApply(args.ViewType, c.Destroy, c.View) 49 50 if diags.HasErrors() { 51 view.Diagnostics(diags) 52 view.HelpPrompt() 53 return 1 54 } 55 56 // Check for user-supplied plugin path 57 var err error 58 if c.pluginPath, err = c.loadPluginPath(); err != nil { 59 diags = diags.Append(err) 60 view.Diagnostics(diags) 61 return 1 62 } 63 64 // Attempt to load the plan file, if specified 65 planFile, diags := c.LoadPlanFile(args.PlanPath) 66 if diags.HasErrors() { 67 view.Diagnostics(diags) 68 return 1 69 } 70 71 // Check for invalid combination of plan file and variable overrides 72 if planFile != nil && !args.Vars.Empty() { 73 diags = diags.Append(tfdiags.Sourceless( 74 tfdiags.Error, 75 "Can't set variables when applying a saved plan", 76 "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.", 77 )) 78 view.Diagnostics(diags) 79 return 1 80 } 81 82 // FIXME: the -input flag value is needed to initialize the backend and the 83 // operation, but there is no clear path to pass this value down, so we 84 // continue to mutate the Meta object state for now. 85 c.Meta.input = args.InputEnabled 86 87 // FIXME: the -parallelism flag is used to control the concurrency of 88 // Terraform operations. At the moment, this value is used both to 89 // initialize the backend via the ContextOpts field inside CLIOpts, and to 90 // set a largely unused field on the Operation request. Again, there is no 91 // clear path to pass this value down, so we continue to mutate the Meta 92 // object state for now. 93 c.Meta.parallelism = args.Operation.Parallelism 94 95 // Prepare the backend, passing the plan file if present, and the 96 // backend-specific arguments 97 be, beDiags := c.PrepareBackend(planFile, args.State) 98 diags = diags.Append(beDiags) 99 if diags.HasErrors() { 100 view.Diagnostics(diags) 101 return 1 102 } 103 104 // Build the operation request 105 opReq, opDiags := c.OperationRequest(be, view, planFile, args.Operation, args.AutoApprove) 106 diags = diags.Append(opDiags) 107 108 // Collect variable value and add them to the operation request 109 diags = diags.Append(c.GatherVariables(opReq, args.Vars)) 110 111 // Before we delegate to the backend, we'll print any warning diagnostics 112 // we've accumulated here, since the backend will start fresh with its own 113 // diagnostics. 114 view.Diagnostics(diags) 115 if diags.HasErrors() { 116 return 1 117 } 118 diags = nil 119 120 // Run the operation 121 op, err := c.RunOperation(be, opReq) 122 if err != nil { 123 diags = diags.Append(err) 124 view.Diagnostics(diags) 125 return 1 126 } 127 128 if op.Result != backend.OperationSuccess { 129 return op.Result.ExitStatus() 130 } 131 132 // Render the resource count and outputs, unless those counts are being 133 // rendered already in a remote Terraform process. 134 if rb, isRemoteBackend := be.(BackendWithRemoteTerraformVersion); !isRemoteBackend || rb.IsLocalOperations() { 135 view.ResourceCount(args.State.StateOutPath) 136 if !c.Destroy && op.State != nil { 137 view.Outputs(op.State.RootModule().OutputValues) 138 } 139 } 140 141 view.Diagnostics(diags) 142 143 if diags.HasErrors() { 144 return 1 145 } 146 147 return 0 148 } 149 150 func (c *ApplyCommand) LoadPlanFile(path string) (*planfile.Reader, tfdiags.Diagnostics) { 151 var planFile *planfile.Reader 152 var diags tfdiags.Diagnostics 153 154 // Try to load plan if path is specified 155 if path != "" { 156 var err error 157 planFile, err = c.PlanFile(path) 158 if err != nil { 159 diags = diags.Append(tfdiags.Sourceless( 160 tfdiags.Error, 161 fmt.Sprintf("Failed to load %q as a plan file", path), 162 fmt.Sprintf("Error: %s", err), 163 )) 164 return nil, diags 165 } 166 167 // If the path doesn't look like a plan, both planFile and err will be 168 // nil. In that case, the user is probably trying to use the positional 169 // argument to specify a configuration path. Point them at -chdir. 170 if planFile == nil { 171 diags = diags.Append(tfdiags.Sourceless( 172 tfdiags.Error, 173 fmt.Sprintf("Failed to load %q as a plan file", path), 174 "The specified path is a directory, not a plan file. You can use the global -chdir flag to use this directory as the configuration root.", 175 )) 176 return nil, diags 177 } 178 179 // If we successfully loaded a plan but this is a destroy operation, 180 // explain that this is not supported. 181 if c.Destroy { 182 diags = diags.Append(tfdiags.Sourceless( 183 tfdiags.Error, 184 "Destroy can't be called with a plan file", 185 fmt.Sprintf("If this plan was created using plan -destroy, apply it using:\n terraform apply %q", path), 186 )) 187 return nil, diags 188 } 189 } 190 191 return planFile, diags 192 } 193 194 func (c *ApplyCommand) PrepareBackend(planFile *planfile.Reader, args *arguments.State) (backend.Enhanced, tfdiags.Diagnostics) { 195 var diags tfdiags.Diagnostics 196 197 // FIXME: we need to apply the state arguments to the meta object here 198 // because they are later used when initializing the backend. Carving a 199 // path to pass these arguments to the functions that need them is 200 // difficult but would make their use easier to understand. 201 c.Meta.applyStateArguments(args) 202 203 // Load the backend 204 var be backend.Enhanced 205 var beDiags tfdiags.Diagnostics 206 if planFile == nil { 207 backendConfig, configDiags := c.loadBackendConfig(".") 208 diags = diags.Append(configDiags) 209 if configDiags.HasErrors() { 210 return nil, diags 211 } 212 213 be, beDiags = c.Backend(&BackendOpts{ 214 Config: backendConfig, 215 }) 216 } else { 217 plan, err := planFile.ReadPlan() 218 if err != nil { 219 diags = diags.Append(tfdiags.Sourceless( 220 tfdiags.Error, 221 "Failed to read plan from plan file", 222 fmt.Sprintf("Cannot read the plan from the given plan file: %s.", err), 223 )) 224 return nil, diags 225 } 226 if plan.Backend.Config == nil { 227 // Should never happen; always indicates a bug in the creation of the plan file 228 diags = diags.Append(tfdiags.Sourceless( 229 tfdiags.Error, 230 "Failed to read plan from plan file", 231 "The given plan file does not have a valid backend configuration. This is a bug in the Terraform command that generated this plan file.", 232 )) 233 return nil, diags 234 } 235 be, beDiags = c.BackendForPlan(plan.Backend) 236 } 237 238 diags = diags.Append(beDiags) 239 if beDiags.HasErrors() { 240 return nil, diags 241 } 242 return be, diags 243 } 244 245 func (c *ApplyCommand) OperationRequest( 246 be backend.Enhanced, 247 view views.Apply, 248 planFile *planfile.Reader, 249 args *arguments.Operation, 250 autoApprove bool, 251 ) (*backend.Operation, tfdiags.Diagnostics) { 252 var diags tfdiags.Diagnostics 253 254 // Applying changes with dev overrides in effect could make it impossible 255 // to switch back to a release version if the schema isn't compatible, 256 // so we'll warn about it. 257 diags = diags.Append(c.providerDevOverrideRuntimeWarnings()) 258 259 // Build the operation 260 opReq := c.Operation(be) 261 opReq.AutoApprove = autoApprove 262 opReq.ConfigDir = "." 263 opReq.PlanMode = args.PlanMode 264 opReq.Hooks = view.Hooks() 265 opReq.PlanFile = planFile 266 opReq.PlanRefresh = args.Refresh 267 opReq.Targets = args.Targets 268 opReq.ForceReplace = args.ForceReplace 269 opReq.Type = backend.OperationTypeApply 270 opReq.View = view.Operation() 271 272 var err error 273 opReq.ConfigLoader, err = c.initConfigLoader() 274 if err != nil { 275 diags = diags.Append(fmt.Errorf("Failed to initialize config loader: %s", err)) 276 return nil, diags 277 } 278 279 return opReq, diags 280 } 281 282 func (c *ApplyCommand) GatherVariables(opReq *backend.Operation, args *arguments.Vars) tfdiags.Diagnostics { 283 var diags tfdiags.Diagnostics 284 285 // FIXME the arguments package currently trivially gathers variable related 286 // arguments in a heterogenous slice, in order to minimize the number of 287 // code paths gathering variables during the transition to this structure. 288 // Once all commands that gather variables have been converted to this 289 // structure, we could move the variable gathering code to the arguments 290 // package directly, removing this shim layer. 291 292 varArgs := args.All() 293 items := make([]rawFlag, len(varArgs)) 294 for i := range varArgs { 295 items[i].Name = varArgs[i].Name 296 items[i].Value = varArgs[i].Value 297 } 298 c.Meta.variableArgs = rawFlags{items: &items} 299 opReq.Variables, diags = c.collectVariableValues() 300 301 return diags 302 } 303 304 func (c *ApplyCommand) Help() string { 305 if c.Destroy { 306 return c.helpDestroy() 307 } 308 309 return c.helpApply() 310 } 311 312 func (c *ApplyCommand) Synopsis() string { 313 if c.Destroy { 314 return "Destroy previously-created infrastructure" 315 } 316 317 return "Create or update infrastructure" 318 } 319 320 func (c *ApplyCommand) helpApply() string { 321 helpText := ` 322 Usage: terraform [global options] apply [options] [PLAN] 323 324 Creates or updates infrastructure according to Terraform configuration 325 files in the current directory. 326 327 By default, Terraform will generate a new plan and present it for your 328 approval before taking any action. You can optionally provide a plan 329 file created by a previous call to "terraform plan", in which case 330 Terraform will take the actions described in that plan without any 331 confirmation prompt. 332 333 Options: 334 335 -auto-approve Skip interactive approval of plan before applying. 336 337 -backup=path Path to backup the existing state file before 338 modifying. Defaults to the "-state-out" path with 339 ".backup" extension. Set to "-" to disable backup. 340 341 -compact-warnings If Terraform produces any warnings that are not 342 accompanied by errors, show them in a more compact 343 form that includes only the summary messages. 344 345 -lock=false Don't hold a state lock during the operation. This is 346 dangerous if others might concurrently run commands 347 against the same workspace. 348 349 -lock-timeout=0s Duration to retry a state lock. 350 351 -input=true Ask for input for variables if not directly set. 352 353 -no-color If specified, output won't contain any color. 354 355 -parallelism=n Limit the number of parallel resource operations. 356 Defaults to 10. 357 358 -state=path Path to read and save state (unless state-out 359 is specified). Defaults to "terraform.tfstate". 360 361 -state-out=path Path to write state to that is different than 362 "-state". This can be used to preserve the old 363 state. 364 365 If you don't provide a saved plan file then this command will also accept 366 all of the plan-customization options accepted by the terraform plan command. 367 For more information on those options, run: 368 terraform plan -help 369 ` 370 return strings.TrimSpace(helpText) 371 } 372 373 func (c *ApplyCommand) helpDestroy() string { 374 helpText := ` 375 Usage: terraform [global options] destroy [options] 376 377 Destroy Terraform-managed infrastructure. 378 379 This command is a convenience alias for: 380 terraform apply -destroy 381 382 This command also accepts many of the plan-customization options accepted by 383 the terraform plan command. For more information on those options, run: 384 terraform plan -help 385 ` 386 return strings.TrimSpace(helpText) 387 }