core: Use OutputState in JSON instead of map

This commit forward ports the changes made for 0.6.17, in order to store
the type and sensitive flag against outputs.

It also refactors the logic of the import for V0 to V1 state, and
fixes up the call sites of the new format for outputs in V2 state.

Finally we fix up tests which did not previously set a state version
where one is required.
This commit is contained in:
James Nugent 2016-05-11 20:05:02 -04:00
parent eedc523281
commit 3ea3c657b5
26 changed files with 786 additions and 151 deletions

View File

@ -36,7 +36,7 @@ func TestTemplateRendering(t *testing.T) {
Config: testTemplateConfig(tt.template, tt.vars),
Check: func(s *terraform.State) error {
got := s.RootModule().Outputs["rendered"]
if tt.want != got {
if tt.want != got.Value {
return fmt.Errorf("template:\n%s\nvars:\n%s\ngot:\n%s\nwant:\n%s\n", tt.template, tt.vars, got, tt.want)
}
return nil
@ -65,7 +65,7 @@ func TestTemplateVariableChange(t *testing.T) {
Check: func(i int, want string) r.TestCheckFunc {
return func(s *terraform.State) error {
got := s.RootModule().Outputs["rendered"]
if want != got {
if want != got.Value {
return fmt.Errorf("[%d] got:\n%q\nwant:\n%q\n", i, got, want)
}
return nil

View File

@ -54,7 +54,11 @@ func dataSourceRemoteStateRead(d *schema.ResourceData, meta interface{}) error {
var outputs map[string]interface{}
if !state.State().Empty() {
outputs = state.State().RootModule().Outputs
outputValueMap := make(map[string]string)
for key, output := range state.State().RootModule().Outputs {
//This is ok for 0.6.17 as outputs will have been strings
outputValueMap[key] = output.Value.(string)
}
}
d.SetId(time.Now().UTC().String())

View File

@ -50,7 +50,7 @@ EOT
}
`, testPrivateKey),
Check: func(s *terraform.State) error {
gotUntyped := s.RootModule().Outputs["key_pem"]
gotUntyped := s.RootModule().Outputs["key_pem"].Value
got, ok := gotUntyped.(string)
if !ok {

View File

@ -47,7 +47,7 @@ EOT
}
`, testCertRequest, testCACert, testCAPrivateKey),
Check: func(s *terraform.State) error {
gotUntyped := s.RootModule().Outputs["cert_pem"]
gotUntyped := s.RootModule().Outputs["cert_pem"].Value
got, ok := gotUntyped.(string)
if !ok {
return fmt.Errorf("output for \"cert_pem\" is not a string")

View File

@ -29,7 +29,7 @@ func TestPrivateKeyRSA(t *testing.T) {
}
`,
Check: func(s *terraform.State) error {
gotPrivateUntyped := s.RootModule().Outputs["private_key_pem"]
gotPrivateUntyped := s.RootModule().Outputs["private_key_pem"].Value
gotPrivate, ok := gotPrivateUntyped.(string)
if !ok {
return fmt.Errorf("output for \"private_key_pem\" is not a string")
@ -42,7 +42,7 @@ func TestPrivateKeyRSA(t *testing.T) {
return fmt.Errorf("private key PEM looks too long for a 2048-bit key (got %v characters)", len(gotPrivate))
}
gotPublicUntyped := s.RootModule().Outputs["public_key_pem"]
gotPublicUntyped := s.RootModule().Outputs["public_key_pem"].Value
gotPublic, ok := gotPublicUntyped.(string)
if !ok {
return fmt.Errorf("output for \"public_key_pem\" is not a string")
@ -51,7 +51,7 @@ func TestPrivateKeyRSA(t *testing.T) {
return fmt.Errorf("public key is missing public key PEM preamble")
}
gotPublicSSHUntyped := s.RootModule().Outputs["public_key_openssh"]
gotPublicSSHUntyped := s.RootModule().Outputs["public_key_openssh"].Value
gotPublicSSH, ok := gotPublicSSHUntyped.(string)
if !ok {
return fmt.Errorf("output for \"public_key_openssh\" is not a string")
@ -74,7 +74,7 @@ func TestPrivateKeyRSA(t *testing.T) {
}
`,
Check: func(s *terraform.State) error {
gotUntyped := s.RootModule().Outputs["key_pem"]
gotUntyped := s.RootModule().Outputs["key_pem"].Value
got, ok := gotUntyped.(string)
if !ok {
return fmt.Errorf("output for \"key_pem\" is not a string")
@ -112,7 +112,7 @@ func TestPrivateKeyECDSA(t *testing.T) {
}
`,
Check: func(s *terraform.State) error {
gotPrivateUntyped := s.RootModule().Outputs["private_key_pem"]
gotPrivateUntyped := s.RootModule().Outputs["private_key_pem"].Value
gotPrivate, ok := gotPrivateUntyped.(string)
if !ok {
return fmt.Errorf("output for \"private_key_pem\" is not a string")
@ -122,7 +122,7 @@ func TestPrivateKeyECDSA(t *testing.T) {
return fmt.Errorf("Private key is missing EC key PEM preamble")
}
gotPublicUntyped := s.RootModule().Outputs["public_key_pem"]
gotPublicUntyped := s.RootModule().Outputs["public_key_pem"].Value
gotPublic, ok := gotPublicUntyped.(string)
if !ok {
return fmt.Errorf("output for \"public_key_pem\" is not a string")
@ -132,7 +132,7 @@ func TestPrivateKeyECDSA(t *testing.T) {
return fmt.Errorf("public key is missing public key PEM preamble")
}
gotPublicSSH := s.RootModule().Outputs["public_key_openssh"]
gotPublicSSH := s.RootModule().Outputs["public_key_openssh"].Value.(string)
if gotPublicSSH != "" {
return fmt.Errorf("P224 EC key should not generate OpenSSH public key")
}
@ -157,7 +157,7 @@ func TestPrivateKeyECDSA(t *testing.T) {
}
`,
Check: func(s *terraform.State) error {
gotPrivateUntyped := s.RootModule().Outputs["private_key_pem"]
gotPrivateUntyped := s.RootModule().Outputs["private_key_pem"].Value
gotPrivate, ok := gotPrivateUntyped.(string)
if !ok {
return fmt.Errorf("output for \"private_key_pem\" is not a string")
@ -166,7 +166,7 @@ func TestPrivateKeyECDSA(t *testing.T) {
return fmt.Errorf("Private key is missing EC key PEM preamble")
}
gotPublicUntyped := s.RootModule().Outputs["public_key_pem"]
gotPublicUntyped := s.RootModule().Outputs["public_key_pem"].Value
gotPublic, ok := gotPublicUntyped.(string)
if !ok {
return fmt.Errorf("output for \"public_key_pem\" is not a string")
@ -175,7 +175,7 @@ func TestPrivateKeyECDSA(t *testing.T) {
return fmt.Errorf("public key is missing public key PEM preamble")
}
gotPublicSSHUntyped := s.RootModule().Outputs["public_key_openssh"]
gotPublicSSHUntyped := s.RootModule().Outputs["public_key_openssh"].Value
gotPublicSSH, ok := gotPublicSSHUntyped.(string)
if !ok {
return fmt.Errorf("output for \"public_key_openssh\" is not a string")

View File

@ -60,7 +60,7 @@ EOT
}
`, testPrivateKey),
Check: func(s *terraform.State) error {
gotUntyped := s.RootModule().Outputs["key_pem"]
gotUntyped := s.RootModule().Outputs["key_pem"].Value
got, ok := gotUntyped.(string)
if !ok {
return fmt.Errorf("output for \"public_key_openssh\" is not a string")

View File

@ -415,7 +415,7 @@ func outputsAsString(state *terraform.State, schema []*config.Output, includeHea
}
v := outputs[k]
switch typedV := v.(type) {
switch typedV := v.Value.(type) {
case string:
outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, typedV))
case []interface{}:

View File

@ -116,6 +116,7 @@ func testReadPlan(t *testing.T, path string) *terraform.Plan {
// testState returns a test State structure that we use for a lot of tests.
func testState() *terraform.State {
return &terraform.State{
Version: 2,
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},

View File

@ -95,7 +95,7 @@ func (c *OutputCommand) Run(args []string) int {
return 1
}
switch output := v.(type) {
switch output := v.Value.(type) {
case string:
c.Ui.Output(output)
return 0
@ -137,7 +137,7 @@ func (c *OutputCommand) Run(args []string) int {
return 1
}
default:
panic(fmt.Errorf("Unknown output type: %T", output))
panic(fmt.Errorf("Unknown output type: %T", v.Value.(string)))
}
return 0

View File

@ -16,8 +16,11 @@ func TestOutput(t *testing.T) {
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]interface{}{
"foo": "bar",
Outputs: map[string]*terraform.OutputState{
"foo": &terraform.OutputState{
Value: "bar",
Type: "string",
},
},
},
},
@ -52,14 +55,20 @@ func TestModuleOutput(t *testing.T) {
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]interface{}{
"foo": "bar",
Outputs: map[string]*terraform.OutputState{
"foo": &terraform.OutputState{
Value: "bar",
Type: "string",
},
},
},
&terraform.ModuleState{
Path: []string{"root", "my_module"},
Outputs: map[string]interface{}{
"blah": "tastatur",
Outputs: map[string]*terraform.OutputState{
"blah": &terraform.OutputState{
Value: "tastatur",
Type: "string",
},
},
},
},
@ -96,8 +105,11 @@ func TestMissingModuleOutput(t *testing.T) {
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]interface{}{
"foo": "bar",
Outputs: map[string]*terraform.OutputState{
"foo": &terraform.OutputState{
Value: "bar",
Type: "string",
},
},
},
},
@ -129,8 +141,11 @@ func TestOutput_badVar(t *testing.T) {
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]interface{}{
"foo": "bar",
Outputs: map[string]*terraform.OutputState{
"foo": &terraform.OutputState{
Value: "bar",
Type: "string",
},
},
},
},
@ -160,9 +175,15 @@ func TestOutput_blank(t *testing.T) {
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]interface{}{
"foo": "bar",
"name": "john-doe",
Outputs: map[string]*terraform.OutputState{
"foo": &terraform.OutputState{
Value: "bar",
Type: "string",
},
"name": &terraform.OutputState{
Value: "john-doe",
Type: "string",
},
},
},
},
@ -253,7 +274,7 @@ func TestOutput_noVars(t *testing.T) {
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]interface{}{},
Outputs: map[string]*terraform.OutputState{},
},
},
}
@ -282,8 +303,11 @@ func TestOutput_stateDefault(t *testing.T) {
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]interface{}{
"foo": "bar",
Outputs: map[string]*terraform.OutputState{
"foo": &terraform.OutputState{
Value: "bar",
Type: "string",
},
},
},
},

View File

@ -565,7 +565,7 @@ func TestCheckOutput(name, value string) TestCheckFunc {
return fmt.Errorf("Not found: %s", name)
}
if rs != value {
if rs.Value != value {
return fmt.Errorf(
"Output '%s': expected %#v, got %#v",
name,

View File

@ -322,6 +322,7 @@ func (c *AtlasClient) handleConflict(msg string, state []byte) error {
var buf bytes.Buffer
if err := terraform.WriteState(proposedState, &buf); err != nil {
return conflictHandlingError(err)
}
return c.Put(buf.Bytes())
} else {

View File

@ -87,6 +87,7 @@ func TestAtlasClient_NoConflict(t *testing.T) {
if err := terraform.WriteState(state, &stateJson); err != nil {
t.Fatalf("err: %s", err)
}
if err := client.Put(stateJson.Bytes()); err != nil {
t.Fatalf("err: %s", err)
}
@ -111,7 +112,11 @@ func TestAtlasClient_LegitimateConflict(t *testing.T) {
}
// Changing the state but not the serial. Should generate a conflict.
state.RootModule().Outputs["drift"] = "happens"
state.RootModule().Outputs["drift"] = &terraform.OutputState{
Type: "string",
Sensitive: false,
Value: "happens",
}
var stateJson bytes.Buffer
if err := terraform.WriteState(state, &stateJson); err != nil {
@ -255,7 +260,11 @@ var testStateModuleOrderChange = []byte(
"grandchild"
],
"outputs": {
"foo": "bar2"
"foo": {
"sensitive": false,
"type": "string",
"value": "bar"
}
},
"resources": null
},
@ -266,7 +275,11 @@ var testStateModuleOrderChange = []byte(
"grandchild"
],
"outputs": {
"foo": "bar1"
"foo": {
"sensitive": false,
"type": "string",
"value": "bar"
}
},
"resources": null
}
@ -284,7 +297,11 @@ var testStateSimple = []byte(
"root"
],
"outputs": {
"foo": "bar"
"foo": {
"sensitive": false,
"type": "string",
"value": "bar"
}
},
"resources": null
}

View File

@ -36,8 +36,12 @@ func TestState(t *testing.T, s interface{}) {
if ws, ok := s.(StateWriter); ok {
current.Modules = append(current.Modules, &terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]interface{}{
"bar": "baz",
Outputs: map[string]*terraform.OutputState{
"bar": &terraform.OutputState{
Type: "string",
Sensitive: false,
Value: "baz",
},
},
})
@ -93,8 +97,14 @@ func TestState(t *testing.T, s interface{}) {
current = &currentCopy
current.Modules = []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root", "somewhere"},
Outputs: map[string]interface{}{"serialCheck": "true"},
Path: []string{"root", "somewhere"},
Outputs: map[string]*terraform.OutputState{
"serialCheck": &terraform.OutputState{
Type: "string",
Sensitive: false,
Value: "true",
},
},
},
}
if err := writer.WriteState(current); err != nil {
@ -123,8 +133,12 @@ func TestStateInitial() *terraform.State {
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root", "child"},
Outputs: map[string]interface{}{
"foo": "bar",
Outputs: map[string]*terraform.OutputState{
"foo": &terraform.OutputState{
Type: "string",
Sensitive: false,
Value: "bar",
},
},
},
},

View File

@ -1008,8 +1008,12 @@ func TestContext2Apply_moduleDestroyOrder(t *testing.T) {
},
},
},
Outputs: map[string]interface{}{
"a_output": "a",
Outputs: map[string]*OutputState{
"a_output": &OutputState{
Type: "string",
Sensitive: false,
Value: "a",
},
},
},
},
@ -1408,7 +1412,7 @@ func TestContext2Apply_multiVar(t *testing.T) {
actual := state.RootModule().Outputs["output"]
expected := "bar0,bar1,bar2"
if actual != expected {
if actual.Value != expected {
t.Fatalf("bad: \n%s", actual)
}
@ -1436,7 +1440,7 @@ func TestContext2Apply_multiVar(t *testing.T) {
actual := state.RootModule().Outputs["output"]
expected := "bar0"
if actual != expected {
if actual.Value != expected {
t.Fatalf("bad: \n%s", actual)
}
}
@ -1477,9 +1481,17 @@ func TestContext2Apply_outputOrphan(t *testing.T) {
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Outputs: map[string]interface{}{
"foo": "bar",
"bar": "baz",
Outputs: map[string]*OutputState{
"foo": &OutputState{
Type: "string",
Sensitive: false,
Value: "bar",
},
"bar": &OutputState{
Type: "string",
Sensitive: false,
Value: "baz",
},
},
},
},

View File

@ -452,8 +452,12 @@ func TestContext2Refresh_output(t *testing.T) {
},
},
Outputs: map[string]interface{}{
"foo": "foo",
Outputs: map[string]*OutputState{
"foo": &OutputState{
Type: "string",
Sensitive: false,
Value: "foo",
},
},
},
},
@ -738,9 +742,15 @@ func TestContext2Refresh_orphanModule(t *testing.T) {
},
},
},
Outputs: map[string]interface{}{
"id": "i-bcd234",
"grandchild_id": "i-cde345",
Outputs: map[string]*OutputState{
"id": &OutputState{
Value: "i-bcd234",
Type: "string",
},
"grandchild_id": &OutputState{
Value: "i-cde345",
Type: "string",
},
},
},
&ModuleState{
@ -752,8 +762,11 @@ func TestContext2Refresh_orphanModule(t *testing.T) {
},
},
},
Outputs: map[string]interface{}{
"id": "i-cde345",
Outputs: map[string]*OutputState{
"id": &OutputState{
Value: "i-cde345",
Type: "string",
},
},
},
},

View File

@ -38,8 +38,9 @@ func (n *EvalDeleteOutput) Eval(ctx EvalContext) (interface{}, error) {
// EvalWriteOutput is an EvalNode implementation that writes the output
// for the given name to the current state.
type EvalWriteOutput struct {
Name string
Value *config.RawConfig
Name string
Sensitive bool
Value *config.RawConfig
}
// TODO: test
@ -80,11 +81,23 @@ func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) {
switch valueTyped := valueRaw.(type) {
case string:
mod.Outputs[n.Name] = valueTyped
mod.Outputs[n.Name] = &OutputState{
Type: "string",
Sensitive: n.Sensitive,
Value: valueTyped,
}
case []interface{}:
mod.Outputs[n.Name] = valueTyped
mod.Outputs[n.Name] = &OutputState{
Type: "list",
Sensitive: n.Sensitive,
Value: valueTyped,
}
case map[string]interface{}:
mod.Outputs[n.Name] = valueTyped
mod.Outputs[n.Name] = &OutputState{
Type: "map",
Sensitive: n.Sensitive,
Value: valueTyped,
}
default:
return nil, fmt.Errorf("output %s is not a valid type (%T)\n", n.Name, valueTyped)
}

View File

@ -48,8 +48,9 @@ func (n *GraphNodeConfigOutput) EvalTree() EvalNode {
Node: &EvalSequence{
Nodes: []EvalNode{
&EvalWriteOutput{
Name: n.Output.Name,
Value: n.Output.RawConfig,
Name: n.Output.Name,
Sensitive: n.Output.Sensitive,
Value: n.Output.RawConfig,
},
},
},

View File

@ -161,8 +161,8 @@ func (i *Interpolater) valueModuleVar(
result[n] = unknownVariable()
} else {
// Get the value from the outputs
if value, ok := mod.Outputs[v.Field]; ok {
output, err := hil.InterfaceToVariable(value)
if outputState, ok := mod.Outputs[v.Field]; ok {
output, err := hil.InterfaceToVariable(outputState.Value)
if err != nil {
return err
}

View File

@ -68,8 +68,11 @@ func TestInterpolater_moduleVariable(t *testing.T) {
},
&ModuleState{
Path: []string{RootModuleName, "child"},
Outputs: map[string]interface{}{
"foo": "bar",
Outputs: map[string]*OutputState{
"foo": &OutputState{
Type: "string",
Value: "bar",
},
},
},
},

View File

@ -6,7 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"io/ioutil"
"reflect"
"sort"
"strconv"
@ -14,6 +14,7 @@ import (
"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/config"
"github.com/mitchellh/copystructure"
)
const (
@ -547,6 +548,69 @@ func (r *RemoteState) GoString() string {
return fmt.Sprintf("*%#v", *r)
}
// OutputState is used to track the state relevant to a single output.
type OutputState struct {
// Sensitive describes whether the output is considered sensitive,
// which may lead to masking the value on screen in some cases.
Sensitive bool `json:"sensitive"`
// Type describes the structure of Value. Valid values are "string",
// "map" and "list"
Type string `json:"type"`
// Value contains the value of the output, in the structure described
// by the Type field.
Value interface{} `json:"value"`
}
func (s *OutputState) String() string {
// This is a v0.6.x implementation only
return fmt.Sprintf("%s", s.Value.(string))
}
// Equal compares two OutputState structures for equality. nil values are
// considered equal.
func (s *OutputState) Equal(other *OutputState) bool {
if s == nil && other == nil {
return true
}
if s == nil || other == nil {
return false
}
if s.Type != other.Type {
return false
}
if s.Sensitive != other.Sensitive {
return false
}
if !reflect.DeepEqual(s.Value, other.Value) {
return false
}
return true
}
func (s *OutputState) deepcopy() *OutputState {
if s == nil {
return nil
}
valueCopy, err := copystructure.Copy(s.Value)
if err != nil {
panic(fmt.Errorf("Error copying output value: %s", err))
}
n := &OutputState{
Type: s.Type,
Sensitive: s.Sensitive,
Value: valueCopy,
}
return n
}
// ModuleState is used to track all the state relevant to a single
// module. Previous to Terraform 0.3, all state belonged to the "root"
// module.
@ -558,7 +622,7 @@ type ModuleState struct {
// Outputs declared by the module and maintained for each module
// even though only the root module technically needs to be kept.
// This allows operators to inspect values at the boundaries.
Outputs map[string]interface{} `json:"outputs"`
Outputs map[string]*OutputState `json:"outputs"`
// Resources is a mapping of the logically named resource to
// the state of the resource. Each resource may actually have
@ -593,7 +657,7 @@ func (m *ModuleState) Equal(other *ModuleState) bool {
return false
}
for k, v := range m.Outputs {
if !reflect.DeepEqual(other.Outputs[k], v) {
if !other.Outputs[k].Equal(v) {
return false
}
}
@ -683,7 +747,7 @@ func (m *ModuleState) View(id string) *ModuleState {
func (m *ModuleState) init() {
if m.Outputs == nil {
m.Outputs = make(map[string]interface{})
m.Outputs = make(map[string]*OutputState)
}
if m.Resources == nil {
m.Resources = make(map[string]*ResourceState)
@ -696,14 +760,14 @@ func (m *ModuleState) deepcopy() *ModuleState {
}
n := &ModuleState{
Path: make([]string, len(m.Path)),
Outputs: make(map[string]interface{}, len(m.Outputs)),
Outputs: make(map[string]*OutputState, len(m.Outputs)),
Resources: make(map[string]*ResourceState, len(m.Resources)),
Dependencies: make([]string, len(m.Dependencies)),
}
copy(n.Path, m.Path)
copy(n.Dependencies, m.Dependencies)
for k, v := range m.Outputs {
n.Outputs[k] = v
n.Outputs[k] = v.deepcopy()
}
for k, v := range m.Resources {
n.Resources[k] = v.deepcopy()
@ -722,7 +786,7 @@ func (m *ModuleState) prune() {
}
for k, v := range m.Outputs {
if v == config.UnknownVariableValue {
if v.Value == config.UnknownVariableValue {
delete(m.Outputs, k)
}
}
@ -823,7 +887,7 @@ func (m *ModuleState) String() string {
for _, k := range ks {
v := m.Outputs[k]
switch vTyped := v.(type) {
switch vTyped := v.Value.(type) {
case string:
buf.WriteString(fmt.Sprintf("%s = %s\n", k, vTyped))
case []interface{}:
@ -1386,6 +1450,10 @@ func (e *EphemeralState) DeepCopy() *EphemeralState {
return n
}
type jsonStateVersionIdentifier struct {
Version int `json:"version"`
}
// ReadState reads a state structure out of a reader in the format that
// was written by WriteState.
func ReadState(src io.Reader) (*State, error) {
@ -1402,14 +1470,59 @@ func ReadState(src io.Reader) (*State, error) {
if err != nil {
return nil, err
}
return upgradeV0State(old)
return old.upgrade()
}
// Otherwise, must be V2 or V3 - V2 reads as V3 however so we need take
// no special action here - new state will be written as V3.
dec := json.NewDecoder(buf)
// If we are JSON we buffer the whole thing in memory so we can read it twice.
// This is suboptimal, but will work for now.
jsonBytes, err := ioutil.ReadAll(buf)
if err != nil {
return nil, fmt.Errorf("Reading state file failed: %v", err)
}
versionIdentifier := &jsonStateVersionIdentifier{}
if err := json.Unmarshal(jsonBytes, versionIdentifier); err != nil {
return nil, fmt.Errorf("Decoding state file version failed: %v", err)
}
switch versionIdentifier.Version {
case 0:
return nil, fmt.Errorf("State version 0 is not supported as JSON.")
case 1:
old, err := ReadStateV1(jsonBytes)
if err != nil {
return nil, err
}
return old.upgrade()
case 2:
state, err := ReadStateV2(jsonBytes)
if err != nil {
return nil, err
}
return state, nil
default:
return nil, fmt.Errorf("State version %d not supported, please update.",
versionIdentifier.Version)
}
}
func ReadStateV1(jsonBytes []byte) (*stateV1, error) {
state := &stateV1{}
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)
}
return state, nil
}
func ReadStateV2(jsonBytes []byte) (*State, error) {
state := &State{}
if err := dec.Decode(state); err != nil {
if err := json.Unmarshal(jsonBytes, state); err != nil {
return nil, fmt.Errorf("Decoding state file failed: %v", err)
}
@ -1477,56 +1590,6 @@ func WriteState(d *State, dst io.Writer) error {
return nil
}
// upgradeV0State is used to upgrade a V0 state representation
// into a proper State representation.
func upgradeV0State(old *StateV0) (*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]interface{}, len(old.Outputs))
for key, value := range old.Outputs {
oldOutputs[key] = 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
instance := &InstanceState{
ID: rs.ID,
Attributes: rs.Attributes,
}
// Check if this is the primary or tainted instance
if _, ok := old.Tainted[id]; ok {
newRs.Tainted = append(newRs.Tainted, instance)
} else {
newRs.Primary = instance
}
// 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
}
// moduleStateSort implements sort.Interface to sort module states
type moduleStateSort []*ModuleState

View File

@ -113,8 +113,12 @@ func TestStateAdd(t *testing.T) {
"module.foo",
&ModuleState{
Path: rootModulePath,
Outputs: map[string]interface{}{
"foo": "bar",
Outputs: map[string]*OutputState{
"foo": &OutputState{
Type: "string",
Sensitive: false,
Value: "bar",
},
},
Dependencies: []string{"foo"},
Resources: map[string]*ResourceState{
@ -139,8 +143,12 @@ func TestStateAdd(t *testing.T) {
Modules: []*ModuleState{
&ModuleState{
Path: []string{"root", "foo"},
Outputs: map[string]interface{}{
"foo": "bar",
Outputs: map[string]*OutputState{
"foo": &OutputState{
Type: "string",
Sensitive: false,
Value: "bar",
},
},
Dependencies: []string{"foo"},
Resources: map[string]*ResourceState{

View File

@ -7,6 +7,7 @@ import (
"strings"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/terraform/config"
)
@ -81,12 +82,10 @@ func TestStateOutputTypeRoundTrip(t *testing.T) {
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Outputs: map[string]interface{}{
"string_output": "String Value",
"list_output": []interface{}{"List", "Value"},
"map_output": map[string]interface{}{
"key1": "Map",
"key2": "Value",
Outputs: map[string]*OutputState{
"string_output": &OutputState{
Value: "String Value",
Type: "string",
},
},
},
@ -1221,6 +1220,35 @@ func TestReadUpgradeStateV1toV2(t *testing.T) {
}
}
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{
@ -1241,7 +1269,7 @@ func TestReadUpgradeState(t *testing.T) {
t.Fatalf("err: %s", err)
}
upgraded, err := upgradeV0State(state)
upgraded, err := state.upgrade()
if err != nil {
t.Fatalf("err: %s", err)
}
@ -1329,6 +1357,7 @@ func TestReadStateNewVersion(t *testing.T) {
func TestReadStateTFVersion(t *testing.T) {
type tfVersion struct {
Version int `json:"version"`
TFVersion string `json:"terraform_version"`
}
@ -1355,7 +1384,10 @@ func TestReadStateTFVersion(t *testing.T) {
}
for _, tc := range cases {
buf, err := json.Marshal(&tfVersion{tc.Written})
buf, err := json.Marshal(&tfVersion{
Version: 2,
TFVersion: tc.Written,
})
if err != nil {
t.Fatalf("err: %v", err)
}
@ -1443,7 +1475,7 @@ func TestUpgradeV0State(t *testing.T) {
"bar": struct{}{},
},
}
state, err := upgradeV0State(old)
state, err := old.upgrade()
if err != nil {
t.Fatalf("err: %v", err)
}
@ -1456,7 +1488,7 @@ func TestUpgradeV0State(t *testing.T) {
if len(root.Outputs) != 1 {
t.Fatalf("bad outputs: %v", root.Outputs)
}
if root.Outputs["ip"] != "127.0.0.1" {
if root.Outputs["ip"].Value != "127.0.0.1" {
t.Fatalf("bad outputs: %v", root.Outputs)
}
@ -1588,3 +1620,37 @@ const testV1State = `{
]
}
`
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

@ -11,6 +11,7 @@ import (
"sync"
"github.com/hashicorp/terraform/config"
"log"
)
// The format byte is prefixed into the state file format so that we have
@ -311,3 +312,57 @@ func ReadStateV0(src io.Reader) (*StateV0, error) {
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
instance := &InstanceState{
ID: rs.ID,
Attributes: rs.Attributes,
}
// Check if this is the primary or tainted instance
if _, ok := old.Tainted[id]; ok {
newRs.Tainted = append(newRs.Tainted, instance)
} else {
newRs.Primary = instance
}
// 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
}

334
terraform/state_v1.go Normal file
View File

@ -0,0 +1,334 @@
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.
type stateV1 struct {
// Version is the protocol version. "1" for a StateV1.
Version int `json:"version"`
// Serial is incremented on any operation that modifies
// the State file. It is used to detect potentially conflicting
// updates.
Serial int64 `json:"serial"`
// Remote is used to track the metadata required to
// pull and push state files from a remote storage endpoint.
Remote *remoteStateV1 `json:"remote,omitempty"`
// Modules contains all the modules in a breadth-first order
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"`
// Config is used to store arbitrary configuration that
// is type specific
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
Path []string `json:"path"`
// Outputs declared by the module and maintained for each module
// even though only the root module technically needs to be kept.
// This allows operators to inspect values at the boundaries.
Outputs map[string]string `json:"outputs"`
// Resources is a mapping of the logically named resource to
// the state of the resource. Each resource may actually have
// N instances underneath, although a user only needs to think
// about the 1:1 case.
Resources map[string]*resourceStateV1 `json:"resources"`
// Dependencies are a list of things that this module relies on
// existing to remain intact. For example: an module may depend
// on a VPC ID given by an aws_vpc resource.
//
// Terraform uses this information to build valid destruction
// orders and to warn the user if they're destroying a module 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 []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
// this value, it won't be persisted.
Type string `json:"type"`
// 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 []string `json:"depends_on,omitempty"`
// Primary is the current active instance for this resource.
// It can be replaced but only after a successful creation.
// This is the instances on which providers will act.
Primary *instanceStateV1 `json:"primary"`
// Tainted is used to track any underlying instances that
// have been created but are in a bad or unknown state and
// need to be cleaned up subsequently. In the
// standard case, there is only at most a single instance.
// However, in pathological cases, it is possible for the number
// of instances to accumulate.
Tainted []*instanceStateV1 `json:"tainted,omitempty"`
// Deposed is used in the mechanics of CreateBeforeDestroy: the existing
// Primary is Deposed to get it out of the way for the replacement Primary to
// be created by Apply. If the replacement Primary creates successfully, the
// Deposed instance is cleaned up. If there were problems creating the
// replacement, the instance remains in the Deposed list so it can be
// destroyed in a future run. Functionally, Deposed instances are very
// similar to Tainted instances in that Terraform is only tracking them in
// order to remember to destroy them.
Deposed []*instanceStateV1 `json:"deposed,omitempty"`
// Provider is used when a resource is connected to a provider with an alias.
// If this string is empty, the resource is connected to the default provider,
// e.g. "aws_instance" goes with the "aws" provider.
// If the resource block contained a "provider" key, that value will be set here.
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)
}
tainted := make([]*InstanceState, len(old.Tainted))
for i, v := range old.Tainted {
upgraded, err := v.upgrade()
if err != nil {
return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err)
}
tainted[i] = upgraded
}
if len(tainted) == 0 {
tainted = nil
}
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,
Tainted: tainted,
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.
ID string `json:"id"`
// 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 `json:"attributes,omitempty"`
// Ephemeral is used to store any state associated with this instance
// that is necessary for the Terraform run to complete, but is not
// persisted to a state file.
Ephemeral ephemeralStateV1 `json:"-"`
// Meta is a simple K/V map that is persisted to the State but otherwise
// ignored by Terraform core. It's meant to be used for accounting by
// external client code.
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

@ -11,9 +11,15 @@ func TestAddOutputOrphanTransformer(t *testing.T) {
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Outputs: map[string]interface{}{
"foo": "bar",
"bar": "baz",
Outputs: map[string]*OutputState{
"foo": &OutputState{
Value: "bar",
Type: "string",
},
"bar": &OutputState{
Value: "baz",
Type: "string",
},
},
},
},