| // Copyright IBM Corp. 2019, 2026 | |
| // SPDX-License-Identifier: MPL-2.0 | |
| package tfjson | |
| import ( | |
| "bytes" | |
| "encoding/json" | |
| "errors" | |
| "fmt" | |
| "github.com/hashicorp/go-version" | |
| ) | |
| // PlanFormatVersionConstraints defines the versions of the JSON plan format | |
| // that are supported by this package. | |
| var PlanFormatVersionConstraints = ">= 0.1, < 2.0" | |
| // ResourceMode is a string representation of the resource type found | |
| // in certain fields in the plan. | |
| type ResourceMode string | |
| const ( | |
| // DataResourceMode is the resource mode for data sources. | |
| DataResourceMode ResourceMode = "data" | |
| // ManagedResourceMode is the resource mode for managed resources. | |
| ManagedResourceMode ResourceMode = "managed" | |
| ) | |
| // Plan represents the entire contents of an output Terraform plan. | |
| type Plan struct { | |
| // useJSONNumber opts into the behavior of calling | |
| // json.Decoder.UseNumber prior to decoding the plan, which turns | |
| // numbers into json.Numbers instead of float64s. Set it using | |
| // Plan.UseJSONNumber. | |
| useJSONNumber bool | |
| // The version of the plan format. This should always match the | |
| // PlanFormatVersion constant in this package, or else an unmarshal | |
| // will be unstable. | |
| FormatVersion string `json:"format_version,omitempty"` | |
| // The version of Terraform used to make the plan. | |
| TerraformVersion string `json:"terraform_version,omitempty"` | |
| // The variables set in the root module when creating the plan. | |
| Variables map[string]*PlanVariable `json:"variables,omitempty"` | |
| // The common state representation of resources within this plan. | |
| // This is a product of the existing state merged with the diff for | |
| // this plan. | |
| PlannedValues *StateValues `json:"planned_values,omitempty"` | |
| // The change operations for resources and data sources within this plan | |
| // resulting from resource drift. | |
| ResourceDrift []*ResourceChange `json:"resource_drift,omitempty"` | |
| // The change operations for resources and data sources within this | |
| // plan. | |
| ResourceChanges []*ResourceChange `json:"resource_changes,omitempty"` | |
| // DeferredChanges contains the change operations for resources that are deferred | |
| // for this plan. | |
| DeferredChanges []*DeferredResourceChange `json:"deferred_changes,omitempty"` | |
| // Complete indicates that all resources have successfully planned changes. | |
| // This will be false if there are DeferredChanges or if the -target flag is used. | |
| // | |
| // Complete was introduced in Terraform 1.8 and will be nil for all previous | |
| // Terraform versions. | |
| Complete *bool `json:"complete,omitempty"` | |
| // The change operations for outputs within this plan. | |
| OutputChanges map[string]*Change `json:"output_changes,omitempty"` | |
| // The Terraform state prior to the plan operation. This is the | |
| // same format as PlannedValues, without the current diff merged. | |
| PriorState *State `json:"prior_state,omitempty"` | |
| // The Terraform configuration used to make the plan. | |
| Config *Config `json:"configuration,omitempty"` | |
| // RelevantAttributes represents any resource instances and their | |
| // attributes which may have contributed to the planned changes | |
| RelevantAttributes []ResourceAttribute `json:"relevant_attributes,omitempty"` | |
| // Checks contains the results of any conditional checks executed, or | |
| // planned to be executed, during this plan. | |
| Checks []CheckResultStatic `json:"checks,omitempty"` | |
| // Timestamp contains the static timestamp that Terraform considers to be | |
| // the time this plan executed, in UTC. | |
| Timestamp string `json:"timestamp,omitempty"` | |
| ActionInvocations []*ActionInvocation `json:"action_invocations,omitempty"` | |
| } | |
| // ResourceAttribute describes a full path to a resource attribute | |
| type ResourceAttribute struct { | |
| // Resource describes resource instance address (e.g. null_resource.foo) | |
| Resource string `json:"resource"` | |
| // Attribute describes the attribute path using a lossy representation | |
| // of cty.Path. (e.g. ["id"] or ["objects", 0, "val"]). | |
| Attribute []json.RawMessage `json:"attribute"` | |
| } | |
| // UseJSONNumber controls whether the Plan will be decoded using the | |
| // json.Number behavior or the float64 behavior. When b is true, the Plan will | |
| // represent numbers in PlanOutputs as json.Numbers. When b is false, the | |
| // Plan will represent numbers in PlanOutputs as float64s. | |
| func (p *Plan) UseJSONNumber(b bool) { | |
| p.useJSONNumber = b | |
| } | |
| // Validate checks to ensure that the plan is present, and the | |
| // version matches the version supported by this library. | |
| func (p *Plan) Validate() error { | |
| if p == nil { | |
| return errors.New("plan is nil") | |
| } | |
| if p.FormatVersion == "" { | |
| return errors.New("unexpected plan input, format version is missing") | |
| } | |
| constraint, err := version.NewConstraint(PlanFormatVersionConstraints) | |
| if err != nil { | |
| return fmt.Errorf("invalid version constraint: %w", err) | |
| } | |
| version, err := version.NewVersion(p.FormatVersion) | |
| if err != nil { | |
| return fmt.Errorf("invalid format version %q: %w", p.FormatVersion, err) | |
| } | |
| if !constraint.Check(version) { | |
| return fmt.Errorf("unsupported plan format version: %q does not satisfy %q", | |
| version, constraint) | |
| } | |
| return nil | |
| } | |
| // UnmarshalJSON implements json.Unmarshaler for Plan. | |
| // | |
| // As per established convention this method should only ever | |
| // be invoked *indirectly* via [encoding/json] library. | |
| func (p *Plan) UnmarshalJSON(b []byte) error { | |
| type rawPlan Plan | |
| var plan rawPlan | |
| dec := json.NewDecoder(bytes.NewReader(b)) | |
| if p.useJSONNumber { | |
| dec.UseNumber() | |
| } | |
| err := dec.Decode(&plan) | |
| if err != nil { | |
| return err | |
| } | |
| *p = *(*Plan)(&plan) | |
| return p.Validate() | |
| } | |
| // ResourceChange is a description of an individual change action | |
| // that Terraform plans to use to move from the prior state to a new | |
| // state matching the configuration. | |
| type ResourceChange struct { | |
| // The absolute resource address. | |
| Address string `json:"address,omitempty"` | |
| // The absolute address that this resource instance had | |
| // at the conclusion of a previous plan. | |
| PreviousAddress string `json:"previous_address,omitempty"` | |
| // The module portion of the above address. Omitted if the instance | |
| // is in the root module. | |
| ModuleAddress string `json:"module_address,omitempty"` | |
| // The resource mode. | |
| Mode ResourceMode `json:"mode,omitempty"` | |
| // The resource type, example: "aws_instance" for aws_instance.foo. | |
| Type string `json:"type,omitempty"` | |
| // The resource name, example: "foo" for aws_instance.foo. | |
| Name string `json:"name,omitempty"` | |
| // The instance key for any resources that have been created using | |
| // "count" or "for_each". If neither of these apply the key will be | |
| // empty. | |
| // | |
| // This value can be either an integer (int) or a string. | |
| Index interface{} `json:"index,omitempty"` | |
| // The name of the provider this resource belongs to. This allows | |
| // the provider to be interpreted unambiguously in the unusual | |
| // situation where a provider offers a resource type whose name | |
| // does not start with its own name, such as the "googlebeta" | |
| // provider offering "google_compute_instance". | |
| ProviderName string `json:"provider_name,omitempty"` | |
| // An identifier used during replacement operations, and can be | |
| // used to identify the exact resource being replaced in state. | |
| DeposedKey string `json:"deposed,omitempty"` | |
| // The data describing the change that will be made to this object. | |
| Change *Change `json:"change,omitempty"` | |
| // ActionReason is an optional keyword providing extra context for why the | |
| // actions in Change.Actions were chosen, for example that a resource must | |
| // be replaced because its existing object was tainted. It is empty when | |
| // Terraform did not report a reason. This detail is for display purposes | |
| // only and should not be used to make decisions. | |
| ActionReason ActionReason `json:"action_reason,omitempty"` | |
| } | |
| // ActionReason is a keyword representing the optional reason Terraform reports | |
| // for the actions proposed in a ResourceChange. The set of possible values may | |
| // grow in future Terraform versions, so consumers should treat unrecognized | |
| // values as equivalent to ActionReasonNone. | |
| // | |
| // The canonical list of reason keywords is defined by Terraform in | |
| // internal/command/jsonplan/plan.go (the ResourceInstance* constants): | |
| // https://github.com/hashicorp/terraform/blob/main/internal/command/jsonplan/plan.go | |
| type ActionReason string | |
| const ( | |
| // ActionReasonNone is the absence of any specific reason; Terraform omits | |
| // the action_reason property in this case. | |
| ActionReasonNone ActionReason = "" | |
| // ActionReasonReplaceBecauseCannotUpdate indicates a resource must be | |
| // replaced because the requested change is not possible without doing so. | |
| ActionReasonReplaceBecauseCannotUpdate ActionReason = "replace_because_cannot_update" | |
| // ActionReasonReplaceBecauseTainted indicates a resource must be replaced | |
| // because its existing object was marked as tainted. | |
| ActionReasonReplaceBecauseTainted ActionReason = "replace_because_tainted" | |
| // ActionReasonReplaceByRequest indicates a resource must be replaced because | |
| // the user explicitly requested it (e.g. "terraform plan -replace=..."). | |
| ActionReasonReplaceByRequest ActionReason = "replace_by_request" | |
| // ActionReasonReplaceByTriggers indicates a resource must be replaced | |
| // because of a change in a value referenced by its replace_triggered_by. | |
| ActionReasonReplaceByTriggers ActionReason = "replace_by_triggers" | |
| // ActionReasonDeleteBecauseNoResourceConfig indicates a resource will be | |
| // destroyed because it has no matching configuration block. | |
| ActionReasonDeleteBecauseNoResourceConfig ActionReason = "delete_because_no_resource_config" | |
| // ActionReasonDeleteBecauseWrongRepetition indicates a resource instance | |
| // will be destroyed because its instance key does not match the resource's | |
| // count/for_each repetition mode. | |
| ActionReasonDeleteBecauseWrongRepetition ActionReason = "delete_because_wrong_repetition" | |
| // ActionReasonDeleteBecauseCountIndex indicates a resource instance will be | |
| // destroyed because its index is out of range for the count value. | |
| ActionReasonDeleteBecauseCountIndex ActionReason = "delete_because_count_index" | |
| // ActionReasonDeleteBecauseEachKey indicates a resource instance will be | |
| // destroyed because its key is not present in the for_each value. | |
| ActionReasonDeleteBecauseEachKey ActionReason = "delete_because_each_key" | |
| // ActionReasonDeleteBecauseNoModule indicates a resource instance will be | |
| // destroyed because its containing module instance is no longer declared. | |
| ActionReasonDeleteBecauseNoModule ActionReason = "delete_because_no_module" | |
| // ActionReasonDeleteBecauseNoMoveTarget indicates a resource was moved to an | |
| // address that has no configuration, so it will be destroyed. | |
| ActionReasonDeleteBecauseNoMoveTarget ActionReason = "delete_because_no_move_target" | |
| // ActionReasonReadBecauseConfigUnknown indicates a data resource will be | |
| // read during apply because its configuration contains unknown values. | |
| ActionReasonReadBecauseConfigUnknown ActionReason = "read_because_config_unknown" | |
| // ActionReasonReadBecauseDependencyPending indicates a data resource will be | |
| // read during apply because it depends on a resource or module with changes | |
| // pending. | |
| ActionReasonReadBecauseDependencyPending ActionReason = "read_because_dependency_pending" | |
| // ActionReasonReadBecauseCheckNested indicates a data resource nested within | |
| // a check block will be read during apply. | |
| ActionReasonReadBecauseCheckNested ActionReason = "read_because_check_nested" | |
| ) | |
| // Change is the representation of a proposed change for an object. | |
| type Change struct { | |
| // The action to be carried out by this change. | |
| Actions Actions `json:"actions,omitempty"` | |
| // Before and After are representations of the object value both | |
| // before and after the action. For create and delete actions, | |
| // either Before or After is unset (respectively). For no-op | |
| // actions, both values will be identical. After will be incomplete | |
| // if there are values within it that won't be known until after | |
| // apply. | |
| Before interface{} `json:"before,"` | |
| After interface{} `json:"after,omitempty"` | |
| // A deep object of booleans that denotes any values that are | |
| // unknown in a resource. These values were previously referred to | |
| // as "computed" values. | |
| // | |
| // If the value cannot be found in this map, then its value should | |
| // be available within After, so long as the operation supports it. | |
| AfterUnknown interface{} `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 interface{} `json:"before_sensitive,omitempty"` | |
| AfterSensitive interface{} `json:"after_sensitive,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"` | |
| // ReplacePaths contains a set of paths that point to attributes/elements | |
| // that are causing the overall resource to be replaced rather than simply | |
| // updated. | |
| // | |
| // This field is always a slice of indexes, where an index in this context | |
| // is either an integer pointing to a child of a set/list, or a string | |
| // pointing to the child of a map, object, or block. | |
| ReplacePaths []interface{} `json:"replace_paths,omitempty"` | |
| // BeforeIdentity and AfterIdentity are representations of the resource | |
| // identity value both before and after the action. | |
| BeforeIdentity interface{} `json:"before_identity,omitempty"` | |
| AfterIdentity interface{} `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 or identity 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 interface{} `json:"identity,omitempty"` | |
| } | |
| // PlanVariable is a top-level variable in the Terraform plan. | |
| type PlanVariable struct { | |
| // The value for this variable at plan time. | |
| Value interface{} `json:"value,omitempty"` | |
| } | |
| // DeferredResourceChange is a description of a resource change that has been | |
| // deferred for some reason. | |
| type DeferredResourceChange struct { | |
| // Reason is the reason why this resource change was deferred. | |
| Reason string `json:"reason,omitempty"` | |
| // Change contains any information we have about the deferred change. | |
| ResourceChange *ResourceChange `json:"resource_change,omitempty"` | |
| } | |
| type ActionInvocation struct { | |
| // Address is the absolute action address | |
| Address string `json:"address,omitempty"` | |
| // Type is the type of the action | |
| Type string `json:"type,omitempty"` | |
| // Name is the name of the action | |
| Name string `json:"name,omitempty"` | |
| // ConfigValues is the JSON representation of the values in the config block of the action | |
| ConfigValues interface{} `json:"config_values,omitempty"` | |
| ConfigSensitive interface{} `json:"config_sensitive,omitempty"` | |
| ConfigUnknown interface{} `json:"config_unknown,omitempty"` | |
| // ProviderName allows the property "type" to be interpreted unambiguously | |
| // in the unusual situation where a provider offers a type whose | |
| // name does not start with its own name, such as the "googlebeta" provider | |
| // offering "google_compute_instance". | |
| ProviderName string `json:"provider_name,omitempty"` | |
| LifecycleActionTrigger *LifecycleActionTrigger `json:"lifecycle_action_trigger,omitempty"` | |
| InvokeActionTrigger *InvokeActionTrigger `json:"invoke_action_trigger,omitempty"` | |
| } | |
| type LifecycleActionTrigger struct { | |
| TriggeringResourceAddress string `json:"triggering_resource_address,omitempty"` | |
| ActionTriggerEvent string `json:"action_trigger_event,omitempty"` | |
| ActionTriggerBlockIndex int `json:"action_trigger_block_index"` | |
| ActionsListIndex int `json:"actions_list_index"` | |
| } | |
| type InvokeActionTrigger struct{} |