github.com/paybyphone/terraform@v0.9.5-0.20170613192930-9706042ddd51/command/apply.go (about)

     1  package command
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"os"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/hashicorp/go-getter"
    12  	"github.com/hashicorp/terraform/backend"
    13  	"github.com/hashicorp/terraform/config"
    14  	"github.com/hashicorp/terraform/config/module"
    15  	"github.com/hashicorp/terraform/terraform"
    16  )
    17  
    18  // ApplyCommand is a Command implementation that applies a Terraform
    19  // configuration and actually builds or changes infrastructure.
    20  type ApplyCommand struct {
    21  	Meta
    22  
    23  	// If true, then this apply command will become the "destroy"
    24  	// command. It is just like apply but only processes a destroy.
    25  	Destroy bool
    26  
    27  	// When this channel is closed, the apply will be cancelled.
    28  	ShutdownCh <-chan struct{}
    29  }
    30  
    31  func (c *ApplyCommand) Run(args []string) int {
    32  	var destroyForce, refresh bool
    33  	args = c.Meta.process(args, true)
    34  
    35  	cmdName := "apply"
    36  	if c.Destroy {
    37  		cmdName = "destroy"
    38  	}
    39  
    40  	cmdFlags := c.Meta.flagSet(cmdName)
    41  	if c.Destroy {
    42  		cmdFlags.BoolVar(&destroyForce, "force", false, "force")
    43  	}
    44  	cmdFlags.BoolVar(&refresh, "refresh", true, "refresh")
    45  	cmdFlags.IntVar(
    46  		&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism")
    47  	cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path")
    48  	cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
    49  	cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
    50  	cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state")
    51  	cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout")
    52  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    53  	if err := cmdFlags.Parse(args); err != nil {
    54  		return 1
    55  	}
    56  
    57  	// Get the args. The "maybeInit" flag tracks whether we may need to
    58  	// initialize the configuration from a remote path. This is true as long
    59  	// as we have an argument.
    60  	args = cmdFlags.Args()
    61  	maybeInit := len(args) == 1
    62  	configPath, err := ModulePath(args)
    63  	if err != nil {
    64  		c.Ui.Error(err.Error())
    65  		return 1
    66  	}
    67  
    68  	if !c.Destroy && maybeInit {
    69  		// We need the pwd for the getter operation below
    70  		pwd, err := os.Getwd()
    71  		if err != nil {
    72  			c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
    73  			return 1
    74  		}
    75  
    76  		// Do a detect to determine if we need to do an init + apply.
    77  		if detected, err := getter.Detect(configPath, pwd, getter.Detectors); err != nil {
    78  			c.Ui.Error(fmt.Sprintf(
    79  				"Invalid path: %s", err))
    80  			return 1
    81  		} else if !strings.HasPrefix(detected, "file") {
    82  			// If this isn't a file URL then we're doing an init +
    83  			// apply.
    84  			var init InitCommand
    85  			init.Meta = c.Meta
    86  			if code := init.Run([]string{detected}); code != 0 {
    87  				return code
    88  			}
    89  
    90  			// Change the config path to be the cwd
    91  			configPath = pwd
    92  		}
    93  	}
    94  
    95  	// Check if the path is a plan
    96  	plan, err := c.Plan(configPath)
    97  	if err != nil {
    98  		c.Ui.Error(err.Error())
    99  		return 1
   100  	}
   101  	if c.Destroy && plan != nil {
   102  		c.Ui.Error(fmt.Sprintf(
   103  			"Destroy can't be called with a plan file."))
   104  		return 1
   105  	}
   106  	if plan != nil {
   107  		// Reset the config path for backend loading
   108  		configPath = ""
   109  	}
   110  
   111  	// Load the module if we don't have one yet (not running from plan)
   112  	var mod *module.Tree
   113  	if plan == nil {
   114  		mod, err = c.Module(configPath)
   115  		if err != nil {
   116  			c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
   117  			return 1
   118  		}
   119  	}
   120  
   121  	/*
   122  		terraform.SetDebugInfo(DefaultDataDir)
   123  
   124  		// Check for the legacy graph
   125  		if experiment.Enabled(experiment.X_legacyGraph) {
   126  			c.Ui.Output(c.Colorize().Color(
   127  				"[reset][bold][yellow]" +
   128  					"Legacy graph enabled! This will use the graph from Terraform 0.7.x\n" +
   129  					"to execute this operation. This will be removed in the future so\n" +
   130  					"please report any issues causing you to use this to the Terraform\n" +
   131  					"project.\n\n"))
   132  		}
   133  	*/
   134  
   135  	var conf *config.Config
   136  	if mod != nil {
   137  		conf = mod.Config()
   138  	}
   139  
   140  	// Load the backend
   141  	b, err := c.Backend(&BackendOpts{
   142  		Config: conf,
   143  		Plan:   plan,
   144  	})
   145  	if err != nil {
   146  		c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
   147  		return 1
   148  	}
   149  
   150  	// If we're not forcing and we're destroying, verify with the
   151  	// user at this point.
   152  	if !destroyForce && c.Destroy {
   153  		// Default destroy message
   154  		desc := "Terraform will delete all your managed infrastructure.\n" +
   155  			"There is no undo. Only 'yes' will be accepted to confirm."
   156  
   157  		// If targets are specified, list those to user
   158  		if c.Meta.targets != nil {
   159  			var descBuffer bytes.Buffer
   160  			descBuffer.WriteString("Terraform will delete the following infrastructure:\n")
   161  			for _, target := range c.Meta.targets {
   162  				descBuffer.WriteString("\t")
   163  				descBuffer.WriteString(target)
   164  				descBuffer.WriteString("\n")
   165  			}
   166  			descBuffer.WriteString("There is no undo. Only 'yes' will be accepted to confirm")
   167  			desc = descBuffer.String()
   168  		}
   169  
   170  		v, err := c.UIInput().Input(&terraform.InputOpts{
   171  			Id:          "destroy",
   172  			Query:       "Do you really want to destroy?",
   173  			Description: desc,
   174  		})
   175  		if err != nil {
   176  			c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err))
   177  			return 1
   178  		}
   179  		if v != "yes" {
   180  			c.Ui.Output("Destroy cancelled.")
   181  			return 1
   182  		}
   183  	}
   184  
   185  	// Build the operation
   186  	opReq := c.Operation()
   187  	opReq.Destroy = c.Destroy
   188  	opReq.Module = mod
   189  	opReq.Plan = plan
   190  	opReq.PlanRefresh = refresh
   191  	opReq.Type = backend.OperationTypeApply
   192  
   193  	// Perform the operation
   194  	ctx, ctxCancel := context.WithCancel(context.Background())
   195  	defer ctxCancel()
   196  	op, err := b.Operation(ctx, opReq)
   197  	if err != nil {
   198  		c.Ui.Error(fmt.Sprintf("Error starting operation: %s", err))
   199  		return 1
   200  	}
   201  
   202  	// Wait for the operation to complete or an interrupt to occur
   203  	select {
   204  	case <-c.ShutdownCh:
   205  		// Cancel our context so we can start gracefully exiting
   206  		ctxCancel()
   207  
   208  		// Notify the user
   209  		c.Ui.Output(outputInterrupt)
   210  
   211  		// Still get the result, since there is still one
   212  		select {
   213  		case <-c.ShutdownCh:
   214  			c.Ui.Error(
   215  				"Two interrupts received. Exiting immediately. Note that data\n" +
   216  					"loss may have occurred.")
   217  			return 1
   218  		case <-op.Done():
   219  		}
   220  	case <-op.Done():
   221  		if err := op.Err; err != nil {
   222  			c.Ui.Error(err.Error())
   223  			return 1
   224  		}
   225  	}
   226  
   227  	if !c.Destroy {
   228  		// Get the right module that we used. If we ran a plan, then use
   229  		// that module.
   230  		if plan != nil {
   231  			mod = plan.Module
   232  		}
   233  
   234  		if outputs := outputsAsString(op.State, terraform.RootModulePath, mod.Config().Outputs, true); outputs != "" {
   235  			c.Ui.Output(c.Colorize().Color(outputs))
   236  		}
   237  	}
   238  
   239  	return 0
   240  }
   241  
   242  func (c *ApplyCommand) Help() string {
   243  	if c.Destroy {
   244  		return c.helpDestroy()
   245  	}
   246  
   247  	return c.helpApply()
   248  }
   249  
   250  func (c *ApplyCommand) Synopsis() string {
   251  	if c.Destroy {
   252  		return "Destroy Terraform-managed infrastructure"
   253  	}
   254  
   255  	return "Builds or changes infrastructure"
   256  }
   257  
   258  func (c *ApplyCommand) helpApply() string {
   259  	helpText := `
   260  Usage: terraform apply [options] [DIR-OR-PLAN]
   261  
   262    Builds or changes infrastructure according to Terraform configuration
   263    files in DIR.
   264  
   265    By default, apply scans the current directory for the configuration
   266    and applies the changes appropriately. However, a path to another
   267    configuration or an execution plan can be provided. Execution plans can be
   268    used to only execute a pre-determined set of actions.
   269  
   270    DIR can also be a SOURCE as given to the "init" command. In this case,
   271    apply behaves as though "init" was called followed by "apply". This only
   272    works for sources that aren't files, and only if the current working
   273    directory is empty of Terraform files. This is a shortcut for getting
   274    started.
   275  
   276  Options:
   277  
   278    -backup=path           Path to backup the existing state file before
   279                           modifying. Defaults to the "-state-out" path with
   280                           ".backup" extension. Set to "-" to disable backup.
   281  
   282    -lock=true             Lock the state file when locking is supported.
   283  
   284    -lock-timeout=0s       Duration to retry a state lock.
   285  
   286    -input=true            Ask for input for variables if not directly set.
   287  
   288    -no-color              If specified, output won't contain any color.
   289  
   290    -parallelism=n         Limit the number of parallel resource operations.
   291                           Defaults to 10.
   292  
   293    -refresh=true          Update state prior to checking for differences. This
   294                           has no effect if a plan file is given to apply.
   295  
   296    -state=path            Path to read and save state (unless state-out
   297                           is specified). Defaults to "terraform.tfstate".
   298  
   299    -state-out=path        Path to write state to that is different than
   300                           "-state". This can be used to preserve the old
   301                           state.
   302  
   303    -target=resource       Resource to target. Operation will be limited to this
   304                           resource and its dependencies. This flag can be used
   305                           multiple times.
   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    -lock=true             Lock the state file when locking is supported.
   334  
   335    -lock-timeout=0s       Duration to retry a state lock.
   336  
   337    -no-color              If specified, output won't contain any color.
   338  
   339    -parallelism=n         Limit the number of concurrent operations.
   340                           Defaults to 10.
   341  
   342    -refresh=true          Update state prior to checking for differences. This
   343                           has no effect if a plan file is given to apply.
   344  
   345    -state=path            Path to read and save state (unless state-out
   346                           is specified). Defaults to "terraform.tfstate".
   347  
   348    -state-out=path        Path to write state to that is different than
   349                           "-state". This can be used to preserve the old
   350                           state.
   351  
   352    -target=resource       Resource to target. Operation will be limited to this
   353                           resource and its dependencies. This flag can be used
   354                           multiple times.
   355  
   356    -var 'foo=bar'         Set a variable in the Terraform configuration. This
   357                           flag can be set multiple times.
   358  
   359    -var-file=foo          Set variables in the Terraform configuration from
   360                           a file. If "terraform.tfvars" is present, it will be
   361                           automatically loaded if this flag is not specified.
   362  
   363  
   364  `
   365  	return strings.TrimSpace(helpText)
   366  }
   367  
   368  func outputsAsString(state *terraform.State, modPath []string, schema []*config.Output, includeHeader bool) string {
   369  	if state == nil {
   370  		return ""
   371  	}
   372  
   373  	ms := state.ModuleByPath(modPath)
   374  	if ms == nil {
   375  		return ""
   376  	}
   377  
   378  	outputs := ms.Outputs
   379  	outputBuf := new(bytes.Buffer)
   380  	if len(outputs) > 0 {
   381  		schemaMap := make(map[string]*config.Output)
   382  		if schema != nil {
   383  			for _, s := range schema {
   384  				schemaMap[s.Name] = s
   385  			}
   386  		}
   387  
   388  		if includeHeader {
   389  			outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n")
   390  		}
   391  
   392  		// Output the outputs in alphabetical order
   393  		keyLen := 0
   394  		ks := make([]string, 0, len(outputs))
   395  		for key, _ := range outputs {
   396  			ks = append(ks, key)
   397  			if len(key) > keyLen {
   398  				keyLen = len(key)
   399  			}
   400  		}
   401  		sort.Strings(ks)
   402  
   403  		for _, k := range ks {
   404  			schema, ok := schemaMap[k]
   405  			if ok && schema.Sensitive {
   406  				outputBuf.WriteString(fmt.Sprintf("%s = <sensitive>\n", k))
   407  				continue
   408  			}
   409  
   410  			v := outputs[k]
   411  			switch typedV := v.Value.(type) {
   412  			case string:
   413  				outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, typedV))
   414  			case []interface{}:
   415  				outputBuf.WriteString(formatListOutput("", k, typedV))
   416  				outputBuf.WriteString("\n")
   417  			case map[string]interface{}:
   418  				outputBuf.WriteString(formatMapOutput("", k, typedV))
   419  				outputBuf.WriteString("\n")
   420  			}
   421  		}
   422  	}
   423  
   424  	return strings.TrimSpace(outputBuf.String())
   425  }
   426  
   427  const outputInterrupt = `Interrupt received.
   428  Please wait for Terraform to exit or data loss may occur.
   429  Gracefully shutting down...`