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