github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/command/jsonplan/plan.go (about)

     1  package jsonplan
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"sort"
     7  
     8  	"github.com/zclconf/go-cty/cty"
     9  	ctyjson "github.com/zclconf/go-cty/cty/json"
    10  
    11  	"github.com/hashicorp/terraform/internal/addrs"
    12  	"github.com/hashicorp/terraform/internal/command/jsonconfig"
    13  	"github.com/hashicorp/terraform/internal/command/jsonstate"
    14  	"github.com/hashicorp/terraform/internal/configs"
    15  	"github.com/hashicorp/terraform/internal/plans"
    16  	"github.com/hashicorp/terraform/internal/states"
    17  	"github.com/hashicorp/terraform/internal/states/statefile"
    18  	"github.com/hashicorp/terraform/internal/terraform"
    19  	"github.com/hashicorp/terraform/version"
    20  )
    21  
    22  // FormatVersion represents the version of the json format and will be
    23  // incremented for any change to this format that requires changes to a
    24  // consuming parser.
    25  const FormatVersion = "0.2"
    26  
    27  // Plan is the top-level representation of the json format of a plan. It includes
    28  // the complete config and current state.
    29  type plan struct {
    30  	FormatVersion    string      `json:"format_version,omitempty"`
    31  	TerraformVersion string      `json:"terraform_version,omitempty"`
    32  	Variables        variables   `json:"variables,omitempty"`
    33  	PlannedValues    stateValues `json:"planned_values,omitempty"`
    34  	// ResourceDrift and ResourceChanges are sorted in a user-friendly order
    35  	// that is undefined at this time, but consistent.
    36  	ResourceDrift   []resourceChange  `json:"resource_drift,omitempty"`
    37  	ResourceChanges []resourceChange  `json:"resource_changes,omitempty"`
    38  	OutputChanges   map[string]change `json:"output_changes,omitempty"`
    39  	PriorState      json.RawMessage   `json:"prior_state,omitempty"`
    40  	Config          json.RawMessage   `json:"configuration,omitempty"`
    41  }
    42  
    43  func newPlan() *plan {
    44  	return &plan{
    45  		FormatVersion: FormatVersion,
    46  	}
    47  }
    48  
    49  // Change is the representation of a proposed change for an object.
    50  type change struct {
    51  	// Actions are the actions that will be taken on the object selected by the
    52  	// properties below. Valid actions values are:
    53  	//    ["no-op"]
    54  	//    ["create"]
    55  	//    ["read"]
    56  	//    ["update"]
    57  	//    ["delete", "create"]
    58  	//    ["create", "delete"]
    59  	//    ["delete"]
    60  	// The two "replace" actions are represented in this way to allow callers to
    61  	// e.g. just scan the list for "delete" to recognize all three situations
    62  	// where the object will be deleted, allowing for any new deletion
    63  	// combinations that might be added in future.
    64  	Actions []string `json:"actions,omitempty"`
    65  
    66  	// Before and After are representations of the object value both before and
    67  	// after the action. For ["create"] and ["delete"] actions, either "before"
    68  	// or "after" is unset (respectively). For ["no-op"], the before and after
    69  	// values are identical. The "after" value will be incomplete if there are
    70  	// values within it that won't be known until after apply.
    71  	Before json.RawMessage `json:"before,omitempty"`
    72  	After  json.RawMessage `json:"after,omitempty"`
    73  
    74  	// AfterUnknown is an object value with similar structure to After, but
    75  	// with all unknown leaf values replaced with true, and all known leaf
    76  	// values omitted.  This can be combined with After to reconstruct a full
    77  	// value after the action, including values which will only be known after
    78  	// apply.
    79  	AfterUnknown json.RawMessage `json:"after_unknown,omitempty"`
    80  
    81  	// BeforeSensitive and AfterSensitive are object values with similar
    82  	// structure to Before and After, but with all sensitive leaf values
    83  	// replaced with true, and all non-sensitive leaf values omitted. These
    84  	// objects should be combined with Before and After to prevent accidental
    85  	// display of sensitive values in user interfaces.
    86  	BeforeSensitive json.RawMessage `json:"before_sensitive,omitempty"`
    87  	AfterSensitive  json.RawMessage `json:"after_sensitive,omitempty"`
    88  
    89  	// ReplacePaths is an array of arrays representing a set of paths into the
    90  	// object value which resulted in the action being "replace". This will be
    91  	// omitted if the action is not replace, or if no paths caused the
    92  	// replacement (for example, if the resource was tainted). Each path
    93  	// consists of one or more steps, each of which will be a number or a
    94  	// string.
    95  	ReplacePaths json.RawMessage `json:"replace_paths,omitempty"`
    96  }
    97  
    98  type output struct {
    99  	Sensitive bool            `json:"sensitive"`
   100  	Value     json.RawMessage `json:"value,omitempty"`
   101  }
   102  
   103  // variables is the JSON representation of the variables provided to the current
   104  // plan.
   105  type variables map[string]*variable
   106  
   107  type variable struct {
   108  	Value json.RawMessage `json:"value,omitempty"`
   109  }
   110  
   111  // Marshal returns the json encoding of a terraform plan.
   112  func Marshal(
   113  	config *configs.Config,
   114  	p *plans.Plan,
   115  	sf *statefile.File,
   116  	schemas *terraform.Schemas,
   117  ) ([]byte, error) {
   118  	output := newPlan()
   119  	output.TerraformVersion = version.String()
   120  
   121  	err := output.marshalPlanVariables(p.VariableValues, schemas)
   122  	if err != nil {
   123  		return nil, fmt.Errorf("error in marshalPlanVariables: %s", err)
   124  	}
   125  
   126  	// output.PlannedValues
   127  	err = output.marshalPlannedValues(p.Changes, schemas)
   128  	if err != nil {
   129  		return nil, fmt.Errorf("error in marshalPlannedValues: %s", err)
   130  	}
   131  
   132  	// output.ResourceDrift
   133  	err = output.marshalResourceDrift(p.PrevRunState, p.PriorState, schemas)
   134  	if err != nil {
   135  		return nil, fmt.Errorf("error in marshalResourceDrift: %s", err)
   136  	}
   137  
   138  	// output.ResourceChanges
   139  	err = output.marshalResourceChanges(p.Changes, schemas)
   140  	if err != nil {
   141  		return nil, fmt.Errorf("error in marshalResourceChanges: %s", err)
   142  	}
   143  
   144  	// output.OutputChanges
   145  	err = output.marshalOutputChanges(p.Changes)
   146  	if err != nil {
   147  		return nil, fmt.Errorf("error in marshaling output changes: %s", err)
   148  	}
   149  
   150  	// output.PriorState
   151  	if sf != nil && !sf.State.Empty() {
   152  		output.PriorState, err = jsonstate.Marshal(sf, schemas)
   153  		if err != nil {
   154  			return nil, fmt.Errorf("error marshaling prior state: %s", err)
   155  		}
   156  	}
   157  
   158  	// output.Config
   159  	output.Config, err = jsonconfig.Marshal(config, schemas)
   160  	if err != nil {
   161  		return nil, fmt.Errorf("error marshaling config: %s", err)
   162  	}
   163  
   164  	ret, err := json.Marshal(output)
   165  	return ret, err
   166  }
   167  
   168  func (p *plan) marshalPlanVariables(vars map[string]plans.DynamicValue, schemas *terraform.Schemas) error {
   169  	if len(vars) == 0 {
   170  		return nil
   171  	}
   172  
   173  	p.Variables = make(variables, len(vars))
   174  
   175  	for k, v := range vars {
   176  		val, err := v.Decode(cty.DynamicPseudoType)
   177  		if err != nil {
   178  			return err
   179  		}
   180  		valJSON, err := ctyjson.Marshal(val, val.Type())
   181  		if err != nil {
   182  			return err
   183  		}
   184  		p.Variables[k] = &variable{
   185  			Value: valJSON,
   186  		}
   187  	}
   188  	return nil
   189  }
   190  
   191  func (p *plan) marshalResourceDrift(oldState, newState *states.State, schemas *terraform.Schemas) error {
   192  	// Our goal here is to build a data structure of the same shape as we use
   193  	// to describe planned resource changes, but in this case we'll be
   194  	// taking the old and new values from different state snapshots rather
   195  	// than from a real "Changes" object.
   196  	//
   197  	// In doing this we make an assumption that drift detection can only
   198  	// ever show objects as updated or removed, and will never show anything
   199  	// as created because we only refresh objects we were already tracking
   200  	// after the previous run. This means we can use oldState as our baseline
   201  	// for what resource instances we might include, and check for each item
   202  	// whether it's present in newState. If we ever have some mechanism to
   203  	// detect "additive drift" later then we'll need to take a different
   204  	// approach here, but we have no plans for that at the time of writing.
   205  	//
   206  	// We also assume that both states have had all managed resource objects
   207  	// upgraded to match the current schemas given in schemas, so we shouldn't
   208  	// need to contend with oldState having old-shaped objects even if the
   209  	// user changed provider versions since the last run.
   210  
   211  	if newState.ManagedResourcesEqual(oldState) {
   212  		// Nothing to do, because we only detect and report drift for managed
   213  		// resource instances.
   214  		return nil
   215  	}
   216  	for _, ms := range oldState.Modules {
   217  		for _, rs := range ms.Resources {
   218  			if rs.Addr.Resource.Mode != addrs.ManagedResourceMode {
   219  				// Drift reporting is only for managed resources
   220  				continue
   221  			}
   222  
   223  			provider := rs.ProviderConfig.Provider
   224  			for key, oldIS := range rs.Instances {
   225  				if oldIS.Current == nil {
   226  					// Not interested in instances that only have deposed objects
   227  					continue
   228  				}
   229  				addr := rs.Addr.Instance(key)
   230  				newIS := newState.ResourceInstance(addr)
   231  
   232  				schema, _ := schemas.ResourceTypeConfig(
   233  					provider,
   234  					addr.Resource.Resource.Mode,
   235  					addr.Resource.Resource.Type,
   236  				)
   237  				if schema == nil {
   238  					return fmt.Errorf("no schema found for %s (in provider %s)", addr, provider)
   239  				}
   240  				ty := schema.ImpliedType()
   241  
   242  				oldObj, err := oldIS.Current.Decode(ty)
   243  				if err != nil {
   244  					return fmt.Errorf("failed to decode previous run data for %s: %s", addr, err)
   245  				}
   246  
   247  				var newObj *states.ResourceInstanceObject
   248  				if newIS != nil && newIS.Current != nil {
   249  					newObj, err = newIS.Current.Decode(ty)
   250  					if err != nil {
   251  						return fmt.Errorf("failed to decode refreshed data for %s: %s", addr, err)
   252  					}
   253  				}
   254  
   255  				var oldVal, newVal cty.Value
   256  				oldVal = oldObj.Value
   257  				if newObj != nil {
   258  					newVal = newObj.Value
   259  				} else {
   260  					newVal = cty.NullVal(ty)
   261  				}
   262  
   263  				if oldVal.RawEquals(newVal) {
   264  					// No drift if the two values are semantically equivalent
   265  					continue
   266  				}
   267  
   268  				oldSensitive := jsonstate.SensitiveAsBool(oldVal)
   269  				newSensitive := jsonstate.SensitiveAsBool(newVal)
   270  				oldVal, _ = oldVal.UnmarkDeep()
   271  				newVal, _ = newVal.UnmarkDeep()
   272  
   273  				var before, after []byte
   274  				var beforeSensitive, afterSensitive []byte
   275  				before, err = ctyjson.Marshal(oldVal, oldVal.Type())
   276  				if err != nil {
   277  					return fmt.Errorf("failed to encode previous run data for %s as JSON: %s", addr, err)
   278  				}
   279  				after, err = ctyjson.Marshal(newVal, oldVal.Type())
   280  				if err != nil {
   281  					return fmt.Errorf("failed to encode refreshed data for %s as JSON: %s", addr, err)
   282  				}
   283  				beforeSensitive, err = ctyjson.Marshal(oldSensitive, oldSensitive.Type())
   284  				if err != nil {
   285  					return fmt.Errorf("failed to encode previous run data sensitivity for %s as JSON: %s", addr, err)
   286  				}
   287  				afterSensitive, err = ctyjson.Marshal(newSensitive, newSensitive.Type())
   288  				if err != nil {
   289  					return fmt.Errorf("failed to encode refreshed data sensitivity for %s as JSON: %s", addr, err)
   290  				}
   291  
   292  				// We can only detect updates and deletes as drift.
   293  				action := plans.Update
   294  				if newVal.IsNull() {
   295  					action = plans.Delete
   296  				}
   297  
   298  				change := resourceChange{
   299  					Address:       addr.String(),
   300  					ModuleAddress: addr.Module.String(),
   301  					Mode:          "managed", // drift reporting is only for managed resources
   302  					Name:          addr.Resource.Resource.Name,
   303  					Type:          addr.Resource.Resource.Type,
   304  					ProviderName:  provider.String(),
   305  
   306  					Change: change{
   307  						Actions:         actionString(action.String()),
   308  						Before:          json.RawMessage(before),
   309  						BeforeSensitive: json.RawMessage(beforeSensitive),
   310  						After:           json.RawMessage(after),
   311  						AfterSensitive:  json.RawMessage(afterSensitive),
   312  						// AfterUnknown is never populated here because
   313  						// values in a state are always fully known.
   314  					},
   315  				}
   316  				p.ResourceDrift = append(p.ResourceDrift, change)
   317  			}
   318  		}
   319  	}
   320  
   321  	sort.Slice(p.ResourceChanges, func(i, j int) bool {
   322  		return p.ResourceChanges[i].Address < p.ResourceChanges[j].Address
   323  	})
   324  
   325  	return nil
   326  }
   327  
   328  func (p *plan) marshalResourceChanges(changes *plans.Changes, schemas *terraform.Schemas) error {
   329  	if changes == nil {
   330  		// Nothing to do!
   331  		return nil
   332  	}
   333  	for _, rc := range changes.Resources {
   334  		var r resourceChange
   335  		addr := rc.Addr
   336  		r.Address = addr.String()
   337  
   338  		dataSource := addr.Resource.Resource.Mode == addrs.DataResourceMode
   339  		// We create "delete" actions for data resources so we can clean up
   340  		// their entries in state, but this is an implementation detail that
   341  		// users shouldn't see.
   342  		if dataSource && rc.Action == plans.Delete {
   343  			continue
   344  		}
   345  
   346  		schema, _ := schemas.ResourceTypeConfig(
   347  			rc.ProviderAddr.Provider,
   348  			addr.Resource.Resource.Mode,
   349  			addr.Resource.Resource.Type,
   350  		)
   351  		if schema == nil {
   352  			return fmt.Errorf("no schema found for %s (in provider %s)", r.Address, rc.ProviderAddr.Provider)
   353  		}
   354  
   355  		changeV, err := rc.Decode(schema.ImpliedType())
   356  		if err != nil {
   357  			return err
   358  		}
   359  		// We drop the marks from the change, as decoding is only an
   360  		// intermediate step to re-encode the values as json
   361  		changeV.Before, _ = changeV.Before.UnmarkDeep()
   362  		changeV.After, _ = changeV.After.UnmarkDeep()
   363  
   364  		var before, after []byte
   365  		var beforeSensitive, afterSensitive []byte
   366  		var afterUnknown cty.Value
   367  
   368  		if changeV.Before != cty.NilVal {
   369  			before, err = ctyjson.Marshal(changeV.Before, changeV.Before.Type())
   370  			if err != nil {
   371  				return err
   372  			}
   373  			marks := rc.BeforeValMarks
   374  			if schema.ContainsSensitive() {
   375  				marks = append(marks, schema.ValueMarks(changeV.Before, nil)...)
   376  			}
   377  			bs := jsonstate.SensitiveAsBool(changeV.Before.MarkWithPaths(marks))
   378  			beforeSensitive, err = ctyjson.Marshal(bs, bs.Type())
   379  			if err != nil {
   380  				return err
   381  			}
   382  		}
   383  		if changeV.After != cty.NilVal {
   384  			if changeV.After.IsWhollyKnown() {
   385  				after, err = ctyjson.Marshal(changeV.After, changeV.After.Type())
   386  				if err != nil {
   387  					return err
   388  				}
   389  				afterUnknown = cty.EmptyObjectVal
   390  			} else {
   391  				filteredAfter := omitUnknowns(changeV.After)
   392  				if filteredAfter.IsNull() {
   393  					after = nil
   394  				} else {
   395  					after, err = ctyjson.Marshal(filteredAfter, filteredAfter.Type())
   396  					if err != nil {
   397  						return err
   398  					}
   399  				}
   400  				afterUnknown = unknownAsBool(changeV.After)
   401  			}
   402  			marks := rc.AfterValMarks
   403  			if schema.ContainsSensitive() {
   404  				marks = append(marks, schema.ValueMarks(changeV.After, nil)...)
   405  			}
   406  			as := jsonstate.SensitiveAsBool(changeV.After.MarkWithPaths(marks))
   407  			afterSensitive, err = ctyjson.Marshal(as, as.Type())
   408  			if err != nil {
   409  				return err
   410  			}
   411  		}
   412  
   413  		a, err := ctyjson.Marshal(afterUnknown, afterUnknown.Type())
   414  		if err != nil {
   415  			return err
   416  		}
   417  		replacePaths, err := encodePaths(rc.RequiredReplace)
   418  		if err != nil {
   419  			return err
   420  		}
   421  
   422  		r.Change = change{
   423  			Actions:         actionString(rc.Action.String()),
   424  			Before:          json.RawMessage(before),
   425  			After:           json.RawMessage(after),
   426  			AfterUnknown:    a,
   427  			BeforeSensitive: json.RawMessage(beforeSensitive),
   428  			AfterSensitive:  json.RawMessage(afterSensitive),
   429  			ReplacePaths:    replacePaths,
   430  		}
   431  
   432  		if rc.DeposedKey != states.NotDeposed {
   433  			r.Deposed = rc.DeposedKey.String()
   434  		}
   435  
   436  		key := addr.Resource.Key
   437  		if key != nil {
   438  			r.Index = key
   439  		}
   440  
   441  		switch addr.Resource.Resource.Mode {
   442  		case addrs.ManagedResourceMode:
   443  			r.Mode = "managed"
   444  		case addrs.DataResourceMode:
   445  			r.Mode = "data"
   446  		default:
   447  			return fmt.Errorf("resource %s has an unsupported mode %s", r.Address, addr.Resource.Resource.Mode.String())
   448  		}
   449  		r.ModuleAddress = addr.Module.String()
   450  		r.Name = addr.Resource.Resource.Name
   451  		r.Type = addr.Resource.Resource.Type
   452  		r.ProviderName = rc.ProviderAddr.Provider.String()
   453  
   454  		switch rc.ActionReason {
   455  		case plans.ResourceInstanceChangeNoReason:
   456  			r.ActionReason = "" // will be omitted in output
   457  		case plans.ResourceInstanceReplaceBecauseCannotUpdate:
   458  			r.ActionReason = "replace_because_cannot_update"
   459  		case plans.ResourceInstanceReplaceBecauseTainted:
   460  			r.ActionReason = "replace_because_tainted"
   461  		case plans.ResourceInstanceReplaceByRequest:
   462  			r.ActionReason = "replace_by_request"
   463  		default:
   464  			return fmt.Errorf("resource %s has an unsupported action reason %s", r.Address, rc.ActionReason)
   465  		}
   466  
   467  		p.ResourceChanges = append(p.ResourceChanges, r)
   468  
   469  	}
   470  
   471  	sort.Slice(p.ResourceChanges, func(i, j int) bool {
   472  		return p.ResourceChanges[i].Address < p.ResourceChanges[j].Address
   473  	})
   474  
   475  	return nil
   476  }
   477  
   478  func (p *plan) marshalOutputChanges(changes *plans.Changes) error {
   479  	if changes == nil {
   480  		// Nothing to do!
   481  		return nil
   482  	}
   483  
   484  	p.OutputChanges = make(map[string]change, len(changes.Outputs))
   485  	for _, oc := range changes.Outputs {
   486  		changeV, err := oc.Decode()
   487  		if err != nil {
   488  			return err
   489  		}
   490  		// We drop the marks from the change, as decoding is only an
   491  		// intermediate step to re-encode the values as json
   492  		changeV.Before, _ = changeV.Before.UnmarkDeep()
   493  		changeV.After, _ = changeV.After.UnmarkDeep()
   494  
   495  		var before, after []byte
   496  		afterUnknown := cty.False
   497  		if changeV.Before != cty.NilVal {
   498  			before, err = ctyjson.Marshal(changeV.Before, changeV.Before.Type())
   499  			if err != nil {
   500  				return err
   501  			}
   502  		}
   503  		if changeV.After != cty.NilVal {
   504  			if changeV.After.IsWhollyKnown() {
   505  				after, err = ctyjson.Marshal(changeV.After, changeV.After.Type())
   506  				if err != nil {
   507  					return err
   508  				}
   509  			} else {
   510  				afterUnknown = cty.True
   511  			}
   512  		}
   513  
   514  		// The only information we have in the plan about output sensitivity is
   515  		// a boolean which is true if the output was or is marked sensitive. As
   516  		// a result, BeforeSensitive and AfterSensitive will be identical, and
   517  		// either false or true.
   518  		outputSensitive := cty.False
   519  		if oc.Sensitive {
   520  			outputSensitive = cty.True
   521  		}
   522  		sensitive, err := ctyjson.Marshal(outputSensitive, outputSensitive.Type())
   523  		if err != nil {
   524  			return err
   525  		}
   526  
   527  		a, _ := ctyjson.Marshal(afterUnknown, afterUnknown.Type())
   528  
   529  		c := change{
   530  			Actions:         actionString(oc.Action.String()),
   531  			Before:          json.RawMessage(before),
   532  			After:           json.RawMessage(after),
   533  			AfterUnknown:    a,
   534  			BeforeSensitive: json.RawMessage(sensitive),
   535  			AfterSensitive:  json.RawMessage(sensitive),
   536  		}
   537  
   538  		p.OutputChanges[oc.Addr.OutputValue.Name] = c
   539  	}
   540  
   541  	return nil
   542  }
   543  
   544  func (p *plan) marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) error {
   545  	// marshal the planned changes into a module
   546  	plan, err := marshalPlannedValues(changes, schemas)
   547  	if err != nil {
   548  		return err
   549  	}
   550  	p.PlannedValues.RootModule = plan
   551  
   552  	// marshalPlannedOutputs
   553  	outputs, err := marshalPlannedOutputs(changes)
   554  	if err != nil {
   555  		return err
   556  	}
   557  	p.PlannedValues.Outputs = outputs
   558  
   559  	return nil
   560  }
   561  
   562  // omitUnknowns recursively walks the src cty.Value and returns a new cty.Value,
   563  // omitting any unknowns.
   564  //
   565  // The result also normalizes some types: all sequence types are turned into
   566  // tuple types and all mapping types are converted to object types, since we
   567  // assume the result of this is just going to be serialized as JSON (and thus
   568  // lose those distinctions) anyway.
   569  func omitUnknowns(val cty.Value) cty.Value {
   570  	ty := val.Type()
   571  	switch {
   572  	case val.IsNull():
   573  		return val
   574  	case !val.IsKnown():
   575  		return cty.NilVal
   576  	case ty.IsPrimitiveType():
   577  		return val
   578  	case ty.IsListType() || ty.IsTupleType() || ty.IsSetType():
   579  		var vals []cty.Value
   580  		it := val.ElementIterator()
   581  		for it.Next() {
   582  			_, v := it.Element()
   583  			newVal := omitUnknowns(v)
   584  			if newVal != cty.NilVal {
   585  				vals = append(vals, newVal)
   586  			} else if newVal == cty.NilVal && ty.IsListType() {
   587  				// list length may be significant, so we will turn unknowns into nulls
   588  				vals = append(vals, cty.NullVal(v.Type()))
   589  			}
   590  		}
   591  		// We use tuple types always here, because the work we did above
   592  		// may have caused the individual elements to have different types,
   593  		// and we're doing this work to produce JSON anyway and JSON marshalling
   594  		// represents all of these sequence types as an array.
   595  		return cty.TupleVal(vals)
   596  	case ty.IsMapType() || ty.IsObjectType():
   597  		vals := make(map[string]cty.Value)
   598  		it := val.ElementIterator()
   599  		for it.Next() {
   600  			k, v := it.Element()
   601  			newVal := omitUnknowns(v)
   602  			if newVal != cty.NilVal {
   603  				vals[k.AsString()] = newVal
   604  			}
   605  		}
   606  		// We use object types always here, because the work we did above
   607  		// may have caused the individual elements to have different types,
   608  		// and we're doing this work to produce JSON anyway and JSON marshalling
   609  		// represents both of these mapping types as an object.
   610  		return cty.ObjectVal(vals)
   611  	default:
   612  		// Should never happen, since the above should cover all types
   613  		panic(fmt.Sprintf("omitUnknowns cannot handle %#v", val))
   614  	}
   615  }
   616  
   617  // recursively iterate through a cty.Value, replacing unknown values (including
   618  // null) with cty.True and known values with cty.False.
   619  //
   620  // The result also normalizes some types: all sequence types are turned into
   621  // tuple types and all mapping types are converted to object types, since we
   622  // assume the result of this is just going to be serialized as JSON (and thus
   623  // lose those distinctions) anyway.
   624  //
   625  // For map/object values, all known attribute values will be omitted instead of
   626  // returning false, as this results in a more compact serialization.
   627  func unknownAsBool(val cty.Value) cty.Value {
   628  	ty := val.Type()
   629  	switch {
   630  	case val.IsNull():
   631  		return cty.False
   632  	case !val.IsKnown():
   633  		if ty.IsPrimitiveType() || ty.Equals(cty.DynamicPseudoType) {
   634  			return cty.True
   635  		}
   636  		fallthrough
   637  	case ty.IsPrimitiveType():
   638  		return cty.BoolVal(!val.IsKnown())
   639  	case ty.IsListType() || ty.IsTupleType() || ty.IsSetType():
   640  		length := val.LengthInt()
   641  		if length == 0 {
   642  			// If there are no elements then we can't have unknowns
   643  			return cty.EmptyTupleVal
   644  		}
   645  		vals := make([]cty.Value, 0, length)
   646  		it := val.ElementIterator()
   647  		for it.Next() {
   648  			_, v := it.Element()
   649  			vals = append(vals, unknownAsBool(v))
   650  		}
   651  		// The above transform may have changed the types of some of the
   652  		// elements, so we'll always use a tuple here in case we've now made
   653  		// different elements have different types. Our ultimate goal is to
   654  		// marshal to JSON anyway, and all of these sequence types are
   655  		// indistinguishable in JSON.
   656  		return cty.TupleVal(vals)
   657  	case ty.IsMapType() || ty.IsObjectType():
   658  		var length int
   659  		switch {
   660  		case ty.IsMapType():
   661  			length = val.LengthInt()
   662  		default:
   663  			length = len(val.Type().AttributeTypes())
   664  		}
   665  		if length == 0 {
   666  			// If there are no elements then we can't have unknowns
   667  			return cty.EmptyObjectVal
   668  		}
   669  		vals := make(map[string]cty.Value)
   670  		it := val.ElementIterator()
   671  		for it.Next() {
   672  			k, v := it.Element()
   673  			vAsBool := unknownAsBool(v)
   674  			// Omit all of the "false"s for known values for more compact
   675  			// serialization
   676  			if !vAsBool.RawEquals(cty.False) {
   677  				vals[k.AsString()] = unknownAsBool(v)
   678  			}
   679  		}
   680  		// The above transform may have changed the types of some of the
   681  		// elements, so we'll always use an object here in case we've now made
   682  		// different elements have different types. Our ultimate goal is to
   683  		// marshal to JSON anyway, and all of these mapping types are
   684  		// indistinguishable in JSON.
   685  		return cty.ObjectVal(vals)
   686  	default:
   687  		// Should never happen, since the above should cover all types
   688  		panic(fmt.Sprintf("unknownAsBool cannot handle %#v", val))
   689  	}
   690  }
   691  
   692  func actionString(action string) []string {
   693  	switch {
   694  	case action == "NoOp":
   695  		return []string{"no-op"}
   696  	case action == "Create":
   697  		return []string{"create"}
   698  	case action == "Delete":
   699  		return []string{"delete"}
   700  	case action == "Update":
   701  		return []string{"update"}
   702  	case action == "CreateThenDelete":
   703  		return []string{"create", "delete"}
   704  	case action == "Read":
   705  		return []string{"read"}
   706  	case action == "DeleteThenCreate":
   707  		return []string{"delete", "create"}
   708  	default:
   709  		return []string{action}
   710  	}
   711  }
   712  
   713  // encodePaths lossily encodes a cty.PathSet into an array of arrays of step
   714  // values, such as:
   715  //
   716  //   [["length"],["triggers",0,"value"]]
   717  //
   718  // The lossiness is that we cannot distinguish between an IndexStep with string
   719  // key and a GetAttr step. This is fine with JSON output, because JSON's type
   720  // system means that those two steps are equivalent anyway: both are object
   721  // indexes.
   722  //
   723  // JavaScript (or similar dynamic language) consumers of these values can
   724  // recursively apply the steps to a given object using an index operation for
   725  // each step.
   726  func encodePaths(pathSet cty.PathSet) (json.RawMessage, error) {
   727  	if pathSet.Empty() {
   728  		return nil, nil
   729  	}
   730  
   731  	pathList := pathSet.List()
   732  	jsonPaths := make([]json.RawMessage, 0, len(pathList))
   733  
   734  	for _, path := range pathList {
   735  		steps := make([]json.RawMessage, 0, len(path))
   736  		for _, step := range path {
   737  			switch s := step.(type) {
   738  			case cty.IndexStep:
   739  				key, err := ctyjson.Marshal(s.Key, s.Key.Type())
   740  				if err != nil {
   741  					return nil, fmt.Errorf("Failed to marshal index step key %#v: %s", s.Key, err)
   742  				}
   743  				steps = append(steps, key)
   744  			case cty.GetAttrStep:
   745  				name, err := json.Marshal(s.Name)
   746  				if err != nil {
   747  					return nil, fmt.Errorf("Failed to marshal get attr step name %#v: %s", s.Name, err)
   748  				}
   749  				steps = append(steps, name)
   750  			default:
   751  				return nil, fmt.Errorf("Unsupported path step %#v (%t)", step, step)
   752  			}
   753  		}
   754  		jsonPath, err := json.Marshal(steps)
   755  		if err != nil {
   756  			return nil, err
   757  		}
   758  		jsonPaths = append(jsonPaths, jsonPath)
   759  	}
   760  
   761  	return json.Marshal(jsonPaths)
   762  }