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