Merge pull request #6752 from hashicorp/f-output-state-0.7

core: Forward port OutputState to 0.7
This commit is contained in:
James Nugent 2016-05-18 13:39:31 -05:00
commit 17442abff2
26 changed files with 787 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,8 @@ func (c *OutputCommand) Run(args []string) int {
return 1
}
default:
panic(fmt.Errorf("Unknown output type: %T", output))
c.Ui.Error(fmt.Sprintf("Unknown output type: %T", v.Type))
return 1
}
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",
},
},
},
},