Merge pull request #7082 from hashicorp/b-empty-map-types

core: Make lists and maps distinguishable in state
This commit is contained in:
James Nugent 2016-06-09 12:36:24 +02:00
commit 57cf9fd295
32 changed files with 1051 additions and 1004 deletions

View File

@ -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()
},
})
}

View File

@ -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
}

View File

@ -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"

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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",
}),
},

View File

@ -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
}
}

View File

@ -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",
}),
}

View File

@ -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
}

View File

@ -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",
},
},

View File

@ -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",
},
},

View File

@ -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",
},
}

View File

@ -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)}
}

View File

@ -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",
},

View File

@ -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": [
{

View File

@ -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
}

View File

@ -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(

View File

@ -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))
}
}

View File

@ -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)

View File

@ -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"
]
}
]
}
`

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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"
]
}
]
}
`

View File

@ -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"
}
}
}
}
}
]
}
`

View File

@ -1,3 +0,0 @@
.DS_Store
.idea
*.iml

View File

@ -1,3 +0,0 @@
sudo: false
language: go
go: 1.5

View File

@ -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)

8
vendor/vendor.json vendored
View File

@ -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",