From 1b673746fd0d7a0f97127bf59d1e85b9e99afd62 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Mon, 5 Jun 2017 17:08:02 -0700 Subject: [PATCH] core: don't allow core or providers to change between plan and apply The information stored in a plan is tightly coupled to the Terraform core and provider plugins that were used to create it, since we have no mechanism to "upgrade" a plan to reflect schema changes and so mismatching versions are likely to lead to the "diffs didn't match during apply" error. To allow us to catch this early and return an error message that _doesn't_ say it's a bug in Terraform, we'll remember the Terraform version and plugin binaries that created a particular plan and then require that those match when loading the plan in order to apply it. The planFormatVersion is increased here so that plan files produced by earlier Terraform versions _without_ this information won't be accepted by this new version, and also that older versions won't try to process plans created by newer versions. --- terraform/context.go | 5 ++++ terraform/context_plan_test.go | 7 ++++++ terraform/plan.go | 30 +++++++++++++++++++++-- terraform/plan_test.go | 45 +++++++++++++++++++++++++++++++++- 4 files changed, 84 insertions(+), 3 deletions(-) diff --git a/terraform/context.go b/terraform/context.go index 0302eba4a8..649af3a7b1 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -106,6 +106,7 @@ type Context struct { l sync.Mutex // Lock acquired during any task parallelSem Semaphore providerInputConfig map[string]map[string]interface{} + providerSHA256s map[string][]byte runLock sync.Mutex runCond *sync.Cond runContext context.Context @@ -218,6 +219,7 @@ func NewContext(opts *ContextOpts) (*Context, error) { parallelSem: NewSemaphore(par), providerInputConfig: make(map[string]map[string]interface{}), + providerSHA256s: opts.ProviderSHA256s, sh: sh, }, nil } @@ -529,6 +531,9 @@ func (c *Context) Plan() (*Plan, error) { Vars: c.variables, State: c.state, Targets: c.targets, + + TerraformVersion: VersionString(), + ProviderSHA256s: c.providerSHA256s, } var operation walkOperation diff --git a/terraform/context_plan_test.go b/terraform/context_plan_test.go index 7c231063f3..39baa426d6 100644 --- a/terraform/context_plan_test.go +++ b/terraform/context_plan_test.go @@ -22,6 +22,9 @@ func TestContext2Plan_basic(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), + ProviderSHA256s: map[string][]byte{ + "aws": []byte("placeholder"), + }, }) plan, err := ctx.Plan() @@ -33,6 +36,10 @@ func TestContext2Plan_basic(t *testing.T) { t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) } + if !reflect.DeepEqual(plan.ProviderSHA256s, ctx.providerSHA256s) { + t.Errorf("wrong ProviderSHA256s %#v; want %#v", plan.ProviderSHA256s, ctx.providerSHA256s) + } + actual := strings.TrimSpace(plan.String()) expected := strings.TrimSpace(testTerraformPlanStr) if actual != expected { diff --git a/terraform/plan.go b/terraform/plan.go index ea0884505a..31b26bb684 100644 --- a/terraform/plan.go +++ b/terraform/plan.go @@ -31,6 +31,9 @@ type Plan struct { Vars map[string]interface{} Targets []string + TerraformVersion string + ProviderSHA256s map[string][]byte + // Backend is the backend that this plan should use and store data with. Backend *BackendState @@ -42,17 +45,40 @@ type Plan struct { // The following fields in opts are overridden by the plan: Config, // Diff, State, Variables. func (p *Plan) Context(opts *ContextOpts) (*Context, error) { + var err error + opts, err = p.contextOpts(opts) + if err != nil { + return nil, err + } + return NewContext(opts) +} + +// contextOpts mutates the given base ContextOpts in place to use input +// objects obtained from the receiving plan. +func (p *Plan) contextOpts(base *ContextOpts) (*ContextOpts, error) { + opts := base + opts.Diff = p.Diff opts.Module = p.Module opts.State = p.State opts.Targets = p.Targets + opts.ProviderSHA256s = p.ProviderSHA256s + + thisVersion := VersionString() + if p.TerraformVersion != "" && p.TerraformVersion != thisVersion { + return nil, fmt.Errorf( + "plan was created with a different version of Terraform (created with %s, but running %s)", + p.TerraformVersion, thisVersion, + ) + } + opts.Variables = make(map[string]interface{}) for k, v := range p.Vars { opts.Variables[k] = v } - return NewContext(opts) + return opts, nil } func (p *Plan) String() string { @@ -86,7 +112,7 @@ func (p *Plan) init() { // the ability in the future to change the file format if we want for any // reason. const planFormatMagic = "tfplan" -const planFormatVersion byte = 1 +const planFormatVersion byte = 2 // ReadPlan reads a plan structure out of a reader in the format that // was written by WritePlan. diff --git a/terraform/plan_test.go b/terraform/plan_test.go index 9515efbaaa..4f654f6a08 100644 --- a/terraform/plan_test.go +++ b/terraform/plan_test.go @@ -2,11 +2,54 @@ package terraform import ( "bytes" + "reflect" "strings" - "testing" + + "github.com/hashicorp/terraform/config/module" ) +func TestPlanContextOpts(t *testing.T) { + plan := &Plan{ + Diff: &Diff{ + Modules: []*ModuleDiff{ + { + Path: []string{"test"}, + }, + }, + }, + Module: module.NewTree("test", nil), + State: &State{ + TFVersion: "sigil", + }, + Vars: map[string]interface{}{"foo": "bar"}, + Targets: []string{"baz"}, + + TerraformVersion: VersionString(), + ProviderSHA256s: map[string][]byte{ + "test": []byte("placeholder"), + }, + } + + got, err := plan.contextOpts(&ContextOpts{}) + if err != nil { + t.Fatalf("error creating context: %s", err) + } + + want := &ContextOpts{ + Diff: plan.Diff, + Module: plan.Module, + State: plan.State, + Variables: plan.Vars, + Targets: plan.Targets, + ProviderSHA256s: plan.ProviderSHA256s, + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("wrong result\ngot: %#v\nwant %#v", got, want) + } +} + func TestReadWritePlan(t *testing.T) { plan := &Plan{ Module: testModule(t, "new-good"),