github.com/chanzuckerberg/terraform@v0.11.12-beta1/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  	defer func() {
   150  		err := opReq.StateLocker.Unlock(nil)
   151  		if err != nil {
   152  			c.Ui.Error(err.Error())
   153  		}
   154  	}()
   155  
   156  	// Get the configuration
   157  	config := ctx.Module().Config()
   158  	if name == "" {
   159  		if config.Atlas == nil || config.Atlas.Name == "" {
   160  			c.Ui.Error(
   161  				"The name of this Terraform configuration in Atlas must be\n" +
   162  					"specified within your configuration or the command-line. To\n" +
   163  					"set it on the command-line, use the `-name` parameter.")
   164  			return 1
   165  		}
   166  		name = config.Atlas.Name
   167  	}
   168  
   169  	// Initialize the client if it isn't given.
   170  	if c.client == nil {
   171  		// Make sure to nil out our client so our token isn't sitting around
   172  		defer func() { c.client = nil }()
   173  
   174  		// Initialize it to the default client, we set custom settings later
   175  		client := atlas.DefaultClient()
   176  		if atlasAddress != "" {
   177  			client, err = atlas.NewClient(atlasAddress)
   178  			if err != nil {
   179  				c.Ui.Error(fmt.Sprintf("Error initializing Atlas client: %s", err))
   180  				return 1
   181  			}
   182  		}
   183  
   184  		client.DefaultHeader.Set(version.Header, version.Version)
   185  
   186  		if atlasToken != "" {
   187  			client.Token = atlasToken
   188  		}
   189  
   190  		c.client = &atlasPushClient{Client: client}
   191  	}
   192  
   193  	// Get the variables we already have in atlas
   194  	atlasVars, err := c.client.Get(name)
   195  	if err != nil {
   196  		c.Ui.Error(fmt.Sprintf(
   197  			"Error looking up previously pushed configuration: %s", err))
   198  		return 1
   199  	}
   200  
   201  	// Set remote variables in the context if we don't have a value here. These
   202  	// don't have to be correct, it just prevents the Input walk from prompting
   203  	// the user for input.
   204  	ctxVars := ctx.Variables()
   205  	atlasVarSentry := "ATLAS_78AC153CA649EAA44815DAD6CBD4816D"
   206  	for k, _ := range atlasVars {
   207  		if _, ok := ctxVars[k]; !ok {
   208  			ctx.SetVariable(k, atlasVarSentry)
   209  		}
   210  	}
   211  
   212  	// Ask for input
   213  	if err := ctx.Input(c.InputMode()); err != nil {
   214  		c.Ui.Error(fmt.Sprintf(
   215  			"Error while asking for variable input:\n\n%s", err))
   216  		return 1
   217  	}
   218  
   219  	// Now that we've gone through the input walk, we can be sure we have all
   220  	// the variables we're going to get.
   221  	// We are going to keep these separate from the atlas variables until
   222  	// upload, so we can notify the user which local variables we're sending.
   223  	serializedVars, err := tfVars(ctx.Variables())
   224  	if err != nil {
   225  		c.Ui.Error(fmt.Sprintf(
   226  			"An error has occurred while serializing the variables for uploading:\n"+
   227  				"%s", err))
   228  		return 1
   229  	}
   230  
   231  	// Get the absolute path for our data directory, since the Extra field
   232  	// value below needs to be absolute.
   233  	dataDirAbs, err := filepath.Abs(c.DataDir())
   234  	if err != nil {
   235  		c.Ui.Error(fmt.Sprintf(
   236  			"Error while expanding the data directory %q: %s", c.DataDir(), err))
   237  		return 1
   238  	}
   239  
   240  	// Build the archiving options, which includes everything it can
   241  	// by default according to VCS rules but forcing the data directory.
   242  	archiveOpts := &archive.ArchiveOpts{
   243  		VCS: archiveVCS,
   244  		Extra: map[string]string{
   245  			DefaultDataDir: archive.ExtraEntryDir,
   246  		},
   247  	}
   248  
   249  	// Always store the state file in here so we can find state
   250  	statePathKey := fmt.Sprintf("%s/%s", DefaultDataDir, DefaultStateFilename)
   251  	archiveOpts.Extra[statePathKey] = filepath.Join(dataDirAbs, DefaultStateFilename)
   252  	if moduleUpload {
   253  		// If we're uploading modules, explicitly add that directory if exists.
   254  		moduleKey := fmt.Sprintf("%s/%s", DefaultDataDir, "modules")
   255  		moduleDir := filepath.Join(dataDirAbs, "modules")
   256  		_, err := os.Stat(moduleDir)
   257  		if err == nil {
   258  			archiveOpts.Extra[moduleKey] = filepath.Join(dataDirAbs, "modules")
   259  		}
   260  		if err != nil && !os.IsNotExist(err) {
   261  			c.Ui.Error(fmt.Sprintf(
   262  				"Error checking for module dir %q: %s", moduleDir, err))
   263  			return 1
   264  		}
   265  	} else {
   266  		// If we're not uploading modules, explicitly exclude add that
   267  		archiveOpts.Exclude = append(
   268  			archiveOpts.Exclude,
   269  			filepath.Join(c.DataDir(), "modules"))
   270  	}
   271  
   272  	archiveR, err := archive.CreateArchive(configPath, archiveOpts)
   273  	if err != nil {
   274  		c.Ui.Error(fmt.Sprintf(
   275  			"An error has occurred while archiving the module for uploading:\n"+
   276  				"%s", err))
   277  		return 1
   278  	}
   279  
   280  	// List of the vars we're uploading to display to the user.
   281  	// We always upload all vars from atlas, but only report them if they are overwritten.
   282  	var setVars []string
   283  
   284  	// variables to upload
   285  	var uploadVars []atlas.TFVar
   286  
   287  	// first add all the variables we want to send which have been serialized
   288  	// from the local context.
   289  	for _, sv := range serializedVars {
   290  		_, inOverwrite := overwriteMap[sv.Key]
   291  		_, inAtlas := atlasVars[sv.Key]
   292  
   293  		// We have a variable that's not in atlas, so always send it.
   294  		if !inAtlas {
   295  			uploadVars = append(uploadVars, sv)
   296  			setVars = append(setVars, sv.Key)
   297  		}
   298  
   299  		// We're overwriting an atlas variable.
   300  		// We also want to check that we
   301  		// don't send the dummy sentry value back to atlas. This could happen
   302  		// if it's specified as an overwrite on the cli, but we didn't set a
   303  		// new value.
   304  		if inAtlas && inOverwrite && sv.Value != atlasVarSentry {
   305  			uploadVars = append(uploadVars, sv)
   306  			setVars = append(setVars, sv.Key)
   307  
   308  			// remove this value from the atlas vars, because we're going to
   309  			// send back the remainder regardless.
   310  			delete(atlasVars, sv.Key)
   311  		}
   312  	}
   313  
   314  	// now send back all the existing atlas vars, inserting any overwrites from the cli.
   315  	for k, av := range atlasVars {
   316  		if v, ok := cliVars[k]; ok {
   317  			av.Value = v
   318  			setVars = append(setVars, k)
   319  		}
   320  		uploadVars = append(uploadVars, av)
   321  	}
   322  
   323  	sort.Strings(setVars)
   324  	if len(setVars) > 0 {
   325  		c.Ui.Output(
   326  			"The following variables will be set or overwritten within Atlas from\n" +
   327  				"their local values. All other variables are already set within Atlas.\n" +
   328  				"If you want to modify the value of a variable, use the Atlas web\n" +
   329  				"interface or set it locally and use the -overwrite flag.\n\n")
   330  		for _, v := range setVars {
   331  			c.Ui.Output(fmt.Sprintf("  * %s", v))
   332  		}
   333  
   334  		// Newline
   335  		c.Ui.Output("")
   336  	}
   337  
   338  	// Upsert!
   339  	opts := &pushUpsertOptions{
   340  		Name:      name,
   341  		Archive:   archiveR,
   342  		Variables: ctx.Variables(),
   343  		TFVars:    uploadVars,
   344  	}
   345  
   346  	c.Ui.Output("Uploading Terraform configuration...")
   347  	vsn, err := c.client.Upsert(opts)
   348  	if err != nil {
   349  		c.Ui.Error(fmt.Sprintf(
   350  			"An error occurred while uploading the module:\n\n%s", err))
   351  		return 1
   352  	}
   353  
   354  	c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
   355  		"[reset][bold][green]Configuration %q uploaded! (v%d)",
   356  		name, vsn)))
   357  
   358  	c.showDiagnostics(diags)
   359  	if diags.HasErrors() {
   360  		return 1
   361  	}
   362  
   363  	return 0
   364  }
   365  
   366  func (c *PushCommand) Help() string {
   367  	helpText := `
   368  Usage: terraform push [options] [DIR]
   369  
   370    Upload this Terraform module to an Atlas server for remote
   371    infrastructure management.
   372  
   373  Options:
   374  
   375    -atlas-address=<url> An alternate address to an Atlas instance. Defaults
   376                         to https://atlas.hashicorp.com
   377  
   378    -upload-modules=true If true (default), then the modules are locked at
   379                         their current checkout and uploaded completely. This
   380                         prevents Atlas from running "terraform get".
   381  
   382    -name=<name>         Name of the configuration in Atlas. This can also
   383                         be set in the configuration itself. Format is
   384                         typically: "username/name".
   385  
   386    -token=<token>       Access token to use to upload. If blank or unspecified,
   387                         the ATLAS_TOKEN environmental variable will be used.
   388  
   389    -overwrite=foo       Variable keys that should overwrite values in Atlas.
   390                         Otherwise, variables already set in Atlas will overwrite
   391                         local values. This flag can be repeated.
   392  
   393    -var 'foo=bar'       Set a variable in the Terraform configuration. This
   394                         flag can be set multiple times.
   395  
   396    -var-file=foo        Set variables in the Terraform configuration from
   397                         a file. If "terraform.tfvars" or any ".auto.tfvars"
   398                         files are present, they will be automatically loaded.
   399  
   400    -vcs=true            If true (default), push will upload only files
   401                         committed to your VCS, if detected.
   402  
   403    -no-color            If specified, output won't contain any color.
   404  
   405  `
   406  	return strings.TrimSpace(helpText)
   407  }
   408  
   409  func sortedKeys(m map[string]interface{}) []string {
   410  	var keys []string
   411  	for k := range m {
   412  		keys = append(keys, k)
   413  	}
   414  	sort.Strings(keys)
   415  	return keys
   416  }
   417  
   418  // build the set of TFVars for push
   419  func tfVars(vars map[string]interface{}) ([]atlas.TFVar, error) {
   420  	var tfVars []atlas.TFVar
   421  	var err error
   422  
   423  RANGE:
   424  	for _, k := range sortedKeys(vars) {
   425  		v := vars[k]
   426  
   427  		var hcl []byte
   428  		tfv := atlas.TFVar{Key: k}
   429  
   430  		switch v := v.(type) {
   431  		case string:
   432  			tfv.Value = v
   433  
   434  		default:
   435  			// everything that's not a string is now HCL encoded
   436  			hcl, err = encodeHCL(v)
   437  			if err != nil {
   438  				break RANGE
   439  			}
   440  
   441  			tfv.Value = string(hcl)
   442  			tfv.IsHCL = true
   443  		}
   444  
   445  		tfVars = append(tfVars, tfv)
   446  	}
   447  
   448  	return tfVars, err
   449  }
   450  
   451  func (c *PushCommand) Synopsis() string {
   452  	return "Upload this Terraform module to Atlas to run"
   453  }
   454  
   455  // pushClient is implemented internally to control where pushes go. This is
   456  // either to Atlas or a mock for testing. We still return a map to make it
   457  // easier to check for variable existence when filtering the overrides.
   458  type pushClient interface {
   459  	Get(string) (map[string]atlas.TFVar, error)
   460  	Upsert(*pushUpsertOptions) (int, error)
   461  }
   462  
   463  type pushUpsertOptions struct {
   464  	Name      string
   465  	Archive   *archive.Archive
   466  	Variables map[string]interface{}
   467  	TFVars    []atlas.TFVar
   468  }
   469  
   470  type atlasPushClient struct {
   471  	Client *atlas.Client
   472  }
   473  
   474  func (c *atlasPushClient) Get(name string) (map[string]atlas.TFVar, error) {
   475  	user, name, err := atlas.ParseSlug(name)
   476  	if err != nil {
   477  		return nil, err
   478  	}
   479  
   480  	version, err := c.Client.TerraformConfigLatest(user, name)
   481  	if err != nil {
   482  		return nil, err
   483  	}
   484  
   485  	variables := make(map[string]atlas.TFVar)
   486  
   487  	if version == nil {
   488  		return variables, nil
   489  	}
   490  
   491  	// Variables is superseded by TFVars
   492  	if version.TFVars == nil {
   493  		for k, v := range version.Variables {
   494  			variables[k] = atlas.TFVar{Key: k, Value: v}
   495  		}
   496  	} else {
   497  		for _, v := range version.TFVars {
   498  			variables[v.Key] = v
   499  		}
   500  	}
   501  
   502  	return variables, nil
   503  }
   504  
   505  func (c *atlasPushClient) Upsert(opts *pushUpsertOptions) (int, error) {
   506  	user, name, err := atlas.ParseSlug(opts.Name)
   507  	if err != nil {
   508  		return 0, err
   509  	}
   510  
   511  	data := &atlas.TerraformConfigVersion{
   512  		TFVars: opts.TFVars,
   513  	}
   514  
   515  	version, err := c.Client.CreateTerraformConfigVersion(
   516  		user, name, data, opts.Archive, opts.Archive.Size)
   517  	if err != nil {
   518  		return 0, err
   519  	}
   520  
   521  	return version, nil
   522  }
   523  
   524  type mockPushClient struct {
   525  	File string
   526  
   527  	GetCalled bool
   528  	GetName   string
   529  	GetResult map[string]atlas.TFVar
   530  	GetError  error
   531  
   532  	UpsertCalled  bool
   533  	UpsertOptions *pushUpsertOptions
   534  	UpsertVersion int
   535  	UpsertError   error
   536  }
   537  
   538  func (c *mockPushClient) Get(name string) (map[string]atlas.TFVar, error) {
   539  	c.GetCalled = true
   540  	c.GetName = name
   541  	return c.GetResult, c.GetError
   542  }
   543  
   544  func (c *mockPushClient) Upsert(opts *pushUpsertOptions) (int, error) {
   545  	f, err := os.Create(c.File)
   546  	if err != nil {
   547  		return 0, err
   548  	}
   549  	defer f.Close()
   550  
   551  	data := opts.Archive
   552  	size := opts.Archive.Size
   553  	if _, err := io.CopyN(f, data, size); err != nil {
   554  		return 0, err
   555  	}
   556  
   557  	c.UpsertCalled = true
   558  	c.UpsertOptions = opts
   559  	return c.UpsertVersion, c.UpsertError
   560  }