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  }