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:
parent
bef3b76c7a
commit
de87267697
|
@ -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
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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"},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue