github.com/bengesoff/terraform@v0.3.1-0.20141018223233-b25a53629922/command/apply.go (about)

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