Merge pull request #7803 from hashicorp/jbardin/tf_vars-push

Add tf_vars to the variables sent in push
This commit is contained in:
James Bardin 2016-07-28 16:31:04 -04:00 committed by GitHub
commit 9dec28bccf
7 changed files with 415 additions and 58 deletions

190
command/hcl_printer.go Normal file
View File

@ -0,0 +1,190 @@
package command
// Marshal an object as an hcl value.
import (
"bytes"
"fmt"
"regexp"
"github.com/hashicorp/hcl/hcl/printer"
)
// This will only work operate on []interface{}, map[string]interface{}, and
// primitive types.
func encodeHCL(i interface{}) ([]byte, error) {
state := &encodeState{}
err := state.encode(i)
if err != nil {
return nil, err
}
hcl := state.Bytes()
if len(hcl) == 0 {
return hcl, nil
}
// the HCL parser requires an assignment. Strip it off again later
fakeAssignment := append([]byte("X = "), hcl...)
// use the real hcl parser to verify our output, and format it canonically
hcl, err = printer.Format(fakeAssignment)
if err != nil {
return nil, err
}
// now strip that first assignment off
eq := regexp.MustCompile(`=\s+`).FindIndex(hcl)
return hcl[eq[1]:], nil
}
type encodeState struct {
bytes.Buffer
}
func (e *encodeState) encode(i interface{}) error {
switch v := i.(type) {
case []interface{}:
return e.encodeList(v)
case map[string]interface{}:
return e.encodeMap(v)
case int, int8, int32, int64, uint8, uint32, uint64:
return e.encodeInt(i)
case float32, float64:
return e.encodeFloat(i)
case string:
return e.encodeString(v)
case nil:
return nil
default:
return fmt.Errorf("invalid type %T", i)
}
}
func (e *encodeState) encodeList(l []interface{}) error {
e.WriteString("[")
for i, v := range l {
err := e.encode(v)
if err != nil {
return err
}
if i < len(l)-1 {
e.WriteString(", ")
}
}
e.WriteString("]")
return nil
}
func (e *encodeState) encodeMap(m map[string]interface{}) error {
e.WriteString("{\n")
for i, k := range sortedKeys(m) {
v := m[k]
e.WriteString(k + " = ")
err := e.encode(v)
if err != nil {
return err
}
if i < len(m)-1 {
e.WriteString("\n")
}
}
e.WriteString("}")
return nil
}
func (e *encodeState) encodeInt(i interface{}) error {
_, err := fmt.Fprintf(e, "%d", i)
return err
}
func (e *encodeState) encodeFloat(f interface{}) error {
_, err := fmt.Fprintf(e, "%f", f)
return err
}
func (e *encodeState) encodeString(s string) error {
e.Write(quoteHCLString(s))
return nil
}
// Quote an HCL string, which may contain interpolations.
// Since the string was already parsed from HCL, we have to assume the
// required characters are sanely escaped. All we need to do is escape double
// quotes in the string, unless they are in an interpolation block.
func quoteHCLString(s string) []byte {
out := make([]byte, 0, len(s))
out = append(out, '"')
// our parse states
var (
outer = 1 // the starting state for the string
dollar = 2 // look for '{' in the next character
interp = 3 // inside an interpolation block
escape = 4 // take the next character and pop back to prev state
)
// we could have nested interpolations
state := stack{}
state.push(outer)
for i := 0; i < len(s); i++ {
switch state.peek() {
case outer:
switch s[i] {
case '"':
out = append(out, '\\')
case '$':
state.push(dollar)
case '\\':
state.push(escape)
}
case dollar:
state.pop()
switch s[i] {
case '{':
state.push(interp)
case '\\':
state.push(escape)
}
case interp:
switch s[i] {
case '}':
state.pop()
}
case escape:
state.pop()
}
out = append(out, s[i])
}
out = append(out, '"')
return out
}
type stack []int
func (s *stack) push(i int) {
*s = append(*s, i)
}
func (s *stack) pop() int {
last := len(*s) - 1
i := (*s)[last]
*s = (*s)[:last]
return i
}
func (s *stack) peek() int {
return (*s)[len(*s)-1]
}

