github.com/alkar/terraform@v0.9.6-0.20170517124458-a4cddf6ebf59/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  	// Load the backend
   136  	b, err := c.Backend(&BackendOpts{
   137  		ConfigPath: configPath,
   138  		Plan:       plan,
   139  	})
   140  	if err != nil {
   141  		c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
   142  		return 1
   143  	}
   144  
   145  	// If we're not forcing and we're destroying, verify with the
   146  	// user at this point.
   147  	if !destroyForce && c.Destroy {
   148  		// Default destroy message
   149  		desc := "Terraform will delete all your managed infrastructure.\n" +
   150  			"There is no undo. Only 'yes' will be accepted to confirm."
   151  
   152  		// If targets are specified, list those to user
   153  		if c.Meta.targets != nil {
   154  			var descBuffer bytes.Buffer
   155  			descBuffer.WriteString("Terraform will delete the following infrastructure:\n")
   156  			for _, target := range c.Meta.targets {
   157  				descBuffer.WriteString("\t")
   158  				descBuffer.WriteString(target)
   159  				descBuffer.WriteString("\n")
   160  			}
   161  			descBuffer.WriteString("There is no undo. Only 'yes' will be accepted to confirm")
   162  			desc = descBuffer.String()
   163  		}
   164  
   165  		v, err := c.UIInput().Input(&terraform.InputOpts{
   166  			Id:          "destroy",
   167  			Query:       "Do you really want to destroy?",
   168  			Description: desc,
   169  		})
   170  		if err != nil {
   171  			c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err))
   172  			return 1
   173  		}
   174  		if v != "yes" {
   175  			c.Ui.Output("Destroy cancelled.")
   176  			return 1
   177  		}
   178  	}
   179  
   180  	// Build the operation
   181  	opReq := c.Operation()
   182  	opReq.Destroy = c.Destroy
   183  	opReq.Module = mod
   184  	opReq.Plan = plan
   185  	opReq.PlanRefresh = refresh
   186  	opReq.Type = backend.OperationTypeApply
   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(outputInterrupt)
   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    -lock-timeout=0s       Duration to retry a state lock.
   280  
   281    -input=true            Ask for input for variables if not directly set.
   282  
   283    -no-color              If specified, output won't contain any color.
   284  
   285    -parallelism=n         Limit the number of parallel resource operations.
   286                           Defaults to 10.
   287  
   288    -refresh=true          Update state prior to checking for differences. This
   289                           has no effect if a plan file is given to apply.
   290  
   291    -state=path            Path to read and save state (unless state-out
   292                           is specified). Defaults to "terraform.tfstate".
   293  
   294    -state-out=path        Path to write state to that is different than
   295                           "-state". This can be used to preserve the old
   296                           state.
   297  
   298    -target=resource       Resource to target. Operation will be limited to this
   299                           resource and its dependencies. This flag can be used
   300                           multiple times.
   301  
   302    -var 'foo=bar'         Set a variable in the Terraform configuration. This
   303                           flag can be set multiple times.
   304  
   305    -var-file=foo          Set variables in the Terraform configuration from
   306                           a file. If "terraform.tfvars" is present, it will be
   307                           automatically loaded if this flag is not specified.
   308  
   309  
   310  `
   311  	return strings.TrimSpace(helpText)
   312  }
   313  
   314  func (c *ApplyCommand) helpDestroy() string {
   315  	helpText := `
   316  Usage: terraform destroy [options] [DIR]
   317  
   318    Destroy Terraform-managed infrastructure.
   319  
   320  Options:
   321  
   322    -backup=path           Path to backup the existing state file before
   323                           modifying. Defaults to the "-state-out" path with
   324                           ".backup" extension. Set to "-" to disable backup.
   325  
   326    -force                 Don't ask for input for destroy confirmation.
   327  
   328    -lock=true             Lock the state file when locking is supported.
   329  
   330    -lock-timeout=0s       Duration to retry a state lock.
   331  
   332    -no-color              If specified, output won't contain any color.
   333  
   334    -parallelism=n         Limit the number of concurrent operations.
   335                           Defaults to 10.
   336  
   337    -refresh=true          Update state prior to checking for differences. This
   338                           has no effect if a plan file is given to apply.
   339  
   340    -state=path            Path to read and save state (unless state-out
   341                           is specified). Defaults to "terraform.tfstate".
   342  
   343    -state-out=path        Path to write state to that is different than
   344                           "-state". This can be used to preserve the old
   345                           state.
   346  
   347    -target=resource       Resource to target. Operation will be limited to this
   348                           resource and its dependencies. This flag can be used
   349                           multiple times.
   350  
   351    -var 'foo=bar'         Set a variable in the Terraform configuration. This
   352                           flag can be set multiple times.
   353  
   354    -var-file=foo          Set variables in the Terraform configuration from
   355                           a file. If "terraform.tfvars" is present, it will be
   356                           automatically loaded if this flag is not specified.
   357  
   358  
   359  `
   360  	return strings.TrimSpace(helpText)
   361  }
   362  
   363  func outputsAsString(state *terraform.State, modPath []string, schema []*config.Output, includeHeader bool) string {
   364  	if state == nil {
   365  		return ""
   366  	}
   367  
   368  	ms := state.ModuleByPath(modPath)
   369  	if ms == nil {
   370  		return ""
   371  	}
   372  
   373  	outputs := ms.Outputs
   374  	outputBuf := new(bytes.Buffer)
   375  	if len(outputs) > 0 {
   376  		schemaMap := make(map[string]*config.Output)
   377  		if schema != nil {
   378  			for _, s := range schema {
   379  				schemaMap[s.Name] = s
   380  			}
   381  		}
   382  
   383  		if includeHeader {
   384  			outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n")
   385  		}
   386  
   387  		// Output the outputs in alphabetical order
   388  		keyLen := 0
   389  		ks := make([]string, 0, len(outputs))
   390  		for key, _ := range outputs {
   391  			ks = append(ks, key)
   392  			if len(key) > keyLen {
   393  				keyLen = len(key)
   394  			}
   395  		}
   396  		sort.Strings(ks)
   397  
   398  		for _, k := range ks {
   399  			schema, ok := schemaMap[k]
   400  			if ok && schema.Sensitive {
   401  				outputBuf.WriteString(fmt.Sprintf("%s = <sensitive>\n", k))
   402  				continue
   403  			}
   404  
   405  			v := outputs[k]
   406  			switch typedV := v.Value.(type) {
   407  			case string:
   408  				outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, typedV))
   409  			case []interface{}:
   410  				outputBuf.WriteString(formatListOutput("", k, typedV))
   411  				outputBuf.WriteString("\n")
   412  			case map[string]interface{}:
   413  				outputBuf.WriteString(formatMapOutput("", k, typedV))
   414  				outputBuf.WriteString("\n")
   415  			}
   416  		}
   417  	}
   418  
   419  	return strings.TrimSpace(outputBuf.String())
   420  }
   421  
   422  const outputInterrupt = `Interrupt received.
   423  Please wait for Terraform to exit or data loss may occur.
   424  Gracefully shutting down...`