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