github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/plans/planfile/tfplan.go (about)

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