Merge pull request #200 from renescheepers/rs/add-action-reason
Add ActionReason to ResourceChange
diff --git a/plan.go b/plan.go
index 8875200..d82bc10 100644
--- a/plan.go
+++ b/plan.go
@@ -208,8 +208,85 @@
// 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.
diff --git a/plan_test.go b/plan_test.go
index 2235678..e38b6d6 100644
--- a/plan_test.go
+++ b/plan_test.go
@@ -137,6 +137,27 @@
}
}
+func TestPlan_actionReason(t *testing.T) {
+ f, err := os.Open("testdata/action_reason/plan.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer f.Close()
+
+ var plan *Plan
+ if err := json.NewDecoder(f).Decode(&plan); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := plan.Validate(); err != nil {
+ t.Fatal(err)
+ }
+
+ if got, want := plan.ResourceChanges[0].ActionReason, ActionReasonReplaceBecauseTainted; got != want {
+ t.Fatalf("unexpected action reason: got %q, want %q", got, want)
+ }
+}
+
func TestPlan_actionInvocations(t *testing.T) {
f, err := os.Open("testdata/actions/plan.json")
if err != nil {
diff --git a/testdata/action_reason/plan.json b/testdata/action_reason/plan.json
new file mode 100644
index 0000000..1b1ad14
--- /dev/null
+++ b/testdata/action_reason/plan.json
@@ -0,0 +1 @@
+{"format_version":"1.2","terraform_version":"1.15.0","planned_values":{"root_module":{"resources":[{"address":"null_resource.example","mode":"managed","type":"null_resource","name":"example","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"triggers":{"name":"demo"}},"sensitive_values":{"triggers":{}}}]}},"resource_changes":[{"address":"null_resource.example","mode":"managed","type":"null_resource","name":"example","provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["delete","create"],"before":{"id":"8868159889619753631","triggers":{"name":"demo"}},"after":{"triggers":{"name":"demo"}},"after_unknown":{"id":true,"triggers":{}},"before_sensitive":{"triggers":{}},"after_sensitive":{"triggers":{}}},"action_reason":"replace_because_tainted"}],"complete":true,"prior_state":{"format_version":"1.0","terraform_version":"1.15.0","values":{"root_module":{"resources":[{"address":"null_resource.example","mode":"managed","type":"null_resource","name":"example","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"id":"8868159889619753631","triggers":{"name":"demo"}},"sensitive_values":{"triggers":{}},"tainted":true}]}}},"configuration":{"provider_config":{"null":{"name":"null","full_name":"registry.terraform.io/hashicorp/null","version_constraint":"~\u003e 3.0"}},"root_module":{"resources":[{"address":"null_resource.example","mode":"managed","type":"null_resource","name":"example","provider_config_key":"null","expressions":{"triggers":{"constant_value":{"name":"demo"}}},"schema_version":0}]}},"timestamp":"2026-06-16T10:21:47Z"}