From d2001275dc272ad1ef1ebc6d768b9937292bbd4a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jun 2014 10:33:26 -0700 Subject: [PATCH] terraform: initial Plan structure This is REALLY heavy and would be really hard to maintain any sort of compatibility with, but it is what we're going to do during dev initially (if we don't ship with it) in order to just get stuff working. --- config/raw_config.go | 47 ++++++++++++++++++++++++----- config/raw_config_test.go | 6 ++++ terraform/plan.go | 63 +++++++++++++++++++++++++++++++++++++++ terraform/plan_test.go | 60 +++++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 terraform/plan.go create mode 100644 terraform/plan_test.go 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) + } +}