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