diff --git a/config/raw_config.go b/config/raw_config.go index df76cb1ac..12a43c54b 100644 --- a/config/raw_config.go +++ b/config/raw_config.go @@ -1,6 +1,9 @@ package config import ( + "bytes" + "encoding/gob" + "github.com/mitchellh/copystructure" "github.com/mitchellh/reflectwalk" ) @@ -31,16 +34,12 @@ type RawConfig struct { // NewRawConfig creates a new RawConfig structure and populates the // publicly readable struct fields. func NewRawConfig(raw map[string]interface{}) (*RawConfig, error) { - walker := new(variableDetectWalker) - if err := reflectwalk.Walk(raw, walker); err != nil { + result := &RawConfig{Raw: raw} + if err := result.init(); err != nil { return nil, err } - return &RawConfig{ - Raw: raw, - Variables: walker.Variables, - config: raw, - }, nil + return result, nil } // Config returns the entire configuration with the variables @@ -82,8 +81,42 @@ func (r *RawConfig) Interpolate(vs map[string]string) error { return nil } +func (r *RawConfig) init() error { + walker := new(variableDetectWalker) + if err := reflectwalk.Walk(r.Raw, walker); err != nil { + return err + } + + r.Variables = walker.Variables + r.config = r.Raw + return nil +} + // UnknownKeys returns the keys of the configuration that are unknown // because they had interpolated variables that must be computed. func (r *RawConfig) UnknownKeys() []string { return r.unknownKeys } + +// See GobEncode +func (r *RawConfig) GobDecode(b []byte) error { + err := gob.NewDecoder(bytes.NewReader(b)).Decode(&r.Raw) + if err != nil { + return err + } + + return r.init() +} + +// GobEncode is a custom Gob encoder to use so that we only include the +// raw configuration. Interpolated variables and such are lost and the +// tree of interpolated variables is recomputed on decode, since it is +// referentially transparent. +func (r *RawConfig) GobEncode() ([]byte, error) { + var buf bytes.Buffer + if err := gob.NewEncoder(&buf).Encode(r.Raw); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/config/raw_config_test.go b/config/raw_config_test.go index da3353a76..eefec193f 100644 --- a/config/raw_config_test.go +++ b/config/raw_config_test.go @@ -1,6 +1,7 @@ package config import ( + "encoding/gob" "reflect" "testing" ) @@ -119,3 +120,8 @@ func TestRawConfig_unknown(t *testing.T) { t.Fatalf("bad: %#v", rc.UnknownKeys()) } } + +func TestRawConfig_implGob(t *testing.T) { + var _ gob.GobDecoder = new(RawConfig) + var _ gob.GobEncoder = new(RawConfig) +} diff --git a/terraform/plan.go b/terraform/plan.go new file mode 100644 index 000000000..b7bc9f159 --- /dev/null +++ b/terraform/plan.go @@ -0,0 +1,63 @@ +package terraform + +import ( + "encoding/gob" + "errors" + "fmt" + "io" + + "github.com/hashicorp/terraform/config" +) + +// Plan represents a single Terraform execution plan, which contains +// all the information necessary to make an infrastructure change. +type Plan struct { + Config *config.Config + Diff *Diff + State *State + Vars map[string]string +} + +// The format byte is prefixed into the plan file format so that we have +// the ability in the future to change the file format if we want for any +// reason. +const planFormatByte byte = 1 + +// ReadPlan reads a plan structure out of a reader in the format that +// was written by WritePlan. +func ReadPlan(src io.Reader) (*Plan, error) { + var result *Plan + + 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 plan version byte") + } + + if formatByte[0] != planFormatByte { + return nil, fmt.Errorf("unknown plan file version: %d", formatByte[0]) + } + + dec := gob.NewDecoder(src) + if err := dec.Decode(&result); err != nil { + return nil, err + } + + return result, nil +} + +// WritePlan writes a plan somewhere in a binary format. +func WritePlan(d *Plan, dst io.Writer) error { + n, err := dst.Write([]byte{planFormatByte}) + if err != nil { + return err + } + if n != 1 { + return errors.New("failed to write plan version byte") + } + + return gob.NewEncoder(dst).Encode(d) +} diff --git a/terraform/plan_test.go b/terraform/plan_test.go new file mode 100644 index 000000000..09fe9fe28 --- /dev/null +++ b/terraform/plan_test.go @@ -0,0 +1,60 @@ +package terraform + +import ( + "bytes" + "reflect" + + "testing" +) + +func TestReadWritePlan(t *testing.T) { + tf := testTerraform(t, "new-good") + plan := &Plan{ + Config: tf.config, + Diff: &Diff{ + Resources: map[string]*ResourceDiff{ + "nodeA": &ResourceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "foo": &ResourceAttrDiff{ + Old: "foo", + New: "bar", + }, + "bar": &ResourceAttrDiff{ + Old: "foo", + NewComputed: true, + }, + "longfoo": &ResourceAttrDiff{ + Old: "foo", + New: "bar", + RequiresNew: true, + }, + }, + }, + }, + }, + State: &State{ + Resources: map[string]*ResourceState{ + "foo": &ResourceState{ + ID: "bar", + }, + }, + }, + Vars: map[string]string{ + "foo": "bar", + }, + } + + buf := new(bytes.Buffer) + if err := WritePlan(plan, buf); err != nil { + t.Fatalf("err: %s", err) + } + + actual, err := ReadPlan(buf) + if err != nil { + t.Fatalf("err: %s", err) + } + + if !reflect.DeepEqual(actual, plan) { + t.Fatalf("bad: %#v", actual) + } +}