mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 18:01:01 -06:00
2b80df0163
We can't generate a valid plan file without a backend configuration to write into it, but it's the responsibility of the caller (the command package) to manage the backend configuration mechanism, so we require it to tell us what to write here. This feels a little strange because the backend in principle knows its own config, but in practice the backend only knows the _processed_ version of the config, not the raw configuration value that was used to configure it.
475 lines
14 KiB
Go
475 lines
14 KiB
Go
package planfile
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
|
|
"github.com/golang/protobuf/proto"
|
|
|
|
"github.com/hashicorp/terraform/addrs"
|
|
"github.com/hashicorp/terraform/plans"
|
|
"github.com/hashicorp/terraform/plans/internal/planproto"
|
|
"github.com/hashicorp/terraform/states"
|
|
"github.com/hashicorp/terraform/tfdiags"
|
|
"github.com/hashicorp/terraform/version"
|
|
)
|
|
|
|
const tfplanFormatVersion = 3
|
|
const tfplanFilename = "tfplan"
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// This file deals with the internal structure of the "tfplan" sub-file within
|
|
// the plan file format. It's all private API, wrapped by methods defined
|
|
// elsewhere. This is the only file that should import the
|
|
// ../internal/planproto package, which contains the ugly stubs generated
|
|
// by the protobuf compiler.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// readTfplan reads a protobuf-encoded description from the plan portion of
|
|
// a plan file, which is stored in a special file in the archive called
|
|
// "tfplan".
|
|
func readTfplan(r io.Reader) (*plans.Plan, error) {
|
|
src, err := ioutil.ReadAll(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var rawPlan planproto.Plan
|
|
err = proto.Unmarshal(src, &rawPlan)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse error: %s", err)
|
|
}
|
|
|
|
if rawPlan.Version != tfplanFormatVersion {
|
|
return nil, fmt.Errorf("unsupported plan file format version %d; only version %d is supported", rawPlan.Version, tfplanFormatVersion)
|
|
}
|
|
|
|
if rawPlan.TerraformVersion != version.String() {
|
|
return nil, fmt.Errorf("plan file was created by Terraform %s, but this is %s; plan files cannot be transferred between different Terraform versions", rawPlan.TerraformVersion, version.String())
|
|
}
|
|
|
|
plan := &plans.Plan{
|
|
VariableValues: map[string]plans.DynamicValue{},
|
|
Changes: &plans.Changes{
|
|
Outputs: []*plans.OutputChangeSrc{},
|
|
Resources: []*plans.ResourceInstanceChangeSrc{},
|
|
},
|
|
|
|
ProviderSHA256s: map[string][]byte{},
|
|
}
|
|
|
|
for _, rawOC := range rawPlan.OutputChanges {
|
|
name := rawOC.Name
|
|
change, err := changeFromTfplan(rawOC.Change)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid plan for output %q: %s", name, err)
|
|
}
|
|
|
|
plan.Changes.Outputs = append(plan.Changes.Outputs, &plans.OutputChangeSrc{
|
|
// All output values saved in the plan file are root module outputs,
|
|
// since we don't retain others. (They can be easily recomputed
|
|
// during apply).
|
|
Addr: addrs.OutputValue{Name: name}.Absolute(addrs.RootModuleInstance),
|
|
ChangeSrc: *change,
|
|
Sensitive: rawOC.Sensitive,
|
|
})
|
|
}
|
|
|
|
for _, rawRC := range rawPlan.ResourceChanges {
|
|
change, err := resourceChangeFromTfplan(rawRC)
|
|
if err != nil {
|
|
// errors from resourceChangeFromTfplan already include context
|
|
return nil, err
|
|
}
|
|
|
|
plan.Changes.Resources = append(plan.Changes.Resources, change)
|
|
}
|
|
|
|
for _, rawTargetAddr := range rawPlan.TargetAddrs {
|
|
target, diags := addrs.ParseTargetStr(rawTargetAddr)
|
|
if diags.HasErrors() {
|
|
return nil, fmt.Errorf("plan contains invalid target address %q: %s", target, diags.Err())
|
|
}
|
|
plan.TargetAddrs = append(plan.TargetAddrs, target.Subject)
|
|
}
|
|
|
|
for name, rawHashObj := range rawPlan.ProviderHashes {
|
|
if len(rawHashObj.Sha256) == 0 {
|
|
return nil, fmt.Errorf("no SHA256 hash for provider %q plugin", name)
|
|
}
|
|
|
|
plan.ProviderSHA256s[name] = rawHashObj.Sha256
|
|
}
|
|
|
|
for name, rawVal := range rawPlan.Variables {
|
|
val, err := valueFromTfplan(rawVal)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid value for input variable %q: %s", name, err)
|
|
}
|
|
plan.VariableValues[name] = val
|
|
}
|
|
|
|
if rawBackend := rawPlan.Backend; rawBackend == nil {
|
|
return nil, fmt.Errorf("plan file has no backend settings; backend settings are required")
|
|
} else {
|
|
config, err := valueFromTfplan(rawBackend.Config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("plan file has invalid backend configuration: %s", err)
|
|
}
|
|
plan.Backend = plans.Backend{
|
|
Type: rawBackend.Type,
|
|
Config: config,
|
|
Workspace: rawBackend.Workspace,
|
|
}
|
|
}
|
|
|
|
return plan, nil
|
|
}
|
|
|
|
func resourceChangeFromTfplan(rawChange *planproto.ResourceInstanceChange) (*plans.ResourceInstanceChangeSrc, error) {
|
|
if rawChange == nil {
|
|
// Should never happen in practice, since protobuf can't represent
|
|
// a nil value in a list.
|
|
return nil, fmt.Errorf("resource change object is absent")
|
|
}
|
|
|
|
ret := &plans.ResourceInstanceChangeSrc{}
|
|
|
|
moduleAddr := addrs.RootModuleInstance
|
|
if rawChange.ModulePath != "" {
|
|
var diags tfdiags.Diagnostics
|
|
moduleAddr, diags = addrs.ParseModuleInstanceStr(rawChange.ModulePath)
|
|
if diags.HasErrors() {
|
|
return nil, diags.Err()
|
|
}
|
|
}
|
|
|
|
providerAddr, diags := addrs.ParseAbsProviderConfigStr(rawChange.Provider)
|
|
if diags.HasErrors() {
|
|
return nil, diags.Err()
|
|
}
|
|
ret.ProviderAddr = providerAddr
|
|
|
|
var mode addrs.ResourceMode
|
|
switch rawChange.Mode {
|
|
case planproto.ResourceInstanceChange_managed:
|
|
mode = addrs.ManagedResourceMode
|
|
case planproto.ResourceInstanceChange_data:
|
|
mode = addrs.DataResourceMode
|
|
default:
|
|
return nil, fmt.Errorf("resource has invalid mode %s", rawChange.Mode)
|
|
}
|
|
|
|
typeName := rawChange.Type
|
|
name := rawChange.Name
|
|
|
|
resAddr := addrs.Resource{
|
|
Mode: mode,
|
|
Type: typeName,
|
|
Name: name,
|
|
}
|
|
|
|
var instKey addrs.InstanceKey
|
|
switch rawTk := rawChange.InstanceKey.(type) {
|
|
case nil:
|
|
case *planproto.ResourceInstanceChange_Int:
|
|
instKey = addrs.IntKey(rawTk.Int)
|
|
case *planproto.ResourceInstanceChange_Str:
|
|
instKey = addrs.StringKey(rawTk.Str)
|
|
default:
|
|
return nil, fmt.Errorf("instance of %s has invalid key type %T", resAddr.Absolute(moduleAddr), rawChange.InstanceKey)
|
|
}
|
|
|
|
ret.Addr = resAddr.Instance(instKey).Absolute(moduleAddr)
|
|
|
|
if rawChange.DeposedKey != "" {
|
|
if len(rawChange.DeposedKey) != 8 {
|
|
return nil, fmt.Errorf("deposed object for %s has invalid deposed key %q", ret.Addr, rawChange.DeposedKey)
|
|
}
|
|
ret.DeposedKey = states.DeposedKey(rawChange.DeposedKey)
|
|
}
|
|
|
|
change, err := changeFromTfplan(rawChange.Change)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid plan for resource %s: %s", ret.Addr, err)
|
|
}
|
|
|
|
ret.ChangeSrc = *change
|
|
|
|
if len(rawChange.Private) != 0 {
|
|
ret.Private = rawChange.Private
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func changeFromTfplan(rawChange *planproto.Change) (*plans.ChangeSrc, error) {
|
|
if rawChange == nil {
|
|
return nil, fmt.Errorf("change object is absent")
|
|
}
|
|
|
|
ret := &plans.ChangeSrc{}
|
|
|
|
// -1 indicates that there is no index. We'll customize these below
|
|
// depending on the change action, and then decode.
|
|
beforeIdx, afterIdx := -1, -1
|
|
|
|
switch rawChange.Action {
|
|
case planproto.Action_NOOP:
|
|
ret.Action = plans.NoOp
|
|
beforeIdx = 0
|
|
afterIdx = 0
|
|
case planproto.Action_CREATE:
|
|
ret.Action = plans.Create
|
|
afterIdx = 0
|
|
case planproto.Action_READ:
|
|
ret.Action = plans.Read
|
|
beforeIdx = 0
|
|
afterIdx = 1
|
|
case planproto.Action_UPDATE:
|
|
ret.Action = plans.Update
|
|
beforeIdx = 0
|
|
afterIdx = 1
|
|
case planproto.Action_DELETE:
|
|
ret.Action = plans.Delete
|
|
beforeIdx = 0
|
|
case planproto.Action_CREATE_THEN_DELETE:
|
|
ret.Action = plans.CreateThenDelete
|
|
beforeIdx = 0
|
|
afterIdx = 1
|
|
case planproto.Action_DELETE_THEN_CREATE:
|
|
ret.Action = plans.DeleteThenCreate
|
|
beforeIdx = 0
|
|
afterIdx = 1
|
|
default:
|
|
return nil, fmt.Errorf("invalid change action %s", rawChange.Action)
|
|
}
|
|
|
|
if beforeIdx != -1 {
|
|
if l := len(rawChange.Values); l <= beforeIdx {
|
|
return nil, fmt.Errorf("incorrect number of values (%d) for %s change", l, rawChange.Action)
|
|
}
|
|
var err error
|
|
ret.Before, err = valueFromTfplan(rawChange.Values[beforeIdx])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid \"before\" value: %s", err)
|
|
}
|
|
if ret.Before == nil {
|
|
return nil, fmt.Errorf("missing \"before\" value: %s", err)
|
|
}
|
|
}
|
|
if afterIdx != -1 {
|
|
if l := len(rawChange.Values); l <= afterIdx {
|
|
return nil, fmt.Errorf("incorrect number of values (%d) for %s change", l, rawChange.Action)
|
|
}
|
|
var err error
|
|
ret.After, err = valueFromTfplan(rawChange.Values[afterIdx])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid \"after\" value: %s", err)
|
|
}
|
|
if ret.After == nil {
|
|
return nil, fmt.Errorf("missing \"after\" value: %s", err)
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func valueFromTfplan(rawV *planproto.DynamicValue) (plans.DynamicValue, error) {
|
|
if len(rawV.Msgpack) == 0 { // len(0) because that's the default value for a "bytes" in protobuf
|
|
return nil, fmt.Errorf("dynamic value does not have msgpack serialization")
|
|
}
|
|
|
|
return plans.DynamicValue(rawV.Msgpack), nil
|
|
}
|
|
|
|
// writeTfplan serializes the given plan into the protobuf-based format used
|
|
// for the "tfplan" portion of a plan file.
|
|
func writeTfplan(plan *plans.Plan, w io.Writer) error {
|
|
if plan == nil {
|
|
return fmt.Errorf("cannot write plan file for nil plan")
|
|
}
|
|
if plan.Changes == nil {
|
|
return fmt.Errorf("cannot write plan file with nil changeset")
|
|
}
|
|
|
|
rawPlan := &planproto.Plan{
|
|
Version: tfplanFormatVersion,
|
|
TerraformVersion: version.String(),
|
|
ProviderHashes: map[string]*planproto.Hash{},
|
|
|
|
Variables: map[string]*planproto.DynamicValue{},
|
|
OutputChanges: []*planproto.OutputChange{},
|
|
ResourceChanges: []*planproto.ResourceInstanceChange{},
|
|
}
|
|
|
|
for _, oc := range plan.Changes.Outputs {
|
|
// When serializing a plan we only retain the root outputs, since
|
|
// changes to these are externally-visible side effects (e.g. via
|
|
// terraform_remote_state).
|
|
if !oc.Addr.Module.IsRoot() {
|
|
continue
|
|
}
|
|
|
|
name := oc.Addr.OutputValue.Name
|
|
|
|
// Writing outputs as cty.DynamicPseudoType forces the stored values
|
|
// to also contain dynamic type information, so we can recover the
|
|
// original type when we read the values back in readTFPlan.
|
|
protoChange, err := changeToTfplan(&oc.ChangeSrc)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot write output value %q: %s", name, err)
|
|
}
|
|
|
|
rawPlan.OutputChanges = append(rawPlan.OutputChanges, &planproto.OutputChange{
|
|
Name: name,
|
|
Change: protoChange,
|
|
Sensitive: oc.Sensitive,
|
|
})
|
|
}
|
|
|
|
for _, rc := range plan.Changes.Resources {
|
|
rawRC, err := resourceChangeToTfplan(rc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rawPlan.ResourceChanges = append(rawPlan.ResourceChanges, rawRC)
|
|
}
|
|
|
|
for _, targetAddr := range plan.TargetAddrs {
|
|
rawPlan.TargetAddrs = append(rawPlan.TargetAddrs, targetAddr.String())
|
|
}
|
|
|
|
for name, hash := range plan.ProviderSHA256s {
|
|
rawPlan.ProviderHashes[name] = &planproto.Hash{
|
|
Sha256: hash,
|
|
}
|
|
}
|
|
|
|
for name, val := range plan.VariableValues {
|
|
rawPlan.Variables[name] = valueToTfplan(val)
|
|
}
|
|
|
|
if plan.Backend.Type == "" || plan.Backend.Config == nil {
|
|
// This suggests a bug in the code that created the plan, since it
|
|
// ought to always have a backend populated, even if it's the default
|
|
// "local" backend with a local state file.
|
|
return fmt.Errorf("plan does not have a backend configuration")
|
|
}
|
|
|
|
rawPlan.Backend = &planproto.Backend{
|
|
Type: plan.Backend.Type,
|
|
Config: valueToTfplan(plan.Backend.Config),
|
|
Workspace: plan.Backend.Workspace,
|
|
}
|
|
|
|
src, err := proto.Marshal(rawPlan)
|
|
if err != nil {
|
|
return fmt.Errorf("serialization error: %s", err)
|
|
}
|
|
|
|
_, err = w.Write(src)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write plan to plan file: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func resourceChangeToTfplan(change *plans.ResourceInstanceChangeSrc) (*planproto.ResourceInstanceChange, error) {
|
|
ret := &planproto.ResourceInstanceChange{}
|
|
|
|
ret.ModulePath = change.Addr.Module.String()
|
|
|
|
relAddr := change.Addr.Resource
|
|
|
|
switch relAddr.Resource.Mode {
|
|
case addrs.ManagedResourceMode:
|
|
ret.Mode = planproto.ResourceInstanceChange_managed
|
|
case addrs.DataResourceMode:
|
|
ret.Mode = planproto.ResourceInstanceChange_data
|
|
default:
|
|
return nil, fmt.Errorf("resource %s has unsupported mode %s", relAddr, relAddr.Resource.Mode)
|
|
}
|
|
|
|
ret.Type = relAddr.Resource.Type
|
|
ret.Name = relAddr.Resource.Name
|
|
|
|
switch tk := relAddr.Key.(type) {
|
|
case nil:
|
|
// Nothing to do, then.
|
|
case addrs.IntKey:
|
|
ret.InstanceKey = &planproto.ResourceInstanceChange_Int{
|
|
Int: int64(tk),
|
|
}
|
|
case addrs.StringKey:
|
|
ret.InstanceKey = &planproto.ResourceInstanceChange_Str{
|
|
Str: string(tk),
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("resource %s has unsupported instance key type %T", relAddr, relAddr.Key)
|
|
}
|
|
|
|
ret.DeposedKey = string(change.DeposedKey)
|
|
ret.Provider = change.ProviderAddr.String()
|
|
|
|
valChange, err := changeToTfplan(&change.ChangeSrc)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to serialize resource %s change: %s", relAddr, err)
|
|
}
|
|
ret.Change = valChange
|
|
|
|
if len(change.Private) > 0 {
|
|
ret.Private = change.Private
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func changeToTfplan(change *plans.ChangeSrc) (*planproto.Change, error) {
|
|
ret := &planproto.Change{}
|
|
|
|
before := valueToTfplan(change.Before)
|
|
after := valueToTfplan(change.After)
|
|
|
|
switch change.Action {
|
|
case plans.NoOp:
|
|
ret.Action = planproto.Action_NOOP
|
|
ret.Values = []*planproto.DynamicValue{before} // before and after should be identical
|
|
case plans.Create:
|
|
ret.Action = planproto.Action_CREATE
|
|
ret.Values = []*planproto.DynamicValue{after}
|
|
case plans.Read:
|
|
ret.Action = planproto.Action_READ
|
|
ret.Values = []*planproto.DynamicValue{before, after}
|
|
case plans.Update:
|
|
ret.Action = planproto.Action_UPDATE
|
|
ret.Values = []*planproto.DynamicValue{before, after}
|
|
case plans.Delete:
|
|
ret.Action = planproto.Action_DELETE
|
|
ret.Values = []*planproto.DynamicValue{before}
|
|
case plans.DeleteThenCreate:
|
|
ret.Action = planproto.Action_DELETE_THEN_CREATE
|
|
ret.Values = []*planproto.DynamicValue{before, after}
|
|
case plans.CreateThenDelete:
|
|
ret.Action = planproto.Action_CREATE_THEN_DELETE
|
|
ret.Values = []*planproto.DynamicValue{before, after}
|
|
default:
|
|
return nil, fmt.Errorf("invalid change action %s", change.Action)
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func valueToTfplan(val plans.DynamicValue) *planproto.DynamicValue {
|
|
if val == nil {
|
|
// protobuf can't represent nil, so we'll represent it as a
|
|
// DynamicValue that has no serializations at all.
|
|
return &planproto.DynamicValue{}
|
|
}
|
|
return &planproto.DynamicValue{
|
|
Msgpack: []byte(val),
|
|
}
|
|
}
|