github.com/icebourg/terraform@v0.6.5-0.20151015205227-263cc1b85535/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  		v, err := c.UIInput().Input(&terraform.InputOpts{
   115  			Id:    "destroy",
   116  			Query: "Do you really want to destroy?",
   117  			Description: "Terraform will delete all your managed infrastructure.\n" +
   118  				"There is no undo. Only 'yes' will be accepted to confirm.",
   119  		})
   120  		if err != nil {
   121  			c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err))
   122  			return 1
   123  		}
   124  		if v != "yes" {
   125  			c.Ui.Output("Destroy cancelled.")
   126  			return 1
   127  		}
   128  	}
   129  	if !planned {
   130  		if err := ctx.Input(c.InputMode()); err != nil {
   131  			c.Ui.Error(fmt.Sprintf("Error configuring: %s", err))
   132  			return 1
   133  		}
   134  	}
   135  	if !validateContext(ctx, c.Ui) {
   136  		return 1
   137  	}
   138  
   139  	// Plan if we haven't already
   140  	if !planned {
   141  		if refresh {
   142  			if _, err := ctx.Refresh(); err != nil {
   143  				c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
   144  				return 1
   145  			}
   146  		}
   147  
   148  		if _, err := ctx.Plan(); err != nil {
   149  			c.Ui.Error(fmt.Sprintf(
   150  				"Error creating plan: %s", err))
   151  			return 1
   152  		}
   153  	}
   154  
   155  	// Setup the state hook for continuous state updates
   156  	{
   157  		state, err := c.State()
   158  		if err != nil {
   159  			c.Ui.Error(fmt.Sprintf(
   160  				"Error reading state: %s", err))
   161  			return 1
   162  		}
   163  
   164  		stateHook.State = state
   165  	}
   166  
   167  	// Start the apply in a goroutine so that we can be interrupted.
   168  	var state *terraform.State
   169  	var applyErr error
   170  	doneCh := make(chan struct{})
   171  	go func() {
   172  		defer close(doneCh)
   173  		state, applyErr = ctx.Apply()
   174  	}()
   175  
   176  	// Wait for the apply to finish or for us to be interrupted so
   177  	// we can handle it properly.
   178  	err = nil
   179  	select {
   180  	case <-c.ShutdownCh:
   181  		c.Ui.Output("Interrupt received. Gracefully shutting down...")
   182  
   183  		// Stop execution
   184  		go ctx.Stop()
   185  
   186  		// Still get the result, since there is still one
   187  		select {
   188  		case <-c.ShutdownCh:
   189  			c.Ui.Error(
   190  				"Two interrupts received. Exiting immediately. Note that data\n" +
   191  					"loss may have occurred.")
   192  			return 1
   193  		case <-doneCh:
   194  		}
   195  	case <-doneCh:
   196  	}
   197  
   198  	// Persist the state
   199  	if state != nil {
   200  		if err := c.Meta.PersistState(state); err != nil {
   201  			c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err))
   202  			return 1
   203  		}
   204  	}
   205  
   206  	if applyErr != nil {
   207  		c.Ui.Error(fmt.Sprintf(
   208  			"Error applying plan:\n\n"+
   209  				"%s\n\n"+
   210  				"Terraform does not automatically rollback in the face of errors.\n"+
   211  				"Instead, your Terraform state file has been partially updated with\n"+
   212  				"any resources that successfully completed. Please address the error\n"+
   213  				"above and apply again to incrementally change your infrastructure.",
   214  			multierror.Flatten(applyErr)))
   215  		return 1
   216  	}
   217  
   218  	c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
   219  		"[reset][bold][green]\n"+
   220  			"Apply complete! Resources: %d added, %d changed, %d destroyed.",
   221  		countHook.Added,
   222  		countHook.Changed,
   223  		countHook.Removed)))
   224  
   225  	if countHook.Added > 0 || countHook.Changed > 0 {
   226  		c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
   227  			"[reset]\n"+
   228  				"The state of your infrastructure has been saved to the path\n"+
   229  				"below. This state is required to modify and destroy your\n"+
   230  				"infrastructure, so keep it safe. To inspect the complete state\n"+
   231  				"use the `terraform show` command.\n\n"+
   232  				"State path: %s",
   233  			c.Meta.StateOutPath())))
   234  	}
   235  
   236  	if !c.Destroy {
   237  		if outputs := outputsAsString(state); outputs != "" {
   238  			c.Ui.Output(c.Colorize().Color(outputs))
   239  		}
   240  	}
   241  
   242  	return 0
   243  }
   244  
   245  func (c *ApplyCommand) Help() string {
   246  	if c.Destroy {
   247  		return c.helpDestroy()
   248  	}
   249  
   250  	return c.helpApply()
   251  }
   252  
   253  func (c *ApplyCommand) Synopsis() string {
   254  	if c.Destroy {
   255  		return "Destroy Terraform-managed infrastructure"
   256  	}
   257  
   258  	return "Builds or changes infrastructure"
   259  }
   260  
   261  func (c *ApplyCommand) helpApply() string {
   262  	helpText := `
   263  Usage: terraform apply [options] [DIR]
   264  
   265    Builds or changes infrastructure according to Terraform configuration
   266    files in DIR.
   267  
   268    DIR can also be a SOURCE as given to the "init" command. In this case,
   269    apply behaves as though "init" was called followed by "apply". This only
   270    works for sources that aren't files, and only if the current working
   271    directory is empty of Terraform files. This is a shortcut for getting
   272    started.
   273  
   274  Options:
   275  
   276    -backup=path           Path to backup the existing state file before
   277                           modifying. Defaults to the "-state-out" path with
   278                           ".backup" extension. Set to "-" to disable backup.
   279  
   280    -input=true            Ask for input for variables if not directly set.
   281  
   282    -no-color              If specified, output won't contain any color.
   283  
   284    -parallelism=n         Limit the number of concurrent operations.
   285                           Defaults to 10.
   286  
   287    -refresh=true          Update state prior to checking for differences. This
   288                           has no effect if a plan file is given to apply.
   289  
   290    -state=path            Path to read and save state (unless state-out
   291                           is specified). Defaults to "terraform.tfstate".
   292  
   293    -state-out=path        Path to write state to that is different than
   294                           "-state". This can be used to preserve the old
   295                           state.
   296  
   297    -target=resource       Resource to target. Operation will be limited to this
   298                           resource and its dependencies. This flag can be used
   299                           multiple times.
   300  
   301    -var 'foo=bar'         Set a variable in the Terraform configuration. This
   302                           flag can be set multiple times.
   303  
   304    -var-file=foo          Set variables in the Terraform configuration from
   305                           a file. If "terraform.tfvars" is present, it will be
   306                           automatically loaded if this flag is not specified.
   307  
   308  
   309  `
   310  	return strings.TrimSpace(helpText)
   311  }
   312  
   313  func (c *ApplyCommand) helpDestroy() string {
   314  	helpText := `
   315  Usage: terraform destroy [options] [DIR]
   316  
   317    Destroy Terraform-managed infrastructure.
   318  
   319  Options:
   320  
   321    -backup=path           Path to backup the existing state file before
   322                           modifying. Defaults to the "-state-out" path with
   323                           ".backup" extension. Set to "-" to disable backup.
   324  
   325    -force                 Don't ask for input for destroy confirmation.
   326  
   327    -no-color              If specified, output won't contain any color.
   328  
   329    -parallelism=n         Limit the number of concurrent operations.
   330                           Defaults to 10.
   331  
   332    -refresh=true          Update state prior to checking for differences. This
   333                           has no effect if a plan file is given to apply.
   334  
   335    -state=path            Path to read and save state (unless state-out
   336                           is specified). Defaults to "terraform.tfstate".
   337  
   338    -state-out=path        Path to write state to that is different than
   339                           "-state". This can be used to preserve the old
   340                           state.
   341  
   342    -target=resource       Resource to target. Operation will be limited to this
   343                           resource and its dependencies. This flag can be used
   344                           multiple times.
   345  
   346    -var 'foo=bar'         Set a variable in the Terraform configuration. This
   347                           flag can be set multiple times.
   348  
   349    -var-file=foo          Set variables in the Terraform configuration from
   350                           a file. If "terraform.tfvars" is present, it will be
   351                           automatically loaded if this flag is not specified.
   352  
   353  
   354  `
   355  	return strings.TrimSpace(helpText)
   356  }
   357  
   358  func outputsAsString(state *terraform.State) string {
   359  	if state == nil {
   360  		return ""
   361  	}
   362  
   363  	outputs := state.RootModule().Outputs
   364  	outputBuf := new(bytes.Buffer)
   365  	if len(outputs) > 0 {
   366  		outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n")
   367  
   368  		// Output the outputs in alphabetical order
   369  		keyLen := 0
   370  		keys := make([]string, 0, len(outputs))
   371  		for key, _ := range outputs {
   372  			keys = append(keys, key)
   373  			if len(key) > keyLen {
   374  				keyLen = len(key)
   375  			}
   376  		}
   377  		sort.Strings(keys)
   378  
   379  		for _, k := range keys {
   380  			v := outputs[k]
   381  
   382  			outputBuf.WriteString(fmt.Sprintf(
   383  				"  %s%s = %s\n",
   384  				k,
   385  				strings.Repeat(" ", keyLen-len(k)),
   386  				v))
   387  		}
   388  	}
   389  
   390  	return strings.TrimSpace(outputBuf.String())
   391  }