terraform/command/push.go

437 lines
11 KiB
Go
Raw Normal View History

2015-03-05 05:42:26 +01:00
package command
import (
"fmt"
2015-03-05 23:55:15 +01:00
"io"
2015-03-05 05:42:26 +01:00
"os"
2015-03-05 21:37:13 +01:00
"path/filepath"
2015-06-29 22:41:07 +02:00
"sort"
2015-03-05 05:42:26 +01:00
"strings"
2015-03-05 21:37:13 +01:00
"github.com/hashicorp/atlas-go/archive"
"github.com/hashicorp/atlas-go/v1"
"github.com/hashicorp/terraform/terraform"
2015-03-05 05:42:26 +01:00
)
type PushCommand struct {
Meta
2015-03-05 23:55:15 +01:00
// client is the client to use for the actual push operations.
// If this isn't set, then the Atlas client is used. This should
// really only be set for testing reasons (and is hence not exported).
client pushClient
2015-03-05 05:42:26 +01:00
}
func (c *PushCommand) Run(args []string) int {
2015-03-25 01:45:19 +01:00
var atlasAddress, atlasToken string
2015-03-25 01:03:59 +01:00
var archiveVCS, moduleUpload bool
var name string
2015-06-29 22:57:58 +02:00
var overwrite []string
2015-04-08 00:34:06 +02:00
args = c.Meta.process(args, true)
2015-03-06 23:49:22 +01:00
cmdFlags := c.Meta.flagSet("push")
2015-03-25 01:45:19 +01:00
cmdFlags.StringVar(&atlasAddress, "atlas-address", "", "")
2015-03-05 05:42:26 +01:00
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&atlasToken, "token", "", "")
2015-03-25 01:42:40 +01:00
cmdFlags.BoolVar(&moduleUpload, "upload-modules", true, "")
cmdFlags.StringVar(&name, "name", "", "")
2015-03-25 01:03:59 +01:00
cmdFlags.BoolVar(&archiveVCS, "vcs", true, "")
2015-06-29 22:57:58 +02:00
cmdFlags.Var((*FlagStringSlice)(&overwrite), "overwrite", "")
2015-03-05 05:42:26 +01:00
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
// Make a map of the set values
2015-06-29 22:57:58 +02:00
overwriteMap := make(map[string]struct{}, len(overwrite))
for _, v := range overwrite {
overwriteMap[v] = struct{}{}
}
2015-03-05 05:42:26 +01:00
// The pwd is used for the configuration path if one is not given
pwd, err := os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
return 1
}
// Get the path to the configuration depending on the args.
var configPath string
args = cmdFlags.Args()
if len(args) > 1 {
c.Ui.Error("The apply command expects at most one argument.")
cmdFlags.Usage()
return 1
} else if len(args) == 1 {
configPath = args[0]
} else {
configPath = pwd
}
// Verify the state is remote, we can't push without a remote state
s, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
return 1
}
if !s.State().IsRemote() {
c.Ui.Error(
"Remote state is not enabled. For Atlas to run Terraform\n" +
"for you, remote state must be used and configured. Remote\n" +
"state via any backend is accepted, not just Atlas. To\n" +
"configure remote state, use the `terraform remote config`\n" +
"command.")
return 1
}
// Build the context based on the arguments given
2015-03-05 23:55:15 +01:00
ctx, planned, err := c.Context(contextOpts{
2015-03-05 05:42:26 +01:00
Path: configPath,
StatePath: c.Meta.statePath,
})
2015-03-05 05:42:26 +01:00
if err != nil {
c.Ui.Error(err.Error())
return 1
}
if planned {
c.Ui.Error(
"A plan file cannot be given as the path to the configuration.\n" +
"A path to a module (directory with configuration) must be given.")
return 1
}
// Get the configuration
config := ctx.Module().Config()
if name == "" {
if config.Atlas == nil || config.Atlas.Name == "" {
c.Ui.Error(
"The name of this Terraform configuration in Atlas must be\n" +
"specified within your configuration or the command-line. To\n" +
"set it on the command-line, use the `-name` parameter.")
return 1
}
name = config.Atlas.Name
}
// Initialize the client if it isn't given.
if c.client == nil {
// Make sure to nil out our client so our token isn't sitting around
defer func() { c.client = nil }()
// Initialize it to the default client, we set custom settings later
client := atlas.DefaultClient()
2015-03-25 01:45:19 +01:00
if atlasAddress != "" {
client, err = atlas.NewClient(atlasAddress)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing Atlas client: %s", err))
return 1
}
}
client.DefaultHeader.Set(terraform.VersionHeader, terraform.Version)
if atlasToken != "" {
client.Token = atlasToken
}
c.client = &atlasPushClient{Client: client}
}
// Get the variables we might already have
2015-06-29 22:41:07 +02:00
atlasVars, err := c.client.Get(name)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error looking up previously pushed configuration: %s", err))
return 1
}
2015-06-29 22:41:07 +02:00
for k, v := range atlasVars {
2015-06-29 22:57:58 +02:00
if _, ok := overwriteMap[k]; ok {
continue
}
ctx.SetVariable(k, v)
}
2015-03-05 23:55:15 +01:00
// Ask for input
if err := ctx.Input(c.InputMode()); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error while asking for variable input:\n\n%s", err))
return 1
}
2015-03-05 21:37:13 +01:00
// Build the archiving options, which includes everything it can
// by default according to VCS rules but forcing the data directory.
archiveOpts := &archive.ArchiveOpts{
2015-03-25 01:03:59 +01:00
VCS: archiveVCS,
Extra: map[string]string{
DefaultDataDir: c.DataDir(),
},
2015-03-05 21:37:13 +01:00
}
if !moduleUpload {
// If we're not uploading modules, then exclude the modules dir.
2015-03-05 21:37:13 +01:00
archiveOpts.Exclude = append(
archiveOpts.Exclude,
filepath.Join(c.DataDir(), "modules"))
}
2015-03-05 23:55:15 +01:00
archiveR, err := archive.CreateArchive(configPath, archiveOpts)
2015-03-05 21:37:13 +01:00
if err != nil {
c.Ui.Error(fmt.Sprintf(
"An error has occurred while archiving the module for uploading:\n"+
"%s", err))
return 1
}
2015-06-29 22:41:07 +02:00
// Output to the user the variables that will be uploaded
var setVars []string
for k, _ := range ctx.Variables() {
2015-06-29 22:57:58 +02:00
if _, ok := overwriteMap[k]; !ok {
2015-06-29 22:41:07 +02:00
if _, ok := atlasVars[k]; ok {
// Atlas variable not within override, so it came from Atlas
continue
}
}
// This variable was set from the local value
setVars = append(setVars, k)
}
sort.Strings(setVars)
if len(setVars) > 0 {
c.Ui.Output(
2015-06-29 22:58:54 +02:00
"The following variables will be set or overwritten within Atlas from\n" +
2015-06-29 22:41:07 +02:00
"their local values. All other variables are already set within Atlas.\n" +
"If you want to modify the value of a variable, use the Atlas web\n" +
"interface or set it locally and use the -overwrite flag.\n\n")
2015-06-29 22:41:07 +02:00
for _, v := range setVars {
c.Ui.Output(fmt.Sprintf(" * %s", v))
}
// Newline
c.Ui.Output("")
}
variables := ctx.Variables()
serializedVars, err := tfVars(variables)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"An error has occurred while serializing the variables for uploading:\n"+
"%s", err))
return 1
}
2015-03-05 23:55:15 +01:00
// Upsert!
opts := &pushUpsertOptions{
Name: name,
Archive: archiveR,
Variables: ctx.Variables(),
TFVars: serializedVars,
}
2015-06-29 22:41:07 +02:00
c.Ui.Output("Uploading Terraform configuration...")
vsn, err := c.client.Upsert(opts)
if err != nil {
2015-03-05 23:55:15 +01:00
c.Ui.Error(fmt.Sprintf(
"An error occurred while uploading the module:\n\n%s", err))
return 1
}
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
2015-03-25 01:41:26 +01:00
"[reset][bold][green]Configuration %q uploaded! (v%d)",
name, vsn)))
2015-03-05 05:42:26 +01:00
return 0
}
func (c *PushCommand) Help() string {
helpText := `
Usage: terraform push [options] [DIR]
Upload this Terraform module to an Atlas server for remote
infrastructure management.
Options:
2015-03-25 01:45:19 +01:00
-atlas-address=<url> An alternate address to an Atlas instance. Defaults
to https://atlas.hashicorp.com
2015-03-25 01:42:40 +01:00
-upload-modules=true If true (default), then the modules are locked at
2015-03-05 05:42:26 +01:00
their current checkout and uploaded completely. This
prevents Atlas from running "terraform get".
-name=<name> Name of the configuration in Atlas. This can also
be set in the configuration itself. Format is
typically: "username/name".
2015-03-25 01:41:26 +01:00
-token=<token> Access token to use to upload. If blank or unspecified,
the ATLAS_TOKEN environmental variable will be used.
2015-03-05 05:42:26 +01:00
-overwrite=foo Variable keys that should overwrite values in Atlas.
Otherwise, variables already set in Atlas will overwrite
local values. This flag can be repeated.
-var 'foo=bar' Set a variable in the Terraform configuration. This
flag can be set multiple times.
-var-file=foo Set variables in the Terraform configuration from
a file. If "terraform.tfvars" is present, it will be
automatically loaded if this flag is not specified.
2015-03-25 01:03:59 +01:00
-vcs=true If true (default), push will upload only files
2015-09-11 20:56:20 +02:00
committed to your VCS, if detected.
2015-03-25 01:03:59 +01:00
2015-06-22 14:14:01 +02:00
-no-color If specified, output won't contain any color.
2015-03-05 05:42:26 +01:00
`
return strings.TrimSpace(helpText)
}
func sortedKeys(m map[string]interface{}) []string {
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
// build the set of TFVars for push
func tfVars(vars map[string]interface{}) ([]atlas.TFVar, error) {
var tfVars []atlas.TFVar
var err error
RANGE:
for _, k := range sortedKeys(vars) {
v := vars[k]
var hcl []byte
tfv := atlas.TFVar{Key: k}
switch v := v.(type) {
case string:
tfv.Value = v
case []interface{}:
hcl, err = encodeHCL(v)
if err != nil {
break RANGE
}
tfv.Value = string(hcl)
tfv.IsHCL = true
case map[string]interface{}:
hcl, err = encodeHCL(v)
if err != nil {
break RANGE
}
tfv.Value = string(hcl)
tfv.IsHCL = true
default:
err = fmt.Errorf("unknown type %T for variable %s", v, k)
}
tfVars = append(tfVars, tfv)
}
return tfVars, err
}
2015-03-05 05:42:26 +01:00
func (c *PushCommand) Synopsis() string {
return "Upload this Terraform module to Atlas to run"
}
2015-03-05 23:55:15 +01:00
// pushClient is implementd internally to control where pushes go. This is
// either to Atlas or a mock for testing.
type pushClient interface {
Get(string) (map[string]interface{}, error)
Upsert(*pushUpsertOptions) (int, error)
}
type pushUpsertOptions struct {
Name string
Archive *archive.Archive
Variables map[string]interface{}
TFVars []atlas.TFVar
2015-03-05 23:55:15 +01:00
}
type atlasPushClient struct {
Client *atlas.Client
}
func (c *atlasPushClient) Get(name string) (map[string]interface{}, error) {
user, name, err := atlas.ParseSlug(name)
if err != nil {
return nil, err
}
version, err := c.Client.TerraformConfigLatest(user, name)
if err != nil {
return nil, err
}
var variables map[string]interface{}
if version != nil {
// TODO: merge variables and TFVars
//variables = version.Variables
}
return variables, nil
}
func (c *atlasPushClient) Upsert(opts *pushUpsertOptions) (int, error) {
user, name, err := atlas.ParseSlug(opts.Name)
if err != nil {
return 0, err
}
data := &atlas.TerraformConfigVersion{
TFVars: opts.TFVars,
}
version, err := c.Client.CreateTerraformConfigVersion(
user, name, data, opts.Archive, opts.Archive.Size)
if err != nil {
return 0, err
}
return version, nil
}
2015-03-05 23:55:15 +01:00
type mockPushClient struct {
File string
GetCalled bool
GetName string
GetResult map[string]interface{}
GetError error
UpsertCalled bool
UpsertOptions *pushUpsertOptions
UpsertVersion int
UpsertError error
2015-03-05 23:55:15 +01:00
}
func (c *mockPushClient) Get(name string) (map[string]interface{}, error) {
c.GetCalled = true
c.GetName = name
return c.GetResult, c.GetError
}
func (c *mockPushClient) Upsert(opts *pushUpsertOptions) (int, error) {
2015-03-05 23:55:15 +01:00
f, err := os.Create(c.File)
if err != nil {
return 0, err
2015-03-05 23:55:15 +01:00
}
defer f.Close()
data := opts.Archive
size := opts.Archive.Size
2015-03-05 23:55:15 +01:00
if _, err := io.CopyN(f, data, size); err != nil {
return 0, err
2015-03-05 23:55:15 +01:00
}
c.UpsertCalled = true
c.UpsertOptions = opts
return c.UpsertVersion, c.UpsertError
2015-03-05 23:55:15 +01:00
}