github.com/jgadling/terraform@v0.3.8-0.20150227214559-abd68c2c87bc/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/terraform/config/module"
    11  	"github.com/hashicorp/terraform/terraform"
    12  )
    13  
    14  // ApplyCommand is a Command implementation that applies a Terraform
    15  // configuration and actually builds or changes infrastructure.
    16  type ApplyCommand struct {
    17  	Meta
    18  
    19  	// If true, then this apply command will become the "destroy"
    20  	// command. It is just like apply but only processes a destroy.
    21  	Destroy bool
    22  
    23  	// When this channel is closed, the apply will be cancelled.
    24  	ShutdownCh <-chan struct{}
    25  }
    26  
    27  func (c *ApplyCommand) Run(args []string) int {
    28  	var destroyForce, refresh bool
    29  	args = c.Meta.process(args, true)
    30  
    31  	cmdName := "apply"
    32  	if c.Destroy {
    33  		cmdName = "destroy"
    34  	}
    35  
    36  	cmdFlags := c.Meta.flagSet(cmdName)
    37  	if c.Destroy {
    38  		cmdFlags.BoolVar(&destroyForce, "force", false, "force")
    39  	}
    40  	cmdFlags.BoolVar(&refresh, "refresh", true, "refresh")
    41  	cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
    42  	cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
    43  	cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
    44  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    45  	if err := cmdFlags.Parse(args); err != nil {
    46  		return 1
    47  	}
    48  
    49  	pwd, err := os.Getwd()
    50  	if err != nil {
    51  		c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
    52  		return 1
    53  	}
    54  
    55  	var configPath string
    56  	maybeInit := true
    57  	args = cmdFlags.Args()
    58  	if len(args) > 1 {
    59  		c.Ui.Error("The apply command expects at most one argument.")
    60  		cmdFlags.Usage()
    61  		return 1
    62  	} else if len(args) == 1 {
    63  		configPath = args[0]
    64  	} else {
    65  		configPath = pwd
    66  		maybeInit = false
    67  	}
    68  
    69  	// Prepare the extra hooks to count resources
    70  	countHook := new(CountHook)
    71  	stateHook := new(StateHook)
    72  	c.Meta.extraHooks = []terraform.Hook{countHook, stateHook}
    73  
    74  	if !c.Destroy && maybeInit {
    75  		// Do a detect to determine if we need to do an init + apply.
    76  		if detected, err := module.Detect(configPath, pwd); err != nil {
    77  			c.Ui.Error(fmt.Sprintf(
    78  				"Invalid path: %s", err))
    79  			return 1
    80  		} else if !strings.HasPrefix(detected, "file") {
    81  			// If this isn't a file URL then we're doing an init +
    82  			// apply.
    83  			var init InitCommand
    84  			init.Meta = c.Meta
    85  			if code := init.Run([]string{detected}); code != 0 {
    86  				return code
    87  			}
    88  
    89  			// Change the config path to be the cwd
    90  			configPath = pwd
    91  		}
    92  	}
    93  
    94  	// Build the context based on the arguments given
    95  	ctx, planned, err := c.Context(contextOpts{
    96  		Path:      configPath,
    97  		StatePath: c.Meta.statePath,
    98  	})
    99  	if err != nil {
   100  		c.Ui.Error(err.Error())
   101  		return 1
   102  	}
   103  	if c.Destroy && planned {
   104  		c.Ui.Error(fmt.Sprintf(
   105  			"Destroy can't be called with a plan file."))
   106  		return 1
   107  	}
   108  	if !destroyForce && c.Destroy {
   109  		v, err := c.UIInput().Input(&terraform.InputOpts{
   110  			Id:    "destroy",
   111  			Query: "Do you really want to destroy?",
   112  			Description: "Terraform will delete all your managed infrastructure.\n" +
   113  				"There is no undo. Only 'yes' will be accepted to confirm.",
   114  		})
   115  		if err != nil {
   116  			c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err))
   117  			return 1
   118  		}
   119  		if v != "yes" {
   120  			c.Ui.Output("Destroy cancelled.")
   121  			return 1
   122  		}
   123  	}
   124  	if !planned {
   125  		if err := ctx.Input(c.InputMode()); err != nil {
   126  			c.Ui.Error(fmt.Sprintf("Error configuring: %s", err))
   127  			return 1
   128  		}
   129  	}
   130  	if !validateContext(ctx, c.Ui) {
   131  		return 1
   132  	}
   133  
   134  	// Plan if we haven't already
   135  	if !planned {
   136  		if refresh {
   137  			if _, err := ctx.Refresh(); err != nil {
   138  				c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
   139  				return 1
   140  			}
   141  		}
   142  
   143  		var opts terraform.PlanOpts
   144  		if c.Destroy {
   145  			opts.Destroy = true
   146  		}
   147  
   148  		if _, err := ctx.Plan(&opts); 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 continous 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  			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 we have outputs, then output those at the end.
   237  	var outputs map[string]string
   238  	if !c.Destroy && state != nil {
   239  		outputs = state.RootModule().Outputs
   240  	}
   241  	if len(outputs) > 0 {
   242  		outputBuf := new(bytes.Buffer)
   243  		outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n")
   244  
   245  		// Output the outputs in alphabetical order
   246  		keyLen := 0
   247  		keys := make([]string, 0, len(outputs))
   248  		for key, _ := range outputs {
   249  			keys = append(keys, key)
   250  			if len(key) > keyLen {
   251  				keyLen = len(key)
   252  			}
   253  		}
   254  		sort.Strings(keys)
   255  
   256  		for _, k := range keys {
   257  			v := outputs[k]
   258  
   259  			outputBuf.WriteString(fmt.Sprintf(
   260  				"  %s%s = %s\n",
   261  				k,
   262  				strings.Repeat(" ", keyLen-len(k)),
   263  				v))
   264  		}
   265  
   266  		c.Ui.Output(c.Colorize().Color(
   267  			strings.TrimSpace(outputBuf.String())))
   268  	}
   269  
   270  	return 0
   271  }
   272  
   273  func (c *ApplyCommand) Help() string {
   274  	if c.Destroy {
   275  		return c.helpDestroy()
   276  	}
   277  
   278  	return c.helpApply()
   279  }
   280  
   281  func (c *ApplyCommand) Synopsis() string {
   282  	if c.Destroy {
   283  		return "Destroy Terraform-managed infrastructure"
   284  	}
   285  
   286  	return "Builds or changes infrastructure"
   287  }
   288  
   289  func (c *ApplyCommand) helpApply() string {
   290  	helpText := `
   291  Usage: terraform apply [options] [DIR]
   292  
   293    Builds or changes infrastructure according to Terraform configuration
   294    files in DIR.
   295  
   296    DIR can also be a SOURCE as given to the "init" command. In this case,
   297    apply behaves as though "init" was called followed by "apply". This only
   298    works for sources that aren't files, and only if the current working
   299    directory is empty of Terraform files. This is a shortcut for getting
   300    started.
   301  
   302  Options:
   303  
   304    -backup=path           Path to backup the existing state file before
   305                           modifying. Defaults to the "-state-out" path with
   306                           ".backup" extension. Set to "-" to disable backup.
   307  
   308    -input=true            Ask for input for variables if not directly set.
   309  
   310    -no-color              If specified, output won't contain any color.
   311  
   312    -refresh=true          Update state prior to checking for differences. This
   313                           has no effect if a plan file is given to apply.
   314  
   315    -state=path            Path to read and save state (unless state-out
   316                           is specified). Defaults to "terraform.tfstate".
   317  
   318    -state-out=path        Path to write state to that is different than
   319                           "-state". This can be used to preserve the old
   320                           state.
   321  
   322    -var 'foo=bar'         Set a variable in the Terraform configuration. This
   323                           flag can be set multiple times.
   324  
   325    -var-file=foo          Set variables in the Terraform configuration from
   326                           a file. If "terraform.tfvars" is present, it will be
   327                           automatically loaded if this flag is not specified.
   328  
   329  
   330  `
   331  	return strings.TrimSpace(helpText)
   332  }
   333  
   334  func (c *ApplyCommand) helpDestroy() string {
   335  	helpText := `
   336  Usage: terraform destroy [options] [DIR]
   337  
   338    Destroy Terraform-managed infrastructure.
   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    -force                 Don't ask for input for destroy confirmation.
   347  
   348    -no-color              If specified, output won't contain any color.
   349  
   350    -refresh=true          Update state prior to checking for differences. This
   351                           has no effect if a plan file is given to apply.
   352  
   353    -state=path            Path to read and save state (unless state-out
   354                           is specified). Defaults to "terraform.tfstate".
   355  
   356    -state-out=path        Path to write state to that is different than
   357                           "-state". This can be used to preserve the old
   358                           state.
   359  
   360    -var 'foo=bar'         Set a variable in the Terraform configuration. This
   361                           flag can be set multiple times.
   362  
   363    -var-file=foo          Set variables in the Terraform configuration from
   364                           a file. If "terraform.tfvars" is present, it will be
   365                           automatically loaded if this flag is not specified.
   366  
   367  
   368  `
   369  	return strings.TrimSpace(helpText)
   370  }