github.com/paulmey/terraform@v0.5.2-0.20150519145237-046e9b4c884d/command/push.go (about)

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