| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package jsonplan | |
| import ( | |
| "encoding/json" | |
| "fmt" | |
| "sort" | |
| "strings" | |
| "time" | |
| "github.com/zclconf/go-cty/cty" | |
| ctyjson "github.com/zclconf/go-cty/cty/json" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/command/jsonchecks" | |
| "github.com/hashicorp/terraform/internal/command/jsonconfig" | |
| "github.com/hashicorp/terraform/internal/command/jsonstate" | |
| "github.com/hashicorp/terraform/internal/configs" | |
| "github.com/hashicorp/terraform/internal/lang/marks" | |
| "github.com/hashicorp/terraform/internal/plans" | |
| "github.com/hashicorp/terraform/internal/providers" | |
| "github.com/hashicorp/terraform/internal/states" | |
| "github.com/hashicorp/terraform/internal/states/statefile" | |
| "github.com/hashicorp/terraform/internal/terraform" | |
| "github.com/hashicorp/terraform/version" | |
| ) | |
| // FormatVersion represents the version of the json format and will be | |
| // incremented for any change to this format that requires changes to a | |
| // consuming parser. | |
| const ( | |
| FormatVersion = "1.2" | |
| ResourceInstanceReplaceBecauseCannotUpdate = "replace_because_cannot_update" | |
| ResourceInstanceReplaceBecauseTainted = "replace_because_tainted" | |
| ResourceInstanceReplaceByRequest = "replace_by_request" | |
| ResourceInstanceReplaceByTriggers = "replace_by_triggers" | |
| ResourceInstanceDeleteBecauseNoResourceConfig = "delete_because_no_resource_config" | |
| ResourceInstanceDeleteBecauseWrongRepetition = "delete_because_wrong_repetition" | |
| ResourceInstanceDeleteBecauseCountIndex = "delete_because_count_index" | |
| ResourceInstanceDeleteBecauseEachKey = "delete_because_each_key" | |
| ResourceInstanceDeleteBecauseNoModule = "delete_because_no_module" | |
| ResourceInstanceDeleteBecauseNoMoveTarget = "delete_because_no_move_target" | |
| ResourceInstanceReadBecauseConfigUnknown = "read_because_config_unknown" | |
| ResourceInstanceReadBecauseDependencyPending = "read_because_dependency_pending" | |
| ResourceInstanceReadBecauseCheckNested = "read_because_check_nested" | |
| DeferredReasonUnknown = "unknown" | |
| DeferredReasonInstanceCountUnknown = "instance_count_unknown" | |
| DeferredReasonResourceConfigUnknown = "resource_config_unknown" | |
| DeferredReasonProviderConfigUnknown = "provider_config_unknown" | |
| DeferredReasonDeferredPrereq = "deferred_prereq" | |
| DeferredReasonAbsentPrereq = "absent_prereq" | |
| ) | |
| // plan is the top-level representation of the json format of a plan. It includes | |
| // the complete config and current state. | |
| type plan struct { | |
| FormatVersion string `json:"format_version,omitempty"` | |
| TerraformVersion string `json:"terraform_version,omitempty"` | |
| Variables variables `json:"variables,omitempty"` | |
| PlannedValues stateValues `json:"planned_values,omitempty"` | |
| // ResourceDrift and ResourceChanges are sorted in a user-friendly order | |
| // that is undefined at this time, but consistent. | |
| ResourceDrift []ResourceChange `json:"resource_drift,omitempty"` | |
| ResourceChanges []ResourceChange `json:"resource_changes,omitempty"` | |
| DeferredChanges []DeferredResourceChange `json:"deferred_changes,omitempty"` | |
| DeferredActionInvocations []DeferredActionInvocation `json:"deferred_action_invocations,omitempty"` | |
| OutputChanges map[string]Change `json:"output_changes,omitempty"` | |
| ActionInvocations []ActionInvocation `json:"action_invocations,omitempty"` | |
| PriorState json.RawMessage `json:"prior_state,omitempty"` | |
| Config json.RawMessage `json:"configuration,omitempty"` | |
| RelevantAttributes []ResourceAttr `json:"relevant_attributes,omitempty"` | |
| Checks json.RawMessage `json:"checks,omitempty"` | |
| Timestamp string `json:"timestamp,omitempty"` | |
| Applyable bool `json:"applyable"` | |
| Complete bool `json:"complete"` | |
| Errored bool `json:"errored"` | |
| } | |
| func newPlan() *plan { | |
| return &plan{ | |
| FormatVersion: FormatVersion, | |
| } | |
| } | |
| // ResourceAttr contains the address and attribute of an external for the | |
| // RelevantAttributes in the plan. | |
| type ResourceAttr struct { | |
| Resource string `json:"resource"` | |
| Attr json.RawMessage `json:"attribute"` | |
| } | |
| // Change is the representation of a proposed change for an object. | |
| type Change struct { | |
| // Actions are the actions that will be taken on the object selected by the | |
| // properties below. Valid actions values are: | |
| // ["no-op"] | |
| // ["create"] | |
| // ["read"] | |
| // ["update"] | |
| // ["delete", "create"] (replace) | |
| // ["create", "delete"] (replace) | |
| // ["delete"] | |
| // ["forget"] | |
| // ["create", "forget"] (replace) | |
| // The three "replace" actions are represented in this way to allow callers | |
| // to, e.g., just scan the list for "delete" to recognize all three | |
| // situations where the object will be deleted, allowing for any new | |
| // deletion combinations that might be added in future. | |
| Actions []string `json:"actions,omitempty"` | |
| // Before and After are representations of the object value both before and | |
| // after the action. For ["create"] and ["delete"]/["forget"] actions, | |
| // either "before" or "after" is unset (respectively). For ["no-op"], the | |
| // before and after values are identical. The "after" value will be | |
| // incomplete if there are values within it that won't be known until after | |
| // apply. | |
| Before json.RawMessage `json:"before,omitempty"` | |
| After json.RawMessage `json:"after,omitempty"` | |
| // AfterUnknown is an object value with similar structure to After, but | |
| // with all unknown leaf values replaced with true, and all known leaf | |
| // values omitted. This can be combined with After to reconstruct a full | |
| // value after the action, including values which will only be known after | |
| // apply. | |
| AfterUnknown json.RawMessage `json:"after_unknown,omitempty"` | |
| // BeforeSensitive and AfterSensitive are object values with similar | |
| // structure to Before and After, but with all sensitive leaf values | |
| // replaced with true, and all non-sensitive leaf values omitted. These | |
| // objects should be combined with Before and After to prevent accidental | |
| // display of sensitive values in user interfaces. | |
| BeforeSensitive json.RawMessage `json:"before_sensitive,omitempty"` | |
| AfterSensitive json.RawMessage `json:"after_sensitive,omitempty"` | |
| // ReplacePaths is an array of arrays representing a set of paths into the | |
| // object value which resulted in the action being "replace". This will be | |
| // omitted if the action is not replace, or if no paths caused the | |
| // replacement (for example, if the resource was tainted). Each path | |
| // consists of one or more steps, each of which will be a number or a | |
| // string. | |
| ReplacePaths json.RawMessage `json:"replace_paths,omitempty"` | |
| // Importing contains the import metadata about this operation. If importing | |
| // is present (ie. not null) then the change is an import operation in | |
| // addition to anything mentioned in the actions field. The actual contents | |
| // of the Importing struct is subject to change, so downstream consumers | |
| // should treat any values in here as strictly optional. | |
| Importing *Importing `json:"importing,omitempty"` | |
| // GeneratedConfig contains any HCL config generated for this resource | |
| // during planning as a string. | |
| // | |
| // If this is populated, then Importing should also be populated but this | |
| // might change in the future. However, not all Importing changes will | |
| // contain generated config. | |
| GeneratedConfig string `json:"generated_config,omitempty"` | |
| // BeforeIdentity and AfterIdentity are representations of the resource | |
| // identity value both before and after the action. | |
| BeforeIdentity json.RawMessage `json:"before_identity,omitempty"` | |
| AfterIdentity json.RawMessage `json:"after_identity,omitempty"` | |
| } | |
| // Importing is a nested object for the resource import metadata. | |
| type Importing struct { | |
| // The original ID of this resource used to target it as part of planned | |
| // import operation. | |
| ID string `json:"id,omitempty"` | |
| // Unknown indicates the ID was unknown at the time of planning. This | |
| // would have led to the overall change being deferred, as such this should | |
| // only be true when processing changes from the deferred changes list. | |
| Unknown bool `json:"unknown,omitempty"` | |
| // The identity can be used instead of the ID to target the resource as part | |
| // of the planned import operation. | |
| Identity json.RawMessage `json:"identity,omitempty"` | |
| } | |
| type output struct { | |
| Sensitive bool `json:"sensitive"` | |
| Type json.RawMessage `json:"type,omitempty"` | |
| Value json.RawMessage `json:"value,omitempty"` | |
| } | |
| // variables is the JSON representation of the variables provided to the current | |
| // plan. | |
| type variables map[string]*variable | |
| type variable struct { | |
| Value json.RawMessage `json:"value,omitempty"` | |
| } | |
| // MarshalForRenderer returns the pre-json encoding changes of the requested | |
| // plan, in a format available to the structured renderer. | |
| // | |
| // This function does a small part of the Marshal function, as it only returns | |
| // the part of the plan required by the jsonformat.Plan renderer. | |
| func MarshalForRenderer( | |
| p *plans.Plan, | |
| schemas *terraform.Schemas, | |
| ) (map[string]Change, []ResourceChange, []ResourceChange, []ResourceAttr, []ActionInvocation, error) { | |
| output := newPlan() | |
| var err error | |
| if output.OutputChanges, err = MarshalOutputChanges(p.Changes); err != nil { | |
| return nil, nil, nil, nil, nil, err | |
| } | |
| if output.ResourceChanges, err = MarshalResourceChanges(p.Changes.Resources, schemas); err != nil { | |
| return nil, nil, nil, nil, nil, err | |
| } | |
| if len(p.DriftedResources) > 0 { | |
| // In refresh-only mode, we render all resources marked as drifted, | |
| // including those which have moved without other changes. In other plan | |
| // modes, move-only changes will be included in the planned changes, so | |
| // we skip them here. | |
| var driftedResources []*plans.ResourceInstanceChangeSrc | |
| if p.UIMode == plans.RefreshOnlyMode { | |
| driftedResources = p.DriftedResources | |
| } else { | |
| for _, dr := range p.DriftedResources { | |
| if dr.Action != plans.NoOp { | |
| driftedResources = append(driftedResources, dr) | |
| } | |
| } | |
| } | |
| output.ResourceDrift, err = MarshalResourceChanges(driftedResources, schemas) | |
| if err != nil { | |
| return nil, nil, nil, nil, nil, err | |
| } | |
| } | |
| if err := output.marshalRelevantAttrs(p); err != nil { | |
| return nil, nil, nil, nil, nil, err | |
| } | |
| if output.ActionInvocations, err = MarshalActionInvocations(p.Changes.ActionInvocations, schemas); err != nil { | |
| return nil, nil, nil, nil, nil, err | |
| } | |
| return output.OutputChanges, output.ResourceChanges, output.ResourceDrift, output.RelevantAttributes, output.ActionInvocations, nil | |
| } | |
| // Marshal returns the json encoding of a terraform plan. | |
| func Marshal( | |
| config *configs.Config, | |
| p *plans.Plan, | |
| sf *statefile.File, | |
| schemas *terraform.Schemas, | |
| ) ([]byte, error) { | |
| output := newPlan() | |
| output.TerraformVersion = version.String() | |
| output.Timestamp = p.Timestamp.Format(time.RFC3339) | |
| output.Applyable = p.Applyable | |
| output.Complete = p.Complete | |
| output.Errored = p.Errored | |
| err := output.marshalPlanVariables(p.VariableValues, config.Module.Variables) | |
| if err != nil { | |
| return nil, fmt.Errorf("error in marshalPlanVariables: %s", err) | |
| } | |
| // output.PlannedValues | |
| err = output.marshalPlannedValues(p.Changes, schemas) | |
| if err != nil { | |
| return nil, fmt.Errorf("error in marshalPlannedValues: %s", err) | |
| } | |
| // output.ResourceDrift | |
| if len(p.DriftedResources) > 0 { | |
| // In refresh-only mode, we render all resources marked as drifted, | |
| // including those which have moved without other changes. In other plan | |
| // modes, move-only changes will be included in the planned changes, so | |
| // we skip them here. | |
| var driftedResources []*plans.ResourceInstanceChangeSrc | |
| if p.UIMode == plans.RefreshOnlyMode { | |
| driftedResources = p.DriftedResources | |
| } else { | |
| for _, dr := range p.DriftedResources { | |
| if dr.Action != plans.NoOp { | |
| driftedResources = append(driftedResources, dr) | |
| } | |
| } | |
| } | |
| output.ResourceDrift, err = MarshalResourceChanges(driftedResources, schemas) | |
| if err != nil { | |
| return nil, fmt.Errorf("error in marshaling resource drift: %s", err) | |
| } | |
| } | |
| if err := output.marshalRelevantAttrs(p); err != nil { | |
| return nil, fmt.Errorf("error marshaling relevant attributes for external changes: %s", err) | |
| } | |
| // output.ResourceChanges | |
| if p.Changes != nil { | |
| output.ResourceChanges, err = MarshalResourceChanges(p.Changes.Resources, schemas) | |
| if err != nil { | |
| return nil, fmt.Errorf("error in marshaling resource changes: %s", err) | |
| } | |
| } | |
| if p.DeferredResources != nil { | |
| output.DeferredChanges, err = MarshalDeferredResourceChanges(p.DeferredResources, schemas) | |
| if err != nil { | |
| return nil, fmt.Errorf("error in marshaling deferred resource changes: %s", err) | |
| } | |
| } | |
| if p.DeferredActionInvocations != nil { | |
| output.DeferredActionInvocations, err = MarshalDeferredActionInvocations(p.DeferredActionInvocations, schemas) | |
| if err != nil { | |
| return nil, fmt.Errorf("error in marshaling deferred action invocations: %s", err) | |
| } | |
| } | |
| // output.OutputChanges | |
| if output.OutputChanges, err = MarshalOutputChanges(p.Changes); err != nil { | |
| return nil, fmt.Errorf("error in marshaling output changes: %s", err) | |
| } | |
| // output.Checks | |
| if p.Checks != nil && p.Checks.ConfigResults.Len() > 0 { | |
| output.Checks = jsonchecks.MarshalCheckStates(p.Checks) | |
| } | |
| // output.PriorState | |
| if sf != nil && !sf.State.Empty() { | |
| output.PriorState, err = jsonstate.Marshal(sf, schemas) | |
| if err != nil { | |
| return nil, fmt.Errorf("error marshaling prior state: %s", err) | |
| } | |
| } | |
| // output.Config | |
| output.Config, err = jsonconfig.Marshal(config, schemas) | |
| if err != nil { | |
| return nil, fmt.Errorf("error marshaling config: %s", err) | |
| } | |
| // output.Changes.ActionInvocations | |
| if p.Changes.ActionInvocations != nil { | |
| if output.ActionInvocations, err = MarshalActionInvocations(p.Changes.ActionInvocations, schemas); err != nil { | |
| return nil, fmt.Errorf("error marshaling action invocations: %s", err) | |
| } | |
| } | |
| return json.Marshal(output) | |
| } | |
| func (p *plan) marshalPlanVariables(vars map[string]plans.DynamicValue, decls map[string]*configs.Variable) error { | |
| p.Variables = make(variables, len(vars)) | |
| for k, v := range vars { | |
| val, err := v.Decode(cty.DynamicPseudoType) | |
| if err != nil { | |
| return err | |
| } | |
| valJSON, err := ctyjson.Marshal(val, val.Type()) | |
| if err != nil { | |
| return err | |
| } | |
| p.Variables[k] = &variable{ | |
| Value: valJSON, | |
| } | |
| } | |
| // In Terraform v1.1 and earlier we had some confusion about which subsystem | |
| // of Terraform was the one responsible for substituting in default values | |
| // for unset module variables, with root module variables being handled in | |
| // three different places while child module variables were only handled | |
| // during the Terraform Core graph walk. | |
| // | |
| // For Terraform v1.2 and later we rationalized that by having the Terraform | |
| // Core graph walk always be responsible for selecting defaults regardless | |
| // of root vs. child module, but unfortunately our earlier accidental | |
| // misbehavior bled out into the public interface by making the defaults | |
| // show up in the "vars" map to this function. Those are now correctly | |
| // omitted (so that the plan file only records the variables _actually_ | |
| // set by the caller) but consumers of the JSON plan format may be depending | |
| // on our old behavior and so we'll fake it here just in time so that | |
| // outside consumers won't see a behavior change. | |
| for name, decl := range decls { | |
| if _, ok := p.Variables[name]; ok { | |
| continue | |
| } | |
| if val := decl.Default; val != cty.NilVal { | |
| valJSON, err := ctyjson.Marshal(val, val.Type()) | |
| if err != nil { | |
| return err | |
| } | |
| p.Variables[name] = &variable{ | |
| Value: valJSON, | |
| } | |
| } | |
| } | |
| if len(p.Variables) == 0 { | |
| p.Variables = nil // omit this property if there are no variables to describe | |
| } | |
| return nil | |
| } | |
| // MarshalResourceChanges converts the provided internal representation of | |
| // ResourceInstanceChangeSrc objects into the public structured JSON changes. | |
| // | |
| // This function is referenced directly from the structured renderer tests, to | |
| // ensure parity between the renderers. It probably shouldn't be used anywhere | |
| // else. | |
| func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schemas *terraform.Schemas) ([]ResourceChange, error) { | |
| var ret []ResourceChange | |
| var sortedResources []*plans.ResourceInstanceChangeSrc | |
| sortedResources = append(sortedResources, resources...) | |
| sort.Slice(sortedResources, func(i, j int) bool { | |
| if !sortedResources[i].Addr.Equal(sortedResources[j].Addr) { | |
| return sortedResources[i].Addr.Less(sortedResources[j].Addr) | |
| } | |
| return sortedResources[i].DeposedKey < sortedResources[j].DeposedKey | |
| }) | |
| for _, rc := range sortedResources { | |
| dataSource := rc.Addr.Resource.Resource.Mode == addrs.DataResourceMode | |
| // We create "delete" actions for data resources so we can clean up | |
| // their entries in state, but this is an implementation detail that | |
| // users shouldn't see. | |
| if dataSource && rc.Action == plans.Delete { | |
| continue | |
| } | |
| r, err := marshalResourceChange(rc, schemas) | |
| if err != nil { | |
| return nil, err | |
| } | |
| ret = append(ret, r) | |
| } | |
| return ret, nil | |
| } | |
| func marshalResourceChange(rc *plans.ResourceInstanceChangeSrc, schemas *terraform.Schemas) (ResourceChange, error) { | |
| var r ResourceChange | |
| addr := rc.Addr | |
| r.Address = addr.String() | |
| if !addr.Equal(rc.PrevRunAddr) { | |
| r.PreviousAddress = rc.PrevRunAddr.String() | |
| } | |
| schema := schemas.ResourceTypeConfig( | |
| rc.ProviderAddr.Provider, | |
| addr.Resource.Resource.Mode, | |
| addr.Resource.Resource.Type, | |
| ) | |
| if schema.Body == nil { | |
| return r, fmt.Errorf("no schema found for %s (in provider %s)", r.Address, rc.ProviderAddr.Provider) | |
| } | |
| changeV, err := rc.Decode(schema) | |
| if err != nil { | |
| return r, err | |
| } | |
| // We drop the marks from the change, as decoding is only an | |
| // intermediate step to re-encode the values as json | |
| changeV.Before, _ = changeV.Before.UnmarkDeep() | |
| changeV.After, _ = changeV.After.UnmarkDeep() | |
| var before, after []byte | |
| var beforeSensitive, afterSensitive []byte | |
| var afterUnknown cty.Value | |
| if changeV.Before != cty.NilVal { | |
| before, err = ctyjson.Marshal(changeV.Before, changeV.Before.Type()) | |
| if err != nil { | |
| return r, err | |
| } | |
| sensitivePaths := rc.BeforeSensitivePaths | |
| sensitivePaths = append(sensitivePaths, schema.Body.SensitivePaths(changeV.Before, nil)...) | |
| bs := jsonstate.SensitiveAsBool(marks.MarkPaths(changeV.Before, marks.Sensitive, sensitivePaths)) | |
| beforeSensitive, err = ctyjson.Marshal(bs, bs.Type()) | |
| if err != nil { | |
| return r, err | |
| } | |
| } | |
| if changeV.After != cty.NilVal { | |
| if changeV.After.IsWhollyKnown() { | |
| after, err = ctyjson.Marshal(changeV.After, changeV.After.Type()) | |
| if err != nil { | |
| return r, err | |
| } | |
| afterUnknown = cty.EmptyObjectVal | |
| } else { | |
| filteredAfter := omitUnknowns(changeV.After) | |
| if filteredAfter.IsNull() { | |
| after = nil | |
| } else { | |
| after, err = ctyjson.Marshal(filteredAfter, filteredAfter.Type()) | |
| if err != nil { | |
| return r, err | |
| } | |
| } | |
| afterUnknown = unknownAsBool(changeV.After) | |
| } | |
| sensitivePaths := rc.AfterSensitivePaths | |
| sensitivePaths = append(sensitivePaths, schema.Body.SensitivePaths(changeV.After, nil)...) | |
| as := jsonstate.SensitiveAsBool(marks.MarkPaths(changeV.After, marks.Sensitive, sensitivePaths)) | |
| afterSensitive, err = ctyjson.Marshal(as, as.Type()) | |
| if err != nil { | |
| return r, err | |
| } | |
| } | |
| a, err := ctyjson.Marshal(afterUnknown, afterUnknown.Type()) | |
| if err != nil { | |
| return r, err | |
| } | |
| replacePaths, err := encodePaths(rc.RequiredReplace) | |
| if err != nil { | |
| return r, err | |
| } | |
| var importing *Importing | |
| if rc.Importing != nil { | |
| if rc.Importing.Unknown { | |
| importing = &Importing{Unknown: true} | |
| } else { | |
| if rc.Importing.ID != "" { | |
| importing = &Importing{ID: rc.Importing.ID} | |
| } else { | |
| identity, err := rc.Importing.Identity.Decode(schema.Identity.ImpliedType()) | |
| if err != nil { | |
| return r, err | |
| } | |
| rawIdentity, err := ctyjson.Marshal(identity, identity.Type()) | |
| if err != nil { | |
| return r, err | |
| } | |
| importing = &Importing{ | |
| Identity: json.RawMessage(rawIdentity), | |
| } | |
| } | |
| } | |
| } | |
| var beforeIdentity, afterIdentity []byte | |
| if schema.Identity != nil && rc.BeforeIdentity != nil { | |
| identity, err := rc.BeforeIdentity.Decode(schema.Identity.ImpliedType()) | |
| if err != nil { | |
| return r, err | |
| } | |
| beforeIdentity, err = ctyjson.Marshal(identity, identity.Type()) | |
| if err != nil { | |
| return r, err | |
| } | |
| } | |
| if schema.Identity != nil && rc.AfterIdentity != nil { | |
| identity, err := rc.AfterIdentity.Decode(schema.Identity.ImpliedType()) | |
| if err != nil { | |
| return r, err | |
| } | |
| afterIdentity, err = ctyjson.Marshal(identity, identity.Type()) | |
| if err != nil { | |
| return r, err | |
| } | |
| } | |
| r.Change = Change{ | |
| Actions: actionString(rc.Action.String()), | |
| Before: json.RawMessage(before), | |
| After: json.RawMessage(after), | |
| AfterUnknown: a, | |
| BeforeSensitive: json.RawMessage(beforeSensitive), | |
| AfterSensitive: json.RawMessage(afterSensitive), | |
| ReplacePaths: replacePaths, | |
| Importing: importing, | |
| GeneratedConfig: rc.GeneratedConfig, | |
| BeforeIdentity: json.RawMessage(beforeIdentity), | |
| AfterIdentity: json.RawMessage(afterIdentity), | |
| } | |
| if rc.DeposedKey != states.NotDeposed { | |
| r.Deposed = rc.DeposedKey.String() | |
| } | |
| key := addr.Resource.Key | |
| if key != nil { | |
| if key == addrs.WildcardKey { | |
| // The wildcard key should only be set for a deferred instance. | |
| r.IndexUnknown = true | |
| } else { | |
| value := key.Value() | |
| if r.Index, err = ctyjson.Marshal(value, value.Type()); err != nil { | |
| return r, err | |
| } | |
| } | |
| } | |
| switch addr.Resource.Resource.Mode { | |
| case addrs.ManagedResourceMode: | |
| r.Mode = jsonstate.ManagedResourceMode | |
| case addrs.DataResourceMode: | |
| r.Mode = jsonstate.DataResourceMode | |
| default: | |
| return r, fmt.Errorf("resource %s has an unsupported mode %s", r.Address, addr.Resource.Resource.Mode.String()) | |
| } | |
| r.ModuleAddress = addr.Module.String() | |
| r.Name = addr.Resource.Resource.Name | |
| r.Type = addr.Resource.Resource.Type | |
| r.ProviderName = rc.ProviderAddr.Provider.String() | |
| switch rc.ActionReason { | |
| case plans.ResourceInstanceChangeNoReason: | |
| r.ActionReason = "" // will be omitted in output | |
| case plans.ResourceInstanceReplaceBecauseCannotUpdate: | |
| r.ActionReason = ResourceInstanceReplaceBecauseCannotUpdate | |
| case plans.ResourceInstanceReplaceBecauseTainted: | |
| r.ActionReason = ResourceInstanceReplaceBecauseTainted | |
| case plans.ResourceInstanceReplaceByRequest: | |
| r.ActionReason = ResourceInstanceReplaceByRequest | |
| case plans.ResourceInstanceReplaceByTriggers: | |
| r.ActionReason = ResourceInstanceReplaceByTriggers | |
| case plans.ResourceInstanceDeleteBecauseNoResourceConfig: | |
| r.ActionReason = ResourceInstanceDeleteBecauseNoResourceConfig | |
| case plans.ResourceInstanceDeleteBecauseWrongRepetition: | |
| r.ActionReason = ResourceInstanceDeleteBecauseWrongRepetition | |
| case plans.ResourceInstanceDeleteBecauseCountIndex: | |
| r.ActionReason = ResourceInstanceDeleteBecauseCountIndex | |
| case plans.ResourceInstanceDeleteBecauseEachKey: | |
| r.ActionReason = ResourceInstanceDeleteBecauseEachKey | |
| case plans.ResourceInstanceDeleteBecauseNoModule: | |
| r.ActionReason = ResourceInstanceDeleteBecauseNoModule | |
| case plans.ResourceInstanceDeleteBecauseNoMoveTarget: | |
| r.ActionReason = ResourceInstanceDeleteBecauseNoMoveTarget | |
| case plans.ResourceInstanceReadBecauseConfigUnknown: | |
| r.ActionReason = ResourceInstanceReadBecauseConfigUnknown | |
| case plans.ResourceInstanceReadBecauseDependencyPending: | |
| r.ActionReason = ResourceInstanceReadBecauseDependencyPending | |
| case plans.ResourceInstanceReadBecauseCheckNested: | |
| r.ActionReason = ResourceInstanceReadBecauseCheckNested | |
| default: | |
| return r, fmt.Errorf("resource %s has an unsupported action reason %s", r.Address, rc.ActionReason) | |
| } | |
| return r, nil | |
| } | |
| // MarshalDeferredResourceChanges converts the provided internal representation | |
| // of DeferredResourceInstanceChangeSrc objects into the public structured JSON | |
| // changes. | |
| // This is public to make testing easier. | |
| func MarshalDeferredResourceChanges(resources []*plans.DeferredResourceInstanceChangeSrc, schemas *terraform.Schemas) ([]DeferredResourceChange, error) { | |
| var ret []DeferredResourceChange | |
| var sortedResources []*plans.DeferredResourceInstanceChangeSrc | |
| sortedResources = append(sortedResources, resources...) | |
| sort.Slice(sortedResources, func(i, j int) bool { | |
| if !sortedResources[i].ChangeSrc.Addr.Equal(sortedResources[j].ChangeSrc.Addr) { | |
| return sortedResources[i].ChangeSrc.Addr.Less(sortedResources[j].ChangeSrc.Addr) | |
| } | |
| return sortedResources[i].ChangeSrc.DeposedKey < sortedResources[j].ChangeSrc.DeposedKey | |
| }) | |
| for _, rc := range sortedResources { | |
| change, err := marshalResourceChange(rc.ChangeSrc, schemas) | |
| if err != nil { | |
| return nil, err | |
| } | |
| deferredChange := DeferredResourceChange{ | |
| ResourceChange: change, | |
| } | |
| switch rc.DeferredReason { | |
| case providers.DeferredReasonInstanceCountUnknown: | |
| deferredChange.Reason = DeferredReasonInstanceCountUnknown | |
| case providers.DeferredReasonResourceConfigUnknown: | |
| deferredChange.Reason = DeferredReasonResourceConfigUnknown | |
| case providers.DeferredReasonProviderConfigUnknown: | |
| deferredChange.Reason = DeferredReasonProviderConfigUnknown | |
| case providers.DeferredReasonAbsentPrereq: | |
| deferredChange.Reason = DeferredReasonAbsentPrereq | |
| case providers.DeferredReasonDeferredPrereq: | |
| deferredChange.Reason = DeferredReasonDeferredPrereq | |
| default: | |
| // If we find a reason we don't know about, we'll just mark it as | |
| // unknown. This is a bit of a safety net to ensure that we don't | |
| // break if new reasons are introduced in future versions of the | |
| // provider protocol. | |
| deferredChange.Reason = DeferredReasonUnknown | |
| } | |
| ret = append(ret, deferredChange) | |
| } | |
| return ret, nil | |
| } | |
| // MarshalOutputChanges converts the provided internal representation of | |
| // Changes objects into the structured JSON representation. | |
| // | |
| // This function is referenced directly from the structured renderer tests, to | |
| // ensure parity between the renderers. It probably shouldn't be used anywhere | |
| // else. | |
| func MarshalOutputChanges(changes *plans.ChangesSrc) (map[string]Change, error) { | |
| if changes == nil { | |
| // Nothing to do! | |
| return nil, nil | |
| } | |
| outputChanges := make(map[string]Change, len(changes.Outputs)) | |
| for _, oc := range changes.Outputs { | |
| // Skip output changes that are not from the root module. | |
| // These are automatically stripped from plans that are written to disk | |
| // elsewhere, we just need to duplicate the logic here in case anyone | |
| // is converting this plan directly from memory. | |
| if !oc.Addr.Module.IsRoot() { | |
| continue | |
| } | |
| changeV, err := oc.Decode() | |
| if err != nil { | |
| return nil, err | |
| } | |
| // We drop the marks from the change, as decoding is only an | |
| // intermediate step to re-encode the values as json | |
| changeV.Before, _ = changeV.Before.UnmarkDeep() | |
| changeV.After, _ = changeV.After.UnmarkDeep() | |
| var before, after []byte | |
| var afterUnknown cty.Value | |
| if changeV.Before != cty.NilVal { | |
| before, err = ctyjson.Marshal(changeV.Before, changeV.Before.Type()) | |
| if err != nil { | |
| return nil, err | |
| } | |
| } | |
| if changeV.After != cty.NilVal { | |
| if changeV.After.IsWhollyKnown() { | |
| after, err = ctyjson.Marshal(changeV.After, changeV.After.Type()) | |
| if err != nil { | |
| return nil, err | |
| } | |
| afterUnknown = cty.False | |
| } else { | |
| filteredAfter := omitUnknowns(changeV.After) | |
| if filteredAfter.IsNull() { | |
| after = nil | |
| } else { | |
| after, err = ctyjson.Marshal(filteredAfter, filteredAfter.Type()) | |
| if err != nil { | |
| return nil, err | |
| } | |
| } | |
| afterUnknown = unknownAsBool(changeV.After) | |
| } | |
| } | |
| // The only information we have in the plan about output sensitivity is | |
| // a boolean which is true if the output was or is marked sensitive. As | |
| // a result, BeforeSensitive and AfterSensitive will be identical, and | |
| // either false or true. | |
| outputSensitive := cty.False | |
| if oc.Sensitive { | |
| outputSensitive = cty.True | |
| } | |
| sensitive, err := ctyjson.Marshal(outputSensitive, outputSensitive.Type()) | |
| if err != nil { | |
| return nil, err | |
| } | |
| a, _ := ctyjson.Marshal(afterUnknown, afterUnknown.Type()) | |
| c := Change{ | |
| Actions: actionString(oc.Action.String()), | |
| Before: json.RawMessage(before), | |
| After: json.RawMessage(after), | |
| AfterUnknown: a, | |
| BeforeSensitive: json.RawMessage(sensitive), | |
| AfterSensitive: json.RawMessage(sensitive), | |
| // Just to be explicit, outputs cannot be imported so this is always | |
| // nil. | |
| Importing: nil, | |
| } | |
| outputChanges[oc.Addr.OutputValue.Name] = c | |
| } | |
| return outputChanges, nil | |
| } | |
| func (p *plan) marshalPlannedValues(changes *plans.ChangesSrc, schemas *terraform.Schemas) error { | |
| // marshal the planned changes into a module | |
| plan, err := marshalPlannedValues(changes, schemas) | |
| if err != nil { | |
| return err | |
| } | |
| p.PlannedValues.RootModule = plan | |
| // marshalPlannedOutputs | |
| outputs, err := marshalPlannedOutputs(changes) | |
| if err != nil { | |
| return err | |
| } | |
| p.PlannedValues.Outputs = outputs | |
| return nil | |
| } | |
| func (p *plan) marshalRelevantAttrs(plan *plans.Plan) error { | |
| for _, ra := range plan.RelevantAttributes { | |
| addr := ra.Resource.String() | |
| path, err := encodePath(ra.Attr) | |
| if err != nil { | |
| return err | |
| } | |
| p.RelevantAttributes = append(p.RelevantAttributes, ResourceAttr{addr, path}) | |
| } | |
| // we want our outputs to be deterministic, so we'll sort the attributes | |
| // here. The order of the attributes is not important, as long as it is | |
| // stable. | |
| sort.SliceStable(p.RelevantAttributes, func(i, j int) bool { | |
| return strings.Compare(fmt.Sprintf("%#v", plan.RelevantAttributes[i]), fmt.Sprintf("%#v", plan.RelevantAttributes[j])) < 0 | |
| }) | |
| return nil | |
| } | |
| // omitUnknowns recursively walks the src cty.Value and returns a new cty.Value, | |
| // omitting any unknowns. | |
| // | |
| // The result also normalizes some types: all sequence types are turned into | |
| // tuple types and all mapping types are converted to object types, since we | |
| // assume the result of this is just going to be serialized as JSON (and thus | |
| // lose those distinctions) anyway. | |
| func omitUnknowns(val cty.Value) cty.Value { | |
| ty := val.Type() | |
| switch { | |
| case val.IsNull(): | |
| return val | |
| case !val.IsKnown(): | |
| return cty.NilVal | |
| case ty.IsPrimitiveType(): | |
| return val | |
| case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): | |
| var vals []cty.Value | |
| it := val.ElementIterator() | |
| for it.Next() { | |
| _, v := it.Element() | |
| newVal := omitUnknowns(v) | |
| if newVal != cty.NilVal { | |
| vals = append(vals, newVal) | |
| } else if newVal == cty.NilVal { | |
| // element order is how we correlate unknownness, so we must | |
| // replace unknowns with nulls | |
| vals = append(vals, cty.NullVal(v.Type())) | |
| } | |
| } | |
| // We use tuple types always here, because the work we did above | |
| // may have caused the individual elements to have different types, | |
| // and we're doing this work to produce JSON anyway and JSON marshalling | |
| // represents all of these sequence types as an array. | |
| return cty.TupleVal(vals) | |
| case ty.IsMapType() || ty.IsObjectType(): | |
| vals := make(map[string]cty.Value) | |
| it := val.ElementIterator() | |
| for it.Next() { | |
| k, v := it.Element() | |
| newVal := omitUnknowns(v) | |
| if newVal != cty.NilVal { | |
| vals[k.AsString()] = newVal | |
| } | |
| } | |
| // We use object types always here, because the work we did above | |
| // may have caused the individual elements to have different types, | |
| // and we're doing this work to produce JSON anyway and JSON marshalling | |
| // represents both of these mapping types as an object. | |
| return cty.ObjectVal(vals) | |
| default: | |
| // Should never happen, since the above should cover all types | |
| panic(fmt.Sprintf("omitUnknowns cannot handle %#v", val)) | |
| } | |
| } | |
| // recursively iterate through a cty.Value, replacing unknown values (including | |
| // null) with cty.True and known values with cty.False. | |
| // | |
| // The result also normalizes some types: all sequence types are turned into | |
| // tuple types and all mapping types are converted to object types, since we | |
| // assume the result of this is just going to be serialized as JSON (and thus | |
| // lose those distinctions) anyway. | |
| // | |
| // For map/object values, all known attribute values will be omitted instead of | |
| // returning false, as this results in a more compact serialization. | |
| func unknownAsBool(val cty.Value) cty.Value { | |
| ty := val.Type() | |
| switch { | |
| case val.IsNull(): | |
| return cty.False | |
| case !val.IsKnown(): | |
| if ty.IsPrimitiveType() || ty.Equals(cty.DynamicPseudoType) { | |
| return cty.True | |
| } | |
| fallthrough | |
| case ty.IsPrimitiveType(): | |
| return cty.BoolVal(!val.IsKnown()) | |
| case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): | |
| length := val.LengthInt() | |
| if length == 0 { | |
| // If there are no elements then we can't have unknowns | |
| return cty.EmptyTupleVal | |
| } | |
| vals := make([]cty.Value, 0, length) | |
| it := val.ElementIterator() | |
| for it.Next() { | |
| _, v := it.Element() | |
| vals = append(vals, unknownAsBool(v)) | |
| } | |
| // The above transform may have changed the types of some of the | |
| // elements, so we'll always use a tuple here in case we've now made | |
| // different elements have different types. Our ultimate goal is to | |
| // marshal to JSON anyway, and all of these sequence types are | |
| // indistinguishable in JSON. | |
| return cty.TupleVal(vals) | |
| case ty.IsMapType() || ty.IsObjectType(): | |
| var length int | |
| switch { | |
| case ty.IsMapType(): | |
| length = val.LengthInt() | |
| default: | |
| length = len(val.Type().AttributeTypes()) | |
| } | |
| if length == 0 { | |
| // If there are no elements then we can't have unknowns | |
| return cty.EmptyObjectVal | |
| } | |
| vals := make(map[string]cty.Value) | |
| it := val.ElementIterator() | |
| for it.Next() { | |
| k, v := it.Element() | |
| vAsBool := unknownAsBool(v) | |
| // Omit all of the "false"s for known values for more compact | |
| // serialization | |
| if !vAsBool.RawEquals(cty.False) { | |
| vals[k.AsString()] = vAsBool | |
| } | |
| } | |
| // The above transform may have changed the types of some of the | |
| // elements, so we'll always use an object here in case we've now made | |
| // different elements have different types. Our ultimate goal is to | |
| // marshal to JSON anyway, and all of these mapping types are | |
| // indistinguishable in JSON. | |
| return cty.ObjectVal(vals) | |
| default: | |
| // Should never happen, since the above should cover all types | |
| panic(fmt.Sprintf("unknownAsBool cannot handle %#v", val)) | |
| } | |
| } | |
| func actionString(action string) []string { | |
| switch { | |
| case action == "NoOp": | |
| return []string{"no-op"} | |
| case action == "Create": | |
| return []string{"create"} | |
| case action == "Delete": | |
| return []string{"delete"} | |
| case action == "Update": | |
| return []string{"update"} | |
| case action == "CreateThenDelete": | |
| return []string{"create", "delete"} | |
| case action == "Read": | |
| return []string{"read"} | |
| case action == "DeleteThenCreate": | |
| return []string{"delete", "create"} | |
| case action == "Forget": | |
| return []string{"forget"} | |
| case action == "ForgetThenCreate": | |
| return []string{"forget", "create"} | |
| case action == "CreateThenForget": | |
| return []string{"create", "forget"} | |
| default: | |
| return []string{action} | |
| } | |
| } | |
| // UnmarshalActions reverses the actionString function. | |
| func UnmarshalActions(actions []string) plans.Action { | |
| if len(actions) == 2 { | |
| if actions[0] == "create" && actions[1] == "delete" { | |
| return plans.CreateThenDelete | |
| } | |
| if actions[0] == "delete" && actions[1] == "create" { | |
| return plans.DeleteThenCreate | |
| } | |
| if actions[0] == "create" && actions[1] == "forget" { | |
| return plans.CreateThenForget | |
| } | |
| if actions[0] == "forget" && actions[1] == "create" { | |
| return plans.ForgetThenCreate | |
| } | |
| } | |
| if len(actions) == 1 { | |
| switch actions[0] { | |
| case "create": | |
| return plans.Create | |
| case "delete": | |
| return plans.Delete | |
| case "update": | |
| return plans.Update | |
| case "read": | |
| return plans.Read | |
| case "forget": | |
| return plans.Forget | |
| case "no-op": | |
| return plans.NoOp | |
| } | |
| } | |
| panic("unrecognized action slice: " + strings.Join(actions, ", ")) | |
| } | |
| // encodePaths lossily encodes a cty.PathSet into an array of arrays of step | |
| // values, such as: | |
| // | |
| // [["length"],["triggers",0,"value"]] | |
| // | |
| // The lossiness is that we cannot distinguish between an IndexStep with string | |
| // key and a GetAttr step. This is fine with JSON output, because JSON's type | |
| // system means that those two steps are equivalent anyway: both are object | |
| // indexes. | |
| // | |
| // JavaScript (or similar dynamic language) consumers of these values can | |
| // iterate over the steps starting from the root object to reach the | |
| // value that each path is describing. | |
| func encodePaths(pathSet cty.PathSet) (json.RawMessage, error) { | |
| if pathSet.Empty() { | |
| return nil, nil | |
| } | |
| pathList := pathSet.List() | |
| jsonPaths := make([]json.RawMessage, 0, len(pathList)) | |
| for _, path := range pathList { | |
| jsonPath, err := encodePath(path) | |
| if err != nil { | |
| return nil, err | |
| } | |
| jsonPaths = append(jsonPaths, jsonPath) | |
| } | |
| return json.Marshal(jsonPaths) | |
| } | |
| func encodePath(path cty.Path) (json.RawMessage, error) { | |
| steps := make([]json.RawMessage, 0, len(path)) | |
| for _, step := range path { | |
| switch s := step.(type) { | |
| case cty.IndexStep: | |
| key, err := ctyjson.Marshal(s.Key, s.Key.Type()) | |
| if err != nil { | |
| return nil, fmt.Errorf("Failed to marshal index step key %#v: %s", s.Key, err) | |
| } | |
| steps = append(steps, key) | |
| case cty.GetAttrStep: | |
| name, err := json.Marshal(s.Name) | |
| if err != nil { | |
| return nil, fmt.Errorf("Failed to marshal get attr step name %#v: %s", s.Name, err) | |
| } | |
| steps = append(steps, name) | |
| default: | |
| return nil, fmt.Errorf("Unsupported path step %#v (%t)", step, step) | |
| } | |
| } | |
| return json.Marshal(steps) | |
| } |