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