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