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