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