Add tf_vars to the variables sent in push

Add tf_vars to the data structures sent in terraform push.

This takes any value of type []interface{} or map[string]interface{} and
marshals it as a string representation of the equivalent HCL. This
prevents ambiguity in atlas between a string that looks like a json
structure, and an actual json structure.

For the time being we will need a way to serialize data as HCL, so the
command package has an internal encodeHCL function to do so. We can
remove this if we get complete package for marshaling HCL.
This commit is contained in:
James Bardin 2016-07-25 15:56:56 -04:00
parent bef3b76c7a
commit de87267697
4 changed files with 233 additions and 15 deletions

114
command/hcl_printer.go Normal file
View File

@ -0,0 +1,114 @@
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)
// now strip that first assignment off
eq := regexp.MustCompile(`=\s+`).FindIndex(hcl)
return hcl[eq[1]:], err
}
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) encodeString(s string) error {
_, err := fmt.Fprintf(e, "%q", s)
return err
}
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
}

View File

@ -88,6 +88,7 @@ func (c *PushCommand) Run(args []string) int {
Path: configPath,
StatePath: c.Meta.statePath,
})
if err != nil {
c.Ui.Error(err.Error())
return 1
@ -209,12 +210,23 @@ func (c *PushCommand) Run(args []string) int {
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
}
// Upsert!
opts := &pushUpsertOptions{
Name: name,
Archive: archiveR,
Variables: ctx.Variables(),
TFVars: serializedVars,
}
c.Ui.Output("Uploading Terraform configuration...")
vsn, err := c.client.Upsert(opts)
if err != nil {
@ -272,6 +284,58 @@ Options:
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 {
return "Upload this Terraform module to Atlas to run"
}
@ -287,6 +351,7 @@ type pushUpsertOptions struct {
Name string
Archive *archive.Archive
Variables map[string]interface{}
TFVars []atlas.TFVar
}
type atlasPushClient struct {
@ -306,6 +371,7 @@ func (c *atlasPushClient) Get(name string) (map[string]interface{}, error) {
var variables map[string]interface{}
if version != nil {
// TODO: merge variables and TFVars
//variables = version.Variables
}
@ -319,7 +385,7 @@ func (c *atlasPushClient) Upsert(opts *pushUpsertOptions) (int, error) {
}
data := &atlas.TerraformConfigVersion{
//Variables: opts.Variables,
TFVars: opts.TFVars,
}
version, err := c.Client.CreateTerraformConfigVersion(

View File

@ -10,6 +10,7 @@ import (
"sort"
"testing"
atlas "github.com/hashicorp/atlas-go/v1"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
@ -247,10 +248,8 @@ func TestPush_localOverride(t *testing.T) {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
variables := map[string]interface{}{
"foo": "bar",
"bar": "foo",
}
variables := pushTFVars()
if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
@ -323,12 +322,11 @@ func TestPush_preferAtlas(t *testing.T) {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
variables := map[string]interface{}{
"foo": "old",
"bar": "foo",
}
variables := pushTFVars()
variables["foo"] = "old"
if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) {
t.Fatalf("bad: %#v", client.UpsertOptions)
t.Fatalf("bad: %#v", client.UpsertOptions.Variables)
}
}
@ -394,12 +392,26 @@ func TestPush_tfvars(t *testing.T) {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
variables := map[string]interface{}{
"foo": "bar",
"bar": "foo",
variables := pushTFVars()
// make sure these dind't go missing for some reason
for k, v := range variables {
if !reflect.DeepEqual(client.UpsertOptions.Variables[k], v) {
t.Fatalf("bad: %#v", client.UpsertOptions.Variables)
}
}
if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) {
t.Fatalf("bad: %#v", client.UpsertOptions)
//now check TFVVars
tfvars := []atlas.TFVar{
{"bar", "foo", false},
{"baz", "{\n A = \"a\"\n B = \"b\"\n}\n", true},
{"fob", "[\"a\", \"b\", \"c\"]\n", true},
{"foo", "bar", false},
}
if !reflect.DeepEqual(client.UpsertOptions.TFVars, tfvars) {
t.Fatalf("bad tf_vars: %#v", client.UpsertOptions.TFVars)
}
}
@ -563,3 +575,16 @@ func testArchiveStr(t *testing.T, path string) []string {
sort.Strings(result)
return result
}
// the structure returned from the push-tfvars test fixture
func pushTFVars() map[string]interface{} {
return map[string]interface{}{
"foo": "bar",
"bar": "foo",
"baz": map[string]interface{}{
"A": "a",
"B": "b",
},
"fob": []interface{}{"a", "b", "c"},
}
}

View File

@ -1,6 +1,19 @@
variable "foo" {}
variable "bar" {}
variable "baz" {
type = "map"
default = {
"A" = "a"
"B" = "b"
}
}
variable "fob" {
type = "list"
default = ["a", "b", "c"]
}
resource "test_instance" "foo" {}
atlas {