github.com/heimweh/terraform@v0.7.4/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  	if c.Destroy {
   236  		c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
   237  			"[reset][bold][green]\n"+
   238  				"Destroy complete! Resources: %d destroyed.",
   239  			countHook.Removed)))
   240  	} else {
   241  		c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
   242  			"[reset][bold][green]\n"+
   243  				"Apply complete! Resources: %d added, %d changed, %d destroyed.",
   244  			countHook.Added,
   245  			countHook.Changed,
   246  			countHook.Removed)))
   247  	}
   248  
   249  	if countHook.Added > 0 || countHook.Changed > 0 {
   250  		c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
   251  			"[reset]\n"+
   252  				"The state of your infrastructure has been saved to the path\n"+
   253  				"below. This state is required to modify and destroy your\n"+
   254  				"infrastructure, so keep it safe. To inspect the complete state\n"+
   255  				"use the `terraform show` command.\n\n"+
   256  				"State path: %s",
   257  			c.Meta.StateOutPath())))
   258  	}
   259  
   260  	if !c.Destroy {
   261  		if outputs := outputsAsString(state, terraform.RootModulePath, ctx.Module().Config().Outputs, true); outputs != "" {
   262  			c.Ui.Output(c.Colorize().Color(outputs))
   263  		}
   264  	}
   265  
   266  	return 0
   267  }
   268  
   269  func (c *ApplyCommand) Help() string {
   270  	if c.Destroy {
   271  		return c.helpDestroy()
   272  	}
   273  
   274  	return c.helpApply()
   275  }
   276  
   277  func (c *ApplyCommand) Synopsis() string {
   278  	if c.Destroy {
   279  		return "Destroy Terraform-managed infrastructure"
   280  	}
   281  
   282  	return "Builds or changes infrastructure"
   283  }
   284  
   285  func (c *ApplyCommand) helpApply() string {
   286  	helpText := `
   287  Usage: terraform apply [options] [DIR-OR-PLAN]
   288  
   289    Builds or changes infrastructure according to Terraform configuration
   290    files in DIR.
   291  
   292    By default, apply scans the current directory for the configuration
   293    and applies the changes appropriately. However, a path to another
   294    configuration or an execution plan can be provided. Execution plans can be
   295    used to only execute a pre-determined set of actions.
   296  
   297    DIR can also be a SOURCE as given to the "init" command. In this case,
   298    apply behaves as though "init" was called followed by "apply". This only
   299    works for sources that aren't files, and only if the current working
   300    directory is empty of Terraform files. This is a shortcut for getting
   301    started.
   302  
   303  Options:
   304  
   305    -backup=path           Path to backup the existing state file before
   306                           modifying. Defaults to the "-state-out" path with
   307                           ".backup" extension. Set to "-" to disable backup.
   308  
   309    -input=true            Ask for input for variables if not directly set.
   310  
   311    -no-color              If specified, output won't contain any color.
   312  
   313    -parallelism=n         Limit the number of concurrent operations.
   314                           Defaults to 10.
   315  
   316    -refresh=true          Update state prior to checking for differences. This
   317                           has no effect if a plan file is given to apply.
   318  
   319    -state=path            Path to read and save state (unless state-out
   320                           is specified). Defaults to "terraform.tfstate".
   321  
   322    -state-out=path        Path to write state to that is different than
   323                           "-state". This can be used to preserve the old
   324                           state.
   325  
   326    -target=resource       Resource to target. Operation will be limited to this
   327                           resource and its dependencies. This flag can be used
   328                           multiple times.
   329  
   330    -var 'foo=bar'         Set a variable in the Terraform configuration. This
   331                           flag can be set multiple times.
   332  
   333    -var-file=foo          Set variables in the Terraform configuration from
   334                           a file. If "terraform.tfvars" is present, it will be
   335                           automatically loaded if this flag is not specified.
   336  
   337  
   338  `
   339  	return strings.TrimSpace(helpText)
   340  }
   341  
   342  func (c *ApplyCommand) helpDestroy() string {
   343  	helpText := `
   344  Usage: terraform destroy [options] [DIR]
   345  
   346    Destroy Terraform-managed infrastructure.
   347  
   348  Options:
   349  
   350    -backup=path           Path to backup the existing state file before
   351                           modifying. Defaults to the "-state-out" path with
   352                           ".backup" extension. Set to "-" to disable backup.
   353  
   354    -force                 Don't ask for input for destroy confirmation.
   355  
   356    -no-color              If specified, output won't contain any color.
   357  
   358    -parallelism=n         Limit the number of concurrent operations.
   359                           Defaults to 10.
   360  
   361    -refresh=true          Update state prior to checking for differences. This
   362                           has no effect if a plan file is given to apply.
   363  
   364    -state=path            Path to read and save state (unless state-out
   365                           is specified). Defaults to "terraform.tfstate".
   366  
   367    -state-out=path        Path to write state to that is different than
   368                           "-state". This can be used to preserve the old
   369                           state.
   370  
   371    -target=resource       Resource to target. Operation will be limited to this
   372                           resource and its dependencies. This flag can be used
   373                           multiple times.
   374  
   375    -var 'foo=bar'         Set a variable in the Terraform configuration. This
   376                           flag can be set multiple times.
   377  
   378    -var-file=foo          Set variables in the Terraform configuration from
   379                           a file. If "terraform.tfvars" is present, it will be
   380                           automatically loaded if this flag is not specified.
   381  
   382  
   383  `
   384  	return strings.TrimSpace(helpText)
   385  }
   386  
   387  func outputsAsString(state *terraform.State, modPath []string, schema []*config.Output, includeHeader bool) string {
   388  	if state == nil {
   389  		return ""
   390  	}
   391  
   392  	outputs := state.ModuleByPath(modPath).Outputs
   393  	outputBuf := new(bytes.Buffer)
   394  	if len(outputs) > 0 {
   395  		schemaMap := make(map[string]*config.Output)
   396  		if schema != nil {
   397  			for _, s := range schema {
   398  				schemaMap[s.Name] = s
   399  			}
   400  		}
   401  
   402  		if includeHeader {
   403  			outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n")
   404  		}
   405  
   406  		// Output the outputs in alphabetical order
   407  		keyLen := 0
   408  		ks := make([]string, 0, len(outputs))
   409  		for key, _ := range outputs {
   410  			ks = append(ks, key)
   411  			if len(key) > keyLen {
   412  				keyLen = len(key)
   413  			}
   414  		}
   415  		sort.Strings(ks)
   416  
   417  		for _, k := range ks {
   418  			schema, ok := schemaMap[k]
   419  			if ok && schema.Sensitive {
   420  				outputBuf.WriteString(fmt.Sprintf("%s = <sensitive>\n", k))
   421  				continue
   422  			}
   423  
   424  			v := outputs[k]
   425  			switch typedV := v.Value.(type) {
   426  			case string:
   427  				outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, typedV))
   428  			case []interface{}:
   429  				outputBuf.WriteString(formatListOutput("", k, typedV))
   430  				outputBuf.WriteString("\n")
   431  			case map[string]interface{}:
   432  				outputBuf.WriteString(formatMapOutput("", k, typedV))
   433  				outputBuf.WriteString("\n")
   434  			}
   435  		}
   436  	}
   437  
   438  	return strings.TrimSpace(outputBuf.String())
   439  }