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