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