github.com/jzbruno/terraform@v0.10.3-0.20180104230435-18975d727047/command/apply.go (about)

     1  package command
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"os"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/hashicorp/terraform/tfdiags"
    12  
    13  	"github.com/hashicorp/go-getter"
    14  	"github.com/hashicorp/terraform/backend"
    15  	"github.com/hashicorp/terraform/config"
    16  	"github.com/hashicorp/terraform/config/module"
    17  	"github.com/hashicorp/terraform/terraform"
    18  )
    19  
    20  // ApplyCommand is a Command implementation that applies a Terraform
    21  // configuration and actually builds or changes infrastructure.
    22  type ApplyCommand struct {
    23  	Meta
    24  
    25  	// If true, then this apply command will become the "destroy"
    26  	// command. It is just like apply but only processes a destroy.
    27  	Destroy bool
    28  }
    29  
    30  func (c *ApplyCommand) Run(args []string) int {
    31  	var destroyForce, refresh, autoApprove bool
    32  	args, err := c.Meta.process(args, true)
    33  	if err != nil {
    34  		return 1
    35  	}
    36  
    37  	cmdName := "apply"
    38  	if c.Destroy {
    39  		cmdName = "destroy"
    40  	}
    41  
    42  	cmdFlags := c.Meta.flagSet(cmdName)
    43  	if c.Destroy {
    44  		cmdFlags.BoolVar(&destroyForce, "force", false, "force")
    45  	}
    46  	cmdFlags.BoolVar(&refresh, "refresh", true, "refresh")
    47  	if !c.Destroy {
    48  		cmdFlags.BoolVar(&autoApprove, "auto-approve", false, "skip interactive approval of plan before applying")
    49  	}
    50  	cmdFlags.IntVar(
    51  		&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism")
    52  	cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path")
    53  	cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
    54  	cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
    55  	cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state")
    56  	cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout")
    57  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    58  	if err := cmdFlags.Parse(args); err != nil {
    59  		return 1
    60  	}
    61  
    62  	// Get the args. The "maybeInit" flag tracks whether we may need to
    63  	// initialize the configuration from a remote path. This is true as long
    64  	// as we have an argument.
    65  	args = cmdFlags.Args()
    66  	maybeInit := len(args) == 1
    67  	configPath, err := ModulePath(args)
    68  	if err != nil {
    69  		c.Ui.Error(err.Error())
    70  		return 1
    71  	}
    72  
    73  	// Check for user-supplied plugin path
    74  	if c.pluginPath, err = c.loadPluginPath(); err != nil {
    75  		c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err))
    76  		return 1
    77  	}
    78  
    79  	if !c.Destroy && maybeInit {
    80  		// We need the pwd for the getter operation below
    81  		pwd, err := os.Getwd()
    82  		if err != nil {
    83  			c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
    84  			return 1
    85  		}
    86  
    87  		// Do a detect to determine if we need to do an init + apply.
    88  		if detected, err := getter.Detect(configPath, pwd, getter.Detectors); err != nil {
    89  			c.Ui.Error(fmt.Sprintf(
    90  				"Invalid path: %s", err))
    91  			return 1
    92  		} else if !strings.HasPrefix(detected, "file") {
    93  			// If this isn't a file URL then we're doing an init +
    94  			// apply.
    95  			var init InitCommand
    96  			init.Meta = c.Meta
    97  			if code := init.Run([]string{detected}); code != 0 {
    98  				return code
    99  			}
   100  
   101  			// Change the config path to be the cwd
   102  			configPath = pwd
   103  		}
   104  	}
   105  
   106  	// Check if the path is a plan
   107  	plan, err := c.Plan(configPath)
   108  	if err != nil {
   109  		c.Ui.Error(err.Error())
   110  		return 1
   111  	}
   112  	if c.Destroy && plan != nil {
   113  		c.Ui.Error(fmt.Sprintf(
   114  			"Destroy can't be called with a plan file."))
   115  		return 1
   116  	}
   117  	if plan != nil {
   118  		// Reset the config path for backend loading
   119  		configPath = ""
   120  	}
   121  
   122  	var diags tfdiags.Diagnostics
   123  
   124  	// Load the module if we don't have one yet (not running from plan)
   125  	var mod *module.Tree
   126  	if plan == nil {
   127  		var modDiags tfdiags.Diagnostics
   128  		mod, modDiags = c.Module(configPath)
   129  		diags = diags.Append(modDiags)
   130  		if modDiags.HasErrors() {
   131  			c.showDiagnostics(diags)
   132  			return 1
   133  		}
   134  	}
   135  
   136  	var conf *config.Config
   137  	if mod != nil {
   138  		conf = mod.Config()
   139  	}
   140  
   141  	// Load the backend
   142  	b, err := c.Backend(&BackendOpts{
   143  		Config: conf,
   144  		Plan:   plan,
   145  	})
   146  	if err != nil {
   147  		c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
   148  		return 1
   149  	}
   150  
   151  	// Build the operation
   152  	opReq := c.Operation()
   153  	opReq.Destroy = c.Destroy
   154  	opReq.Module = mod
   155  	opReq.Plan = plan
   156  	opReq.PlanRefresh = refresh
   157  	opReq.Type = backend.OperationTypeApply
   158  	opReq.AutoApprove = autoApprove
   159  	opReq.DestroyForce = destroyForce
   160  
   161  	// Perform the operation
   162  	ctx, ctxCancel := context.WithCancel(context.Background())
   163  	defer ctxCancel()
   164  
   165  	op, err := b.Operation(ctx, opReq)
   166  	if err != nil {
   167  		c.Ui.Error(fmt.Sprintf("Error starting operation: %s", err))
   168  		return 1
   169  	}
   170  
   171  	// Wait for the operation to complete or an interrupt to occur
   172  	select {
   173  	case <-c.ShutdownCh:
   174  		// Cancel our context so we can start gracefully exiting
   175  		ctxCancel()
   176  
   177  		// Notify the user
   178  		c.Ui.Output(outputInterrupt)
   179  
   180  		// Still get the result, since there is still one
   181  		select {
   182  		case <-c.ShutdownCh:
   183  			c.Ui.Error(
   184  				"Two interrupts received. Exiting immediately. Note that data\n" +
   185  					"loss may have occurred.")
   186  			return 1
   187  		case <-op.Done():
   188  		}
   189  	case <-op.Done():
   190  		if err := op.Err; err != nil {
   191  			diags = diags.Append(err)
   192  		}
   193  	}
   194  
   195  	c.showDiagnostics(diags)
   196  	if diags.HasErrors() {
   197  		return 1
   198  	}
   199  
   200  	if !c.Destroy {
   201  		// Get the right module that we used. If we ran a plan, then use
   202  		// that module.
   203  		if plan != nil {
   204  			mod = plan.Module
   205  		}
   206  
   207  		if outputs := outputsAsString(op.State, terraform.RootModulePath, mod.Config().Outputs, true); outputs != "" {
   208  			c.Ui.Output(c.Colorize().Color(outputs))
   209  		}
   210  	}
   211  
   212  	return 0
   213  }
   214  
   215  func (c *ApplyCommand) Help() string {
   216  	if c.Destroy {
   217  		return c.helpDestroy()
   218  	}
   219  
   220  	return c.helpApply()
   221  }
   222  
   223  func (c *ApplyCommand) Synopsis() string {
   224  	if c.Destroy {
   225  		return "Destroy Terraform-managed infrastructure"
   226  	}
   227  
   228  	return "Builds or changes infrastructure"
   229  }
   230  
   231  func (c *ApplyCommand) helpApply() string {
   232  	helpText := `
   233  Usage: terraform apply [options] [DIR-OR-PLAN]
   234  
   235    Builds or changes infrastructure according to Terraform configuration
   236    files in DIR.
   237  
   238    By default, apply scans the current directory for the configuration
   239    and applies the changes appropriately. However, a path to another
   240    configuration or an execution plan can be provided. Execution plans can be
   241    used to only execute a pre-determined set of actions.
   242  
   243  Options:
   244  
   245    -backup=path           Path to backup the existing state file before
   246                           modifying. Defaults to the "-state-out" path with
   247                           ".backup" extension. Set to "-" to disable backup.
   248  
   249    -lock=true             Lock the state file when locking is supported.
   250  
   251    -lock-timeout=0s       Duration to retry a state lock.
   252  
   253    -auto-approve          Skip interactive approval of plan before applying.
   254  
   255    -input=true            Ask for input for variables if not directly set.
   256  
   257    -no-color              If specified, output won't contain any color.
   258  
   259    -parallelism=n         Limit the number of parallel resource operations.
   260                           Defaults to 10.
   261  
   262    -refresh=true          Update state prior to checking for differences. This
   263                           has no effect if a plan file is given to apply.
   264  
   265    -state=path            Path to read and save state (unless state-out
   266                           is specified). Defaults to "terraform.tfstate".
   267  
   268    -state-out=path        Path to write state to that is different than
   269                           "-state". This can be used to preserve the old
   270                           state.
   271  
   272    -target=resource       Resource to target. Operation will be limited to this
   273                           resource and its dependencies. This flag can be used
   274                           multiple times.
   275  
   276    -var 'foo=bar'         Set a variable in the Terraform configuration. This
   277                           flag can be set multiple times.
   278  
   279    -var-file=foo          Set variables in the Terraform configuration from
   280                           a file. If "terraform.tfvars" or any ".auto.tfvars"
   281                           files are present, they will be automatically loaded.
   282  
   283  
   284  `
   285  	return strings.TrimSpace(helpText)
   286  }
   287  
   288  func (c *ApplyCommand) helpDestroy() string {
   289  	helpText := `
   290  Usage: terraform destroy [options] [DIR]
   291  
   292    Destroy Terraform-managed infrastructure.
   293  
   294  Options:
   295  
   296    -backup=path           Path to backup the existing state file before
   297                           modifying. Defaults to the "-state-out" path with
   298                           ".backup" extension. Set to "-" to disable backup.
   299  
   300    -force                 Don't ask for input for destroy confirmation.
   301  
   302    -lock=true             Lock the state file when locking is supported.
   303  
   304    -lock-timeout=0s       Duration to retry a state lock.
   305  
   306    -no-color              If specified, output won't contain any color.
   307  
   308    -parallelism=n         Limit the number of concurrent operations.
   309                           Defaults to 10.
   310  
   311    -refresh=true          Update state prior to checking for differences. This
   312                           has no effect if a plan file is given to apply.
   313  
   314    -state=path            Path to read and save state (unless state-out
   315                           is specified). Defaults to "terraform.tfstate".
   316  
   317    -state-out=path        Path to write state to that is different than
   318                           "-state". This can be used to preserve the old
   319                           state.
   320  
   321    -target=resource       Resource to target. Operation will be limited to this
   322                           resource and its dependencies. This flag can be used
   323                           multiple times.
   324  
   325    -var 'foo=bar'         Set a variable in the Terraform configuration. This
   326                           flag can be set multiple times.
   327  
   328    -var-file=foo          Set variables in the Terraform configuration from
   329                           a file. If "terraform.tfvars" or any ".auto.tfvars"
   330                           files are present, they will be automatically loaded.
   331  
   332  
   333  `
   334  	return strings.TrimSpace(helpText)
   335  }
   336  
   337  func outputsAsString(state *terraform.State, modPath []string, schema []*config.Output, includeHeader bool) string {
   338  	if state == nil {
   339  		return ""
   340  	}
   341  
   342  	ms := state.ModuleByPath(modPath)
   343  	if ms == nil {
   344  		return ""
   345  	}
   346  
   347  	outputs := ms.Outputs
   348  	outputBuf := new(bytes.Buffer)
   349  	if len(outputs) > 0 {
   350  		schemaMap := make(map[string]*config.Output)
   351  		if schema != nil {
   352  			for _, s := range schema {
   353  				schemaMap[s.Name] = s
   354  			}
   355  		}
   356  
   357  		if includeHeader {
   358  			outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n")
   359  		}
   360  
   361  		// Output the outputs in alphabetical order
   362  		keyLen := 0
   363  		ks := make([]string, 0, len(outputs))
   364  		for key, _ := range outputs {
   365  			ks = append(ks, key)
   366  			if len(key) > keyLen {
   367  				keyLen = len(key)
   368  			}
   369  		}
   370  		sort.Strings(ks)
   371  
   372  		for _, k := range ks {
   373  			schema, ok := schemaMap[k]
   374  			if ok && schema.Sensitive {
   375  				outputBuf.WriteString(fmt.Sprintf("%s = <sensitive>\n", k))
   376  				continue
   377  			}
   378  
   379  			v := outputs[k]
   380  			switch typedV := v.Value.(type) {
   381  			case string:
   382  				outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, typedV))
   383  			case []interface{}:
   384  				outputBuf.WriteString(formatListOutput("", k, typedV))
   385  				outputBuf.WriteString("\n")
   386  			case map[string]interface{}:
   387  				outputBuf.WriteString(formatMapOutput("", k, typedV))
   388  				outputBuf.WriteString("\n")
   389  			}
   390  		}
   391  	}
   392  
   393  	return strings.TrimSpace(outputBuf.String())
   394  }
   395  
   396  const outputInterrupt = `Interrupt received.
   397  Please wait for Terraform to exit or data loss may occur.
   398  Gracefully shutting down...`