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