kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/command/apply.go (about)

     1  package command
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"kubeform.dev/terraform-backend-sdk/backend"
     8  	remoteBackend "kubeform.dev/terraform-backend-sdk/backend/remote"
     9  	"kubeform.dev/terraform-backend-sdk/command/arguments"
    10  	"kubeform.dev/terraform-backend-sdk/command/views"
    11  	"kubeform.dev/terraform-backend-sdk/plans/planfile"
    12  	"kubeform.dev/terraform-backend-sdk/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  	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 we're using the remote
   133  	// backend locally, in which case these are rendered remotely
   134  	if rb, isRemoteBackend := be.(*remoteBackend.Remote); !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  }