github.com/ns1/terraform@v0.7.10-0.20161109153551-8949419bef40/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/go-multierror"
    12  	"github.com/hashicorp/terraform/config"
    13  	"github.com/hashicorp/terraform/helper/experiment"
    14  	"github.com/hashicorp/terraform/terraform"
    15  )
    16  
    17  // ApplyCommand is a Command implementation that applies a Terraform
    18  // configuration and actually builds or changes infrastructure.
    19  type ApplyCommand struct {
    20  	Meta
    21  
    22  	// If true, then this apply command will become the "destroy"
    23  	// command. It is just like apply but only processes a destroy.
    24  	Destroy bool
    25  
    26  	// When this channel is closed, the apply will be cancelled.
    27  	ShutdownCh <-chan struct{}
    28  }
    29  
    30  func (c *ApplyCommand) Run(args []string) int {
    31  	var destroyForce, refresh bool
    32  	args = c.Meta.process(args, true)
    33  
    34  	cmdName := "apply"
    35  	if c.Destroy {
    36  		cmdName = "destroy"
    37  	}
    38  
    39  	cmdFlags := c.Meta.flagSet(cmdName)
    40  	if c.Destroy {
    41  		cmdFlags.BoolVar(&destroyForce, "force", false, "force")
    42  	}
    43  	cmdFlags.BoolVar(&refresh, "refresh", true, "refresh")
    44  	cmdFlags.IntVar(
    45  		&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism")
    46  	cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
    47  	cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
    48  	cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
    49  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    50  	if err := cmdFlags.Parse(args); err != nil {
    51  		return 1
    52  	}
    53  
    54  	pwd, err := os.Getwd()
    55  	if err != nil {
    56  		c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
    57  		return 1
    58  	}
    59  
    60  	var configPath string
    61  	maybeInit := true
    62  	args = cmdFlags.Args()
    63  	if len(args) > 1 {
    64  		c.Ui.Error("The apply command expects at most one argument.")
    65  		cmdFlags.Usage()
    66  		return 1
    67  	} else if len(args) == 1 {
    68  		configPath = args[0]
    69  	} else {
    70  		configPath = pwd
    71  		maybeInit = false
    72  	}
    73  
    74  	// Prepare the extra hooks to count resources
    75  	countHook := new(CountHook)
    76  	stateHook := new(StateHook)
    77  	c.Meta.extraHooks = []terraform.Hook{countHook, stateHook}
    78  
    79  	if !c.Destroy && maybeInit {
    80  		// Do a detect to determine if we need to do an init + apply.
    81  		if detected, err := getter.Detect(configPath, pwd, getter.Detectors); err != nil {
    82  			c.Ui.Error(fmt.Sprintf(
    83  				"Invalid path: %s", err))
    84  			return 1
    85  		} else if !strings.HasPrefix(detected, "file") {
    86  			// If this isn't a file URL then we're doing an init +
    87  			// apply.
    88  			var init InitCommand
    89  			init.Meta = c.Meta
    90  			if code := init.Run([]string{detected}); code != 0 {
    91  				return code
    92  			}
    93  
    94  			// Change the config path to be the cwd
    95  			configPath = pwd
    96  		}
    97  	}
    98  
    99  	terraform.SetDebugInfo(DefaultDataDir)
   100  
   101  	// Check for the new apply
   102  	if experiment.Enabled(experiment.X_newApply) && !experiment.Force() {
   103  		desc := "Experimental new apply graph has been enabled. This may still\n" +
   104  			"have bugs, and should be used with care. If you'd like to continue,\n" +
   105  			"you must enter exactly 'yes' as a response."
   106  		v, err := c.UIInput().Input(&terraform.InputOpts{
   107  			Id:          "Xnew-apply",
   108  			Query:       "Experimental feature enabled: new apply graph. Continue?",
   109  			Description: desc,
   110  		})
   111  		if err != nil {
   112  			c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err))
   113  			return 1
   114  		}
   115  		if v != "yes" {
   116  			c.Ui.Output("Apply cancelled.")
   117  			return 1
   118  		}
   119  	}
   120  
   121  	// Check for the new destroy
   122  	if experiment.Enabled(experiment.X_newDestroy) && !experiment.Force() {
   123  		desc := "Experimental new destroy graph has been enabled. This may still\n" +
   124  			"have bugs, and should be used with care. If you'd like to continue,\n" +
   125  			"you must enter exactly 'yes' as a response."
   126  		v, err := c.UIInput().Input(&terraform.InputOpts{
   127  			Id:          "Xnew-destroy",
   128  			Query:       "Experimental feature enabled: new destroy graph. Continue?",
   129  			Description: desc,
   130  		})
   131  		if err != nil {
   132  			c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err))
   133  			return 1
   134  		}
   135  		if v != "yes" {
   136  			c.Ui.Output("Apply cancelled.")
   137  			return 1
   138  		}
   139  	}
   140  
   141  	// This is going to keep track of shadow errors
   142  	var shadowErr error
   143  
   144  	// Build the context based on the arguments given
   145  	ctx, planned, err := c.Context(contextOpts{
   146  		Destroy:     c.Destroy,
   147  		Path:        configPath,
   148  		StatePath:   c.Meta.statePath,
   149  		Parallelism: c.Meta.parallelism,
   150  	})
   151  	if err != nil {
   152  		c.Ui.Error(err.Error())
   153  		return 1
   154  	}
   155  	if c.Destroy && planned {
   156  		c.Ui.Error(fmt.Sprintf(
   157  			"Destroy can't be called with a plan file."))
   158  		return 1
   159  	}
   160  	if !destroyForce && c.Destroy {
   161  		// Default destroy message
   162  		desc := "Terraform will delete all your managed infrastructure.\n" +
   163  			"There is no undo. Only 'yes' will be accepted to confirm."
   164  
   165  		// If targets are specified, list those to user
   166  		if c.Meta.targets != nil {
   167  			var descBuffer bytes.Buffer
   168  			descBuffer.WriteString("Terraform will delete the following infrastructure:\n")
   169  			for _, target := range c.Meta.targets {
   170  				descBuffer.WriteString("\t")
   171  				descBuffer.WriteString(target)
   172  				descBuffer.WriteString("\n")
   173  			}
   174  			descBuffer.WriteString("There is no undo. Only 'yes' will be accepted to confirm")
   175  			desc = descBuffer.String()
   176  		}
   177  
   178  		v, err := c.UIInput().Input(&terraform.InputOpts{
   179  			Id:          "destroy",
   180  			Query:       "Do you really want to destroy?",
   181  			Description: desc,
   182  		})
   183  		if err != nil {
   184  			c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err))
   185  			return 1
   186  		}
   187  		if v != "yes" {
   188  			c.Ui.Output("Destroy cancelled.")
   189  			return 1
   190  		}
   191  	}
   192  	if !planned {
   193  		if err := ctx.Input(c.InputMode()); err != nil {
   194  			c.Ui.Error(fmt.Sprintf("Error configuring: %s", err))
   195  			return 1
   196  		}
   197  
   198  		// Record any shadow errors for later
   199  		if err := ctx.ShadowError(); err != nil {
   200  			shadowErr = multierror.Append(shadowErr, multierror.Prefix(
   201  				err, "input operation:"))
   202  		}
   203  	}
   204  	if !validateContext(ctx, c.Ui) {
   205  		return 1
   206  	}
   207  
   208  	// Plan if we haven't already
   209  	if !planned {
   210  		if refresh {
   211  			if _, err := ctx.Refresh(); err != nil {
   212  				c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
   213  				return 1
   214  			}
   215  		}
   216  
   217  		if _, err := ctx.Plan(); err != nil {
   218  			c.Ui.Error(fmt.Sprintf(
   219  				"Error creating plan: %s", err))
   220  			return 1
   221  		}
   222  
   223  		// Record any shadow errors for later
   224  		if err := ctx.ShadowError(); err != nil {
   225  			shadowErr = multierror.Append(shadowErr, multierror.Prefix(
   226  				err, "plan operation:"))
   227  		}
   228  	}
   229  
   230  	// Setup the state hook for continuous state updates
   231  	{
   232  		state, err := c.State()
   233  		if err != nil {
   234  			c.Ui.Error(fmt.Sprintf(
   235  				"Error reading state: %s", err))
   236  			return 1
   237  		}
   238  
   239  		stateHook.State = state
   240  	}
   241  
   242  	// Start the apply in a goroutine so that we can be interrupted.
   243  	var state *terraform.State
   244  	var applyErr error
   245  	doneCh := make(chan struct{})
   246  	go func() {
   247  		defer close(doneCh)
   248  		state, applyErr = ctx.Apply()
   249  
   250  		// Record any shadow errors for later
   251  		if err := ctx.ShadowError(); err != nil {
   252  			shadowErr = multierror.Append(shadowErr, multierror.Prefix(
   253  				err, "apply operation:"))
   254  		}
   255  	}()
   256  
   257  	// Wait for the apply to finish or for us to be interrupted so
   258  	// we can handle it properly.
   259  	err = nil
   260  	select {
   261  	case <-c.ShutdownCh:
   262  		c.Ui.Output("Interrupt received. Gracefully shutting down...")
   263  
   264  		// Stop execution
   265  		go ctx.Stop()
   266  
   267  		// Still get the result, since there is still one
   268  		select {
   269  		case <-c.ShutdownCh:
   270  			c.Ui.Error(
   271  				"Two interrupts received. Exiting immediately. Note that data\n" +
   272  					"loss may have occurred.")
   273  			return 1
   274  		case <-doneCh:
   275  		}
   276  	case <-doneCh:
   277  	}
   278  
   279  	// Persist the state
   280  	if state != nil {
   281  		if err := c.Meta.PersistState(state); err != nil {
   282  			c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err))
   283  			return 1
   284  		}
   285  	}
   286  
   287  	if applyErr != nil {
   288  		c.Ui.Error(fmt.Sprintf(
   289  			"Error applying plan:\n\n"+
   290  				"%s\n\n"+
   291  				"Terraform does not automatically rollback in the face of errors.\n"+
   292  				"Instead, your Terraform state file has been partially updated with\n"+
   293  				"any resources that successfully completed. Please address the error\n"+
   294  				"above and apply again to incrementally change your infrastructure.",
   295  			multierror.Flatten(applyErr)))
   296  		return 1
   297  	}
   298  
   299  	if c.Destroy {
   300  		c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
   301  			"[reset][bold][green]\n"+
   302  				"Destroy complete! Resources: %d destroyed.",
   303  			countHook.Removed)))
   304  	} else {
   305  		c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
   306  			"[reset][bold][green]\n"+
   307  				"Apply complete! Resources: %d added, %d changed, %d destroyed.",
   308  			countHook.Added,
   309  			countHook.Changed,
   310  			countHook.Removed)))
   311  	}
   312  
   313  	if countHook.Added > 0 || countHook.Changed > 0 {
   314  		c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
   315  			"[reset]\n"+
   316  				"The state of your infrastructure has been saved to the path\n"+
   317  				"below. This state is required to modify and destroy your\n"+
   318  				"infrastructure, so keep it safe. To inspect the complete state\n"+
   319  				"use the `terraform show` command.\n\n"+
   320  				"State path: %s",
   321  			c.Meta.StateOutPath())))
   322  	}
   323  
   324  	if !c.Destroy {
   325  		if outputs := outputsAsString(state, terraform.RootModulePath, ctx.Module().Config().Outputs, true); outputs != "" {
   326  			c.Ui.Output(c.Colorize().Color(outputs))
   327  		}
   328  	}
   329  
   330  	// If we have an error in the shadow graph, let the user know.
   331  	c.outputShadowError(shadowErr, applyErr == nil)
   332  
   333  	return 0
   334  }
   335  
   336  func (c *ApplyCommand) Help() string {
   337  	if c.Destroy {
   338  		return c.helpDestroy()
   339  	}
   340  
   341  	return c.helpApply()
   342  }
   343  
   344  func (c *ApplyCommand) Synopsis() string {
   345  	if c.Destroy {
   346  		return "Destroy Terraform-managed infrastructure"
   347  	}
   348  
   349  	return "Builds or changes infrastructure"
   350  }
   351  
   352  func (c *ApplyCommand) helpApply() string {
   353  	helpText := `
   354  Usage: terraform apply [options] [DIR-OR-PLAN]
   355  
   356    Builds or changes infrastructure according to Terraform configuration
   357    files in DIR.
   358  
   359    By default, apply scans the current directory for the configuration
   360    and applies the changes appropriately. However, a path to another
   361    configuration or an execution plan can be provided. Execution plans can be
   362    used to only execute a pre-determined set of actions.
   363  
   364    DIR can also be a SOURCE as given to the "init" command. In this case,
   365    apply behaves as though "init" was called followed by "apply". This only
   366    works for sources that aren't files, and only if the current working
   367    directory is empty of Terraform files. This is a shortcut for getting
   368    started.
   369  
   370  Options:
   371  
   372    -backup=path           Path to backup the existing state file before
   373                           modifying. Defaults to the "-state-out" path with
   374                           ".backup" extension. Set to "-" to disable backup.
   375  
   376    -input=true            Ask for input for variables if not directly set.
   377  
   378    -no-color              If specified, output won't contain any color.
   379  
   380    -parallelism=n         Limit the number of concurrent operations.
   381                           Defaults to 10.
   382  
   383    -refresh=true          Update state prior to checking for differences. This
   384                           has no effect if a plan file is given to apply.
   385  
   386    -state=path            Path to read and save state (unless state-out
   387                           is specified). Defaults to "terraform.tfstate".
   388  
   389    -state-out=path        Path to write state to that is different than
   390                           "-state". This can be used to preserve the old
   391                           state.
   392  
   393    -target=resource       Resource to target. Operation will be limited to this
   394                           resource and its dependencies. This flag can be used
   395                           multiple times.
   396  
   397    -var 'foo=bar'         Set a variable in the Terraform configuration. This
   398                           flag can be set multiple times.
   399  
   400    -var-file=foo          Set variables in the Terraform configuration from
   401                           a file. If "terraform.tfvars" is present, it will be
   402                           automatically loaded if this flag is not specified.
   403  
   404  
   405  `
   406  	return strings.TrimSpace(helpText)
   407  }
   408  
   409  func (c *ApplyCommand) helpDestroy() string {
   410  	helpText := `
   411  Usage: terraform destroy [options] [DIR]
   412  
   413    Destroy Terraform-managed infrastructure.
   414  
   415  Options:
   416  
   417    -backup=path           Path to backup the existing state file before
   418                           modifying. Defaults to the "-state-out" path with
   419                           ".backup" extension. Set to "-" to disable backup.
   420  
   421    -force                 Don't ask for input for destroy confirmation.
   422  
   423    -no-color              If specified, output won't contain any color.
   424  
   425    -parallelism=n         Limit the number of concurrent operations.
   426                           Defaults to 10.
   427  
   428    -refresh=true          Update state prior to checking for differences. This
   429                           has no effect if a plan file is given to apply.
   430  
   431    -state=path            Path to read and save state (unless state-out
   432                           is specified). Defaults to "terraform.tfstate".
   433  
   434    -state-out=path        Path to write state to that is different than
   435                           "-state". This can be used to preserve the old
   436                           state.
   437  
   438    -target=resource       Resource to target. Operation will be limited to this
   439                           resource and its dependencies. This flag can be used
   440                           multiple times.
   441  
   442    -var 'foo=bar'         Set a variable in the Terraform configuration. This
   443                           flag can be set multiple times.
   444  
   445    -var-file=foo          Set variables in the Terraform configuration from
   446                           a file. If "terraform.tfvars" is present, it will be
   447                           automatically loaded if this flag is not specified.
   448  
   449  
   450  `
   451  	return strings.TrimSpace(helpText)
   452  }
   453  
   454  func outputsAsString(state *terraform.State, modPath []string, schema []*config.Output, includeHeader bool) string {
   455  	if state == nil {
   456  		return ""
   457  	}
   458  
   459  	ms := state.ModuleByPath(modPath)
   460  	if ms == nil {
   461  		return ""
   462  	}
   463  
   464  	outputs := ms.Outputs
   465  	outputBuf := new(bytes.Buffer)
   466  	if len(outputs) > 0 {
   467  		schemaMap := make(map[string]*config.Output)
   468  		if schema != nil {
   469  			for _, s := range schema {
   470  				schemaMap[s.Name] = s
   471  			}
   472  		}
   473  
   474  		if includeHeader {
   475  			outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n")
   476  		}
   477  
   478  		// Output the outputs in alphabetical order
   479  		keyLen := 0
   480  		ks := make([]string, 0, len(outputs))
   481  		for key, _ := range outputs {
   482  			ks = append(ks, key)
   483  			if len(key) > keyLen {
   484  				keyLen = len(key)
   485  			}
   486  		}
   487  		sort.Strings(ks)
   488  
   489  		for _, k := range ks {
   490  			schema, ok := schemaMap[k]
   491  			if ok && schema.Sensitive {
   492  				outputBuf.WriteString(fmt.Sprintf("%s = <sensitive>\n", k))
   493  				continue
   494  			}
   495  
   496  			v := outputs[k]
   497  			switch typedV := v.Value.(type) {
   498  			case string:
   499  				outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, typedV))
   500  			case []interface{}:
   501  				outputBuf.WriteString(formatListOutput("", k, typedV))
   502  				outputBuf.WriteString("\n")
   503  			case map[string]interface{}:
   504  				outputBuf.WriteString(formatMapOutput("", k, typedV))
   505  				outputBuf.WriteString("\n")
   506  			}
   507  		}
   508  	}
   509  
   510  	return strings.TrimSpace(outputBuf.String())
   511  }