Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions builtin/providers/terraform/data_source_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,52 @@ func TestState_defaults(t *testing.T) {
})
}

func TestState_version4(t *testing.T) {
// This test is for our special support for reading state version 4 as
// remote state even though this Terraform version doesn't support that
// version for any other purpose.

resource.UnitTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccState_version4,
Check: resource.ComposeTestCheckFunc(
testAccCheckStateValue("data.terraform_remote_state.foo", "from_resource", "7585419850792319264"),
testAccCheckStateValue("data.terraform_remote_state.foo", "string", "value"),
testAccCheckStateValue("data.terraform_remote_state.foo", "bool", "true"),
testAccCheckStateValue("data.terraform_remote_state.foo", "float", "1.2"),
testAccCheckStateValue("data.terraform_remote_state.foo", "integer", "3"),
testAccCheckStateValue("data.terraform_remote_state.foo", "list.#", "2"),
testAccCheckStateValue("data.terraform_remote_state.foo", "list.0", "value 1"),
testAccCheckStateValue("data.terraform_remote_state.foo", "list.1", "value 2"),
testAccCheckStateValue("data.terraform_remote_state.foo", "set.#", "2"),
testAccCheckStateValue("data.terraform_remote_state.foo", "set.0", "value 1"),
testAccCheckStateValue("data.terraform_remote_state.foo", "set.1", "value 2"),
testAccCheckStateValue("data.terraform_remote_state.foo", "tuple.#", "2"),
testAccCheckStateValue("data.terraform_remote_state.foo", "tuple.0", "value 1"),
testAccCheckStateValue("data.terraform_remote_state.foo", "tuple.1", "value 2"),
testAccCheckStateValue("data.terraform_remote_state.foo", "mixed_tuple.#", "2"),
testAccCheckStateValue("data.terraform_remote_state.foo", "mixed_tuple.0", "value 1"),
testAccCheckStateValue("data.terraform_remote_state.foo", "mixed_tuple.1.#", "1"),
testAccCheckStateValue("data.terraform_remote_state.foo", "mixed_tuple.1.0", "value 2"),
testAccCheckStateValue("data.terraform_remote_state.foo", "map.%", "2"),
testAccCheckStateValue("data.terraform_remote_state.foo", "map.a", "value 1"),
testAccCheckStateValue("data.terraform_remote_state.foo", "map.b", "value 2"),
testAccCheckStateValue("data.terraform_remote_state.foo", "object.%", "2"),
testAccCheckStateValue("data.terraform_remote_state.foo", "object.a", "value 1"),
testAccCheckStateValue("data.terraform_remote_state.foo", "object.b", "value 2"),
testAccCheckStateValue("data.terraform_remote_state.foo", "mixed_object.%", "2"),
testAccCheckStateValue("data.terraform_remote_state.foo", "mixed_object.a", "value 1"),
testAccCheckStateValue("data.terraform_remote_state.foo", "mixed_object.b.#", "1"),
testAccCheckStateValue("data.terraform_remote_state.foo", "mixed_object.b.0", "value 2"),
),
},
},
})
}

