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