Merge pull request #2559 from hashicorp/b-push

command/push: new UX (prefer Atlas over local vars) [GH-2373]
This commit is contained in:
Mitchell Hashimoto 2015-06-29 14:00:43 -07:00
commit 2b0d2f4c7d
3 changed files with 231 additions and 5 deletions

View File

@ -5,6 +5,7 @@ import (
"io"
"os"
"path/filepath"
"sort"
"strings"
"github.com/hashicorp/atlas-go/archive"
@ -24,6 +25,7 @@ func (c *PushCommand) Run(args []string) int {
var atlasAddress, atlasToken string
var archiveVCS, moduleUpload bool
var name string
var overwrite []string
args = c.Meta.process(args, true)
cmdFlags := c.Meta.flagSet("push")
cmdFlags.StringVar(&atlasAddress, "atlas-address", "", "")
@ -32,11 +34,18 @@ func (c *PushCommand) Run(args []string) int {
cmdFlags.BoolVar(&moduleUpload, "upload-modules", true, "")
cmdFlags.StringVar(&name, "name", "", "")
cmdFlags.BoolVar(&archiveVCS, "vcs", true, "")
cmdFlags.Var((*FlagStringSlice)(&overwrite), "overwrite", "")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
// Make a map of the set values
overwriteMap := make(map[string]struct{}, len(overwrite))
for _, v := range overwrite {
overwriteMap[v] = struct{}{}
}
// The pwd is used for the configuration path if one is not given
pwd, err := os.Getwd()
if err != nil {
@ -125,17 +134,17 @@ func (c *PushCommand) Run(args []string) int {
}
// Get the variables we might already have
vars, err := c.client.Get(name)
atlasVars, err := c.client.Get(name)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error looking up previously pushed configuration: %s", err))
return 1
}
for k, v := range vars {
// Local variables override remote ones
if _, exists := ctx.Variables()[k]; exists {
for k, v := range atlasVars {
if _, ok := overwriteMap[k]; ok {
continue
}
ctx.SetVariable(k, v)
}
@ -169,12 +178,41 @@ func (c *PushCommand) Run(args []string) int {
return 1
}
// Output to the user the variables that will be uploaded
var setVars []string
for k, _ := range ctx.Variables() {
if _, ok := overwriteMap[k]; !ok {
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(
"The following variables will be set or overwritten within Atlas from\n" +
"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")
for _, v := range setVars {
c.Ui.Output(fmt.Sprintf(" * %s", v))
}
// Newline
c.Ui.Output("")
}
// Upsert!
opts := &pushUpsertOptions{
Name: name,
Archive: archiveR,
Variables: ctx.Variables(),
}
c.Ui.Output("Uploading Terraform configuration...")
vsn, err := c.client.Upsert(opts)
if err != nil {
c.Ui.Error(fmt.Sprintf(
@ -211,6 +249,10 @@ Options:
-token=<token> Access token to use to upload. If blank or unspecified,
the ATLAS_TOKEN environmental variable will be used.
-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.

View File

@ -179,7 +179,9 @@ func TestPush_inputPartial(t *testing.T) {
}
}
func TestPush_inputTfvars(t *testing.T) {
// This tests that the push command will override Atlas variables
// if requested.
func TestPush_localOverride(t *testing.T) {
// Disable test mode so input would be asked and setup the
// input reader/writers.
test = false
@ -219,6 +221,154 @@ func TestPush_inputTfvars(t *testing.T) {
client: client,
}
path := testFixturePath("push-tfvars")
args := []string{
"-var-file", path + "/terraform.tfvars",
"-vcs=false",
"-overwrite=foo",
path,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := testArchiveStr(t, archivePath)
expected := []string{
".terraform/",
".terraform/terraform.tfstate",
"main.tf",
"terraform.tfvars",
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
if client.UpsertOptions.Name != "foo" {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
variables := map[string]string{
"foo": "bar",
"bar": "foo",
}
if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
}
// This tests that the push command prefers Atlas variables over
// local ones.
func TestPush_preferAtlas(t *testing.T) {
// Disable test mode so input would be asked and setup the
// input reader/writers.
test = false
defer func() { test = true }()
defaultInputReader = bytes.NewBufferString("nope\n")
defaultInputWriter = new(bytes.Buffer)
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Create remote state file, this should be pulled
conf, srv := testRemoteState(t, testState(), 200)
defer srv.Close()
// Persist local remote state
s := terraform.NewState()
s.Serial = 5
s.Remote = conf
testStateFileRemote(t, s)
// Path where the archive will be "uploaded" to
archivePath := testTempFile(t)
defer os.Remove(archivePath)
client := &mockPushClient{File: archivePath}
// Provided vars should override existing ones
client.GetResult = map[string]string{
"foo": "old",
}
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
client: client,
}
path := testFixturePath("push-tfvars")
args := []string{
"-var-file", path + "/terraform.tfvars",
"-vcs=false",
path,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := testArchiveStr(t, archivePath)
expected := []string{
".terraform/",
".terraform/terraform.tfstate",
"main.tf",
"terraform.tfvars",
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
if client.UpsertOptions.Name != "foo" {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
variables := map[string]string{
"foo": "old",
"bar": "foo",
}
if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
}
// This tests that the push command will send the variables in tfvars
func TestPush_tfvars(t *testing.T) {
// Disable test mode so input would be asked and setup the
// input reader/writers.
test = false
defer func() { test = true }()
defaultInputReader = bytes.NewBufferString("nope\n")
defaultInputWriter = new(bytes.Buffer)
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Create remote state file, this should be pulled
conf, srv := testRemoteState(t, testState(), 200)
defer srv.Close()
// Persist local remote state
s := terraform.NewState()
s.Serial = 5
s.Remote = conf
testStateFileRemote(t, s)
// Path where the archive will be "uploaded" to
archivePath := testTempFile(t)
defer os.Remove(archivePath)
client := &mockPushClient{File: archivePath}
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
client: client,
}
path := testFixturePath("push-tfvars")
args := []string{
"-var-file", path + "/terraform.tfvars",

View File

@ -52,10 +52,20 @@ The command-line flags are all optional. The list of available flags are:
* `-no-color` - Disables output with coloring
* `-overwrite=foo` - Marks a specific variable to be updated on Atlas.
Normally, if a variable is already set in Atlas, Terraform will not
send the local value (even if it is different). This forces it to
send the local value to Atlas. This flag can be repeated multiple times.
* `-token=<token>` - Atlas API token to use to authorize the upload.
If blank or unspecified, the `ATLAS_TOKEN` environmental variable
will be used.
* `-var='foo=bar'` - Set the value of a variable for the Terraform configuration.
* `-var-file=foo` - Set the value of variables using a variable file.
* `-vcs=true` - If true (default), then Terraform will detect if a VCS
is in use, such as Git, and will only upload files that are comitted to
version control. If no version control system is detected, Terraform will
@ -78,6 +88,30 @@ all the files to be safe. To exclude certain files, specify the `-exclude`
flag when pushing, or specify the `exclude` parameter in the
[Atlas configuration section](/docs/configuration/atlas.html).
## Terraform Variables
When you `push`, Terraform will automatically set the local values of
your Terraform variables on Atlas. The values are only set if they
don't already exist on Atlas. If you want to force push a certain
variable value to update it, use the `-overwrite` flag.
All the variable values stored on Atlas are encrypted and secured
using [Vault](https://vaultproject.io). We blogged about the
[architecture of our secure storage system](https://hashicorp.com/blog/how-atlas-uses-vault-for-managing-secrets.html) if you want more detail.
The variable values can be updated using the `-overwrite` flag or via
the [Atlas website](https://atlas.hashicorp.com). An example of updating
just a single variable `foo` is shown below:
```
$ terraform push -var 'foo=bar' -overwrite foo
...
```
Both the `-var` and `-overwrite` flag are required. The `-var` flag
sets the value locally (the exact same process as commands such as apply
or plan), and the `-overwrite` flag tells the push command to update Atlas.
## Remote State Requirement
`terraform push` requires that