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.
This commit is contained in:
Martin Atkins 2017-06-05 17:08:02 -07:00
parent 4571a16b15
commit 1b673746fd
4 changed files with 84 additions and 3 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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.

View File

@ -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"),