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