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...`