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