mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
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:
parent
4571a16b15
commit
1b673746fd
@ -106,6 +106,7 @@ type Context struct {
|
|||||||
l sync.Mutex // Lock acquired during any task
|
l sync.Mutex // Lock acquired during any task
|
||||||
parallelSem Semaphore
|
parallelSem Semaphore
|
||||||
providerInputConfig map[string]map[string]interface{}
|
providerInputConfig map[string]map[string]interface{}
|
||||||
|
providerSHA256s map[string][]byte
|
||||||
runLock sync.Mutex
|
runLock sync.Mutex
|
||||||
runCond *sync.Cond
|
runCond *sync.Cond
|
||||||
runContext context.Context
|
runContext context.Context
|
||||||
@ -218,6 +219,7 @@ func NewContext(opts *ContextOpts) (*Context, error) {
|
|||||||
|
|
||||||
parallelSem: NewSemaphore(par),
|
parallelSem: NewSemaphore(par),
|
||||||
providerInputConfig: make(map[string]map[string]interface{}),
|
providerInputConfig: make(map[string]map[string]interface{}),
|
||||||
|
providerSHA256s: opts.ProviderSHA256s,
|
||||||
sh: sh,
|
sh: sh,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -529,6 +531,9 @@ func (c *Context) Plan() (*Plan, error) {
|
|||||||
Vars: c.variables,
|
Vars: c.variables,
|
||||||
State: c.state,
|
State: c.state,
|
||||||
Targets: c.targets,
|
Targets: c.targets,
|
||||||
|
|
||||||
|
TerraformVersion: VersionString(),
|
||||||
|
ProviderSHA256s: c.providerSHA256s,
|
||||||
}
|
}
|
||||||
|
|
||||||
var operation walkOperation
|
var operation walkOperation
|
||||||
|
@ -22,6 +22,9 @@ func TestContext2Plan_basic(t *testing.T) {
|
|||||||
"aws": testProviderFuncFixed(p),
|
"aws": testProviderFuncFixed(p),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ProviderSHA256s: map[string][]byte{
|
||||||
|
"aws": []byte("placeholder"),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
plan, err := ctx.Plan()
|
plan, err := ctx.Plan()
|
||||||
@ -33,6 +36,10 @@ func TestContext2Plan_basic(t *testing.T) {
|
|||||||
t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources)
|
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())
|
actual := strings.TrimSpace(plan.String())
|
||||||
expected := strings.TrimSpace(testTerraformPlanStr)
|
expected := strings.TrimSpace(testTerraformPlanStr)
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
|
@ -31,6 +31,9 @@ type Plan struct {
|
|||||||
Vars map[string]interface{}
|
Vars map[string]interface{}
|
||||||
Targets []string
|
Targets []string
|
||||||
|
|
||||||
|
TerraformVersion string
|
||||||
|
ProviderSHA256s map[string][]byte
|
||||||
|
|
||||||
// Backend is the backend that this plan should use and store data with.
|
// Backend is the backend that this plan should use and store data with.
|
||||||
Backend *BackendState
|
Backend *BackendState
|
||||||
|
|
||||||
@ -42,17 +45,40 @@ type Plan struct {
|
|||||||
// The following fields in opts are overridden by the plan: Config,
|
// The following fields in opts are overridden by the plan: Config,
|
||||||
// Diff, State, Variables.
|
// Diff, State, Variables.
|
||||||
func (p *Plan) Context(opts *ContextOpts) (*Context, error) {
|
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.Diff = p.Diff
|
||||||
opts.Module = p.Module
|
opts.Module = p.Module
|
||||||
opts.State = p.State
|
opts.State = p.State
|
||||||
opts.Targets = p.Targets
|
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{})
|
opts.Variables = make(map[string]interface{})
|
||||||
for k, v := range p.Vars {
|
for k, v := range p.Vars {
|
||||||
opts.Variables[k] = v
|
opts.Variables[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewContext(opts)
|
return opts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plan) String() string {
|
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
|
// the ability in the future to change the file format if we want for any
|
||||||
// reason.
|
// reason.
|
||||||
const planFormatMagic = "tfplan"
|
const planFormatMagic = "tfplan"
|
||||||
const planFormatVersion byte = 1
|
const planFormatVersion byte = 2
|
||||||
|
|
||||||
// ReadPlan reads a plan structure out of a reader in the format that
|
// ReadPlan reads a plan structure out of a reader in the format that
|
||||||
// was written by WritePlan.
|
// was written by WritePlan.
|
||||||
|
@ -2,11 +2,54 @@ package terraform
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"testing"
|
"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) {
|
func TestReadWritePlan(t *testing.T) {
|
||||||
plan := &Plan{
|
plan := &Plan{
|
||||||
Module: testModule(t, "new-good"),
|
Module: testModule(t, "new-good"),
|
||||||
|
Loading…
Reference in New Issue
Block a user