github.com/ricardclau/terraform@v0.6.17-0.20160519222547-283e3ae6b5a9/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  )
    14  
    15  type PushCommand struct {
    16  	Meta
    17  
    18  	// client is the client to use for the actual push operations.
    19  	// If this isn't set, then the Atlas client is used. This should
    20  	// really only be set for testing reasons (and is hence not exported).
    21  	client pushClient
    22  }
    23  
    24  func (c *PushCommand) Run(args []string) int {
    25  	var atlasAddress, atlasToken string
    26  	var archiveVCS, moduleUpload bool
    27  	var name string
    28  	var overwrite []string
    29  	args = c.Meta.process(args, true)
    30  	cmdFlags := c.Meta.flagSet("push")
    31  	cmdFlags.StringVar(&atlasAddress, "atlas-address", "", "")
    32  	cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
    33  	cmdFlags.StringVar(&atlasToken, "token", "", "")
    34  	cmdFlags.BoolVar(&moduleUpload, "upload-modules", true, "")
    35  	cmdFlags.StringVar(&name, "name", "", "")
    36  	cmdFlags.BoolVar(&archiveVCS, "vcs", true, "")
    37  	cmdFlags.Var((*FlagStringSlice)(&overwrite), "overwrite", "")
    38  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    39  	if err := cmdFlags.Parse(args); err != nil {
    40  		return 1
    41  	}
    42  
    43  	// Make a map of the set values
    44  	overwriteMap := make(map[string]struct{}, len(overwrite))
    45  	for _, v := range overwrite {
    46  		overwriteMap[v] = struct{}{}
    47  	}
    48  
    49  	// The pwd is used for the configuration path if one is not given
    50  	pwd, err := os.Getwd()
    51  	if err != nil {
    52  		c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
    53  		return 1
    54  	}
    55  
    56  	// Get the path to the configuration depending on the args.
    57  	var configPath string
    58  	args = cmdFlags.Args()
    59  	if len(args) > 1 {
    60  		c.Ui.Error("The apply command expects at most one argument.")
    61  		cmdFlags.Usage()
    62  		return 1
    63  	} else if len(args) == 1 {
    64  		configPath = args[0]
    65  	} else {
    66  		configPath = pwd
    67  	}
    68  
    69  	// Verify the state is remote, we can't push without a remote state
    70  	s, err := c.State()
    71  	if err != nil {
    72  		c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
    73  		return 1
    74  	}
    75  	if !s.State().IsRemote() {
    76  		c.Ui.Error(
    77  			"Remote state is not enabled. For Atlas to run Terraform\n" +
    78  				"for you, remote state must be used and configured. Remote\n" +
    79  				"state via any backend is accepted, not just Atlas. To\n" +
    80  				"configure remote state, use the `terraform remote config`\n" +
    81  				"command.")
    82  		return 1
    83  	}
    84  
    85  	// Build the context based on the arguments given
    86  	ctx, planned, err := c.Context(contextOpts{
    87  		Path:      configPath,
    88  		StatePath: c.Meta.statePath,
    89  	})
    90  	if err != nil {
    91  		c.Ui.Error(err.Error())
    92  		return 1
    93  	}
    94  	if planned {
    95  		c.Ui.Error(
    96  			"A plan file cannot be given as the path to the configuration.\n" +
    97  				"A path to a module (directory with configuration) must be given.")
    98  		return 1
    99  	}
   100  
   101  	// Get the configuration
   102  	config := ctx.Module().Config()
   103  	if name == "" {
   104  		if config.Atlas == nil || config.Atlas.Name == "" {
   105  			c.Ui.Error(
   106  				"The name of this Terraform configuration in Atlas must be\n" +
   107  					"specified within your configuration or the command-line. To\n" +
   108  					"set it on the command-line, use the `-name` parameter.")
   109  			return 1
   110  		}
   111  		name = config.Atlas.Name
   112  	}
   113  
   114  	// Initialize the client if it isn't given.
   115  	if c.client == nil {
   116  		// Make sure to nil out our client so our token isn't sitting around
   117  		defer func() { c.client = nil }()
   118  
   119  		// Initialize it to the default client, we set custom settings later
   120  		client := atlas.DefaultClient()
   121  		if atlasAddress != "" {
   122  			client, err = atlas.NewClient(atlasAddress)
   123  			if err != nil {
   124  				c.Ui.Error(fmt.Sprintf("Error initializing Atlas client: %s", err))
   125  				return 1
   126  			}
   127  		}
   128  
   129  		if atlasToken != "" {
   130  			client.Token = atlasToken
   131  		}
   132  
   133  		c.client = &atlasPushClient{Client: client}
   134  	}
   135  
   136  	// Get the variables we might already have
   137  	atlasVars, err := c.client.Get(name)
   138  	if err != nil {
   139  		c.Ui.Error(fmt.Sprintf(
   140  			"Error looking up previously pushed configuration: %s", err))
   141  		return 1
   142  	}
   143  	for k, v := range atlasVars {
   144  		if _, ok := overwriteMap[k]; ok {
   145  			continue
   146  		}
   147  
   148  		ctx.SetVariable(k, v)
   149  	}
   150  
   151  	// Ask for input
   152  	if err := ctx.Input(c.InputMode()); err != nil {
   153  		c.Ui.Error(fmt.Sprintf(
   154  			"Error while asking for variable input:\n\n%s", err))
   155  		return 1
   156  	}
   157  
   158  	// Build the archiving options, which includes everything it can
   159  	// by default according to VCS rules but forcing the data directory.
   160  	archiveOpts := &archive.ArchiveOpts{
   161  		VCS: archiveVCS,
   162  		Extra: map[string]string{
   163  			DefaultDataDir: c.DataDir(),
   164  		},
   165  	}
   166  	if !moduleUpload {
   167  		// If we're not uploading modules, then exclude the modules dir.
   168  		archiveOpts.Exclude = append(
   169  			archiveOpts.Exclude,
   170  			filepath.Join(c.DataDir(), "modules"))
   171  	}
   172  
   173  	archiveR, err := archive.CreateArchive(configPath, archiveOpts)
   174  	if err != nil {
   175  		c.Ui.Error(fmt.Sprintf(
   176  			"An error has occurred while archiving the module for uploading:\n"+
   177  				"%s", err))
   178  		return 1
   179  	}
   180  
   181  	// Output to the user the variables that will be uploaded
   182  	var setVars []string
   183  	for k, _ := range ctx.Variables() {
   184  		if _, ok := overwriteMap[k]; !ok {
   185  			if _, ok := atlasVars[k]; ok {
   186  				// Atlas variable not within override, so it came from Atlas
   187  				continue
   188  			}
   189  		}
   190  
   191  		// This variable was set from the local value
   192  		setVars = append(setVars, k)
   193  	}
   194  	sort.Strings(setVars)
   195  	if len(setVars) > 0 {
   196  		c.Ui.Output(
   197  			"The following variables will be set or overwritten within Atlas from\n" +
   198  				"their local values. All other variables are already set within Atlas.\n" +
   199  				"If you want to modify the value of a variable, use the Atlas web\n" +
   200  				"interface or set it locally and use the -overwrite flag.\n\n")
   201  		for _, v := range setVars {
   202  			c.Ui.Output(fmt.Sprintf("  * %s", v))
   203  		}
   204  
   205  		// Newline
   206  		c.Ui.Output("")
   207  	}
   208  
   209  	// Upsert!
   210  	opts := &pushUpsertOptions{
   211  		Name:      name,
   212  		Archive:   archiveR,
   213  		Variables: ctx.Variables(),
   214  	}
   215  	c.Ui.Output("Uploading Terraform configuration...")
   216  	vsn, err := c.client.Upsert(opts)
   217  	if err != nil {
   218  		c.Ui.Error(fmt.Sprintf(
   219  			"An error occurred while uploading the module:\n\n%s", err))
   220  		return 1
   221  	}
   222  
   223  	c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
   224  		"[reset][bold][green]Configuration %q uploaded! (v%d)",
   225  		name, vsn)))
   226  	return 0
   227  }
   228  
   229  func (c *PushCommand) Help() string {
   230  	helpText := `
   231  Usage: terraform push [options] [DIR]
   232  
   233    Upload this Terraform module to an Atlas server for remote
   234    infrastructure management.
   235  
   236  Options:
   237  
   238    -atlas-address=<url> An alternate address to an Atlas instance. Defaults
   239                         to https://atlas.hashicorp.com
   240  
   241    -upload-modules=true If true (default), then the modules are locked at
   242                         their current checkout and uploaded completely. This
   243                         prevents Atlas from running "terraform get".
   244  
   245    -name=<name>         Name of the configuration in Atlas. This can also
   246                         be set in the configuration itself. Format is
   247                         typically: "username/name".
   248  
   249    -token=<token>       Access token to use to upload. If blank or unspecified,
   250                         the ATLAS_TOKEN environmental variable will be used.
   251  
   252    -overwrite=foo       Variable keys that should overwrite values in Atlas.
   253                         Otherwise, variables already set in Atlas will overwrite
   254                         local values. This flag can be repeated.
   255  
   256    -var 'foo=bar'       Set a variable in the Terraform configuration. This
   257                         flag can be set multiple times.
   258  
   259    -var-file=foo        Set variables in the Terraform configuration from
   260                         a file. If "terraform.tfvars" is present, it will be
   261                         automatically loaded if this flag is not specified.
   262  
   263    -vcs=true            If true (default), push will upload only files
   264                         committed to your VCS, if detected.
   265  
   266    -no-color           If specified, output won't contain any color.
   267  
   268  `
   269  	return strings.TrimSpace(helpText)
   270  }
   271  
   272  func (c *PushCommand) Synopsis() string {
   273  	return "Upload this Terraform module to Atlas to run"
   274  }
   275  
   276  // pushClient is implementd internally to control where pushes go. This is
   277  // either to Atlas or a mock for testing.
   278  type pushClient interface {
   279  	Get(string) (map[string]string, error)
   280  	Upsert(*pushUpsertOptions) (int, error)
   281  }
   282  
   283  type pushUpsertOptions struct {
   284  	Name      string
   285  	Archive   *archive.Archive
   286  	Variables map[string]string
   287  }
   288  
   289  type atlasPushClient struct {
   290  	Client *atlas.Client
   291  }
   292  
   293  func (c *atlasPushClient) Get(name string) (map[string]string, error) {
   294  	user, name, err := atlas.ParseSlug(name)
   295  	if err != nil {
   296  		return nil, err
   297  	}
   298  
   299  	version, err := c.Client.TerraformConfigLatest(user, name)
   300  	if err != nil {
   301  		return nil, err
   302  	}
   303  
   304  	var variables map[string]string
   305  	if version != nil {
   306  		variables = version.Variables
   307  	}
   308  
   309  	return variables, nil
   310  }
   311  
   312  func (c *atlasPushClient) Upsert(opts *pushUpsertOptions) (int, error) {
   313  	user, name, err := atlas.ParseSlug(opts.Name)
   314  	if err != nil {
   315  		return 0, err
   316  	}
   317  
   318  	data := &atlas.TerraformConfigVersion{
   319  		Variables: opts.Variables,
   320  	}
   321  
   322  	version, err := c.Client.CreateTerraformConfigVersion(
   323  		user, name, data, opts.Archive, opts.Archive.Size)
   324  	if err != nil {
   325  		return 0, err
   326  	}
   327  
   328  	return version, nil
   329  }
   330  
   331  type mockPushClient struct {
   332  	File string
   333  
   334  	GetCalled bool
   335  	GetName   string
   336  	GetResult map[string]string
   337  	GetError  error
   338  
   339  	UpsertCalled  bool
   340  	UpsertOptions *pushUpsertOptions
   341  	UpsertVersion int
   342  	UpsertError   error
   343  }
   344  
   345  func (c *mockPushClient) Get(name string) (map[string]string, error) {
   346  	c.GetCalled = true
   347  	c.GetName = name
   348  	return c.GetResult, c.GetError
   349  }
   350  
   351  func (c *mockPushClient) Upsert(opts *pushUpsertOptions) (int, error) {
   352  	f, err := os.Create(c.File)
   353  	if err != nil {
   354  		return 0, err
   355  	}
   356  	defer f.Close()
   357  
   358  	data := opts.Archive
   359  	size := opts.Archive.Size
   360  	if _, err := io.CopyN(f, data, size); err != nil {
   361  		return 0, err
   362  	}
   363  
   364  	c.UpsertCalled = true
   365  	c.UpsertOptions = opts
   366  	return c.UpsertVersion, c.UpsertError
   367  }