Merge pull request #7082 from hashicorp/b-empty-map-types
core: Make lists and maps distinguishable in state
This commit is contained in:
commit
57cf9fd295
|
@ -0,0 +1,15 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform/builtin/providers/test"
|
||||
"github.com/hashicorp/terraform/plugin"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func main() {
|
||||
plugin.Serve(&plugin.ServeOpts{
|
||||
ProviderFunc: func() terraform.ResourceProvider {
|
||||
return test.Provider()
|
||||
},
|
||||
})
|
||||
}
|
|
@ -13,38 +13,94 @@ func testResource() *schema.Resource {
|
|||
Update: testResourceUpdate,
|
||||
Delete: testResourceDelete,
|
||||
Schema: map[string]*schema.Schema{
|
||||
"required": &schema.Schema{
|
||||
"required": {
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
"optional": &schema.Schema{
|
||||
"optional": {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
},
|
||||
"optional_bool": &schema.Schema{
|
||||
"optional_bool": {
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
},
|
||||
"optional_force_new": &schema.Schema{
|
||||
"optional_force_new": {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
"optional_computed_map": &schema.Schema{
|
||||
"optional_computed_map": {
|
||||
Type: schema.TypeMap,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"computed_read_only": &schema.Schema{
|
||||
"computed_read_only": {
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
"computed_read_only_force_new": &schema.Schema{
|
||||
"computed_read_only_force_new": {
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
"computed_list": {
|
||||
Type: schema.TypeList,
|
||||
Computed: true,
|
||||
Elem: &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
},
|
||||
},
|
||||
"set": {
|
||||
Type: schema.TypeSet,
|
||||
Optional: true,
|
||||
Elem: &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
},
|
||||
Set: schema.HashString,
|
||||
},
|
||||
"computed_set": {
|
||||
Type: schema.TypeSet,
|
||||
Computed: true,
|
||||
Elem: &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
},
|
||||
Set: schema.HashString,
|
||||
},
|
||||
"map": {
|
||||
Type: schema.TypeMap,
|
||||
Optional: true,
|
||||
},
|
||||
"optional_map": {
|
||||
Type: schema.TypeMap,
|
||||
Optional: true,
|
||||
},
|
||||
"required_map": {
|
||||
Type: schema.TypeMap,
|
||||
Required: true,
|
||||
},
|
||||
"map_that_look_like_set": {
|
||||
Type: schema.TypeMap,
|
||||
Optional: true,
|
||||
Elem: &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
},
|
||||
},
|
||||
"computed_map": {
|
||||
Type: schema.TypeMap,
|
||||
Computed: true,
|
||||
},
|
||||
"list_of_map": {
|
||||
Type: schema.TypeList,
|
||||
Optional: true,
|
||||
Elem: &schema.Schema{
|
||||
Type: schema.TypeMap,
|
||||
Elem: &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +112,10 @@ func testResourceCreate(d *schema.ResourceData, meta interface{}) error {
|
|||
if _, ok := d.GetOk("required"); !ok {
|
||||
return fmt.Errorf("Missing attribute 'required', but it's required!")
|
||||
}
|
||||
if _, ok := d.GetOk("required_map"); !ok {
|
||||
return fmt.Errorf("Missing attribute 'required_map', but it's required!")
|
||||
}
|
||||
|
||||
return testResourceRead(d, meta)
|
||||
}
|
||||
|
||||
|
@ -65,6 +125,9 @@ func testResourceRead(d *schema.ResourceData, meta interface{}) error {
|
|||
if _, ok := d.GetOk("optional_computed_map"); !ok {
|
||||
d.Set("optional_computed_map", map[string]string{})
|
||||
}
|
||||
d.Set("computed_map", map[string]string{"key1": "value1"})
|
||||
d.Set("computed_list", []string{"listval1", "listval2"})
|
||||
d.Set("computed_set", []string{"setval1", "setval2"})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -73,5 +136,6 @@ func testResourceUpdate(d *schema.ResourceData, meta interface{}) error {
|
|||
}
|
||||
|
||||
func testResourceDelete(d *schema.ResourceData, meta interface{}) error {
|
||||
d.SetId("")
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -17,6 +17,9 @@ func TestResource_basic(t *testing.T) {
|
|||
Config: strings.TrimSpace(`
|
||||
resource "test_resource" "foo" {
|
||||
required = "yep"
|
||||
required_map = {
|
||||
key = "value"
|
||||
}
|
||||
}
|
||||
`),
|
||||
Check: func(s *terraform.State) error {
|
||||
|
@ -36,10 +39,13 @@ func TestResource_ignoreChangesRequired(t *testing.T) {
|
|||
resource.TestStep{
|
||||
Config: strings.TrimSpace(`
|
||||
resource "test_resource" "foo" {
|
||||
required = "yep"
|
||||
lifecycle {
|
||||
ignore_changes = ["required"]
|
||||
}
|
||||
required = "yep"
|
||||
required_map = {
|
||||
key = "value"
|
||||
}
|
||||
lifecycle {
|
||||
ignore_changes = ["required"]
|
||||
}
|
||||
}
|
||||
`),
|
||||
Check: func(s *terraform.State) error {
|
||||
|
@ -59,6 +65,9 @@ func TestResource_ignoreChangesEmpty(t *testing.T) {
|
|||
Config: strings.TrimSpace(`
|
||||
resource "test_resource" "foo" {
|
||||
required = "yep"
|
||||
required_map = {
|
||||
key = "value"
|
||||
}
|
||||
optional_force_new = "one"
|
||||
lifecycle {
|
||||
ignore_changes = []
|
||||
|
@ -73,6 +82,9 @@ resource "test_resource" "foo" {
|
|||
Config: strings.TrimSpace(`
|
||||
resource "test_resource" "foo" {
|
||||
required = "yep"
|
||||
required_map = {
|
||||
key = "value"
|
||||
}
|
||||
optional_force_new = "two"
|
||||
lifecycle {
|
||||
ignore_changes = []
|
||||
|
@ -96,6 +108,9 @@ func TestResource_ignoreChangesForceNew(t *testing.T) {
|
|||
Config: strings.TrimSpace(`
|
||||
resource "test_resource" "foo" {
|
||||
required = "yep"
|
||||
required_map = {
|
||||
key = "value"
|
||||
}
|
||||
optional_force_new = "one"
|
||||
lifecycle {
|
||||
ignore_changes = ["optional_force_new"]
|
||||
|
@ -110,6 +125,9 @@ resource "test_resource" "foo" {
|
|||
Config: strings.TrimSpace(`
|
||||
resource "test_resource" "foo" {
|
||||
required = "yep"
|
||||
required_map = {
|
||||
key = "value"
|
||||
}
|
||||
optional_force_new = "two"
|
||||
lifecycle {
|
||||
ignore_changes = ["optional_force_new"]
|
||||
|
@ -135,6 +153,9 @@ func TestResource_ignoreChangesForceNewBoolean(t *testing.T) {
|
|||
Config: strings.TrimSpace(`
|
||||
resource "test_resource" "foo" {
|
||||
required = "yep"
|
||||
required_map = {
|
||||
key = "value"
|
||||
}
|
||||
optional_force_new = "one"
|
||||
optional_bool = true
|
||||
lifecycle {
|
||||
|
@ -150,6 +171,9 @@ resource "test_resource" "foo" {
|
|||
Config: strings.TrimSpace(`
|
||||
resource "test_resource" "foo" {
|
||||
required = "yep"
|
||||
required_map = {
|
||||
key = "value"
|
||||
}
|
||||
optional_force_new = "two"
|
||||
optional_bool = true
|
||||
lifecycle {
|
||||
|
@ -174,6 +198,9 @@ func TestResource_ignoreChangesMap(t *testing.T) {
|
|||
Config: strings.TrimSpace(`
|
||||
resource "test_resource" "foo" {
|
||||
required = "yep"
|
||||
required_map = {
|
||||
key = "value"
|
||||
}
|
||||
optional_computed_map {
|
||||
foo = "bar"
|
||||
}
|
||||
|
@ -190,6 +217,9 @@ resource "test_resource" "foo" {
|
|||
Config: strings.TrimSpace(`
|
||||
resource "test_resource" "foo" {
|
||||
required = "yep"
|
||||
required_map = {
|
||||
key = "value"
|
||||
}
|
||||
optional_computed_map {
|
||||
foo = "bar"
|
||||
no = "update"
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/hil"
|
||||
"github.com/hashicorp/hil/ast"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/hashicorp/terraform/helper/hilmapstructure"
|
||||
"github.com/mitchellh/reflectwalk"
|
||||
)
|
||||
|
||||
|
@ -368,19 +368,19 @@ func (c *Config) Validate() error {
|
|||
raw := make(map[string]interface{})
|
||||
for k, v := range m.RawConfig.Raw {
|
||||
var strVal string
|
||||
if err := mapstructure.WeakDecode(v, &strVal); err == nil {
|
||||
if err := hilmapstructure.WeakDecode(v, &strVal); err == nil {
|
||||
raw[k] = strVal
|
||||
continue
|
||||
}
|
||||
|
||||
var mapVal map[string]interface{}
|
||||
if err := mapstructure.WeakDecode(v, &mapVal); err == nil {
|
||||
if err := hilmapstructure.WeakDecode(v, &mapVal); err == nil {
|
||||
raw[k] = mapVal
|
||||
continue
|
||||
}
|
||||
|
||||
var sliceVal []interface{}
|
||||
if err := mapstructure.WeakDecode(v, &sliceVal); err == nil {
|
||||
if err := hilmapstructure.WeakDecode(v, &sliceVal); err == nil {
|
||||
raw[k] = sliceVal
|
||||
continue
|
||||
}
|
||||
|
@ -919,19 +919,19 @@ func (v *Variable) inferTypeFromDefault() VariableType {
|
|||
}
|
||||
|
||||
var s string
|
||||
if err := mapstructure.WeakDecode(v.Default, &s); err == nil {
|
||||
if err := hilmapstructure.WeakDecode(v.Default, &s); err == nil {
|
||||
v.Default = s
|
||||
return VariableTypeString
|
||||
}
|
||||
|
||||
var m map[string]string
|
||||
if err := mapstructure.WeakDecode(v.Default, &m); err == nil {
|
||||
if err := hilmapstructure.WeakDecode(v.Default, &m); err == nil {
|
||||
v.Default = m
|
||||
return VariableTypeMap
|
||||
}
|
||||
|
||||
var l []string
|
||||
if err := mapstructure.WeakDecode(v.Default, &l); err == nil {
|
||||
if err := hilmapstructure.WeakDecode(v.Default, &l); err == nil {
|
||||
v.Default = l
|
||||
return VariableTypeList
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package hilmapstructure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
var hilMapstructureDecodeHookEmptySlice []interface{}
|
||||
var hilMapstructureDecodeHookStringSlice []string
|
||||
var hilMapstructureDecodeHookEmptyMap map[string]interface{}
|
||||
|
||||
// WeakDecode behaves in the same way as mapstructure.WeakDecode but has a
|
||||
// DecodeHook which defeats the backward compatibility mode of mapstructure
|
||||
// which WeakDecodes []interface{}{} into an empty map[string]interface{}. This
|
||||
// allows us to use WeakDecode (desirable), but not fail on empty lists.
|
||||
func WeakDecode(m interface{}, rawVal interface{}) error {
|
||||
config := &mapstructure.DecoderConfig{
|
||||
DecodeHook: func(source reflect.Type, target reflect.Type, val interface{}) (interface{}, error) {
|
||||
sliceType := reflect.TypeOf(hilMapstructureDecodeHookEmptySlice)
|
||||
stringSliceType := reflect.TypeOf(hilMapstructureDecodeHookStringSlice)
|
||||
mapType := reflect.TypeOf(hilMapstructureDecodeHookEmptyMap)
|
||||
|
||||
if (source == sliceType || source == stringSliceType) && target == mapType {
|
||||
return nil, fmt.Errorf("Cannot convert a []interface{} into a map[string]interface{}")
|
||||
}
|
||||
|
||||
return val, nil
|
||||
},
|
||||
WeaklyTypedInput: true,
|
||||
Result: rawVal,
|
||||
}
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return decoder.Decode(m)
|
||||
}
|
|
@ -76,7 +76,7 @@ func (r *DiffFieldReader) readMap(
|
|||
if !strings.HasPrefix(k, prefix) {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(k, prefix+"#") {
|
||||
if strings.HasPrefix(k, prefix+"%") {
|
||||
// Ignore the count field
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ func TestDiffFieldReader_MapHandling(t *testing.T) {
|
|||
Schema: schema,
|
||||
Diff: &terraform.InstanceDiff{
|
||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
||||
"tags.#": &terraform.ResourceAttrDiff{
|
||||
"tags.%": &terraform.ResourceAttrDiff{
|
||||
Old: "1",
|
||||
New: "2",
|
||||
},
|
||||
|
@ -35,7 +35,7 @@ func TestDiffFieldReader_MapHandling(t *testing.T) {
|
|||
Source: &MapFieldReader{
|
||||
Schema: schema,
|
||||
Map: BasicMapReader(map[string]string{
|
||||
"tags.#": "1",
|
||||
"tags.%": "1",
|
||||
"tags.foo": "bar",
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -53,7 +53,7 @@ func (r *MapFieldReader) readMap(k string) (FieldReadResult, error) {
|
|||
resultSet = true
|
||||
|
||||
key := k[len(prefix):]
|
||||
if key != "#" {
|
||||
if key != "%" && key != "#" {
|
||||
result[key] = v
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ func TestMapFieldReader(t *testing.T) {
|
|||
"listInt.0": "21",
|
||||
"listInt.1": "42",
|
||||
|
||||
"map.#": "2",
|
||||
"map.%": "2",
|
||||
"map.foo": "bar",
|
||||
"map.bar": "baz",
|
||||
|
||||
|
@ -56,7 +56,7 @@ func TestMapFieldReader_extra(t *testing.T) {
|
|||
Map: BasicMapReader(map[string]string{
|
||||
"mapDel": "",
|
||||
|
||||
"mapEmpty.#": "0",
|
||||
"mapEmpty.%": "0",
|
||||
}),
|
||||
}
|
||||
|
||||
|
|
|
@ -165,7 +165,7 @@ func (w *MapFieldWriter) setMap(
|
|||
}
|
||||
|
||||
// Set the count
|
||||
w.result[k+".#"] = strconv.Itoa(len(vs))
|
||||
w.result[k+".%"] = strconv.Itoa(len(vs))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -161,7 +161,7 @@ func TestMapFieldWriter(t *testing.T) {
|
|||
map[string]interface{}{"foo": "bar"},
|
||||
false,
|
||||
map[string]string{
|
||||
"map.#": "1",
|
||||
"map.%": "1",
|
||||
"map.foo": "bar",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1987,10 +1987,10 @@ func TestResourceDataState(t *testing.T) {
|
|||
State: &terraform.InstanceState{
|
||||
Attributes: map[string]string{
|
||||
"config_vars.#": "2",
|
||||
"config_vars.0.#": "2",
|
||||
"config_vars.0.%": "2",
|
||||
"config_vars.0.foo": "bar",
|
||||
"config_vars.0.bar": "bar",
|
||||
"config_vars.1.#": "1",
|
||||
"config_vars.1.%": "1",
|
||||
"config_vars.1.bar": "baz",
|
||||
},
|
||||
},
|
||||
|
@ -2017,9 +2017,9 @@ func TestResourceDataState(t *testing.T) {
|
|||
Result: &terraform.InstanceState{
|
||||
Attributes: map[string]string{
|
||||
"config_vars.#": "2",
|
||||
"config_vars.0.#": "1",
|
||||
"config_vars.0.%": "1",
|
||||
"config_vars.0.foo": "bar",
|
||||
"config_vars.1.#": "1",
|
||||
"config_vars.1.%": "1",
|
||||
"config_vars.1.baz": "bang",
|
||||
},
|
||||
},
|
||||
|
@ -2444,10 +2444,10 @@ func TestResourceDataState(t *testing.T) {
|
|||
Attributes: map[string]string{
|
||||
// TODO: broken, shouldn't bar be removed?
|
||||
"config_vars.#": "2",
|
||||
"config_vars.0.#": "2",
|
||||
"config_vars.0.%": "2",
|
||||
"config_vars.0.foo": "bar",
|
||||
"config_vars.0.bar": "bar",
|
||||
"config_vars.1.#": "1",
|
||||
"config_vars.1.%": "1",
|
||||
"config_vars.1.bar": "baz",
|
||||
},
|
||||
},
|
||||
|
@ -2551,7 +2551,7 @@ func TestResourceDataState(t *testing.T) {
|
|||
|
||||
Result: &terraform.InstanceState{
|
||||
Attributes: map[string]string{
|
||||
"tags.#": "1",
|
||||
"tags.%": "1",
|
||||
"tags.Name": "foo",
|
||||
},
|
||||
},
|
||||
|
@ -2584,7 +2584,7 @@ func TestResourceDataState(t *testing.T) {
|
|||
|
||||
Result: &terraform.InstanceState{
|
||||
Attributes: map[string]string{
|
||||
"tags.#": "0",
|
||||
"tags.%": "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -2690,7 +2690,7 @@ func TestResourceDataState(t *testing.T) {
|
|||
Attributes: map[string]string{
|
||||
"ports.#": "1",
|
||||
"ports.10.index": "10",
|
||||
"ports.10.uuids.#": "1",
|
||||
"ports.10.uuids.%": "1",
|
||||
"ports.10.uuids.80": "value",
|
||||
},
|
||||
},
|
||||
|
@ -2831,7 +2831,7 @@ func TestResourceDataState(t *testing.T) {
|
|||
Attributes: map[string]string{
|
||||
"ports.#": "1",
|
||||
"ports.0.index": "10",
|
||||
"ports.0.uuids.#": "1",
|
||||
"ports.0.uuids.%": "1",
|
||||
"ports.0.uuids.80": "value",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -162,7 +162,7 @@ func TestResourceApply_destroyCreate(t *testing.T) {
|
|||
Attributes: map[string]string{
|
||||
"id": "foo",
|
||||
"foo": "42",
|
||||
"tags.#": "1",
|
||||
"tags.%": "1",
|
||||
"tags.Name": "foo",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -19,10 +19,8 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"log"
|
||||
)
|
||||
|
||||
// Schema is used to describe the structure of a value.
|
||||
|
@ -762,8 +760,8 @@ func (m schemaMap) diffMap(
|
|||
stateExists := o != nil
|
||||
|
||||
// Delete any count values, since we don't use those
|
||||
delete(configMap, "#")
|
||||
delete(stateMap, "#")
|
||||
delete(configMap, "%")
|
||||
delete(stateMap, "%")
|
||||
|
||||
// Check if the number of elements has changed.
|
||||
oldLen, newLen := len(stateMap), len(configMap)
|
||||
|
@ -797,7 +795,7 @@ func (m schemaMap) diffMap(
|
|||
oldStr = ""
|
||||
}
|
||||
|
||||
diff.Attributes[k+".#"] = countSchema.finalizeDiff(
|
||||
diff.Attributes[k+".%"] = countSchema.finalizeDiff(
|
||||
&terraform.ResourceAttrDiff{
|
||||
Old: oldStr,
|
||||
New: newStr,
|
||||
|
@ -1145,8 +1143,6 @@ func (m schemaMap) validateMap(
|
|||
// If raw and reified are equal, this is a string and should
|
||||
// be rejected.
|
||||
reified, reifiedOk := c.Get(k)
|
||||
log.Printf("[jen20] reified: %s", spew.Sdump(reified))
|
||||
log.Printf("[jen20] raw: %s", spew.Sdump(raw))
|
||||
if reifiedOk && raw == reified && !c.IsComputed(k) {
|
||||
return nil, []error{fmt.Errorf("%s: should be a map", k)}
|
||||
}
|
||||
|
|
|
@ -1297,7 +1297,7 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
|
||||
Diff: &terraform.InstanceDiff{
|
||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
||||
"config_vars.#": &terraform.ResourceAttrDiff{
|
||||
"config_vars.%": &terraform.ResourceAttrDiff{
|
||||
Old: "0",
|
||||
New: "1",
|
||||
},
|
||||
|
@ -1473,7 +1473,7 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
Old: "1",
|
||||
New: "0",
|
||||
},
|
||||
"config_vars.0.#": &terraform.ResourceAttrDiff{
|
||||
"config_vars.0.%": &terraform.ResourceAttrDiff{
|
||||
Old: "2",
|
||||
New: "0",
|
||||
},
|
||||
|
@ -1763,7 +1763,7 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
|
||||
Diff: &terraform.InstanceDiff{
|
||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
||||
"vars.#": &terraform.ResourceAttrDiff{
|
||||
"vars.%": &terraform.ResourceAttrDiff{
|
||||
Old: "",
|
||||
NewComputed: true,
|
||||
},
|
||||
|
@ -1783,7 +1783,7 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
|
||||
State: &terraform.InstanceState{
|
||||
Attributes: map[string]string{
|
||||
"vars.#": "0",
|
||||
"vars.%": "0",
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -1799,7 +1799,7 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
|
||||
Diff: &terraform.InstanceDiff{
|
||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
||||
"vars.#": &terraform.ResourceAttrDiff{
|
||||
"vars.%": &terraform.ResourceAttrDiff{
|
||||
Old: "",
|
||||
NewComputed: true,
|
||||
},
|
||||
|
@ -2046,7 +2046,7 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
|
||||
Diff: &terraform.InstanceDiff{
|
||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
||||
"vars.#": &terraform.ResourceAttrDiff{
|
||||
"vars.%": &terraform.ResourceAttrDiff{
|
||||
Old: "0",
|
||||
New: "1",
|
||||
},
|
||||
|
|
|
@ -250,7 +250,7 @@ func (f *fakeAtlas) handler(resp http.ResponseWriter, req *http.Request) {
|
|||
// loads the state.
|
||||
var testStateModuleOrderChange = []byte(
|
||||
`{
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"serial": 1,
|
||||
"modules": [
|
||||
{
|
||||
|
@ -289,7 +289,7 @@ var testStateModuleOrderChange = []byte(
|
|||
|
||||
var testStateSimple = []byte(
|
||||
`{
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"serial": 1,
|
||||
"modules": [
|
||||
{
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/hashicorp/terraform/helper/hilmapstructure"
|
||||
)
|
||||
|
||||
// EvalTypeCheckVariable is an EvalNode which ensures that the variable
|
||||
|
@ -134,19 +134,19 @@ func (n *EvalVariableBlock) Eval(ctx EvalContext) (interface{}, error) {
|
|||
rc := *n.Config
|
||||
for k, v := range rc.Config {
|
||||
var vString string
|
||||
if err := mapstructure.WeakDecode(v, &vString); err == nil {
|
||||
if err := hilmapstructure.WeakDecode(v, &vString); err == nil {
|
||||
n.VariableValues[k] = vString
|
||||
continue
|
||||
}
|
||||
|
||||
var vMap map[string]interface{}
|
||||
if err := mapstructure.WeakDecode(v, &vMap); err == nil {
|
||||
if err := hilmapstructure.WeakDecode(v, &vMap); err == nil {
|
||||
n.VariableValues[k] = vMap
|
||||
continue
|
||||
}
|
||||
|
||||
var vSlice []interface{}
|
||||
if err := mapstructure.WeakDecode(v, &vSlice); err == nil {
|
||||
if err := hilmapstructure.WeakDecode(v, &vSlice); err == nil {
|
||||
n.VariableValues[k] = vSlice
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -353,6 +353,10 @@ func (i *Interpolater) computeResourceVariable(
|
|||
|
||||
unknownVariable := unknownVariable()
|
||||
|
||||
// These variables must be declared early because of the use of GOTO
|
||||
var isList bool
|
||||
var isMap bool
|
||||
|
||||
// Get the information about this resource variable, and verify
|
||||
// that it exists and such.
|
||||
module, _, err := i.resourceVariableInfo(scope, v)
|
||||
|
@ -388,7 +392,9 @@ func (i *Interpolater) computeResourceVariable(
|
|||
}
|
||||
|
||||
// computed list or map attribute
|
||||
if _, ok := r.Primary.Attributes[v.Field+".#"]; ok {
|
||||
_, isList = r.Primary.Attributes[v.Field+".#"]
|
||||
_, isMap = r.Primary.Attributes[v.Field+".%"]
|
||||
if isList || isMap {
|
||||
variable, err := i.interpolateComplexTypeAttribute(v.Field, r.Primary.Attributes)
|
||||
return &variable, err
|
||||
}
|
||||
|
@ -566,49 +572,70 @@ func (i *Interpolater) interpolateComplexTypeAttribute(
|
|||
resourceID string,
|
||||
attributes map[string]string) (ast.Variable, error) {
|
||||
|
||||
attr := attributes[resourceID+".#"]
|
||||
log.Printf("[DEBUG] Interpolating computed complex type attribute %s (%s)",
|
||||
resourceID, attr)
|
||||
// We can now distinguish between lists and maps in state by the count field:
|
||||
// - lists (and by extension, sets) use the traditional .# notation
|
||||
// - maps use the newer .% notation
|
||||
// Consequently here we can decide how to deal with the keys appropriately
|
||||
// based on whether the type is a map of list.
|
||||
if lengthAttr, isList := attributes[resourceID+".#"]; isList {
|
||||
log.Printf("[DEBUG] Interpolating computed list element attribute %s (%s)",
|
||||
resourceID, lengthAttr)
|
||||
|
||||
// In Terraform's internal dotted representation of list-like attributes, the
|
||||
// ".#" count field is marked as unknown to indicate "this whole list is
|
||||
// unknown". We must honor that meaning here so computed references can be
|
||||
// treated properly during the plan phase.
|
||||
if attr == config.UnknownVariableValue {
|
||||
return unknownVariable(), nil
|
||||
}
|
||||
|
||||
// At this stage we don't know whether the item is a list or a map, so we
|
||||
// examine the keys to see whether they are all numeric.
|
||||
var numericKeys []string
|
||||
var allKeys []string
|
||||
numberedListKey := regexp.MustCompile("^" + resourceID + "\\.[0-9]+$")
|
||||
otherListKey := regexp.MustCompile("^" + resourceID + "\\.([^#]+)$")
|
||||
for id, _ := range attributes {
|
||||
if numberedListKey.MatchString(id) {
|
||||
numericKeys = append(numericKeys, id)
|
||||
// In Terraform's internal dotted representation of list-like attributes, the
|
||||
// ".#" count field is marked as unknown to indicate "this whole list is
|
||||
// unknown". We must honor that meaning here so computed references can be
|
||||
// treated properly during the plan phase.
|
||||
if lengthAttr == config.UnknownVariableValue {
|
||||
return unknownVariable(), nil
|
||||
}
|
||||
|
||||
var keys []string
|
||||
listElementKey := regexp.MustCompile("^" + resourceID + "\\.[0-9]+$")
|
||||
for id, _ := range attributes {
|
||||
if listElementKey.MatchString(id) {
|
||||
keys = append(keys, id)
|
||||
}
|
||||
}
|
||||
if submatches := otherListKey.FindAllStringSubmatch(id, -1); len(submatches) > 0 {
|
||||
allKeys = append(allKeys, submatches[0][1])
|
||||
}
|
||||
}
|
||||
|
||||
if len(numericKeys) == len(allKeys) {
|
||||
// This is a list
|
||||
var members []string
|
||||
for _, key := range numericKeys {
|
||||
for _, key := range keys {
|
||||
members = append(members, attributes[key])
|
||||
}
|
||||
// This behaviour still seems very broken to me... it retains BC but is
|
||||
// probably going to cause problems in future
|
||||
sort.Strings(members)
|
||||
|
||||
return hil.InterfaceToVariable(members)
|
||||
} else {
|
||||
// This is a map
|
||||
}
|
||||
|
||||
if lengthAttr, isMap := attributes[resourceID+".%"]; isMap {
|
||||
log.Printf("[DEBUG] Interpolating computed map element attribute %s (%s)",
|
||||
resourceID, lengthAttr)
|
||||
|
||||
// In Terraform's internal dotted representation of map attributes, the
|
||||
// ".%" count field is marked as unknown to indicate "this whole list is
|
||||
// unknown". We must honor that meaning here so computed references can be
|
||||
// treated properly during the plan phase.
|
||||
if lengthAttr == config.UnknownVariableValue {
|
||||
return unknownVariable(), nil
|
||||
}
|
||||
|
||||
var keys []string
|
||||
mapElementKey := regexp.MustCompile("^" + resourceID + "\\.([^%]+)$")
|
||||
for id, _ := range attributes {
|
||||
if submatches := mapElementKey.FindAllStringSubmatch(id, -1); len(submatches) > 0 {
|
||||
keys = append(keys, submatches[0][1])
|
||||
}
|
||||
}
|
||||
|
||||
members := make(map[string]interface{})
|
||||
for _, key := range allKeys {
|
||||
for _, key := range keys {
|
||||
members[key] = attributes[resourceID+"."+key]
|
||||
}
|
||||
return hil.InterfaceToVariable(members)
|
||||
}
|
||||
|
||||
return ast.Variable{}, fmt.Errorf("No complex type %s found", resourceID)
|
||||
}
|
||||
|
||||
func (i *Interpolater) resourceVariableInfo(
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hashicorp/hil"
|
||||
"github.com/hashicorp/hil/ast"
|
||||
"github.com/hashicorp/terraform/config"
|
||||
|
@ -293,7 +294,7 @@ func TestInterpolator_resourceMultiAttributes(t *testing.T) {
|
|||
"name_servers.3": "ns-601.awsdns-11.net",
|
||||
"listeners.#": "1",
|
||||
"listeners.0": "red",
|
||||
"tags.#": "1",
|
||||
"tags.%": "1",
|
||||
"tags.Name": "reindeer",
|
||||
"nothing.#": "0",
|
||||
},
|
||||
|
@ -527,7 +528,9 @@ func testInterpolate(
|
|||
"foo": expectedVar,
|
||||
}
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Fatalf("%q: actual: %#v\nexpected: %#v", n, actual, expected)
|
||||
spew.Config.DisableMethods = true
|
||||
t.Fatalf("%q:\n\n actual: %#v\nexpected: %#v\n\n%s\n\n%s\n\n", n, actual, expected,
|
||||
spew.Sdump(actual), spew.Sdump(expected))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import (
|
|||
|
||||
const (
|
||||
// StateVersion is the current version for our state file
|
||||
StateVersion = 2
|
||||
StateVersion = 3
|
||||
)
|
||||
|
||||
// rootModulePath is the path of the root module
|
||||
|
@ -1379,23 +1379,32 @@ type jsonStateVersionIdentifier struct {
|
|||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
// Check if this is a V0 format - the magic bytes at the start of the file
|
||||
// should be "tfstate" if so. We no longer support upgrading this type of
|
||||
// state but return an error message explaining to a user how they can
|
||||
// upgrade via the 0.6.x series.
|
||||
func testForV0State(buf *bufio.Reader) error {
|
||||
start, err := buf.Peek(len("tfstate"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to check for magic bytes: %v", err)
|
||||
}
|
||||
if string(start) == "tfstate" {
|
||||
return fmt.Errorf("Terraform 0.7 no longer supports upgrading the binary state\n" +
|
||||
"format which was used prior to Terraform 0.3. Please upgrade\n" +
|
||||
"this state file using Terraform 0.6.16 prior to using it with\n" +
|
||||
"Terraform 0.7.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadState reads a state structure out of a reader in the format that
|
||||
// was written by WriteState.
|
||||
func ReadState(src io.Reader) (*State, error) {
|
||||
buf := bufio.NewReader(src)
|
||||
|
||||
// Check if this is a V0 format
|
||||
start, err := buf.Peek(len(stateFormatMagic))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to check for magic bytes: %v", err)
|
||||
}
|
||||
if string(start) == stateFormatMagic {
|
||||
// Read the old state
|
||||
old, err := ReadStateV0(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return old.upgrade()
|
||||
if err := testForV0State(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If we are JSON we buffer the whole thing in memory so we can read it twice.
|
||||
|
@ -1414,17 +1423,39 @@ func ReadState(src io.Reader) (*State, error) {
|
|||
case 0:
|
||||
return nil, fmt.Errorf("State version 0 is not supported as JSON.")
|
||||
case 1:
|
||||
old, err := ReadStateV1(jsonBytes)
|
||||
v1State, err := ReadStateV1(jsonBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return old.upgrade()
|
||||
|
||||
v2State, err := upgradeStateV1ToV2(v1State)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v3State, err := upgradeStateV2ToV3(v2State)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v3State, nil
|
||||
case 2:
|
||||
state, err := ReadStateV2(jsonBytes)
|
||||
v2State, err := ReadStateV2(jsonBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return state, nil
|
||||
v3State, err := upgradeStateV2ToV3(v2State)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v3State, nil
|
||||
case 3:
|
||||
v3State, err := ReadStateV3(jsonBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v3State, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("State version %d not supported, please update.",
|
||||
versionIdentifier.Version)
|
||||
|
@ -1432,20 +1463,52 @@ func ReadState(src io.Reader) (*State, error) {
|
|||
}
|
||||
|
||||
func ReadStateV1(jsonBytes []byte) (*stateV1, error) {
|
||||
state := &stateV1{}
|
||||
v1State := &stateV1{}
|
||||
if err := json.Unmarshal(jsonBytes, v1State); err != nil {
|
||||
return nil, fmt.Errorf("Decoding state file failed: %v", err)
|
||||
}
|
||||
|
||||
if v1State.Version != 1 {
|
||||
return nil, fmt.Errorf("Decoded state version did not match the decoder selection: "+
|
||||
"read %d, expected 1", v1State.Version)
|
||||
}
|
||||
|
||||
return v1State, nil
|
||||
}
|
||||
|
||||
func ReadStateV2(jsonBytes []byte) (*State, error) {
|
||||
state := &State{}
|
||||
if err := json.Unmarshal(jsonBytes, state); err != nil {
|
||||
return nil, fmt.Errorf("Decoding state file failed: %v", err)
|
||||
}
|
||||
|
||||
if state.Version != 1 {
|
||||
return nil, fmt.Errorf("Decoded state version did not match the decoder selection: "+
|
||||
"read %d, expected 1", state.Version)
|
||||
// Check the version, this to ensure we don't read a future
|
||||
// version that we don't understand
|
||||
if state.Version > StateVersion {
|
||||
return nil, fmt.Errorf("State version %d not supported, please update.",
|
||||
state.Version)
|
||||
}
|
||||
|
||||
// Make sure the version is semantic
|
||||
if state.TFVersion != "" {
|
||||
if _, err := version.NewVersion(state.TFVersion); err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"State contains invalid version: %s\n\n"+
|
||||
"Terraform validates the version format prior to writing it. This\n"+
|
||||
"means that this is invalid of the state becoming corrupted through\n"+
|
||||
"some external means. Please manually modify the Terraform version\n"+
|
||||
"field to be a proper semantic version.",
|
||||
state.TFVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort it
|
||||
state.sort()
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func ReadStateV2(jsonBytes []byte) (*State, error) {
|
||||
func ReadStateV3(jsonBytes []byte) (*State, error) {
|
||||
state := &State{}
|
||||
if err := json.Unmarshal(jsonBytes, state); err != nil {
|
||||
return nil, fmt.Errorf("Decoding state file failed: %v", err)
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hashicorp/terraform/config"
|
||||
)
|
||||
|
||||
|
@ -1128,92 +1127,6 @@ func TestInstanceState_MergeDiff_nilDiff(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestReadUpgradeStateV1toV2(t *testing.T) {
|
||||
// ReadState should transparently detect the old version but will upgrade
|
||||
// it on Write.
|
||||
actual, err := ReadState(strings.NewReader(testV1State))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := WriteState(actual, buf); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if actual.Version != 2 {
|
||||
t.Fatalf("bad: State version not incremented; is %d", actual.Version)
|
||||
}
|
||||
|
||||
roundTripped, err := ReadState(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, roundTripped) {
|
||||
t.Fatalf("bad: %#v", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadUpgradeStateV1toV2_outputs(t *testing.T) {
|
||||
// ReadState should transparently detect the old version but will upgrade
|
||||
// it on Write.
|
||||
actual, err := ReadState(strings.NewReader(testV1StateWithOutputs))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := WriteState(actual, buf); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if actual.Version != 2 {
|
||||
t.Fatalf("bad: State version not incremented; is %d", actual.Version)
|
||||
}
|
||||
|
||||
roundTripped, err := ReadState(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, roundTripped) {
|
||||
spew.Config.DisableMethods = true
|
||||
t.Fatalf("bad:\n%s\n\nround tripped:\n%s\n", spew.Sdump(actual), spew.Sdump(roundTripped))
|
||||
spew.Config.DisableMethods = false
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadUpgradeState(t *testing.T) {
|
||||
state := &StateV0{
|
||||
Resources: map[string]*ResourceStateV0{
|
||||
"foo": &ResourceStateV0{
|
||||
ID: "bar",
|
||||
},
|
||||
},
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
if err := testWriteStateV0(state, buf); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// ReadState should transparently detect the old
|
||||
// version and upgrade up so the latest.
|
||||
actual, err := ReadState(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
upgraded, err := state.upgrade()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, upgraded) {
|
||||
t.Fatalf("bad: %#v", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadWriteState(t *testing.T) {
|
||||
state := &State{
|
||||
Serial: 9,
|
||||
|
@ -1385,73 +1298,6 @@ func TestWriteStateTFVersion(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestUpgradeV0State(t *testing.T) {
|
||||
old := &StateV0{
|
||||
Outputs: map[string]string{
|
||||
"ip": "127.0.0.1",
|
||||
},
|
||||
Resources: map[string]*ResourceStateV0{
|
||||
"foo": &ResourceStateV0{
|
||||
Type: "test_resource",
|
||||
ID: "bar",
|
||||
Attributes: map[string]string{
|
||||
"key": "val",
|
||||
},
|
||||
},
|
||||
"bar": &ResourceStateV0{
|
||||
Type: "test_resource",
|
||||
ID: "1234",
|
||||
Attributes: map[string]string{
|
||||
"a": "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
Tainted: map[string]struct{}{
|
||||
"bar": struct{}{},
|
||||
},
|
||||
}
|
||||
state, err := old.upgrade()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if len(state.Modules) != 1 {
|
||||
t.Fatalf("should only have root module: %#v", state.Modules)
|
||||
}
|
||||
root := state.RootModule()
|
||||
|
||||
if len(root.Outputs) != 1 {
|
||||
t.Fatalf("bad outputs: %v", root.Outputs)
|
||||
}
|
||||
if root.Outputs["ip"].Value != "127.0.0.1" {
|
||||
t.Fatalf("bad outputs: %v", root.Outputs)
|
||||
}
|
||||
|
||||
if len(root.Resources) != 2 {
|
||||
t.Fatalf("bad resources: %v", root.Resources)
|
||||
}
|
||||
|
||||
foo := root.Resources["foo"]
|
||||
if foo.Type != "test_resource" {
|
||||
t.Fatalf("bad: %#v", foo)
|
||||
}
|
||||
if foo.Primary == nil || foo.Primary.ID != "bar" ||
|
||||
foo.Primary.Attributes["key"] != "val" {
|
||||
t.Fatalf("bad: %#v", foo)
|
||||
}
|
||||
if foo.Primary.Tainted {
|
||||
t.Fatalf("bad: %#v", foo)
|
||||
}
|
||||
|
||||
bar := root.Resources["bar"]
|
||||
if bar.Type != "test_resource" {
|
||||
t.Fatalf("bad: %#v", bar)
|
||||
}
|
||||
if !bar.Primary.Tainted {
|
||||
t.Fatalf("bad: %#v", bar)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseResourceStateKey(t *testing.T) {
|
||||
cases := []struct {
|
||||
Input string
|
||||
|
@ -1517,68 +1363,3 @@ func TestParseResourceStateKey(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const testV1State = `{
|
||||
"version": 1,
|
||||
"serial": 9,
|
||||
"remote": {
|
||||
"type": "http",
|
||||
"config": {
|
||||
"url": "http://my-cool-server.com/"
|
||||
}
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"path": [
|
||||
"root"
|
||||
],
|
||||
"outputs": null,
|
||||
"resources": {
|
||||
"foo": {
|
||||
"type": "",
|
||||
"primary": {
|
||||
"id": "bar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"depends_on": [
|
||||
"aws_instance.bar"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
const testV1StateWithOutputs = `{
|
||||
"version": 1,
|
||||
"serial": 9,
|
||||
"remote": {
|
||||
"type": "http",
|
||||
"config": {
|
||||
"url": "http://my-cool-server.com/"
|
||||
}
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"path": [
|
||||
"root"
|
||||
],
|
||||
"outputs": {
|
||||
"foo": "bar",
|
||||
"baz": "foo"
|
||||
},
|
||||
"resources": {
|
||||
"foo": {
|
||||
"type": "",
|
||||
"primary": {
|
||||
"id": "bar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"depends_on": [
|
||||
"aws_instance.bar"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/copystructure"
|
||||
)
|
||||
|
||||
// upgradeStateV1ToV2 is used to upgrade a V1 state representation
|
||||
// into a V2 state representation
|
||||
func upgradeStateV1ToV2(old *stateV1) (*State, error) {
|
||||
if old == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
remote, err := old.Remote.upgradeToV2()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading State V1: %v", err)
|
||||
}
|
||||
|
||||
modules := make([]*ModuleState, len(old.Modules))
|
||||
for i, module := range old.Modules {
|
||||
upgraded, err := module.upgradeToV2()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading State V1: %v", err)
|
||||
}
|
||||
modules[i] = upgraded
|
||||
}
|
||||
if len(modules) == 0 {
|
||||
modules = nil
|
||||
}
|
||||
|
||||
newState := &State{
|
||||
Version: 2,
|
||||
Serial: old.Serial,
|
||||
Remote: remote,
|
||||
Modules: modules,
|
||||
}
|
||||
|
||||
newState.sort()
|
||||
|
||||
return newState, nil
|
||||
}
|
||||
|
||||
func (old *remoteStateV1) upgradeToV2() (*RemoteState, error) {
|
||||
if old == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
config, err := copystructure.Copy(old.Config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading RemoteState V1: %v", err)
|
||||
}
|
||||
|
||||
return &RemoteState{
|
||||
Type: old.Type,
|
||||
Config: config.(map[string]string),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (old *moduleStateV1) upgradeToV2() (*ModuleState, error) {
|
||||
if old == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
path, err := copystructure.Copy(old.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err)
|
||||
}
|
||||
|
||||
// Outputs needs upgrading to use the new structure
|
||||
outputs := make(map[string]*OutputState)
|
||||
for key, output := range old.Outputs {
|
||||
outputs[key] = &OutputState{
|
||||
Type: "string",
|
||||
Value: output,
|
||||
Sensitive: false,
|
||||
}
|
||||
}
|
||||
if len(outputs) == 0 {
|
||||
outputs = nil
|
||||
}
|
||||
|
||||
resources := make(map[string]*ResourceState)
|
||||
for key, oldResource := range old.Resources {
|
||||
upgraded, err := oldResource.upgradeToV2()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err)
|
||||
}
|
||||
resources[key] = upgraded
|
||||
}
|
||||
if len(resources) == 0 {
|
||||
resources = nil
|
||||
}
|
||||
|
||||
dependencies, err := copystructure.Copy(old.Dependencies)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err)
|
||||
}
|
||||
|
||||
return &ModuleState{
|
||||
Path: path.([]string),
|
||||
Outputs: outputs,
|
||||
Resources: resources,
|
||||
Dependencies: dependencies.([]string),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (old *resourceStateV1) upgradeToV2() (*ResourceState, error) {
|
||||
if old == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
dependencies, err := copystructure.Copy(old.Dependencies)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err)
|
||||
}
|
||||
|
||||
primary, err := old.Primary.upgradeToV2()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err)
|
||||
}
|
||||
|
||||
deposed := make([]*InstanceState, len(old.Deposed))
|
||||
for i, v := range old.Deposed {
|
||||
upgraded, err := v.upgradeToV2()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err)
|
||||
}
|
||||
deposed[i] = upgraded
|
||||
}
|
||||
if len(deposed) == 0 {
|
||||
deposed = nil
|
||||
}
|
||||
|
||||
return &ResourceState{
|
||||
Type: old.Type,
|
||||
Dependencies: dependencies.([]string),
|
||||
Primary: primary,
|
||||
Deposed: deposed,
|
||||
Provider: old.Provider,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (old *instanceStateV1) upgradeToV2() (*InstanceState, error) {
|
||||
if old == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
attributes, err := copystructure.Copy(old.Attributes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err)
|
||||
}
|
||||
ephemeral, err := old.Ephemeral.upgradeToV2()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err)
|
||||
}
|
||||
meta, err := copystructure.Copy(old.Meta)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err)
|
||||
}
|
||||
|
||||
return &InstanceState{
|
||||
ID: old.ID,
|
||||
Attributes: attributes.(map[string]string),
|
||||
Ephemeral: *ephemeral,
|
||||
Meta: meta.(map[string]string),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (old *ephemeralStateV1) upgradeToV2() (*EphemeralState, error) {
|
||||
connInfo, err := copystructure.Copy(old.ConnInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading EphemeralState V1: %v", err)
|
||||
}
|
||||
return &EphemeralState{
|
||||
ConnInfo: connInfo.(map[string]string),
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The upgrade process from V2 to V3 state does not affect the structure,
|
||||
// so we do not need to redeclare all of the structs involved - we just
|
||||
// take a deep copy of the old structure and assert the version number is
|
||||
// as we expect.
|
||||
func upgradeStateV2ToV3(old *State) (*State, error) {
|
||||
new := old.DeepCopy()
|
||||
|
||||
// Ensure the copied version is v2 before attempting to upgrade
|
||||
if new.Version != 2 {
|
||||
return nil, fmt.Errorf("Cannot appply v2->v3 state upgrade to " +
|
||||
"a state which is not version 2.")
|
||||
}
|
||||
|
||||
// Set the new version number
|
||||
new.Version = 3
|
||||
|
||||
// Change the counts for things which look like maps to use the %
|
||||
// syntax. Remove counts for empty collections - they will be added
|
||||
// back in later.
|
||||
for _, module := range new.Modules {
|
||||
for _, resource := range module.Resources {
|
||||
// Upgrade Primary
|
||||
if resource.Primary != nil {
|
||||
upgradeAttributesV2ToV3(resource.Primary)
|
||||
}
|
||||
|
||||
// Upgrade Deposed
|
||||
if resource.Deposed != nil {
|
||||
for _, deposed := range resource.Deposed {
|
||||
upgradeAttributesV2ToV3(deposed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new, nil
|
||||
}
|
||||
|
||||
func upgradeAttributesV2ToV3(instanceState *InstanceState) error {
|
||||
collectionKeyRegexp := regexp.MustCompile(`^(.*\.)#$`)
|
||||
collectionSubkeyRegexp := regexp.MustCompile(`^([^\.]+)\..*`)
|
||||
|
||||
// Identify the key prefix of anything which is a collection
|
||||
var collectionKeyPrefixes []string
|
||||
for key := range instanceState.Attributes {
|
||||
if submatches := collectionKeyRegexp.FindAllStringSubmatch(key, -1); len(submatches) > 0 {
|
||||
collectionKeyPrefixes = append(collectionKeyPrefixes, submatches[0][1])
|
||||
}
|
||||
}
|
||||
sort.Strings(collectionKeyPrefixes)
|
||||
|
||||
log.Printf("[STATE UPGRADE] Detected the following collections in state: %v", collectionKeyPrefixes)
|
||||
|
||||
// This could be rolled into fewer loops, but it is somewhat clearer this way, and will not
|
||||
// run very often.
|
||||
for _, prefix := range collectionKeyPrefixes {
|
||||
// First get the actual keys that belong to this prefix
|
||||
var potentialKeysMatching []string
|
||||
for key := range instanceState.Attributes {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
potentialKeysMatching = append(potentialKeysMatching, strings.TrimPrefix(key, prefix))
|
||||
}
|
||||
}
|
||||
sort.Strings(potentialKeysMatching)
|
||||
|
||||
var actualKeysMatching []string
|
||||
for _, key := range potentialKeysMatching {
|
||||
if submatches := collectionSubkeyRegexp.FindAllStringSubmatch(key, -1); len(submatches) > 0 {
|
||||
actualKeysMatching = append(actualKeysMatching, submatches[0][1])
|
||||
} else {
|
||||
if key != "#" {
|
||||
actualKeysMatching = append(actualKeysMatching, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
actualKeysMatching = uniqueSortedStrings(actualKeysMatching)
|
||||
|
||||
// Now inspect the keys in order to determine whether this is most likely to be
|
||||
// a map, list or set. There is room for error here, so we log in each case. If
|
||||
// there is no method of telling, we remove the key from the InstanceState in
|
||||
// order that it will be recreated. Again, this could be rolled into fewer loops
|
||||
// but we prefer clarity.
|
||||
|
||||
oldCountKey := fmt.Sprintf("%s#", prefix)
|
||||
|
||||
// First, detect "obvious" maps - which have non-numeric keys (mostly).
|
||||
hasNonNumericKeys := false
|
||||
for _, key := range actualKeysMatching {
|
||||
if _, err := strconv.Atoi(key); err != nil {
|
||||
hasNonNumericKeys = true
|
||||
}
|
||||
}
|
||||
if hasNonNumericKeys {
|
||||
newCountKey := fmt.Sprintf("%s%%", prefix)
|
||||
|
||||
instanceState.Attributes[newCountKey] = instanceState.Attributes[oldCountKey]
|
||||
delete(instanceState.Attributes, oldCountKey)
|
||||
log.Printf("[STATE UPGRADE] Detected %s as a map. Replaced count = %s",
|
||||
strings.TrimSuffix(prefix, "."), instanceState.Attributes[newCountKey])
|
||||
}
|
||||
|
||||
// Now detect empty collections and remove them from state.
|
||||
if len(actualKeysMatching) == 0 {
|
||||
delete(instanceState.Attributes, oldCountKey)
|
||||
log.Printf("[STATE UPGRADE] Detected %s as an empty collection. Removed from state.",
|
||||
strings.TrimSuffix(prefix, "."))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// uniqueSortedStrings removes duplicates from a slice of strings and returns
|
||||
// a sorted slice of the unique strings.
|
||||
func uniqueSortedStrings(input []string) []string {
|
||||
uniquemap := make(map[string]struct{})
|
||||
for _, str := range input {
|
||||
uniquemap[str] = struct{}{}
|
||||
}
|
||||
|
||||
output := make([]string, len(uniquemap))
|
||||
|
||||
i := 0
|
||||
for key := range uniquemap {
|
||||
output[i] = key
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
sort.Strings(output)
|
||||
return output
|
||||
}
|
|
@ -1,367 +0,0 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/terraform/config"
|
||||
)
|
||||
|
||||
// The format byte is prefixed into the state file format so that we have
|
||||
// the ability in the future to change the file format if we want for any
|
||||
// reason.
|
||||
const (
|
||||
stateFormatMagic = "tfstate"
|
||||
stateFormatVersion byte = 1
|
||||
)
|
||||
|
||||
// StateV0 is used to represent the state of Terraform files before
|
||||
// 0.3. It is automatically upgraded to a modern State representation
|
||||
// on start.
|
||||
type StateV0 struct {
|
||||
Outputs map[string]string
|
||||
Resources map[string]*ResourceStateV0
|
||||
Tainted map[string]struct{}
|
||||
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (s *StateV0) init() {
|
||||
s.once.Do(func() {
|
||||
if s.Resources == nil {
|
||||
s.Resources = make(map[string]*ResourceStateV0)
|
||||
}
|
||||
|
||||
if s.Tainted == nil {
|
||||
s.Tainted = make(map[string]struct{})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *StateV0) deepcopy() *StateV0 {
|
||||
result := new(StateV0)
|
||||
result.init()
|
||||
if s != nil {
|
||||
for k, v := range s.Resources {
|
||||
result.Resources[k] = v
|
||||
}
|
||||
for k, v := range s.Tainted {
|
||||
result.Tainted[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// prune is a helper that removes any empty IDs from the state
|
||||
// and cleans it up in general.
|
||||
func (s *StateV0) prune() {
|
||||
for k, v := range s.Resources {
|
||||
if v.ID == "" {
|
||||
delete(s.Resources, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Orphans returns a list of keys of resources that are in the State
|
||||
// but aren't present in the configuration itself. Hence, these keys
|
||||
// represent the state of resources that are orphans.
|
||||
func (s *StateV0) Orphans(c *config.Config) []string {
|
||||
keys := make(map[string]struct{})
|
||||
for k, _ := range s.Resources {
|
||||
keys[k] = struct{}{}
|
||||
}
|
||||
|
||||
for _, r := range c.Resources {
|
||||
delete(keys, r.Id())
|
||||
|
||||
for k, _ := range keys {
|
||||
if strings.HasPrefix(k, r.Id()+".") {
|
||||
delete(keys, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(keys))
|
||||
for k, _ := range keys {
|
||||
result = append(result, k)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *StateV0) String() string {
|
||||
if len(s.Resources) == 0 {
|
||||
return "<no state>"
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
names := make([]string, 0, len(s.Resources))
|
||||
for name, _ := range s.Resources {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
for _, k := range names {
|
||||
rs := s.Resources[k]
|
||||
id := rs.ID
|
||||
if id == "" {
|
||||
id = "<not created>"
|
||||
}
|
||||
|
||||
taintStr := ""
|
||||
if _, ok := s.Tainted[k]; ok {
|
||||
taintStr = " (tainted)"
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("%s:%s\n", k, taintStr))
|
||||
buf.WriteString(fmt.Sprintf(" ID = %s\n", id))
|
||||
|
||||
attrKeys := make([]string, 0, len(rs.Attributes))
|
||||
for ak, _ := range rs.Attributes {
|
||||
if ak == "id" {
|
||||
continue
|
||||
}
|
||||
|
||||
attrKeys = append(attrKeys, ak)
|
||||
}
|
||||
sort.Strings(attrKeys)
|
||||
|
||||
for _, ak := range attrKeys {
|
||||
av := rs.Attributes[ak]
|
||||
buf.WriteString(fmt.Sprintf(" %s = %s\n", ak, av))
|
||||
}
|
||||
|
||||
if len(rs.Dependencies) > 0 {
|
||||
buf.WriteString(fmt.Sprintf("\n Dependencies:\n"))
|
||||
for _, dep := range rs.Dependencies {
|
||||
buf.WriteString(fmt.Sprintf(" %s\n", dep.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.Outputs) > 0 {
|
||||
buf.WriteString("\nOutputs:\n\n")
|
||||
|
||||
ks := make([]string, 0, len(s.Outputs))
|
||||
for k, _ := range s.Outputs {
|
||||
ks = append(ks, k)
|
||||
}
|
||||
sort.Strings(ks)
|
||||
|
||||
for _, k := range ks {
|
||||
v := s.Outputs[k]
|
||||
buf.WriteString(fmt.Sprintf("%s = %s\n", k, v))
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
/// ResourceState holds the state of a resource that is used so that
|
||||
// a provider can find and manage an existing resource as well as for
|
||||
// storing attributes that are uesd to populate variables of child
|
||||
// resources.
|
||||
//
|
||||
// Attributes has attributes about the created resource that are
|
||||
// queryable in interpolation: "${type.id.attr}"
|
||||
//
|
||||
// Extra is just extra data that a provider can return that we store
|
||||
// for later, but is not exposed in any way to the user.
|
||||
type ResourceStateV0 struct {
|
||||
// This is filled in and managed by Terraform, and is the resource
|
||||
// type itself such as "mycloud_instance". If a resource provider sets
|
||||
// this value, it won't be persisted.
|
||||
Type string
|
||||
|
||||
// The attributes below are all meant to be filled in by the
|
||||
// resource providers themselves. Documentation for each are above
|
||||
// each element.
|
||||
|
||||
// A unique ID for this resource. This is opaque to Terraform
|
||||
// and is only meant as a lookup mechanism for the providers.
|
||||
ID string
|
||||
|
||||
// Attributes are basic information about the resource. Any keys here
|
||||
// are accessible in variable format within Terraform configurations:
|
||||
// ${resourcetype.name.attribute}.
|
||||
Attributes map[string]string
|
||||
|
||||
// ConnInfo is used for the providers to export information which is
|
||||
// used to connect to the resource for provisioning. For example,
|
||||
// this could contain SSH or WinRM credentials.
|
||||
ConnInfo map[string]string
|
||||
|
||||
// Extra information that the provider can store about a resource.
|
||||
// This data is opaque, never shown to the user, and is sent back to
|
||||
// the provider as-is for whatever purpose appropriate.
|
||||
Extra map[string]interface{}
|
||||
|
||||
// Dependencies are a list of things that this resource relies on
|
||||
// existing to remain intact. For example: an AWS instance might
|
||||
// depend on a subnet (which itself might depend on a VPC, and so
|
||||
// on).
|
||||
//
|
||||
// Terraform uses this information to build valid destruction
|
||||
// orders and to warn the user if they're destroying a resource that
|
||||
// another resource depends on.
|
||||
//
|
||||
// Things can be put into this list that may not be managed by
|
||||
// Terraform. If Terraform doesn't find a matching ID in the
|
||||
// overall state, then it assumes it isn't managed and doesn't
|
||||
// worry about it.
|
||||
Dependencies []ResourceDependency
|
||||
}
|
||||
|
||||
// MergeDiff takes a ResourceDiff and merges the attributes into
|
||||
// this resource state in order to generate a new state. This new
|
||||
// state can be used to provide updated attribute lookups for
|
||||
// variable interpolation.
|
||||
//
|
||||
// If the diff attribute requires computing the value, and hence
|
||||
// won't be available until apply, the value is replaced with the
|
||||
// computeID.
|
||||
func (s *ResourceStateV0) MergeDiff(d *InstanceDiff) *ResourceStateV0 {
|
||||
var result ResourceStateV0
|
||||
if s != nil {
|
||||
result = *s
|
||||
}
|
||||
|
||||
result.Attributes = make(map[string]string)
|
||||
if s != nil {
|
||||
for k, v := range s.Attributes {
|
||||
result.Attributes[k] = v
|
||||
}
|
||||
}
|
||||
if d != nil {
|
||||
for k, diff := range d.Attributes {
|
||||
if diff.NewRemoved {
|
||||
delete(result.Attributes, k)
|
||||
continue
|
||||
}
|
||||
if diff.NewComputed {
|
||||
result.Attributes[k] = config.UnknownVariableValue
|
||||
continue
|
||||
}
|
||||
|
||||
result.Attributes[k] = diff.New
|
||||
}
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
func (s *ResourceStateV0) GoString() string {
|
||||
return fmt.Sprintf("*%#v", *s)
|
||||
}
|
||||
|
||||
// ResourceDependency maps a resource to another resource that it
|
||||
// depends on to remain intact and uncorrupted.
|
||||
type ResourceDependency struct {
|
||||
// ID of the resource that we depend on. This ID should map
|
||||
// directly to another ResourceState's ID.
|
||||
ID string
|
||||
}
|
||||
|
||||
// ReadStateV0 reads a state structure out of a reader in the format that
|
||||
// was written by WriteState.
|
||||
func ReadStateV0(src io.Reader) (*StateV0, error) {
|
||||
var result *StateV0
|
||||
var err error
|
||||
n := 0
|
||||
|
||||
// Verify the magic bytes
|
||||
magic := make([]byte, len(stateFormatMagic))
|
||||
for n < len(magic) {
|
||||
n, err = src.Read(magic[n:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while reading magic bytes: %s", err)
|
||||
}
|
||||
}
|
||||
if string(magic) != stateFormatMagic {
|
||||
return nil, fmt.Errorf("not a valid state file")
|
||||
}
|
||||
|
||||
// Verify the version is something we can read
|
||||
var formatByte [1]byte
|
||||
n, err = src.Read(formatByte[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n != len(formatByte) {
|
||||
return nil, errors.New("failed to read state version byte")
|
||||
}
|
||||
|
||||
if formatByte[0] != stateFormatVersion {
|
||||
return nil, fmt.Errorf("unknown state file version: %d", formatByte[0])
|
||||
}
|
||||
|
||||
// Decode
|
||||
dec := gob.NewDecoder(src)
|
||||
if err := dec.Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// upgradeV0State is used to upgrade a V0 state representation
|
||||
// into a State (current) representation.
|
||||
func (old *StateV0) upgrade() (*State, error) {
|
||||
s := &State{}
|
||||
s.init()
|
||||
|
||||
// Old format had no modules, so we migrate everything
|
||||
// directly into the root module.
|
||||
root := s.RootModule()
|
||||
|
||||
// Copy the outputs, first converting them to map[string]interface{}
|
||||
oldOutputs := make(map[string]*OutputState, len(old.Outputs))
|
||||
for key, value := range old.Outputs {
|
||||
oldOutputs[key] = &OutputState{
|
||||
Type: "string",
|
||||
Sensitive: false,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
root.Outputs = oldOutputs
|
||||
|
||||
// Upgrade the resources
|
||||
for id, rs := range old.Resources {
|
||||
newRs := &ResourceState{
|
||||
Type: rs.Type,
|
||||
}
|
||||
root.Resources[id] = newRs
|
||||
|
||||
// Migrate to an instance state
|
||||
newRs.Primary = &InstanceState{
|
||||
ID: rs.ID,
|
||||
Attributes: rs.Attributes,
|
||||
}
|
||||
|
||||
// Check if old resource was tainted
|
||||
if _, ok := old.Tainted[id]; ok {
|
||||
newRs.Primary.Tainted = true
|
||||
}
|
||||
|
||||
// Warn if the resource uses Extra, as there is
|
||||
// no upgrade path for this! Now totally deprecated.
|
||||
if len(rs.Extra) > 0 {
|
||||
log.Printf(
|
||||
"[WARN] Resource %s uses deprecated attribute "+
|
||||
"storage, state file upgrade may be incomplete.",
|
||||
rs.ID,
|
||||
)
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/hashstructure"
|
||||
)
|
||||
|
||||
func TestReadWriteStateV0(t *testing.T) {
|
||||
state := &StateV0{
|
||||
Resources: map[string]*ResourceStateV0{
|
||||
"foo": &ResourceStateV0{
|
||||
ID: "bar",
|
||||
ConnInfo: map[string]string{
|
||||
"type": "ssh",
|
||||
"user": "root",
|
||||
"password": "supersecret",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Checksum before the write
|
||||
chksum, err := hashstructure.Hash(state, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("hash: %s", err)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := testWriteStateV0(state, buf); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// Checksum after the write
|
||||
chksumAfter, err := hashstructure.Hash(state, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("hash: %s", err)
|
||||
}
|
||||
|
||||
if chksumAfter != chksum {
|
||||
t.Fatalf("structure changed during serialization!")
|
||||
}
|
||||
|
||||
actual, err := ReadStateV0(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// ReadState should not restore sensitive information!
|
||||
state.Resources["foo"].ConnInfo = nil
|
||||
|
||||
if !reflect.DeepEqual(actual, state) {
|
||||
t.Fatalf("bad: %#v", actual)
|
||||
}
|
||||
}
|
||||
|
||||
// sensitiveState is used to store sensitive state information
|
||||
// that should not be serialized. This is only used temporarily
|
||||
// and is restored into the state.
|
||||
type sensitiveState struct {
|
||||
ConnInfo map[string]map[string]string
|
||||
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (s *sensitiveState) init() {
|
||||
s.once.Do(func() {
|
||||
s.ConnInfo = make(map[string]map[string]string)
|
||||
})
|
||||
}
|
||||
|
||||
// testWriteStateV0 writes a state somewhere in a binary format.
|
||||
// Only for testing now
|
||||
func testWriteStateV0(d *StateV0, dst io.Writer) error {
|
||||
// Write the magic bytes so we can determine the file format later
|
||||
n, err := dst.Write([]byte(stateFormatMagic))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != len(stateFormatMagic) {
|
||||
return errors.New("failed to write state format magic bytes")
|
||||
}
|
||||
|
||||
// Write a version byte so we can iterate on version at some point
|
||||
n, err = dst.Write([]byte{stateFormatVersion})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != 1 {
|
||||
return errors.New("failed to write state version byte")
|
||||
}
|
||||
|
||||
// Prevent sensitive information from being serialized
|
||||
sensitive := &sensitiveState{}
|
||||
sensitive.init()
|
||||
for name, r := range d.Resources {
|
||||
if r.ConnInfo != nil {
|
||||
sensitive.ConnInfo[name] = r.ConnInfo
|
||||
r.ConnInfo = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the state
|
||||
err = gob.NewEncoder(dst).Encode(d)
|
||||
|
||||
// Restore the state
|
||||
for name, info := range sensitive.ConnInfo {
|
||||
d.Resources[name].ConnInfo = info
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
|
@ -1,17 +1,13 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/copystructure"
|
||||
)
|
||||
|
||||
// stateV1 keeps track of a snapshot state-of-the-world that Terraform
|
||||
// can use to keep track of what real world resources it is actually
|
||||
// managing.
|
||||
//
|
||||
// stateV1 is _only used for the purposes of backwards compatibility
|
||||
// and is no longer used in Terraform.
|
||||
//
|
||||
// For the upgrade process, see state_upgrade_v1_to_v2.go
|
||||
type stateV1 struct {
|
||||
// Version is the protocol version. "1" for a StateV1.
|
||||
Version int `json:"version"`
|
||||
|
@ -29,42 +25,6 @@ type stateV1 struct {
|
|||
Modules []*moduleStateV1 `json:"modules"`
|
||||
}
|
||||
|
||||
// upgrade is used to upgrade a V1 state representation
|
||||
// into a State (current) representation.
|
||||
func (old *stateV1) upgrade() (*State, error) {
|
||||
if old == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
remote, err := old.Remote.upgrade()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading State V1: %v", err)
|
||||
}
|
||||
|
||||
modules := make([]*ModuleState, len(old.Modules))
|
||||
for i, module := range old.Modules {
|
||||
upgraded, err := module.upgrade()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading State V1: %v", err)
|
||||
}
|
||||
modules[i] = upgraded
|
||||
}
|
||||
if len(modules) == 0 {
|
||||
modules = nil
|
||||
}
|
||||
|
||||
newState := &State{
|
||||
Version: old.Version,
|
||||
Serial: old.Serial,
|
||||
Remote: remote,
|
||||
Modules: modules,
|
||||
}
|
||||
|
||||
newState.sort()
|
||||
|
||||
return newState, nil
|
||||
}
|
||||
|
||||
type remoteStateV1 struct {
|
||||
// Type controls the client we use for the remote state
|
||||
Type string `json:"type"`
|
||||
|
@ -74,22 +34,6 @@ type remoteStateV1 struct {
|
|||
Config map[string]string `json:"config"`
|
||||
}
|
||||
|
||||
func (old *remoteStateV1) upgrade() (*RemoteState, error) {
|
||||
if old == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
config, err := copystructure.Copy(old.Config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading RemoteState V1: %v", err)
|
||||
}
|
||||
|
||||
return &RemoteState{
|
||||
Type: old.Type,
|
||||
Config: config.(map[string]string),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type moduleStateV1 struct {
|
||||
// Path is the import path from the root module. Modules imports are
|
||||
// always disjoint, so the path represents amodule tree
|
||||
|
@ -121,54 +65,6 @@ type moduleStateV1 struct {
|
|||
Dependencies []string `json:"depends_on,omitempty"`
|
||||
}
|
||||
|
||||
func (old *moduleStateV1) upgrade() (*ModuleState, error) {
|
||||
if old == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
path, err := copystructure.Copy(old.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err)
|
||||
}
|
||||
|
||||
// Outputs needs upgrading to use the new structure
|
||||
outputs := make(map[string]*OutputState)
|
||||
for key, output := range old.Outputs {
|
||||
outputs[key] = &OutputState{
|
||||
Type: "string",
|
||||
Value: output,
|
||||
Sensitive: false,
|
||||
}
|
||||
}
|
||||
if len(outputs) == 0 {
|
||||
outputs = nil
|
||||
}
|
||||
|
||||
resources := make(map[string]*ResourceState)
|
||||
for key, oldResource := range old.Resources {
|
||||
upgraded, err := oldResource.upgrade()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err)
|
||||
}
|
||||
resources[key] = upgraded
|
||||
}
|
||||
if len(resources) == 0 {
|
||||
resources = nil
|
||||
}
|
||||
|
||||
dependencies, err := copystructure.Copy(old.Dependencies)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err)
|
||||
}
|
||||
|
||||
return &ModuleState{
|
||||
Path: path.([]string),
|
||||
Outputs: outputs,
|
||||
Resources: resources,
|
||||
Dependencies: dependencies.([]string),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type resourceStateV1 struct {
|
||||
// This is filled in and managed by Terraform, and is the resource
|
||||
// type itself such as "mycloud_instance". If a resource provider sets
|
||||
|
@ -220,42 +116,6 @@ type resourceStateV1 struct {
|
|||
Provider string `json:"provider,omitempty"`
|
||||
}
|
||||
|
||||
func (old *resourceStateV1) upgrade() (*ResourceState, error) {
|
||||
if old == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
dependencies, err := copystructure.Copy(old.Dependencies)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err)
|
||||
}
|
||||
|
||||
primary, err := old.Primary.upgrade()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err)
|
||||
}
|
||||
|
||||
deposed := make([]*InstanceState, len(old.Deposed))
|
||||
for i, v := range old.Deposed {
|
||||
upgraded, err := v.upgrade()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err)
|
||||
}
|
||||
deposed[i] = upgraded
|
||||
}
|
||||
if len(deposed) == 0 {
|
||||
deposed = nil
|
||||
}
|
||||
|
||||
return &ResourceState{
|
||||
Type: old.Type,
|
||||
Dependencies: dependencies.([]string),
|
||||
Primary: primary,
|
||||
Deposed: deposed,
|
||||
Provider: old.Provider,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type instanceStateV1 struct {
|
||||
// A unique ID for this resource. This is opaque to Terraform
|
||||
// and is only meant as a lookup mechanism for the providers.
|
||||
|
@ -277,45 +137,9 @@ type instanceStateV1 struct {
|
|||
Meta map[string]string `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
func (old *instanceStateV1) upgrade() (*InstanceState, error) {
|
||||
if old == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
attributes, err := copystructure.Copy(old.Attributes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err)
|
||||
}
|
||||
ephemeral, err := old.Ephemeral.upgrade()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err)
|
||||
}
|
||||
meta, err := copystructure.Copy(old.Meta)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err)
|
||||
}
|
||||
|
||||
return &InstanceState{
|
||||
ID: old.ID,
|
||||
Attributes: attributes.(map[string]string),
|
||||
Ephemeral: *ephemeral,
|
||||
Meta: meta.(map[string]string),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ephemeralStateV1 struct {
|
||||
// ConnInfo is used for the providers to export information which is
|
||||
// used to connect to the resource for provisioning. For example,
|
||||
// this could contain SSH or WinRM credentials.
|
||||
ConnInfo map[string]string `json:"-"`
|
||||
}
|
||||
|
||||
func (old *ephemeralStateV1) upgrade() (*EphemeralState, error) {
|
||||
connInfo, err := copystructure.Copy(old.ConnInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error upgrading EphemeralState V1: %v", err)
|
||||
}
|
||||
return &EphemeralState{
|
||||
ConnInfo: connInfo.(map[string]string),
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
// TestReadUpgradeStateV1toV3 tests the state upgrade process from the V1 state
|
||||
// to the current version, and needs editing each time. This means it tests the
|
||||
// entire pipeline of upgrades (which migrate version to version).
|
||||
func TestReadUpgradeStateV1toV3(t *testing.T) {
|
||||
// ReadState should transparently detect the old version but will upgrade
|
||||
// it on Write.
|
||||
actual, err := ReadState(strings.NewReader(testV1State))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := WriteState(actual, buf); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if actual.Version != 3 {
|
||||
t.Fatalf("bad: State version not incremented; is %d", actual.Version)
|
||||
}
|
||||
|
||||
roundTripped, err := ReadState(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, roundTripped) {
|
||||
t.Fatalf("bad: %#v", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadUpgradeStateV1toV3_outputs(t *testing.T) {
|
||||
// ReadState should transparently detect the old version but will upgrade
|
||||
// it on Write.
|
||||
actual, err := ReadState(strings.NewReader(testV1StateWithOutputs))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := WriteState(actual, buf); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if actual.Version != 3 {
|
||||
t.Fatalf("bad: State version not incremented; is %d", actual.Version)
|
||||
}
|
||||
|
||||
roundTripped, err := ReadState(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, roundTripped) {
|
||||
spew.Config.DisableMethods = true
|
||||
t.Fatalf("bad:\n%s\n\nround tripped:\n%s\n", spew.Sdump(actual), spew.Sdump(roundTripped))
|
||||
spew.Config.DisableMethods = false
|
||||
}
|
||||
}
|
||||
|
||||
const testV1State = `{
|
||||
"version": 1,
|
||||
"serial": 9,
|
||||
"remote": {
|
||||
"type": "http",
|
||||
"config": {
|
||||
"url": "http://my-cool-server.com/"
|
||||
}
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"path": [
|
||||
"root"
|
||||
],
|
||||
"outputs": null,
|
||||
"resources": {
|
||||
"foo": {
|
||||
"type": "",
|
||||
"primary": {
|
||||
"id": "bar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"depends_on": [
|
||||
"aws_instance.bar"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
const testV1StateWithOutputs = `{
|
||||
"version": 1,
|
||||
"serial": 9,
|
||||
"remote": {
|
||||
"type": "http",
|
||||
"config": {
|
||||
"url": "http://my-cool-server.com/"
|
||||
}
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"path": [
|
||||
"root"
|
||||
],
|
||||
"outputs": {
|
||||
"foo": "bar",
|
||||
"baz": "foo"
|
||||
},
|
||||
"resources": {
|
||||
"foo": {
|
||||
"type": "",
|
||||
"primary": {
|
||||
"id": "bar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"depends_on": [
|
||||
"aws_instance.bar"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
|
@ -0,0 +1,202 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestReadUpgradeStateV2toV3 tests the state upgrade process from the V2 state
|
||||
// to the current version, and needs editing each time. This means it tests the
|
||||
// entire pipeline of upgrades (which migrate version to version).
|
||||
func TestReadUpgradeStateV2toV3(t *testing.T) {
|
||||
// ReadState should transparently detect the old version but will upgrade
|
||||
// it on Write.
|
||||
upgraded, err := ReadState(strings.NewReader(testV2State))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := WriteState(upgraded, buf); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if upgraded.Version != 3 {
|
||||
t.Fatalf("bad: State version not incremented; is %d", upgraded.Version)
|
||||
}
|
||||
|
||||
// For this test we cannot assert that we match the round trip because an
|
||||
// empty map has been removed from state. Instead we make assertions against
|
||||
// some of the key fields in the _upgraded_ state.
|
||||
instanceState, ok := upgraded.RootModule().Resources["test_resource.main"]
|
||||
if !ok {
|
||||
t.Fatalf("Instance state for test_resource.main was removed from state during upgrade")
|
||||
}
|
||||
|
||||
primary := instanceState.Primary
|
||||
if primary == nil {
|
||||
t.Fatalf("Primary instance was removed from state for test_resource.main")
|
||||
}
|
||||
|
||||
// Non-empty computed map is moved from .# to .%
|
||||
if _, ok := primary.Attributes["computed_map.#"]; ok {
|
||||
t.Fatalf("Count was not upgraded from .# to .%% for computed_map")
|
||||
}
|
||||
if count, ok := primary.Attributes["computed_map.%"]; !ok || count != "1" {
|
||||
t.Fatalf("Count was not in .%% or was not 2 for computed_map")
|
||||
}
|
||||
|
||||
// list_of_map top level retains .#
|
||||
if count, ok := primary.Attributes["list_of_map.#"]; !ok || count != "2" {
|
||||
t.Fatal("Count for list_of_map was migrated incorrectly")
|
||||
}
|
||||
|
||||
// list_of_map.0 is moved from .# to .%
|
||||
if _, ok := primary.Attributes["list_of_map.0.#"]; ok {
|
||||
t.Fatalf("Count was not upgraded from .# to .%% for list_of_map.0")
|
||||
}
|
||||
if count, ok := primary.Attributes["list_of_map.0.%"]; !ok || count != "2" {
|
||||
t.Fatalf("Count was not in .%% or was not 2 for list_of_map.0")
|
||||
}
|
||||
|
||||
// list_of_map.1 is moved from .# to .%
|
||||
if _, ok := primary.Attributes["list_of_map.1.#"]; ok {
|
||||
t.Fatalf("Count was not upgraded from .# to .%% for list_of_map.1")
|
||||
}
|
||||
if count, ok := primary.Attributes["list_of_map.1.%"]; !ok || count != "2" {
|
||||
t.Fatalf("Count was not in .%% or was not 2 for list_of_map.1")
|
||||
}
|
||||
|
||||
// map is moved from .# to .%
|
||||
if _, ok := primary.Attributes["map.#"]; ok {
|
||||
t.Fatalf("Count was not upgraded from .# to .%% for map")
|
||||
}
|
||||
if count, ok := primary.Attributes["map.%"]; !ok || count != "2" {
|
||||
t.Fatalf("Count was not in .%% or was not 2 for map")
|
||||
}
|
||||
|
||||
// optional_computed_map should be removed from state
|
||||
if _, ok := primary.Attributes["optional_computed_map"]; ok {
|
||||
t.Fatal("optional_computed_map was not removed from state")
|
||||
}
|
||||
|
||||
// required_map is moved from .# to .%
|
||||
if _, ok := primary.Attributes["required_map.#"]; ok {
|
||||
t.Fatalf("Count was not upgraded from .# to .%% for required_map")
|
||||
}
|
||||
if count, ok := primary.Attributes["required_map.%"]; !ok || count != "3" {
|
||||
t.Fatalf("Count was not in .%% or was not 3 for map")
|
||||
}
|
||||
|
||||
// computed_list keeps .#
|
||||
if count, ok := primary.Attributes["computed_list.#"]; !ok || count != "2" {
|
||||
t.Fatal("Count was migrated incorrectly for computed_list")
|
||||
}
|
||||
|
||||
// computed_set keeps .#
|
||||
if count, ok := primary.Attributes["computed_set.#"]; !ok || count != "2" {
|
||||
t.Fatal("Count was migrated incorrectly for computed_set")
|
||||
}
|
||||
if val, ok := primary.Attributes["computed_set.2337322984"]; !ok || val != "setval1" {
|
||||
t.Fatal("Set item for computed_set.2337322984 changed or moved")
|
||||
}
|
||||
if val, ok := primary.Attributes["computed_set.307881554"]; !ok || val != "setval2" {
|
||||
t.Fatal("Set item for computed_set.307881554 changed or moved")
|
||||
}
|
||||
|
||||
// string properties are unaffected
|
||||
if val, ok := primary.Attributes["id"]; !ok || val != "testId" {
|
||||
t.Fatal("id was not set correctly after migration")
|
||||
}
|
||||
}
|
||||
|
||||
const testV2State = `{
|
||||
"version": 2,
|
||||
"terraform_version": "0.7.0",
|
||||
"serial": 2,
|
||||
"modules": [
|
||||
{
|
||||
"path": [
|
||||
"root"
|
||||
],
|
||||
"outputs": {
|
||||
"computed_map": {
|
||||
"sensitive": false,
|
||||
"type": "map",
|
||||
"value": {
|
||||
"key1": "value1"
|
||||
}
|
||||
},
|
||||
"computed_set": {
|
||||
"sensitive": false,
|
||||
"type": "list",
|
||||
"value": [
|
||||
"setval1",
|
||||
"setval2"
|
||||
]
|
||||
},
|
||||
"map": {
|
||||
"sensitive": false,
|
||||
"type": "map",
|
||||
"value": {
|
||||
"key": "test",
|
||||
"test": "test"
|
||||
}
|
||||
},
|
||||
"set": {
|
||||
"sensitive": false,
|
||||
"type": "list",
|
||||
"value": [
|
||||
"test1",
|
||||
"test2"
|
||||
]
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"test_resource.main": {
|
||||
"type": "test_resource",
|
||||
"primary": {
|
||||
"id": "testId",
|
||||
"attributes": {
|
||||
"computed_list.#": "2",
|
||||
"computed_list.0": "listval1",
|
||||
"computed_list.1": "listval2",
|
||||
"computed_map.#": "1",
|
||||
"computed_map.key1": "value1",
|
||||
"computed_read_only": "value_from_api",
|
||||
"computed_read_only_force_new": "value_from_api",
|
||||
"computed_set.#": "2",
|
||||
"computed_set.2337322984": "setval1",
|
||||
"computed_set.307881554": "setval2",
|
||||
"id": "testId",
|
||||
"list_of_map.#": "2",
|
||||
"list_of_map.0.#": "2",
|
||||
"list_of_map.0.key1": "value1",
|
||||
"list_of_map.0.key2": "value2",
|
||||
"list_of_map.1.#": "2",
|
||||
"list_of_map.1.key3": "value3",
|
||||
"list_of_map.1.key4": "value4",
|
||||
"map.#": "2",
|
||||
"map.key": "test",
|
||||
"map.test": "test",
|
||||
"map_that_look_like_set.#": "2",
|
||||
"map_that_look_like_set.12352223": "hello",
|
||||
"map_that_look_like_set.36234341": "world",
|
||||
"optional_computed_map.#": "0",
|
||||
"required": "Hello World",
|
||||
"required_map.#": "3",
|
||||
"required_map.key1": "value1",
|
||||
"required_map.key2": "value2",
|
||||
"required_map.key3": "value3",
|
||||
"set.#": "2",
|
||||
"set.2326977762": "test1",
|
||||
"set.331058520": "test2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
|
@ -1,3 +0,0 @@
|
|||
.DS_Store
|
||||
.idea
|
||||
*.iml
|
|
@ -1,3 +0,0 @@
|
|||
sudo: false
|
||||
language: go
|
||||
go: 1.5
|
|
@ -2,14 +2,48 @@ package hil
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/hashicorp/hil/ast"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
var hilMapstructureDecodeHookSlice []interface{}
|
||||
var hilMapstructureDecodeHookStringSlice []string
|
||||
var hilMapstructureDecodeHookMap map[string]interface{}
|
||||
|
||||
// hilMapstructureWeakDecode behaves in the same way as mapstructure.WeakDecode
|
||||
// but has a DecodeHook which defeats the backward compatibility mode of mapstructure
|
||||
// which WeakDecodes []interface{}{} into an empty map[string]interface{}. This
|
||||
// allows us to use WeakDecode (desirable), but not fail on empty lists.
|
||||
func hilMapstructureWeakDecode(m interface{}, rawVal interface{}) error {
|
||||
config := &mapstructure.DecoderConfig{
|
||||
DecodeHook: func(source reflect.Type, target reflect.Type, val interface{}) (interface{}, error) {
|
||||
sliceType := reflect.TypeOf(hilMapstructureDecodeHookSlice)
|
||||
stringSliceType := reflect.TypeOf(hilMapstructureDecodeHookStringSlice)
|
||||
mapType := reflect.TypeOf(hilMapstructureDecodeHookMap)
|
||||
|
||||
if (source == sliceType || source == stringSliceType) && target == mapType {
|
||||
return nil, fmt.Errorf("Cannot convert %s into a %s", source, target)
|
||||
}
|
||||
|
||||
return val, nil
|
||||
},
|
||||
WeaklyTypedInput: true,
|
||||
Result: rawVal,
|
||||
}
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return decoder.Decode(m)
|
||||
}
|
||||
|
||||
func InterfaceToVariable(input interface{}) (ast.Variable, error) {
|
||||
var stringVal string
|
||||
if err := mapstructure.WeakDecode(input, &stringVal); err == nil {
|
||||
if err := hilMapstructureWeakDecode(input, &stringVal); err == nil {
|
||||
return ast.Variable{
|
||||
Type: ast.TypeString,
|
||||
Value: stringVal,
|
||||
|
@ -17,7 +51,7 @@ func InterfaceToVariable(input interface{}) (ast.Variable, error) {
|
|||
}
|
||||
|
||||
var mapVal map[string]interface{}
|
||||
if err := mapstructure.WeakDecode(input, &mapVal); err == nil {
|
||||
if err := hilMapstructureWeakDecode(input, &mapVal); err == nil {
|
||||
elements := make(map[string]ast.Variable)
|
||||
for i, element := range mapVal {
|
||||
varElement, err := InterfaceToVariable(element)
|
||||
|
@ -34,7 +68,7 @@ func InterfaceToVariable(input interface{}) (ast.Variable, error) {
|
|||
}
|
||||
|
||||
var sliceVal []interface{}
|
||||
if err := mapstructure.WeakDecode(input, &sliceVal); err == nil {
|
||||
if err := hilMapstructureWeakDecode(input, &sliceVal); err == nil {
|
||||
elements := make([]ast.Variable, len(sliceVal))
|
||||
for i, element := range sliceVal {
|
||||
varElement, err := InterfaceToVariable(element)
|
||||
|
|
|
@ -836,12 +836,16 @@
|
|||
"revisionTime": "2016-06-07T00:19:40Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "rYMJhG5J/OzcBIRfatklb/+JrJ4=",
|
||||
"path": "github.com/hashicorp/hil",
|
||||
"revision": "01dc167cd239b7ccab78a683b866536cd5904719"
|
||||
"revision": "58c35af3b2c3a72c572851bc9e650b62a61d5ec2",
|
||||
"revisionTime": "2016-06-05T07:46:40Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "eJv3RKa2gk/4khPzo3+pJzThz8w=",
|
||||
"path": "github.com/hashicorp/hil/ast",
|
||||
"revision": "01dc167cd239b7ccab78a683b866536cd5904719"
|
||||
"revision": "42cfb33535beaca9a8ca2ef415930a1bd8f32998",
|
||||
"revisionTime": "2016-06-03T20:14:09Z"
|
||||
},
|
||||
{
|
||||
"path": "github.com/hashicorp/logutils",
|
||||
|
|
Loading…
Reference in New Issue