func testAccCheckStateValue(id, name, value string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[id]
Expand Down Expand Up @@ -223,3 +269,12 @@ data "terraform_remote_state" "foo" {
path = "./test-fixtures/basic.tfstate"
}
}`

const testAccState_version4 = `
data "terraform_remote_state" "foo" {
backend = "local"

config {
path = "./test-fixtures/version4.tfstate"
}
}`
137 changes: 137 additions & 0 deletions builtin/providers/terraform/test-fixtures/version4.tfstate
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
{
"version": 4,
"terraform_version": "0.12.0",
"serial": 2,
"lineage": "7c976222-2974-fd2f-def5-aa1000dc1719",
"outputs": {
"bool": {
"value": true,
"type": "bool"
},
"float": {
"value": 1.2,
"type": "number"
},
"from_resource": {
"value": "7585419850792319264",
"type": "string"
},
"integer": {
"value": 3,
"type": "number"
},
"list": {
"value": [
"value 1",
"value 2"
],
"type": [
"list",
"string"
]
},
"map": {
"value": {
"a": "value 1",
"b": "value 2"
},
"type": [
"map",
"string"
]
},
"mixed_object": {
"value": {
"a": "value 1",
"b": [
"value 2"
]
},
"type": [
"object",
{
"a": "string",
"b": [
"list",
"string"
]
}
]
},
"mixed_tuple": {
"value": [
"value 1",
[
"value 2"
]
],
"type": [
"tuple",
[
"string",
[
"set",
"string"
]
]
]
},
"object": {
"value": {
"a": "value 1",
"b": "value 2"
},
"type": [
"object",
{
"a": "string",
"b": "string"
}
]
},
"set": {
"value": [
"value 1",
"value 2"
],
"type": [
"set",
"string"
]
},
"string": {
"value": "value",
"type": "string"
},
"tuple": {
"value": [
"value 1",
"value 2"
],
"type": [
"tuple",
[
"string",
"string"
]
]
}
},
"resources": [
{
"mode": "managed",
"type": "null_resource",
"name": "baz",
"provider": "provider.null",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "7585419850792319264",
"triggers": null
}
}
]
}
]
}
101 changes: 99 additions & 2 deletions terraform/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -1950,6 +1950,20 @@ func ReadState(src io.Reader) (*State, error) {
}

result = v3State
case 4:
// Version 4 is the one introduced by Terraform v0.12. We only have
// minimal support here for reading the _outputs_ portion of such
// a state, so the result in this case is suitable only for the
// terraform_remote_state data source.
// The result of this can never pass CheckStateVersion, so version
// 4 state snapshots will not be accepted as a starting state for any
// Terraform operation.
v4State, err := readStateV4OutputsOnly(jsonBytes)
if err != nil {
return nil, err
}

result = v4State
default:
return nil, fmt.Errorf("Terraform %s does not support state version %d, please update.",
tfversion.SemVer.String(), versionIdentifier.Version)
Expand Down Expand Up @@ -2070,6 +2084,67 @@ func ReadStateV3(jsonBytes []byte) (*State, error) {
return state, nil
}

func readStateV4OutputsOnly(src []byte) (*State, error) {
type StateV4 struct {
TerraformVersion string `json:"terraform_version"`
Serial int64 `json:"serial"`
Lineage string `json:"lineage"`
RootOutputs map[string]struct {
Value interface{} `json:"value"` // encoding/json's default result is good enough for our purposes here
Sensitive bool `json:"sensitive,omitempty"`
} `json:"outputs"`
}

var raw StateV4
if err := json.Unmarshal(src, &raw); err != nil {
return nil, fmt.Errorf("Decoding state file failed: %s", err)
}

// Now we'll adapt what we read to fit within the V3-oriented state type.
ret := &State{}
ret.Version = 4 // greater than 3, so will never pass CheckStateVersion
ret.TFVersion = raw.TerraformVersion
ret.Serial = raw.Serial
ret.Lineage = raw.Lineage

// The outputs require a little more work because we must deconstruct the
// cty-oriented types and values into as-close-as-possible values that
// mimic what would be saved in state format version 3.
mod := &ModuleState{
Path: []string{"root"},
Outputs: map[string]*OutputState{},
Locals: map[string]interface{}{},
Resources: map[string]*ResourceState{},
}
outputs := mod.Outputs

for k, rawOS := range raw.RootOutputs {
os := &OutputState{}
os.Sensitive = rawOS.Sensitive
os.Value = rawOS.Value
switch tv := os.Value.(type) {
case map[string]interface{}:
os.Type = "map"
case []interface{}:
os.Type = "list"
case float64:
os.Type = "string"
os.Value = strconv.FormatFloat(tv, 'f', -1, 64)
case bool:
os.Type = "string"
os.Value = strconv.FormatBool(tv)
default:
os.Type = "string"
}

outputs[k] = os
}

ret.Modules = append(ret.Modules, mod)

return ret, nil
}

// WriteState writes a state somewhere in a binary format.
func WriteState(d *State, dst io.Writer) error {
// writing a nil state is a noop.
Expand Down Expand Up @@ -2187,7 +2262,7 @@ func (s moduleStateSort) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}

// StateCompatible returns an error if the state is not compatible with the
// CheckStateVersion returns an error if the state is not compatible with the
// current version of terraform.
func CheckStateVersion(state *State) error {
if state == nil {
Expand All @@ -2197,6 +2272,19 @@ func CheckStateVersion(state *State) error {
if state.FromFutureTerraform() {
return fmt.Errorf(stateInvalidTerraformVersionErr, state.TFVersion)
}

if state.Version > 3 {
// We only support version 4 enough for terraform_remote_state to
// read from it, so we need to hard-fail here to prevent any weird
// behavior if a user tries to use Terraform 0.11 against a state
// snapshot created by Terraform v0.12 or later. (terraform_remote_state
// doesn't call CheckStateVersion.)
// We use a different message here because getting here suggests that
// the user tried to rewrite the terraform_version value in a state
// created in a later version in the hope it'd work.
return fmt.Errorf(stateVersionTooNewError, state.Version)
}

return nil
}

Expand All @@ -2214,5 +2302,14 @@ Terraform doesn't allow running any operations against a state
that was written by a future Terraform version. The state is
reporting it is written by Terraform '%s'

Please run at least that version of Terraform to continue.
A newer version of Terraform is required to make changes to the current
workspace.
`

const stateVersionTooNewError = `
The latest state snapshot is using snapshot format %d, which is too new for
this version of Terraform.

A newer version of Terraform is required to make changes to the current
workspace.
`
2 changes: 1 addition & 1 deletion terraform/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1675,7 +1675,7 @@ func TestReadStateNewVersion(t *testing.T) {
Version int
}

buf, err := json.Marshal(&out{StateVersion + 1})
buf, err := json.Marshal(&out{StateVersion + 2})
if err != nil {
t.Fatalf("err: %v", err)
}
Expand Down