github.com/opentofu/opentofu@v1.7.1/internal/plans/planfile/tfplan.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package planfile
     7  
     8  import (
     9  	"fmt"
    10  	"io"
    11  	"time"
    12  
    13  	"github.com/zclconf/go-cty/cty"
    14  	"google.golang.org/protobuf/proto"
    15  
    16  	"github.com/opentofu/opentofu/internal/addrs"
    17  	"github.com/opentofu/opentofu/internal/checks"
    18  	"github.com/opentofu/opentofu/internal/lang/globalref"
    19  	"github.com/opentofu/opentofu/internal/lang/marks"
    20  	"github.com/opentofu/opentofu/internal/plans"
    21  	"github.com/opentofu/opentofu/internal/plans/internal/planproto"
    22  	"github.com/opentofu/opentofu/internal/states"
    23  	"github.com/opentofu/opentofu/version"
    24  )
    25  
    26  const tfplanFormatVersion = 3
    27  const tfplanFilename = "tfplan"
    28  
    29  // ---------------------------------------------------------------------------
    30  // This file deals with the internal structure of the "tfplan" sub-file within
    31  // the plan file format. It's all private API, wrapped by methods defined
    32  // elsewhere. This is the only file that should import the
    33  // ../internal/planproto package, which contains the ugly stubs generated
    34  // by the protobuf compiler.
    35  // ---------------------------------------------------------------------------
    36  
    37  // readTfplan reads a protobuf-encoded description from the plan portion of
    38  // a plan file, which is stored in a special file in the archive called
    39  // "tfplan".
    40  func readTfplan(r io.Reader) (*plans.Plan, error) {
    41  	src, err := io.ReadAll(r)
    42  	if err != nil {
    43  		return nil, err
    44  	}
    45  
    46  	var rawPlan planproto.Plan
    47  	err = proto.Unmarshal(src, &rawPlan)
    48  	if err != nil {
    49  		return nil, fmt.Errorf("parse error: %w", err)
    50  	}
    51  
    52  	if rawPlan.Version != tfplanFormatVersion {
    53  		return nil, fmt.Errorf("unsupported plan file format version %d; only version %d is supported", rawPlan.Version, tfplanFormatVersion)
    54  	}
    55  
    56  	if rawPlan.TerraformVersion != version.String() {
    57  		return nil, fmt.Errorf("plan file was created by OpenTofu or Terraform %s, but this is %s; plan files cannot be transferred between different versions of OpenTofu / Terraform", rawPlan.TerraformVersion, version.String())
    58  	}
    59  
    60  	plan := &plans.Plan{
    61  		VariableValues: map[string]plans.DynamicValue{},
    62  		Changes: &plans.Changes{
    63  			Outputs:   []*plans.OutputChangeSrc{},
    64  			Resources: []*plans.ResourceInstanceChangeSrc{},
    65  		},
    66  		DriftedResources: []*plans.ResourceInstanceChangeSrc{},
    67  		Checks:           &states.CheckResults{},
    68  	}
    69  
    70  	plan.Errored = rawPlan.Errored
    71  
    72  	switch rawPlan.UiMode {
    73  	case planproto.Mode_NORMAL:
    74  		plan.UIMode = plans.NormalMode
    75  	case planproto.Mode_DESTROY:
    76  		plan.UIMode = plans.DestroyMode
    77  	case planproto.Mode_REFRESH_ONLY:
    78  		plan.UIMode = plans.RefreshOnlyMode
    79  	default:
    80  		return nil, fmt.Errorf("plan has invalid mode %s", rawPlan.UiMode)
    81  	}
    82  
    83  	for _, rawOC := range rawPlan.OutputChanges {
    84  		name := rawOC.Name
    85  		change, err := changeFromTfplan(rawOC.Change)
    86  		if err != nil {
    87  			return nil, fmt.Errorf("invalid plan for output %q: %w", name, err)
    88  		}
    89  
    90  		plan.Changes.Outputs = append(plan.Changes.Outputs, &plans.OutputChangeSrc{
    91  			// All output values saved in the plan file are root module outputs,
    92  			// since we don't retain others. (They can be easily recomputed
    93  			// during apply).
    94  			Addr:      addrs.OutputValue{Name: name}.Absolute(addrs.RootModuleInstance),
    95  			ChangeSrc: *change,
    96  			Sensitive: rawOC.Sensitive,
    97  		})
    98  	}
    99  
   100  	plan.Checks.ConfigResults = addrs.MakeMap[addrs.ConfigCheckable, *states.CheckResultAggregate]()
   101  	for _, rawCRs := range rawPlan.CheckResults {
   102  		aggr := &states.CheckResultAggregate{}
   103  		switch rawCRs.Status {
   104  		case planproto.CheckResults_UNKNOWN:
   105  			aggr.Status = checks.StatusUnknown
   106  		case planproto.CheckResults_PASS:
   107  			aggr.Status = checks.StatusPass
   108  		case planproto.CheckResults_FAIL:
   109  			aggr.Status = checks.StatusFail
   110  		case planproto.CheckResults_ERROR:
   111  			aggr.Status = checks.StatusError
   112  		default:
   113  			return nil, fmt.Errorf("aggregate check results for %s have unsupported status %#v", rawCRs.ConfigAddr, rawCRs.Status)
   114  		}
   115  
   116  		var objKind addrs.CheckableKind
   117  		switch rawCRs.Kind {
   118  		case planproto.CheckResults_RESOURCE:
   119  			objKind = addrs.CheckableResource
   120  		case planproto.CheckResults_OUTPUT_VALUE:
   121  			objKind = addrs.CheckableOutputValue
   122  		case planproto.CheckResults_CHECK:
   123  			objKind = addrs.CheckableCheck
   124  		case planproto.CheckResults_INPUT_VARIABLE:
   125  			objKind = addrs.CheckableInputVariable
   126  		default:
   127  			return nil, fmt.Errorf("aggregate check results for %s have unsupported object kind %s", rawCRs.ConfigAddr, objKind)
   128  		}
   129  
   130  		// Some trickiness here: we only have an address parser for
   131  		// addrs.Checkable and not for addrs.ConfigCheckable, but that's okay
   132  		// because once we have an addrs.Checkable we can always derive an
   133  		// addrs.ConfigCheckable from it, and a ConfigCheckable should always
   134  		// be the same syntax as a Checkable with no index information and
   135  		// thus we can reuse the same parser for both here.
   136  		configAddrProxy, diags := addrs.ParseCheckableStr(objKind, rawCRs.ConfigAddr)
   137  		if diags.HasErrors() {
   138  			return nil, diags.Err()
   139  		}
   140  		configAddr := configAddrProxy.ConfigCheckable()
   141  		if configAddr.String() != configAddrProxy.String() {
   142  			// This is how we catch if the config address included index
   143  			// information that would be allowed in a Checkable but not
   144  			// in a ConfigCheckable.
   145  			return nil, fmt.Errorf("invalid checkable config address %s", rawCRs.ConfigAddr)
   146  		}
   147  
   148  		aggr.ObjectResults = addrs.MakeMap[addrs.Checkable, *states.CheckResultObject]()
   149  		for _, rawCR := range rawCRs.Objects {
   150  			objectAddr, diags := addrs.ParseCheckableStr(objKind, rawCR.ObjectAddr)
   151  			if diags.HasErrors() {
   152  				return nil, diags.Err()
   153  			}
   154  			if !addrs.Equivalent(objectAddr.ConfigCheckable(), configAddr) {
   155  				return nil, fmt.Errorf("checkable object %s should not be grouped under %s", objectAddr, configAddr)
   156  			}
   157  
   158  			obj := &states.CheckResultObject{
   159  				FailureMessages: rawCR.FailureMessages,
   160  			}
   161  			switch rawCR.Status {
   162  			case planproto.CheckResults_UNKNOWN:
   163  				obj.Status = checks.StatusUnknown
   164  			case planproto.CheckResults_PASS:
   165  				obj.Status = checks.StatusPass
   166  			case planproto.CheckResults_FAIL:
   167  				obj.Status = checks.StatusFail
   168  			case planproto.CheckResults_ERROR:
   169  				obj.Status = checks.StatusError
   170  			default:
   171  				return nil, fmt.Errorf("object check results for %s has unsupported status %#v", rawCR.ObjectAddr, rawCR.Status)
   172  			}
   173  
   174  			aggr.ObjectResults.Put(objectAddr, obj)
   175  		}
   176  		// If we ended up with no elements in the map then we'll just nil it,
   177  		// primarily just to make life easier for our round-trip tests.
   178  		if aggr.ObjectResults.Len() == 0 {
   179  			aggr.ObjectResults.Elems = nil
   180  		}
   181  
   182  		plan.Checks.ConfigResults.Put(configAddr, aggr)
   183  	}
   184  	// If we ended up with no elements in the map then we'll just nil it,
   185  	// primarily just to make life easier for our round-trip tests.
   186  	if plan.Checks.ConfigResults.Len() == 0 {
   187  		plan.Checks.ConfigResults.Elems = nil
   188  	}
   189  
   190  	for _, rawRC := range rawPlan.ResourceChanges {
   191  		change, err := resourceChangeFromTfplan(rawRC)
   192  		if err != nil {
   193  			// errors from resourceChangeFromTfplan already include context
   194  			return nil, err
   195  		}
   196  
   197  		plan.Changes.Resources = append(plan.Changes.Resources, change)
   198  	}
   199  
   200  	for _, rawRC := range rawPlan.ResourceDrift {
   201  		change, err := resourceChangeFromTfplan(rawRC)
   202  		if err != nil {
   203  			// errors from resourceChangeFromTfplan already include context
   204  			return nil, err
   205  		}
   206  
   207  		plan.DriftedResources = append(plan.DriftedResources, change)
   208  	}
   209  
   210  	for _, rawRA := range rawPlan.RelevantAttributes {
   211  		ra, err := resourceAttrFromTfplan(rawRA)
   212  		if err != nil {
   213  			return nil, err
   214  		}
   215  		plan.RelevantAttributes = append(plan.RelevantAttributes, ra)
   216  	}
   217  
   218  	for _, rawTargetAddr := range rawPlan.TargetAddrs {
   219  		target, diags := addrs.ParseTargetStr(rawTargetAddr)
   220  		if diags.HasErrors() {
   221  			return nil, fmt.Errorf("plan contains invalid target address %q: %w", target, diags.Err())
   222  		}
   223  		plan.TargetAddrs = append(plan.TargetAddrs, target.Subject)
   224  	}
   225  
   226  	for _, rawReplaceAddr := range rawPlan.ForceReplaceAddrs {
   227  		addr, diags := addrs.ParseAbsResourceInstanceStr(rawReplaceAddr)
   228  		if diags.HasErrors() {
   229  			return nil, fmt.Errorf("plan contains invalid force-replace address %q: %w", addr, diags.Err())
   230  		}
   231  		plan.ForceReplaceAddrs = append(plan.ForceReplaceAddrs, addr)
   232  	}
   233  
   234  	for name, rawVal := range rawPlan.Variables {
   235  		val, err := valueFromTfplan(rawVal)
   236  		if err != nil {
   237  			return nil, fmt.Errorf("invalid value for input variable %q: %w", name, err)
   238  		}
   239  		plan.VariableValues[name] = val
   240  	}
   241  
   242  	if rawBackend := rawPlan.Backend; rawBackend == nil {
   243  		return nil, fmt.Errorf("plan file has no backend settings; backend settings are required")
   244  	} else {
   245  		config, err := valueFromTfplan(rawBackend.Config)
   246  		if err != nil {
   247  			return nil, fmt.Errorf("plan file has invalid backend configuration: %w", err)
   248  		}
   249  		plan.Backend = plans.Backend{
   250  			Type:      rawBackend.Type,
   251  			Config:    config,
   252  			Workspace: rawBackend.Workspace,
   253  		}
   254  	}
   255  
   256  	if plan.Timestamp, err = time.Parse(time.RFC3339, rawPlan.Timestamp); err != nil {
   257  		return nil, fmt.Errorf("invalid value for timestamp %s: %w", rawPlan.Timestamp, err)
   258  	}
   259  
   260  	return plan, nil
   261  }
   262  
   263  func resourceChangeFromTfplan(rawChange *planproto.ResourceInstanceChange) (*plans.ResourceInstanceChangeSrc, error) {
   264  	if rawChange == nil {
   265  		// Should never happen in practice, since protobuf can't represent
   266  		// a nil value in a list.
   267  		return nil, fmt.Errorf("resource change object is absent")
   268  	}
   269  
   270  	ret := &plans.ResourceInstanceChangeSrc{}
   271  
   272  	if rawChange.Addr == "" {
   273  		// If "Addr" isn't populated then seems likely that this is a plan
   274  		// file created by an earlier version of OpenTofu, which had the
   275  		// same information spread over various other fields:
   276  		// ModulePath, Mode, Name, Type, and InstanceKey.
   277  		return nil, fmt.Errorf("no instance address for resource instance change; perhaps this plan was created by a different version of OpenTofu or a different version of Terraform?")
   278  	}
   279  
   280  	instAddr, diags := addrs.ParseAbsResourceInstanceStr(rawChange.Addr)
   281  	if diags.HasErrors() {
   282  		return nil, fmt.Errorf("invalid resource instance address %q: %w", rawChange.Addr, diags.Err())
   283  	}
   284  	prevRunAddr := instAddr
   285  	if rawChange.PrevRunAddr != "" {
   286  		prevRunAddr, diags = addrs.ParseAbsResourceInstanceStr(rawChange.PrevRunAddr)
   287  		if diags.HasErrors() {
   288  			return nil, fmt.Errorf("invalid resource instance previous run address %q: %w", rawChange.PrevRunAddr, diags.Err())
   289  		}
   290  	}
   291  
   292  	providerAddr, diags := addrs.ParseAbsProviderConfigStr(rawChange.Provider)
   293  	if diags.HasErrors() {
   294  		return nil, diags.Err()
   295  	}
   296  	ret.ProviderAddr = providerAddr
   297  
   298  	ret.Addr = instAddr
   299  	ret.PrevRunAddr = prevRunAddr
   300  
   301  	if rawChange.DeposedKey != "" {
   302  		if len(rawChange.DeposedKey) != 8 {
   303  			return nil, fmt.Errorf("deposed object for %s has invalid deposed key %q", ret.Addr, rawChange.DeposedKey)
   304  		}
   305  		ret.DeposedKey = states.DeposedKey(rawChange.DeposedKey)
   306  	}
   307  
   308  	ret.RequiredReplace = cty.NewPathSet()
   309  	for _, p := range rawChange.RequiredReplace {
   310  		path, err := pathFromTfplan(p)
   311  		if err != nil {
   312  			return nil, fmt.Errorf("invalid path in required replace: %w", err)
   313  		}
   314  		ret.RequiredReplace.Add(path)
   315  	}
   316  
   317  	change, err := changeFromTfplan(rawChange.Change)
   318  	if err != nil {
   319  		return nil, fmt.Errorf("invalid plan for resource %s: %w", ret.Addr, err)
   320  	}
   321  
   322  	ret.ChangeSrc = *change
   323  
   324  	switch rawChange.ActionReason {
   325  	case planproto.ResourceInstanceActionReason_NONE:
   326  		ret.ActionReason = plans.ResourceInstanceChangeNoReason
   327  	case planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_CANNOT_UPDATE:
   328  		ret.ActionReason = plans.ResourceInstanceReplaceBecauseCannotUpdate
   329  	case planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_TAINTED:
   330  		ret.ActionReason = plans.ResourceInstanceReplaceBecauseTainted
   331  	case planproto.ResourceInstanceActionReason_REPLACE_BY_REQUEST:
   332  		ret.ActionReason = plans.ResourceInstanceReplaceByRequest
   333  	case planproto.ResourceInstanceActionReason_REPLACE_BY_TRIGGERS:
   334  		ret.ActionReason = plans.ResourceInstanceReplaceByTriggers
   335  	case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_RESOURCE_CONFIG:
   336  		ret.ActionReason = plans.ResourceInstanceDeleteBecauseNoResourceConfig
   337  	case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_WRONG_REPETITION:
   338  		ret.ActionReason = plans.ResourceInstanceDeleteBecauseWrongRepetition
   339  	case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_COUNT_INDEX:
   340  		ret.ActionReason = plans.ResourceInstanceDeleteBecauseCountIndex
   341  	case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_EACH_KEY:
   342  		ret.ActionReason = plans.ResourceInstanceDeleteBecauseEachKey
   343  	case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MODULE:
   344  		ret.ActionReason = plans.ResourceInstanceDeleteBecauseNoModule
   345  	case planproto.ResourceInstanceActionReason_READ_BECAUSE_CONFIG_UNKNOWN:
   346  		ret.ActionReason = plans.ResourceInstanceReadBecauseConfigUnknown
   347  	case planproto.ResourceInstanceActionReason_READ_BECAUSE_DEPENDENCY_PENDING:
   348  		ret.ActionReason = plans.ResourceInstanceReadBecauseDependencyPending
   349  	case planproto.ResourceInstanceActionReason_READ_BECAUSE_CHECK_NESTED:
   350  		ret.ActionReason = plans.ResourceInstanceReadBecauseCheckNested
   351  	case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MOVE_TARGET:
   352  		ret.ActionReason = plans.ResourceInstanceDeleteBecauseNoMoveTarget
   353  	default:
   354  		return nil, fmt.Errorf("resource has invalid action reason %s", rawChange.ActionReason)
   355  	}
   356  
   357  	if len(rawChange.Private) != 0 {
   358  		ret.Private = rawChange.Private
   359  	}
   360  
   361  	return ret, nil
   362  }
   363  
   364  func changeFromTfplan(rawChange *planproto.Change) (*plans.ChangeSrc, error) {
   365  	if rawChange == nil {
   366  		return nil, fmt.Errorf("change object is absent")
   367  	}
   368  
   369  	ret := &plans.ChangeSrc{}
   370  
   371  	// -1 indicates that there is no index. We'll customize these below
   372  	// depending on the change action, and then decode.
   373  	beforeIdx, afterIdx := -1, -1
   374  
   375  	switch rawChange.Action {
   376  	case planproto.Action_NOOP:
   377  		ret.Action = plans.NoOp
   378  		beforeIdx = 0
   379  		afterIdx = 0
   380  	case planproto.Action_CREATE:
   381  		ret.Action = plans.Create
   382  		afterIdx = 0
   383  	case planproto.Action_READ:
   384  		ret.Action = plans.Read
   385  		beforeIdx = 0
   386  		afterIdx = 1
   387  	case planproto.Action_UPDATE:
   388  		ret.Action = plans.Update
   389  		beforeIdx = 0
   390  		afterIdx = 1
   391  	case planproto.Action_DELETE:
   392  		ret.Action = plans.Delete
   393  		beforeIdx = 0
   394  	case planproto.Action_FORGET:
   395  		ret.Action = plans.Forget
   396  		beforeIdx = 0
   397  	case planproto.Action_CREATE_THEN_DELETE:
   398  		ret.Action = plans.CreateThenDelete
   399  		beforeIdx = 0
   400  		afterIdx = 1
   401  	case planproto.Action_DELETE_THEN_CREATE:
   402  		ret.Action = plans.DeleteThenCreate
   403  		beforeIdx = 0
   404  		afterIdx = 1
   405  	default:
   406  		return nil, fmt.Errorf("invalid change action %s", rawChange.Action)
   407  	}
   408  
   409  	if beforeIdx != -1 {
   410  		if l := len(rawChange.Values); l <= beforeIdx {
   411  			return nil, fmt.Errorf("incorrect number of values (%d) for %s change", l, rawChange.Action)
   412  		}
   413  		var err error
   414  		ret.Before, err = valueFromTfplan(rawChange.Values[beforeIdx])
   415  		if err != nil {
   416  			return nil, fmt.Errorf("invalid \"before\" value: %w", err)
   417  		}
   418  		if ret.Before == nil {
   419  			return nil, fmt.Errorf("missing \"before\" value: %w", err)
   420  		}
   421  	}
   422  	if afterIdx != -1 {
   423  		if l := len(rawChange.Values); l <= afterIdx {
   424  			return nil, fmt.Errorf("incorrect number of values (%d) for %s change", l, rawChange.Action)
   425  		}
   426  		var err error
   427  		ret.After, err = valueFromTfplan(rawChange.Values[afterIdx])
   428  		if err != nil {
   429  			return nil, fmt.Errorf("invalid \"after\" value: %w", err)
   430  		}
   431  		if ret.After == nil {
   432  			return nil, fmt.Errorf("missing \"after\" value: %w", err)
   433  		}
   434  	}
   435  
   436  	if rawChange.Importing != nil {
   437  		ret.Importing = &plans.ImportingSrc{
   438  			ID: rawChange.Importing.Id,
   439  		}
   440  	}
   441  	ret.GeneratedConfig = rawChange.GeneratedConfig
   442  
   443  	sensitive := cty.NewValueMarks(marks.Sensitive)
   444  	beforeValMarks, err := pathValueMarksFromTfplan(rawChange.BeforeSensitivePaths, sensitive)
   445  	if err != nil {
   446  		return nil, fmt.Errorf("failed to decode before sensitive paths: %w", err)
   447  	}
   448  	afterValMarks, err := pathValueMarksFromTfplan(rawChange.AfterSensitivePaths, sensitive)
   449  	if err != nil {
   450  		return nil, fmt.Errorf("failed to decode after sensitive paths: %w", err)
   451  	}
   452  	if len(beforeValMarks) > 0 {
   453  		ret.BeforeValMarks = beforeValMarks
   454  	}
   455  	if len(afterValMarks) > 0 {
   456  		ret.AfterValMarks = afterValMarks
   457  	}
   458  
   459  	return ret, nil
   460  }
   461  
   462  func valueFromTfplan(rawV *planproto.DynamicValue) (plans.DynamicValue, error) {
   463  	if len(rawV.Msgpack) == 0 { // len(0) because that's the default value for a "bytes" in protobuf
   464  		return nil, fmt.Errorf("dynamic value does not have msgpack serialization")
   465  	}
   466  
   467  	return plans.DynamicValue(rawV.Msgpack), nil
   468  }
   469  
   470  // writeTfplan serializes the given plan into the protobuf-based format used
   471  // for the "tfplan" portion of a plan file.
   472  func writeTfplan(plan *plans.Plan, w io.Writer) error {
   473  	if plan == nil {
   474  		return fmt.Errorf("cannot write plan file for nil plan")
   475  	}
   476  	if plan.Changes == nil {
   477  		return fmt.Errorf("cannot write plan file with nil changeset")
   478  	}
   479  
   480  	rawPlan := &planproto.Plan{
   481  		Version:          tfplanFormatVersion,
   482  		TerraformVersion: version.String(),
   483  
   484  		Variables:       map[string]*planproto.DynamicValue{},
   485  		OutputChanges:   []*planproto.OutputChange{},
   486  		CheckResults:    []*planproto.CheckResults{},
   487  		ResourceChanges: []*planproto.ResourceInstanceChange{},
   488  		ResourceDrift:   []*planproto.ResourceInstanceChange{},
   489  	}
   490  
   491  	rawPlan.Errored = plan.Errored
   492  
   493  	switch plan.UIMode {
   494  	case plans.NormalMode:
   495  		rawPlan.UiMode = planproto.Mode_NORMAL
   496  	case plans.DestroyMode:
   497  		rawPlan.UiMode = planproto.Mode_DESTROY
   498  	case plans.RefreshOnlyMode:
   499  		rawPlan.UiMode = planproto.Mode_REFRESH_ONLY
   500  	default:
   501  		return fmt.Errorf("plan has unsupported mode %s", plan.UIMode)
   502  	}
   503  
   504  	for _, oc := range plan.Changes.Outputs {
   505  		// When serializing a plan we only retain the root outputs, since
   506  		// changes to these are externally-visible side effects (e.g. via
   507  		// terraform_remote_state).
   508  		if !oc.Addr.Module.IsRoot() {
   509  			continue
   510  		}
   511  
   512  		name := oc.Addr.OutputValue.Name
   513  
   514  		// Writing outputs as cty.DynamicPseudoType forces the stored values
   515  		// to also contain dynamic type information, so we can recover the
   516  		// original type when we read the values back in readTFPlan.
   517  		protoChange, err := changeToTfplan(&oc.ChangeSrc)
   518  		if err != nil {
   519  			return fmt.Errorf("cannot write output value %q: %w", name, err)
   520  		}
   521  
   522  		rawPlan.OutputChanges = append(rawPlan.OutputChanges, &planproto.OutputChange{
   523  			Name:      name,
   524  			Change:    protoChange,
   525  			Sensitive: oc.Sensitive,
   526  		})
   527  	}
   528  
   529  	if plan.Checks != nil {
   530  		for _, configElem := range plan.Checks.ConfigResults.Elems {
   531  			crs := configElem.Value
   532  			pcrs := &planproto.CheckResults{
   533  				ConfigAddr: configElem.Key.String(),
   534  			}
   535  			switch crs.Status {
   536  			case checks.StatusUnknown:
   537  				pcrs.Status = planproto.CheckResults_UNKNOWN
   538  			case checks.StatusPass:
   539  				pcrs.Status = planproto.CheckResults_PASS
   540  			case checks.StatusFail:
   541  				pcrs.Status = planproto.CheckResults_FAIL
   542  			case checks.StatusError:
   543  				pcrs.Status = planproto.CheckResults_ERROR
   544  			default:
   545  				return fmt.Errorf("checkable configuration %s has unsupported aggregate status %s", configElem.Key, crs.Status)
   546  			}
   547  			switch kind := configElem.Key.CheckableKind(); kind {
   548  			case addrs.CheckableResource:
   549  				pcrs.Kind = planproto.CheckResults_RESOURCE
   550  			case addrs.CheckableOutputValue:
   551  				pcrs.Kind = planproto.CheckResults_OUTPUT_VALUE
   552  			case addrs.CheckableCheck:
   553  				pcrs.Kind = planproto.CheckResults_CHECK
   554  			case addrs.CheckableInputVariable:
   555  				pcrs.Kind = planproto.CheckResults_INPUT_VARIABLE
   556  			default:
   557  				return fmt.Errorf("checkable configuration %s has unsupported object type kind %s", configElem.Key, kind)
   558  			}
   559  
   560  			for _, objectElem := range configElem.Value.ObjectResults.Elems {
   561  				cr := objectElem.Value
   562  				pcr := &planproto.CheckResults_ObjectResult{
   563  					ObjectAddr:      objectElem.Key.String(),
   564  					FailureMessages: objectElem.Value.FailureMessages,
   565  				}
   566  				switch cr.Status {
   567  				case checks.StatusUnknown:
   568  					pcr.Status = planproto.CheckResults_UNKNOWN
   569  				case checks.StatusPass:
   570  					pcr.Status = planproto.CheckResults_PASS
   571  				case checks.StatusFail:
   572  					pcr.Status = planproto.CheckResults_FAIL
   573  				case checks.StatusError:
   574  					pcr.Status = planproto.CheckResults_ERROR
   575  				default:
   576  					return fmt.Errorf("checkable object %s has unsupported status %s", objectElem.Key, crs.Status)
   577  				}
   578  				pcrs.Objects = append(pcrs.Objects, pcr)
   579  			}
   580  
   581  			rawPlan.CheckResults = append(rawPlan.CheckResults, pcrs)
   582  		}
   583  	}
   584  
   585  	for _, rc := range plan.Changes.Resources {
   586  		rawRC, err := resourceChangeToTfplan(rc)
   587  		if err != nil {
   588  			return err
   589  		}
   590  		rawPlan.ResourceChanges = append(rawPlan.ResourceChanges, rawRC)
   591  	}
   592  
   593  	for _, rc := range plan.DriftedResources {
   594  		rawRC, err := resourceChangeToTfplan(rc)
   595  		if err != nil {
   596  			return err
   597  		}
   598  		rawPlan.ResourceDrift = append(rawPlan.ResourceDrift, rawRC)
   599  	}
   600  
   601  	for _, ra := range plan.RelevantAttributes {
   602  		rawRA, err := resourceAttrToTfplan(ra)
   603  		if err != nil {
   604  			return err
   605  		}
   606  		rawPlan.RelevantAttributes = append(rawPlan.RelevantAttributes, rawRA)
   607  	}
   608  
   609  	for _, targetAddr := range plan.TargetAddrs {
   610  		rawPlan.TargetAddrs = append(rawPlan.TargetAddrs, targetAddr.String())
   611  	}
   612  
   613  	for _, replaceAddr := range plan.ForceReplaceAddrs {
   614  		rawPlan.ForceReplaceAddrs = append(rawPlan.ForceReplaceAddrs, replaceAddr.String())
   615  	}
   616  
   617  	for name, val := range plan.VariableValues {
   618  		rawPlan.Variables[name] = valueToTfplan(val)
   619  	}
   620  
   621  	if plan.Backend.Type == "" || plan.Backend.Config == nil {
   622  		// This suggests a bug in the code that created the plan, since it
   623  		// ought to always have a backend populated, even if it's the default
   624  		// "local" backend with a local state file.
   625  		return fmt.Errorf("plan does not have a backend configuration")
   626  	}
   627  
   628  	rawPlan.Backend = &planproto.Backend{
   629  		Type:      plan.Backend.Type,
   630  		Config:    valueToTfplan(plan.Backend.Config),
   631  		Workspace: plan.Backend.Workspace,
   632  	}
   633  
   634  	rawPlan.Timestamp = plan.Timestamp.Format(time.RFC3339)
   635  
   636  	src, err := proto.Marshal(rawPlan)
   637  	if err != nil {
   638  		return fmt.Errorf("serialization error: %w", err)
   639  	}
   640  
   641  	_, err = w.Write(src)
   642  	if err != nil {
   643  		return fmt.Errorf("failed to write plan to plan file: %w", err)
   644  	}
   645  
   646  	return nil
   647  }
   648  
   649  func resourceAttrToTfplan(ra globalref.ResourceAttr) (*planproto.PlanResourceAttr, error) {
   650  	res := &planproto.PlanResourceAttr{}
   651  
   652  	res.Resource = ra.Resource.String()
   653  	attr, err := pathToTfplan(ra.Attr)
   654  	if err != nil {
   655  		return res, err
   656  	}
   657  	res.Attr = attr
   658  	return res, nil
   659  }
   660  
   661  func resourceAttrFromTfplan(ra *planproto.PlanResourceAttr) (globalref.ResourceAttr, error) {
   662  	var res globalref.ResourceAttr
   663  	if ra.Resource == "" {
   664  		return res, fmt.Errorf("missing resource address from relevant attribute")
   665  	}
   666  
   667  	instAddr, diags := addrs.ParseAbsResourceInstanceStr(ra.Resource)
   668  	if diags.HasErrors() {
   669  		return res, fmt.Errorf("invalid resource instance address %q in relevant attributes: %w", ra.Resource, diags.Err())
   670  	}
   671  
   672  	res.Resource = instAddr
   673  	path, err := pathFromTfplan(ra.Attr)
   674  	if err != nil {
   675  		return res, fmt.Errorf("invalid path in %q relevant attribute: %w", res.Resource, err)
   676  	}
   677  
   678  	res.Attr = path
   679  	return res, nil
   680  }
   681  
   682  func resourceChangeToTfplan(change *plans.ResourceInstanceChangeSrc) (*planproto.ResourceInstanceChange, error) {
   683  	ret := &planproto.ResourceInstanceChange{}
   684  
   685  	if change.PrevRunAddr.Resource.Resource.Type == "" {
   686  		// Suggests that an old caller wasn't yet updated to populate this
   687  		// properly. All code that generates plans should populate this field,
   688  		// even if it's just to write in the same value as in change.Addr.
   689  		change.PrevRunAddr = change.Addr
   690  	}
   691  
   692  	ret.Addr = change.Addr.String()
   693  	ret.PrevRunAddr = change.PrevRunAddr.String()
   694  	if ret.PrevRunAddr == ret.Addr {
   695  		// In the on-disk format we leave PrevRunAddr unpopulated in the common
   696  		// case where it's the same as Addr, and then fill it back in again on
   697  		// read.
   698  		ret.PrevRunAddr = ""
   699  	}
   700  
   701  	ret.DeposedKey = string(change.DeposedKey)
   702  	ret.Provider = change.ProviderAddr.String()
   703  
   704  	requiredReplace := change.RequiredReplace.List()
   705  	ret.RequiredReplace = make([]*planproto.Path, 0, len(requiredReplace))
   706  	for _, p := range requiredReplace {
   707  		path, err := pathToTfplan(p)
   708  		if err != nil {
   709  			return nil, fmt.Errorf("invalid path in required replace: %w", err)
   710  		}
   711  		ret.RequiredReplace = append(ret.RequiredReplace, path)
   712  	}
   713  
   714  	valChange, err := changeToTfplan(&change.ChangeSrc)
   715  	if err != nil {
   716  		return nil, fmt.Errorf("failed to serialize resource %s change: %w", change.Addr, err)
   717  	}
   718  	ret.Change = valChange
   719  
   720  	switch change.ActionReason {
   721  	case plans.ResourceInstanceChangeNoReason:
   722  		ret.ActionReason = planproto.ResourceInstanceActionReason_NONE
   723  	case plans.ResourceInstanceReplaceBecauseCannotUpdate:
   724  		ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_CANNOT_UPDATE
   725  	case plans.ResourceInstanceReplaceBecauseTainted:
   726  		ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_TAINTED
   727  	case plans.ResourceInstanceReplaceByRequest:
   728  		ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BY_REQUEST
   729  	case plans.ResourceInstanceReplaceByTriggers:
   730  		ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BY_TRIGGERS
   731  	case plans.ResourceInstanceDeleteBecauseNoResourceConfig:
   732  		ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_RESOURCE_CONFIG
   733  	case plans.ResourceInstanceDeleteBecauseWrongRepetition:
   734  		ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_WRONG_REPETITION
   735  	case plans.ResourceInstanceDeleteBecauseCountIndex:
   736  		ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_COUNT_INDEX
   737  	case plans.ResourceInstanceDeleteBecauseEachKey:
   738  		ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_EACH_KEY
   739  	case plans.ResourceInstanceDeleteBecauseNoModule:
   740  		ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MODULE
   741  	case plans.ResourceInstanceReadBecauseConfigUnknown:
   742  		ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_CONFIG_UNKNOWN
   743  	case plans.ResourceInstanceReadBecauseDependencyPending:
   744  		ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_DEPENDENCY_PENDING
   745  	case plans.ResourceInstanceReadBecauseCheckNested:
   746  		ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_CHECK_NESTED
   747  	case plans.ResourceInstanceDeleteBecauseNoMoveTarget:
   748  		ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MOVE_TARGET
   749  	default:
   750  		return nil, fmt.Errorf("resource %s has unsupported action reason %s", change.Addr, change.ActionReason)
   751  	}
   752  
   753  	if len(change.Private) > 0 {
   754  		ret.Private = change.Private
   755  	}
   756  
   757  	return ret, nil
   758  }
   759  
   760  func changeToTfplan(change *plans.ChangeSrc) (*planproto.Change, error) {
   761  	ret := &planproto.Change{}
   762  
   763  	before := valueToTfplan(change.Before)
   764  	after := valueToTfplan(change.After)
   765  
   766  	beforeSensitivePaths, err := pathValueMarksToTfplan(change.BeforeValMarks)
   767  	if err != nil {
   768  		return nil, err
   769  	}
   770  	afterSensitivePaths, err := pathValueMarksToTfplan(change.AfterValMarks)
   771  	if err != nil {
   772  		return nil, err
   773  	}
   774  	ret.BeforeSensitivePaths = beforeSensitivePaths
   775  	ret.AfterSensitivePaths = afterSensitivePaths
   776  
   777  	if change.Importing != nil {
   778  		ret.Importing = &planproto.Importing{
   779  			Id: change.Importing.ID,
   780  		}
   781  
   782  	}
   783  	ret.GeneratedConfig = change.GeneratedConfig
   784  
   785  	switch change.Action {
   786  	case plans.NoOp:
   787  		ret.Action = planproto.Action_NOOP
   788  		ret.Values = []*planproto.DynamicValue{before} // before and after should be identical
   789  	case plans.Create:
   790  		ret.Action = planproto.Action_CREATE
   791  		ret.Values = []*planproto.DynamicValue{after}
   792  	case plans.Read:
   793  		ret.Action = planproto.Action_READ
   794  		ret.Values = []*planproto.DynamicValue{before, after}
   795  	case plans.Update:
   796  		ret.Action = planproto.Action_UPDATE
   797  		ret.Values = []*planproto.DynamicValue{before, after}
   798  	case plans.Delete:
   799  		ret.Action = planproto.Action_DELETE
   800  		ret.Values = []*planproto.DynamicValue{before}
   801  	case plans.Forget:
   802  		ret.Action = planproto.Action_FORGET
   803  		ret.Values = []*planproto.DynamicValue{before}
   804  	case plans.DeleteThenCreate:
   805  		ret.Action = planproto.Action_DELETE_THEN_CREATE
   806  		ret.Values = []*planproto.DynamicValue{before, after}
   807  	case plans.CreateThenDelete:
   808  		ret.Action = planproto.Action_CREATE_THEN_DELETE
   809  		ret.Values = []*planproto.DynamicValue{before, after}
   810  	default:
   811  		return nil, fmt.Errorf("invalid change action %s", change.Action)
   812  	}
   813  
   814  	return ret, nil
   815  }
   816  
   817  func valueToTfplan(val plans.DynamicValue) *planproto.DynamicValue {
   818  	if val == nil {
   819  		// protobuf can't represent nil, so we'll represent it as a
   820  		// DynamicValue that has no serializations at all.
   821  		return &planproto.DynamicValue{}
   822  	}
   823  	return &planproto.DynamicValue{
   824  		Msgpack: []byte(val),
   825  	}
   826  }
   827  
   828  func pathValueMarksFromTfplan(paths []*planproto.Path, marks cty.ValueMarks) ([]cty.PathValueMarks, error) {
   829  	ret := make([]cty.PathValueMarks, 0, len(paths))
   830  	for _, p := range paths {
   831  		path, err := pathFromTfplan(p)
   832  		if err != nil {
   833  			return nil, err
   834  		}
   835  		ret = append(ret, cty.PathValueMarks{
   836  			Path:  path,
   837  			Marks: marks,
   838  		})
   839  	}
   840  	return ret, nil
   841  }
   842  
   843  func pathValueMarksToTfplan(pvm []cty.PathValueMarks) ([]*planproto.Path, error) {
   844  	ret := make([]*planproto.Path, 0, len(pvm))
   845  	for _, p := range pvm {
   846  		path, err := pathToTfplan(p.Path)
   847  		if err != nil {
   848  			return nil, err
   849  		}
   850  		ret = append(ret, path)
   851  	}
   852  	return ret, nil
   853  }
   854  
   855  func pathFromTfplan(path *planproto.Path) (cty.Path, error) {
   856  	ret := make([]cty.PathStep, 0, len(path.Steps))
   857  	for _, step := range path.Steps {
   858  		switch s := step.Selector.(type) {
   859  		case *planproto.Path_Step_ElementKey:
   860  			dynamicVal, err := valueFromTfplan(s.ElementKey)
   861  			if err != nil {
   862  				return nil, fmt.Errorf("error decoding path index step: %w", err)
   863  			}
   864  			ty, err := dynamicVal.ImpliedType()
   865  			if err != nil {
   866  				return nil, fmt.Errorf("error determining path index type: %w", err)
   867  			}
   868  			val, err := dynamicVal.Decode(ty)
   869  			if err != nil {
   870  				return nil, fmt.Errorf("error decoding path index value: %w", err)
   871  			}
   872  			ret = append(ret, cty.IndexStep{Key: val})
   873  		case *planproto.Path_Step_AttributeName:
   874  			ret = append(ret, cty.GetAttrStep{Name: s.AttributeName})
   875  		default:
   876  			return nil, fmt.Errorf("Unsupported path step %t", step.Selector)
   877  		}
   878  	}
   879  	return ret, nil
   880  }
   881  
   882  func pathToTfplan(path cty.Path) (*planproto.Path, error) {
   883  	steps := make([]*planproto.Path_Step, 0, len(path))
   884  	for _, step := range path {
   885  		switch s := step.(type) {
   886  		case cty.IndexStep:
   887  			value, err := plans.NewDynamicValue(s.Key, s.Key.Type())
   888  			if err != nil {
   889  				return nil, fmt.Errorf("Error encoding path step: %w", err)
   890  			}
   891  			steps = append(steps, &planproto.Path_Step{
   892  				Selector: &planproto.Path_Step_ElementKey{
   893  					ElementKey: valueToTfplan(value),
   894  				},
   895  			})
   896  		case cty.GetAttrStep:
   897  			steps = append(steps, &planproto.Path_Step{
   898  				Selector: &planproto.Path_Step_AttributeName{
   899  					AttributeName: s.Name,
   900  				},
   901  			})
   902  		default:
   903  			return nil, fmt.Errorf("Unsupported path step %#v (%t)", step, step)
   904  		}
   905  	}
   906  	return &planproto.Path{
   907  		Steps: steps,
   908  	}, nil
   909  }