View File

@ -88,6 +88,7 @@ func (c *PushCommand) Run(args []string) int {
Path: configPath, Path: configPath,
StatePath: c.Meta.statePath, StatePath: c.Meta.statePath,
}) })
if err != nil { if err != nil {
c.Ui.Error(err.Error()) c.Ui.Error(err.Error())
return 1 return 1
@ -136,19 +137,28 @@ func (c *PushCommand) Run(args []string) int {
c.client = &atlasPushClient{Client: client} c.client = &atlasPushClient{Client: client}
} }
// Get the variables we might already have // Get the variables we already have in atlas
atlasVars, err := c.client.Get(name) atlasVars, err := c.client.Get(name)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf( c.Ui.Error(fmt.Sprintf(
"Error looking up previously pushed configuration: %s", err)) "Error looking up previously pushed configuration: %s", err))
return 1 return 1
} }
for k, v := range atlasVars {
if _, ok := overwriteMap[k]; ok {
continue
}
ctx.SetVariable(k, v) // filter any overwrites from the atlas vars
for k := range overwriteMap {
delete(atlasVars, k)
}
// Set remote variables in the context if we don't have a value here. These
// don't have to be correct, it just prevents the Input walk from prompting
// the user for input, The atlas variable may be an hcl-encoded object, but
// we're just going to set it as the raw string value.
ctxVars := ctx.Variables()
for k, av := range atlasVars {
if _, ok := ctxVars[k]; !ok {
ctx.SetVariable(k, av.Value)
}
} }
// Ask for input // Ask for input
@ -158,6 +168,18 @@ func (c *PushCommand) Run(args []string) int {
return 1 return 1
} }
// Now that we've gone through the input walk, we can be sure we have all
// the variables we're going to get.
// We are going to keep these separate from the atlas variables until
// upload, so we can notify the user which local variables we're sending.
serializedVars, err := tfVars(ctx.Variables())
if err != nil {
c.Ui.Error(fmt.Sprintf(
"An error has occurred while serializing the variables for uploading:\n"+
"%s", err))
return 1
}
// Build the archiving options, which includes everything it can // Build the archiving options, which includes everything it can
// by default according to VCS rules but forcing the data directory. // by default according to VCS rules but forcing the data directory.
archiveOpts := &archive.ArchiveOpts{ archiveOpts := &archive.ArchiveOpts{
@ -183,17 +205,23 @@ func (c *PushCommand) Run(args []string) int {
// Output to the user the variables that will be uploaded // Output to the user the variables that will be uploaded
var setVars []string var setVars []string
for k, _ := range ctx.Variables() { // variables to upload
if _, ok := overwriteMap[k]; !ok { var uploadVars []atlas.TFVar
if _, ok := atlasVars[k]; ok {
// Atlas variable not within override, so it came from Atlas // Now we can combine the vars for upload to atlas and list the variables
continue // we're uploading for the user
} for _, sv := range serializedVars {
if av, ok := atlasVars[sv.Key]; ok {
// this belongs to Atlas
uploadVars = append(uploadVars, av)
} else {
// we're uploading our local version
setVars = append(setVars, sv.Key)
uploadVars = append(uploadVars, sv)
} }
// This variable was set from the local value
setVars = append(setVars, k)
} }
sort.Strings(setVars) sort.Strings(setVars)
if len(setVars) > 0 { if len(setVars) > 0 {
c.Ui.Output( c.Ui.Output(
@ -214,7 +242,9 @@ func (c *PushCommand) Run(args []string) int {
Name: name, Name: name,
Archive: archiveR, Archive: archiveR,
Variables: ctx.Variables(), Variables: ctx.Variables(),
TFVars: uploadVars,
} }
c.Ui.Output("Uploading Terraform configuration...") c.Ui.Output("Uploading Terraform configuration...")
vsn, err := c.client.Upsert(opts) vsn, err := c.client.Upsert(opts)
if err != nil { if err != nil {
@ -272,14 +302,67 @@ Options:
return strings.TrimSpace(helpText) 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
}
func (c *PushCommand) Synopsis() string { func (c *PushCommand) Synopsis() string {
return "Upload this Terraform module to Atlas to run" return "Upload this Terraform module to Atlas to run"
} }
// pushClient is implementd internally to control where pushes go. This is // pushClient is implemented internally to control where pushes go. This is
// either to Atlas or a mock for testing. // either to Atlas or a mock for testing. We still return a map to make it
// easier to check for variable existence when filtering the overrides.
type pushClient interface { type pushClient interface {
Get(string) (map[string]interface{}, error) Get(string) (map[string]atlas.TFVar, error)
Upsert(*pushUpsertOptions) (int, error) Upsert(*pushUpsertOptions) (int, error)
} }
@ -287,13 +370,14 @@ type pushUpsertOptions struct {
Name string Name string
Archive *archive.Archive Archive *archive.Archive
Variables map[string]interface{} Variables map[string]interface{}
TFVars []atlas.TFVar
} }
type atlasPushClient struct { type atlasPushClient struct {
Client *atlas.Client Client *atlas.Client
} }
func (c *atlasPushClient) Get(name string) (map[string]interface{}, error) { func (c *atlasPushClient) Get(name string) (map[string]atlas.TFVar, error) {
user, name, err := atlas.ParseSlug(name) user, name, err := atlas.ParseSlug(name)
if err != nil { if err != nil {
return nil, err return nil, err
@ -304,9 +388,21 @@ func (c *atlasPushClient) Get(name string) (map[string]interface{}, error) {
return nil, err return nil, err
} }
var variables map[string]interface{} variables := make(map[string]atlas.TFVar)
if version != nil {
//variables = version.Variables if version == nil {
return variables, nil
}
// Variables is superseded by TFVars
if version.TFVars == nil {
for k, v := range version.Variables {
variables[k] = atlas.TFVar{Key: k, Value: v}
}
} else {
for _, v := range version.TFVars {
variables[v.Key] = v
}
} }
return variables, nil return variables, nil
@ -319,7 +415,7 @@ func (c *atlasPushClient) Upsert(opts *pushUpsertOptions) (int, error) {
} }
data := &atlas.TerraformConfigVersion{ data := &atlas.TerraformConfigVersion{
//Variables: opts.Variables, TFVars: opts.TFVars,
} }
version, err := c.Client.CreateTerraformConfigVersion( version, err := c.Client.CreateTerraformConfigVersion(
@ -336,7 +432,7 @@ type mockPushClient struct {
GetCalled bool GetCalled bool
GetName string GetName string
GetResult map[string]interface{} GetResult map[string]atlas.TFVar
GetError error GetError error
UpsertCalled bool UpsertCalled bool
@ -345,7 +441,7 @@ type mockPushClient struct {
UpsertError error UpsertError error
} }
func (c *mockPushClient) Get(name string) (map[string]interface{}, error) { func (c *mockPushClient) Get(name string) (map[string]atlas.TFVar, error) {
c.GetCalled = true c.GetCalled = true
c.GetName = name c.GetName = name
return c.GetResult, c.GetError return c.GetResult, c.GetError

View File

@ -10,6 +10,7 @@ import (
"sort" "sort"
"testing" "testing"
atlas "github.com/hashicorp/atlas-go/v1"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
@ -118,11 +119,13 @@ func TestPush_input(t *testing.T) {
variables := map[string]interface{}{ variables := map[string]interface{}{
"foo": "foo", "foo": "foo",
} }
if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) { if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) {
t.Fatalf("bad: %#v", client.UpsertOptions.Variables) t.Fatalf("bad: %#v", client.UpsertOptions.Variables)
} }
} }
// We want a variable from atlas to fill a missing variable locally
func TestPush_inputPartial(t *testing.T) { func TestPush_inputPartial(t *testing.T) {
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd) defer testFixCwd(t, tmp, cwd)
@ -142,8 +145,10 @@ func TestPush_inputPartial(t *testing.T) {
defer os.Remove(archivePath) defer os.Remove(archivePath)
client := &mockPushClient{ client := &mockPushClient{
File: archivePath, File: archivePath,
GetResult: map[string]interface{}{"foo": "bar"}, GetResult: map[string]atlas.TFVar{
"foo": atlas.TFVar{Key: "foo", Value: "bar"},
},
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &PushCommand{ c := &PushCommand{
@ -170,12 +175,13 @@ func TestPush_inputPartial(t *testing.T) {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
} }
variables := map[string]interface{}{ expectedTFVars := []atlas.TFVar{
"foo": "bar", {Key: "bar", Value: "foo"},
"bar": "foo", {Key: "foo", Value: "bar"},
} }
if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) { if !reflect.DeepEqual(client.UpsertOptions.TFVars, expectedTFVars) {
t.Fatalf("bad: %#v", client.UpsertOptions) t.Logf("expected: %#v", expectedTFVars)
t.Fatalf("got: %#v", client.UpsertOptions.TFVars)
} }
} }
@ -208,8 +214,11 @@ func TestPush_localOverride(t *testing.T) {
client := &mockPushClient{File: archivePath} client := &mockPushClient{File: archivePath}
// Provided vars should override existing ones // Provided vars should override existing ones
client.GetResult = map[string]interface{}{ client.GetResult = map[string]atlas.TFVar{
"foo": "old", "foo": atlas.TFVar{
Key: "foo",
Value: "old",
},
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &PushCommand{ c := &PushCommand{
@ -247,12 +256,11 @@ func TestPush_localOverride(t *testing.T) {
t.Fatalf("bad: %#v", client.UpsertOptions) t.Fatalf("bad: %#v", client.UpsertOptions)
} }
variables := map[string]interface{}{ expectedTFVars := pushTFVars()
"foo": "bar",
"bar": "foo", if !reflect.DeepEqual(client.UpsertOptions.TFVars, expectedTFVars) {
} t.Logf("expected: %#v", expectedTFVars)
if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) { t.Fatalf("got: %#v", client.UpsertOptions.TFVars)
t.Fatalf("bad: %#v", client.UpsertOptions)
} }
} }
@ -285,8 +293,11 @@ func TestPush_preferAtlas(t *testing.T) {
client := &mockPushClient{File: archivePath} client := &mockPushClient{File: archivePath}
// Provided vars should override existing ones // Provided vars should override existing ones
client.GetResult = map[string]interface{}{ client.GetResult = map[string]atlas.TFVar{
"foo": "old", "foo": atlas.TFVar{
Key: "foo",
Value: "old",
},
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &PushCommand{ c := &PushCommand{
@ -323,12 +334,17 @@ func TestPush_preferAtlas(t *testing.T) {
t.Fatalf("bad: %#v", client.UpsertOptions) t.Fatalf("bad: %#v", client.UpsertOptions)
} }
variables := map[string]interface{}{ // change the expected response to match our change
"foo": "old", expectedTFVars := pushTFVars()
"bar": "foo", for i, v := range expectedTFVars {
if v.Key == "foo" {
expectedTFVars[i] = atlas.TFVar{Key: "foo", Value: "old"}
}
} }
if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) {
t.Fatalf("bad: %#v", client.UpsertOptions) if !reflect.DeepEqual(expectedTFVars, client.UpsertOptions.TFVars) {
t.Logf("expected: %#v", expectedTFVars)
t.Fatalf("got: %#v", client.UpsertOptions.TFVars)
} }
} }
@ -394,12 +410,15 @@ func TestPush_tfvars(t *testing.T) {
t.Fatalf("bad: %#v", client.UpsertOptions) t.Fatalf("bad: %#v", client.UpsertOptions)
} }
variables := map[string]interface{}{ //now check TFVars
"foo": "bar", tfvars := pushTFVars()
"bar": "foo",
} for i, expected := range tfvars {
if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) { got := client.UpsertOptions.TFVars[i]
t.Fatalf("bad: %#v", client.UpsertOptions) if got != expected {
t.Logf("%2d expected: %#v", i, expected)
t.Logf(" got: %#v", got)
}
} }
} }
@ -563,3 +582,25 @@ func testArchiveStr(t *testing.T, path string) []string {
sort.Strings(result) sort.Strings(result)
return result return result
} }
func pushTFVars() []atlas.TFVar {
return []atlas.TFVar{
{"bar", "foo", false},
{"baz", `{
A = "a"
interp = "${file("t.txt")}"
}
`, true},
{"fob", `["a", "quotes \"in\" quotes"]` + "\n", true},
{"foo", "bar", false},
}
}
// the structure returned from the push-tfvars test fixture
func pushTFVarsMap() map[string]atlas.TFVar {
vars := make(map[string]atlas.TFVar)
for _, v := range pushTFVars() {
vars[v.Key] = v
}
return vars
}

View File

@ -1,8 +1,23 @@
variable "foo" {} variable "foo" {}
variable "bar" {} variable "bar" {}
variable "baz" {
type = "map"
default = {
"A" = "a"
interp = "${file("t.txt")}"
}
}
variable "fob" {
type = "list"
default = ["a", "quotes \"in\" quotes"]
}
resource "test_instance" "foo" {} resource "test_instance" "foo" {}
atlas { atlas {
name = "foo" name = "foo"
} }

View File

@ -342,6 +342,7 @@ func (c *Context) Input(mode InputMode) error {
// Ask the user for a value for this variable // Ask the user for a value for this variable
var value string var value string
retry := 0
for { for {
var err error var err error
value, err = c.uiInput.Input(&InputOpts{ value, err = c.uiInput.Input(&InputOpts{
@ -355,7 +356,12 @@ func (c *Context) Input(mode InputMode) error {
} }
if value == "" && v.Required() { if value == "" && v.Required() {
// Redo if it is required. // Redo if it is required, but abort if we keep getting
// blank entries
if retry > 2 {
return fmt.Errorf("missing required value for %q", n)
}
retry++
continue continue
} }

View File

@ -14,7 +14,16 @@ type TerraformConfigVersion struct {
Version int Version int
Remotes []string `json:"remotes"` Remotes []string `json:"remotes"`
Metadata map[string]string `json:"metadata"` Metadata map[string]string `json:"metadata"`
Variables map[string]string `json:"variables"` Variables map[string]string `json:"variables,omitempty"`
TFVars []TFVar `json:"tf_vars"`
}
// TFVar is used to serialize a single Terraform variable sent by the
// manager as a collection of Variables in a Job payload.
type TFVar struct {
Key string `json:"key"`
Value string `json:"value"`
IsHCL bool `json:"hcl"`
} }
// TerraformConfigLatest returns the latest Terraform configuration version. // TerraformConfigLatest returns the latest Terraform configuration version.

6
vendor/vendor.json vendored
View File

@ -1060,11 +1060,11 @@
"revision": "95fa852edca41c06c4ce526af4bb7dec4eaad434" "revision": "95fa852edca41c06c4ce526af4bb7dec4eaad434"
}, },
{ {
"checksumSHA1": "EWGfo74RcoKaYFZNSkvzYRJMgrY=", "checksumSHA1": "yylO3hSRKd0T4mveT9ho2OSARwU=",
"comment": "20141209094003-92-g95fa852", "comment": "20141209094003-92-g95fa852",
"path": "github.com/hashicorp/atlas-go/v1", "path": "github.com/hashicorp/atlas-go/v1",
"revision": "c8b26aa95f096efc0f378b2d2830ca909631d584", "revision": "9be9a611a15ba2f857a99b332fd966896867299a",
"revisionTime": "2016-07-22T13:58:36Z" "revisionTime": "2016-07-26T16:33:11Z"
}, },
{ {
"comment": "v0.6.3-28-g3215b87", "comment": "v0.6.3-28-g3215b87",