github.com/paultyng/terraform@v0.6.11-0.20180227224804-66ff8f8bed40/command/push.go (about)

     1  package command
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/hashicorp/atlas-go/archive"
    12  	"github.com/hashicorp/atlas-go/v1"
    13  	"github.com/hashicorp/terraform/backend"
    14  	"github.com/hashicorp/terraform/config"
    15  	"github.com/hashicorp/terraform/version"
    16  )
    17  
    18  type PushCommand struct {
    19  	Meta
    20  
    21  	// client is the client to use for the actual push operations.
    22  	// If this isn't set, then the Atlas client is used. This should
    23  	// really only be set for testing reasons (and is hence not exported).
    24  	client pushClient
    25  }
    26  
    27  func (c *PushCommand) Run(args []string) int {
    28  	var atlasAddress, atlasToken string
    29  	var archiveVCS, moduleUpload bool
    30  	var name string
    31  	var overwrite []string
    32  	args, err := c.Meta.process(args, true)
    33  	if err != nil {
    34  		return 1
    35  	}
    36  	cmdFlags := c.Meta.flagSet("push")
    37  	cmdFlags.StringVar(&atlasAddress, "atlas-address", "", "")
    38  	cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
    39  	cmdFlags.StringVar(&atlasToken, "token", "", "")
    40  	cmdFlags.BoolVar(&moduleUpload, "upload-modules", true, "")
    41  	cmdFlags.StringVar(&name, "name", "", "")
    42  	cmdFlags.BoolVar(&archiveVCS, "vcs", true, "")
    43  	cmdFlags.Var((*FlagStringSlice)(&overwrite), "overwrite", "")
    44  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    45  	if err := cmdFlags.Parse(args); err != nil {
    46  		return 1
    47  	}
    48  
    49  	// Make a map of the set values
    50  	overwriteMap := make(map[string]struct{}, len(overwrite))
    51  	for _, v := range overwrite {
    52  		overwriteMap[v] = struct{}{}
    53  	}
    54  
    55  	// This is a map of variables specifically from the CLI that we want to overwrite.
    56  	// We need this because there is a chance that the user is trying to modify
    57  	// a variable we don't see in our context, but which exists in this Terraform
    58  	// Enterprise workspace.
    59  	cliVars := make(map[string]string)
    60  	for k, v := range c.variables {
    61  		if _, ok := overwriteMap[k]; ok {
    62  			if val, ok := v.(string); ok {
    63  				cliVars[k] = val
    64  			} else {
    65  				c.Ui.Error(fmt.Sprintf("Error reading value for variable: %s", k))
    66  				return 1
    67  			}
    68  		}
    69  	}
    70  
    71  	// Get the path to the configuration depending on the args.
    72  	configPath, err := ModulePath(cmdFlags.Args())
    73  	if err != nil {
    74  		c.Ui.Error(err.Error())
    75  		return 1
    76  	}
    77  
    78  	// Check if the path is a plan
    79  	plan, err := c.Plan(configPath)
    80  	if err != nil {
    81  		c.Ui.Error(err.Error())
    82  		return 1
    83  	}
    84  	if plan != nil {
    85  		c.Ui.Error(
    86  			"A plan file cannot be given as the path to the configuration.\n" +
    87  				"A path to a module (directory with configuration) must be given.")
    88  		return 1
    89  	}
    90  
    91  	// Load the module
    92  	mod, diags := c.Module(configPath)
    93  	if diags.HasErrors() {
    94  		c.showDiagnostics(diags)
    95  		return 1
    96  	}
    97  	if mod == nil {
    98  		c.Ui.Error(fmt.Sprintf(
    99  			"No configuration files found in the directory: %s\n\n"+
   100  				"This command requires configuration to run.",
   101  			configPath))
   102  		return 1
   103  	}
   104  
   105  	var conf *config.Config
   106  	if mod != nil {
   107  		conf = mod.Config()
   108  	}
   109  
   110  	// Load the backend
   111  	b, err := c.Backend(&BackendOpts{
   112  		Config: conf,
   113  	})
   114  	if err != nil {
   115  		c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
   116  		return 1
   117  	}
   118  
   119  	// We require a non-local backend
   120  	if c.IsLocalBackend(b) {
   121  		c.Ui.Error(
   122  			"A remote backend is not enabled. For Atlas to run Terraform\n" +
   123  				"for you, remote state must be used and configured. Remote \n" +
   124  				"state via any backend is accepted, not just Atlas. To configure\n" +
   125  				"a backend, please see the documentation at the URL below:\n\n" +
   126  				"https://www.terraform.io/docs/state/remote.html")
   127  		return 1
   128  	}
   129  
   130  	// We require a local backend
   131  	local, ok := b.(backend.Local)
   132  	if !ok {
   133  		c.Ui.Error(ErrUnsupportedLocalOp)
   134  		return 1
   135  	}
   136  
   137  	// Build the operation
   138  	opReq := c.Operation()
   139  	opReq.Module = mod
   140  	opReq.Plan = plan
   141  
   142  	// Get the context
   143  	ctx, _, err := local.Context(opReq)
   144  	if err != nil {
   145  		c.Ui.Error(err.Error())
   146  		return 1
   147  	}
   148  
   149  	// Get the configuration
   150  	config := ctx.Module().Config()
   151  	if name == "" {
   152  		if config.Atlas == nil || config.Atlas.Name == "" {
   153  			c.Ui.Error(
   154  				"The name of this Terraform configuration in Atlas must be\n" +
   155  					"specified within your configuration or the command-line. To\n" +
   156  					"set it on the command-line, use the `-name` parameter.")
   157  			return 1
   158  		}
   159  		name = config.Atlas.Name
   160  	}
   161  
   162  	// Initialize the client if it isn't given.
   163  	if c.client == nil {
   164  		// Make sure to nil out our client so our token isn't sitting around
   165  		defer func() { c.client = nil }()
   166  
   167  		// Initialize it to the default client, we set custom settings later
   168  		client := atlas.DefaultClient()
   169  		if atlasAddress != "" {
   170  			client, err = atlas.NewClient(atlasAddress)
   171  			if err != nil {
   172  				c.Ui.Error(fmt.Sprintf("Error initializing Atlas client: %s", err))
   173  				return 1
   174  			}
   175  		}
   176  
   177  		client.DefaultHeader.Set(version.Header, version.Version)
   178  
   179  		if atlasToken != "" {
   180  			client.Token = atlasToken
   181  		}
   182  
   183  		c.client = &atlasPushClient{Client: client}
   184  	}
   185  
   186  	// Get the variables we already have in atlas
   187  	atlasVars, err := c.client.Get(name)
   188  	if err != nil {
   189  		c.Ui.Error(fmt.Sprintf(
   190  			"Error looking up previously pushed configuration: %s", err))
   191  		return 1
   192  	}
   193  
   194  	// Set remote variables in the context if we don't have a value here. These
   195  	// don't have to be correct, it just prevents the Input walk from prompting
   196  	// the user for input.
   197  	ctxVars := ctx.Variables()
   198  	atlasVarSentry := "ATLAS_78AC153CA649EAA44815DAD6CBD4816D"
   199  	for k, _ := range atlasVars {
   200  		if _, ok := ctxVars[k]; !ok {
   201  			ctx.SetVariable(k, atlasVarSentry)
   202  		}
   203  	}
   204  
   205  	// Ask for input
   206  	if err := ctx.Input(c.InputMode()); err != nil {
   207  		c.Ui.Error(fmt.Sprintf(
   208  			"Error while asking for variable input:\n\n%s", err))
   209  		return 1
   210  	}
   211  
   212  	// Now that we've gone through the input walk, we can be sure we have all
   213  	// the variables we're going to get.
   214  	// We are going to keep these separate from the atlas variables until
   215  	// upload, so we can notify the user which local variables we're sending.
   216  	serializedVars, err := tfVars(ctx.Variables())
   217  	if err != nil {
   218  		c.Ui.Error(fmt.Sprintf(
   219  			"An error has occurred while serializing the variables for uploading:\n"+
   220  				"%s", err))
   221  		return 1
   222  	}
   223  
   224  	// Get the absolute path for our data directory, since the Extra field
   225  	// value below needs to be absolute.
   226  	dataDirAbs, err := filepath.Abs(c.DataDir())
   227  	if err != nil {
   228  		c.Ui.Error(fmt.Sprintf(
   229  			"Error while expanding the data directory %q: %s", c.DataDir(), err))
   230  		return 1
   231  	}
   232  
   233  	// Build the archiving options, which includes everything it can
   234  	// by default according to VCS rules but forcing the data directory.
   235  	archiveOpts := &archive.ArchiveOpts{
   236  		VCS: archiveVCS,
   237  		Extra: map[string]string{
   238  			DefaultDataDir: archive.ExtraEntryDir,
   239  		},
   240  	}
   241  
   242  	// Always store the state file in here so we can find state
   243  	statePathKey := fmt.Sprintf("%s/%s", DefaultDataDir, DefaultStateFilename)
   244  	archiveOpts.Extra[statePathKey] = filepath.Join(dataDirAbs, DefaultStateFilename)
   245  	if moduleUpload {
   246  		// If we're uploading modules, explicitly add that directory if exists.
   247  		moduleKey := fmt.Sprintf("%s/%s", DefaultDataDir, "modules")
   248  		moduleDir := filepath.Join(dataDirAbs, "modules")
   249  		_, err := os.Stat(moduleDir)
   250  		if err == nil {
   251  			archiveOpts.Extra[moduleKey] = filepath.Join(dataDirAbs, "modules")
   252  		}
   253  		if err != nil && !os.IsNotExist(err) {
   254  			c.Ui.Error(fmt.Sprintf(
   255  				"Error checking for module dir %q: %s", moduleDir, err))
   256  			return 1
   257  		}
   258  	} else {
   259  		// If we're not uploading modules, explicitly exclude add that
   260  		archiveOpts.Exclude = append(
   261  			archiveOpts.Exclude,
   262  			filepath.Join(c.DataDir(), "modules"))
   263  	}
   264  
   265  	archiveR, err := archive.CreateArchive(configPath, archiveOpts)
   266  	if err != nil {
   267  		c.Ui.Error(fmt.Sprintf(
   268  			"An error has occurred while archiving the module for uploading:\n"+
   269  				"%s", err))
   270  		return 1
   271  	}
   272  
   273  	// List of the vars we're uploading to display to the user.
   274  	// We always upload all vars from atlas, but only report them if they are overwritten.
   275  	var setVars []string
   276  
   277  	// variables to upload
   278  	var uploadVars []atlas.TFVar
   279  
   280  	// first add all the variables we want to send which have been serialized
   281  	// from the local context.
   282  	for _, sv := range serializedVars {
   283  		_, inOverwrite := overwriteMap[sv.Key]
   284  		_, inAtlas := atlasVars[sv.Key]
   285  
   286  		// We have a variable that's not in atlas, so always send it.
   287  		if !inAtlas {
   288  			uploadVars = append(uploadVars, sv)
   289  			setVars = append(setVars, sv.Key)
   290  		}
   291  
   292  		// We're overwriting an atlas variable.
   293  		// We also want to check that we
   294  		// don't send the dummy sentry value back to atlas. This could happen
   295  		// if it's specified as an overwrite on the cli, but we didn't set a
   296  		// new value.
   297  		if inAtlas && inOverwrite && sv.Value != atlasVarSentry {
   298  			uploadVars = append(uploadVars, sv)
   299  			setVars = append(setVars, sv.Key)
   300  
   301  			// remove this value from the atlas vars, because we're going to
   302  			// send back the remainder regardless.
   303  			delete(atlasVars, sv.Key)
   304  		}
   305  	}
   306  
   307  	// now send back all the existing atlas vars, inserting any overwrites from the cli.
   308  	for k, av := range atlasVars {
   309  		if v, ok := cliVars[k]; ok {
   310  			av.Value = v
   311  			setVars = append(setVars, k)
   312  		}
   313  		uploadVars = append(uploadVars, av)
   314  	}
   315  
   316  	sort.Strings(setVars)
   317  	if len(setVars) > 0 {
   318  		c.Ui.Output(
   319  			"The following variables will be set or overwritten within Atlas from\n" +
   320  				"their local values. All other variables are already set within Atlas.\n" +
   321  				"If you want to modify the value of a variable, use the Atlas web\n" +
   322  				"interface or set it locally and use the -overwrite flag.\n\n")
   323  		for _, v := range setVars {
   324  			c.Ui.Output(fmt.Sprintf("  * %s", v))
   325  		}
   326  
   327  		// Newline
   328  		c.Ui.Output("")
   329  	}
   330  
   331  	// Upsert!
   332  	opts := &pushUpsertOptions{
   333  		Name:      name,
   334  		Archive:   archiveR,
   335  		Variables: ctx.Variables(),
   336  		TFVars:    uploadVars,
   337  	}
   338  
   339  	c.Ui.Output("Uploading Terraform configuration...")
   340  	vsn, err := c.client.Upsert(opts)
   341  	if err != nil {
   342  		c.Ui.Error(fmt.Sprintf(
   343  			"An error occurred while uploading the module:\n\n%s", err))
   344  		return 1
   345  	}
   346  
   347  	c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
   348  		"[reset][bold][green]Configuration %q uploaded! (v%d)",
   349  		name, vsn)))
   350  
   351  	c.showDiagnostics(diags)
   352  	if diags.HasErrors() {
   353  		return 1
   354  	}
   355  
   356  	return 0
   357  }
   358  
   359  func (c *PushCommand) Help() string {
   360  	helpText := `
   361  Usage: terraform push [options] [DIR]
   362  
   363    Upload this Terraform module to an Atlas server for remote
   364    infrastructure management.
   365  
   366  Options:
   367  
   368    -atlas-address=<url> An alternate address to an Atlas instance. Defaults
   369                         to https://atlas.hashicorp.com
   370  
   371    -upload-modules=true If true (default), then the modules are locked at
   372                         their current checkout and uploaded completely. This
   373                         prevents Atlas from running "terraform get".
   374  
   375    -name=<name>         Name of the configuration in Atlas. This can also
   376                         be set in the configuration itself. Format is
   377                         typically: "username/name".
   378  
   379    -token=<token>       Access token to use to upload. If blank or unspecified,
   380                         the ATLAS_TOKEN environmental variable will be used.
   381  
   382    -overwrite=foo       Variable keys that should overwrite values in Atlas.
   383                         Otherwise, variables already set in Atlas will overwrite
   384                         local values. This flag can be repeated.
   385  
   386    -var 'foo=bar'       Set a variable in the Terraform configuration. This
   387                         flag can be set multiple times.
   388  
   389    -var-file=foo        Set variables in the Terraform configuration from
   390                         a file. If "terraform.tfvars" or any ".auto.tfvars"
   391                         files are present, they will be automatically loaded.
   392  
   393    -vcs=true            If true (default), push will upload only files
   394                         committed to your VCS, if detected.
   395  
   396    -no-color           If specified, output won't contain any color.
   397  
   398  `
   399  	return strings.TrimSpace(helpText)
   400  }
   401  
   402  func sortedKeys(m map[string]interface{}) []string {
   403  	var keys []string
   404  	for k := range m {
   405  		keys = append(keys, k)
   406  	}
   407  	sort.Strings(keys)
   408  	return keys
   409  }
   410  
   411  // build the set of TFVars for push
   412  func tfVars(vars map[string]interface{}) ([]atlas.TFVar, error) {
   413  	var tfVars []atlas.TFVar
   414  	var err error
   415  
   416  RANGE:
   417  	for _, k := range sortedKeys(vars) {
   418  		v := vars[k]
   419  
   420  		var hcl []byte
   421  		tfv := atlas.TFVar{Key: k}
   422  
   423  		switch v := v.(type) {
   424  		case string:
   425  			tfv.Value = v
   426  
   427  		default:
   428  			// everything that's not a string is now HCL encoded
   429  			hcl, err = encodeHCL(v)
   430  			if err != nil {
   431  				break RANGE
   432  			}
   433  
   434  			tfv.Value = string(hcl)
   435  			tfv.IsHCL = true
   436  		}
   437  
   438  		tfVars = append(tfVars, tfv)
   439  	}
   440  
   441  	return tfVars, err
   442  }
   443  
   444  func (c *PushCommand) Synopsis() string {
   445  	return "Upload this Terraform module to Atlas to run"
   446  }
   447  
   448  // pushClient is implemented internally to control where pushes go. This is
   449  // either to Atlas or a mock for testing. We still return a map to make it
   450  // easier to check for variable existence when filtering the overrides.
   451  type pushClient interface {
   452  	Get(string) (map[string]atlas.TFVar, error)
   453  	Upsert(*pushUpsertOptions) (int, error)
   454  }
   455  
   456  type pushUpsertOptions struct {
   457  	Name      string
   458  	Archive   *archive.Archive
   459  	Variables map[string]interface{}
   460  	TFVars    []atlas.TFVar
   461  }
   462  
   463  type atlasPushClient struct {
   464  	Client *atlas.Client
   465  }
   466  
   467  func (c *atlasPushClient) Get(name string) (map[string]atlas.TFVar, error) {
   468  	user, name, err := atlas.ParseSlug(name)
   469  	if err != nil {
   470  		return nil, err
   471  	}
   472  
   473  	version, err := c.Client.TerraformConfigLatest(user, name)
   474  	if err != nil {
   475  		return nil, err
   476  	}
   477  
   478  	variables := make(map[string]atlas.TFVar)
   479  
   480  	if version == nil {
   481  		return variables, nil
   482  	}
   483  
   484  	// Variables is superseded by TFVars
   485  	if version.TFVars == nil {
   486  		for k, v := range version.Variables {
   487  			variables[k] = atlas.TFVar{Key: k, Value: v}
   488  		}
   489  	} else {
   490  		for _, v := range version.TFVars {
   491  			variables[v.Key] = v
   492  		}
   493  	}
   494  
   495  	return variables, nil
   496  }
   497  
   498  func (c *atlasPushClient) Upsert(opts *pushUpsertOptions) (int, error) {
   499  	user, name, err := atlas.ParseSlug(opts.Name)
   500  	if err != nil {
   501  		return 0, err
   502  	}
   503  
   504  	data := &atlas.TerraformConfigVersion{
   505  		TFVars: opts.TFVars,
   506  	}
   507  
   508  	version, err := c.Client.CreateTerraformConfigVersion(
   509  		user, name, data, opts.Archive, opts.Archive.Size)
   510  	if err != nil {
   511  		return 0, err
   512  	}
   513  
   514  	return version, nil
   515  }
   516  
   517  type mockPushClient struct {
   518  	File string
   519  
   520  	GetCalled bool
   521  	GetName   string
   522  	GetResult map[string]atlas.TFVar
   523  	GetError  error
   524  
   525  	UpsertCalled  bool
   526  	UpsertOptions *pushUpsertOptions
   527  	UpsertVersion int
   528  	UpsertError   error
   529  }
   530  
   531  func (c *mockPushClient) Get(name string) (map[string]atlas.TFVar, error) {
   532  	c.GetCalled = true
   533  	c.GetName = name
   534  	return c.GetResult, c.GetError
   535  }
   536  
   537  func (c *mockPushClient) Upsert(opts *pushUpsertOptions) (int, error) {
   538  	f, err := os.Create(c.File)
   539  	if err != nil {
   540  		return 0, err
   541  	}
   542  	defer f.Close()
   543  
   544  	data := opts.Archive
   545  	size := opts.Archive.Size
   546  	if _, err := io.CopyN(f, data, size); err != nil {
   547  		return 0, err
   548  	}
   549  
   550  	c.UpsertCalled = true
   551  	c.UpsertOptions = opts
   552  	return c.UpsertVersion, c.UpsertError
   553  }