github.com/markdia/terraform@v0.5.1-0.20150508012022-f1ae920aa970/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  		ctx.SetVariable(k, v)
   136  	}
   137  
   138  	// Ask for input
   139  	if err := ctx.Input(c.InputMode()); err != nil {
   140  		c.Ui.Error(fmt.Sprintf(
   141  			"Error while asking for variable input:\n\n%s", err))
   142  		return 1
   143  	}
   144  
   145  	// Build the archiving options, which includes everything it can
   146  	// by default according to VCS rules but forcing the data directory.
   147  	archiveOpts := &archive.ArchiveOpts{
   148  		VCS: archiveVCS,
   149  		Extra: map[string]string{
   150  			DefaultDataDir: c.DataDir(),
   151  		},
   152  	}
   153  	if !moduleUpload {
   154  		// If we're not uploading modules, then exclude the modules dir.
   155  		archiveOpts.Exclude = append(
   156  			archiveOpts.Exclude,
   157  			filepath.Join(c.DataDir(), "modules"))
   158  	}
   159  
   160  	archiveR, err := archive.CreateArchive(configPath, archiveOpts)
   161  	if err != nil {
   162  		c.Ui.Error(fmt.Sprintf(
   163  			"An error has occurred while archiving the module for uploading:\n"+
   164  				"%s", err))
   165  		return 1
   166  	}
   167  
   168  	// Upsert!
   169  	opts := &pushUpsertOptions{
   170  		Name:      name,
   171  		Archive:   archiveR,
   172  		Variables: ctx.Variables(),
   173  	}
   174  	vsn, err := c.client.Upsert(opts)
   175  	if err != nil {
   176  		c.Ui.Error(fmt.Sprintf(
   177  			"An error occurred while uploading the module:\n\n%s", err))
   178  		return 1
   179  	}
   180  
   181  	c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
   182  		"[reset][bold][green]Configuration %q uploaded! (v%d)",
   183  		name, vsn)))
   184  	return 0
   185  }
   186  
   187  func (c *PushCommand) Help() string {
   188  	helpText := `
   189  Usage: terraform push [options] [DIR]
   190  
   191    Upload this Terraform module to an Atlas server for remote
   192    infrastructure management.
   193  
   194  Options:
   195  
   196    -atlas-address=<url> An alternate address to an Atlas instance. Defaults
   197                         to https://atlas.hashicorp.com
   198  
   199    -upload-modules=true If true (default), then the modules are locked at
   200                         their current checkout and uploaded completely. This
   201                         prevents Atlas from running "terraform get".
   202  
   203    -name=<name>         Name of the configuration in Atlas. This can also
   204                         be set in the configuration itself. Format is
   205                         typically: "username/name".
   206  
   207    -token=<token>       Access token to use to upload. If blank or unspecified,
   208                         the ATLAS_TOKEN environmental variable will be used.
   209  
   210    -vcs=true            If true (default), push will upload only files
   211                         comitted to your VCS, if detected.
   212  
   213  `
   214  	return strings.TrimSpace(helpText)
   215  }
   216  
   217  func (c *PushCommand) Synopsis() string {
   218  	return "Upload this Terraform module to Atlas to run"
   219  }
   220  
   221  // pushClient is implementd internally to control where pushes go. This is
   222  // either to Atlas or a mock for testing.
   223  type pushClient interface {
   224  	Get(string) (map[string]string, error)
   225  	Upsert(*pushUpsertOptions) (int, error)
   226  }
   227  
   228  type pushUpsertOptions struct {
   229  	Name      string
   230  	Archive   *archive.Archive
   231  	Variables map[string]string
   232  }
   233  
   234  type atlasPushClient struct {
   235  	Client *atlas.Client
   236  }
   237  
   238  func (c *atlasPushClient) Get(name string) (map[string]string, error) {
   239  	user, name, err := atlas.ParseSlug(name)
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  
   244  	version, err := c.Client.TerraformConfigLatest(user, name)
   245  	if err != nil {
   246  		return nil, err
   247  	}
   248  
   249  	var variables map[string]string
   250  	if version != nil {
   251  		variables = version.Variables
   252  	}
   253  
   254  	return variables, nil
   255  }
   256  
   257  func (c *atlasPushClient) Upsert(opts *pushUpsertOptions) (int, error) {
   258  	user, name, err := atlas.ParseSlug(opts.Name)
   259  	if err != nil {
   260  		return 0, err
   261  	}
   262  
   263  	data := &atlas.TerraformConfigVersion{
   264  		Variables: opts.Variables,
   265  	}
   266  
   267  	version, err := c.Client.CreateTerraformConfigVersion(
   268  		user, name, data, opts.Archive, opts.Archive.Size)
   269  	if err != nil {
   270  		return 0, err
   271  	}
   272  
   273  	return version, nil
   274  }
   275  
   276  type mockPushClient struct {
   277  	File string
   278  
   279  	GetCalled bool
   280  	GetName   string
   281  	GetResult map[string]string
   282  	GetError  error
   283  
   284  	UpsertCalled  bool
   285  	UpsertOptions *pushUpsertOptions
   286  	UpsertVersion int
   287  	UpsertError   error
   288  }
   289  
   290  func (c *mockPushClient) Get(name string) (map[string]string, error) {
   291  	c.GetCalled = true
   292  	c.GetName = name
   293  	return c.GetResult, c.GetError
   294  }
   295  
   296  func (c *mockPushClient) Upsert(opts *pushUpsertOptions) (int, error) {
   297  	f, err := os.Create(c.File)
   298  	if err != nil {
   299  		return 0, err
   300  	}
   301  	defer f.Close()
   302  
   303  	data := opts.Archive
   304  	size := opts.Archive.Size
   305  	if _, err := io.CopyN(f, data, size); err != nil {
   306  		return 0, err
   307  	}
   308  
   309  	c.UpsertCalled = true
   310  	c.UpsertOptions = opts
   311  	return c.UpsertVersion, c.UpsertError
   312  }