Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/command/views/json/message_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const (
MessageDiagnostic MessageType = "diagnostic"

// Operation results
MessageResourceDrift MessageType = "resource_drift"
MessagePlannedChange MessageType = "planned_change"
MessageChangeSummary MessageType = "change_summary"
MessageOutputs MessageType = "outputs"
Expand Down
8 changes: 8 additions & 0 deletions internal/command/views/json_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ func (v *JSONView) PlannedChange(c *json.ResourceInstanceChange) {
)
}

func (v *JSONView) ResourceDrift(c *json.ResourceInstanceChange) {
v.log.Info(
fmt.Sprintf("%s: Drift detected (%s)", c.Resource.Addr, c.Action),
"type", json.MessageResourceDrift,
"change", c,
)
}

func (v *JSONView) ChangeSummary(cs *json.ChangeSummary) {
v.log.Info(
cs.String(),
Expand Down
40 changes: 40 additions & 0 deletions internal/command/views/json_view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,46 @@ func TestJSONView_PlannedChange(t *testing.T) {
testJSONViewOutputEquals(t, done(t).Stdout(), want)
}

func TestJSONView_ResourceDrift(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))

foo, diags := addrs.ParseModuleInstanceStr("module.foo")
if len(diags) > 0 {
t.Fatal(diags.Err())
}
managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"}
cs := &plans.ResourceInstanceChangeSrc{
Addr: managed.Instance(addrs.StringKey("boop")).Absolute(foo),
ChangeSrc: plans.ChangeSrc{
Action: plans.Update,
},
}
jv.ResourceDrift(viewsjson.NewResourceInstanceChange(cs))

want := []map[string]interface{}{
{
"@level": "info",
"@message": `module.foo.test_instance.bar["boop"]: Drift detected (update)`,
"@module": "terraform.ui",
"type": "resource_drift",
"change": map[string]interface{}{
"action": "update",
"resource": map[string]interface{}{
"addr": `module.foo.test_instance.bar["boop"]`,
"implied_provider": "test",
"module": "module.foo",
"resource": `test_instance.bar["boop"]`,
"resource_key": "boop",
"resource_name": "bar",
"resource_type": "test_instance",
},
},
},
}
testJSONViewOutputEquals(t, done(t).Stdout(), want)
}

func TestJSONView_ChangeSummary(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
Expand Down
85 changes: 85 additions & 0 deletions internal/command/views/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import (
"github.com/hashicorp/terraform/internal/command/format"
"github.com/hashicorp/terraform/internal/command/views/json"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/states/statefile"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)

type Operation interface {
Expand Down Expand Up @@ -160,6 +162,12 @@ func (v *OperationJSON) EmergencyDumpState(stateFile *statefile.File) error {
// Log a change summary and a series of "planned" messages for the changes in
// the plan.
func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
if err := v.resourceDrift(plan.PrevRunState, plan.PriorState, schemas); err != nil {
var diags tfdiags.Diagnostics
diags = diags.Append(err)
v.Diagnostics(diags)
}

cs := &json.ChangeSummary{
Operation: json.OperationPlanned,
}
Expand Down Expand Up @@ -188,6 +196,83 @@ func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
v.view.ChangeSummary(cs)
}

func (v *OperationJSON) resourceDrift(oldState, newState *states.State, schemas *terraform.Schemas) error {
if newState.ManagedResourcesEqual(oldState) {
// Nothing to do, because we only detect and report drift for managed
// resource instances.
return nil
}
for _, ms := range oldState.Modules {
for _, rs := range ms.Resources {
if rs.Addr.Resource.Mode != addrs.ManagedResourceMode {
// Drift reporting is only for managed resources
continue
}

provider := rs.ProviderConfig.Provider
for key, oldIS := range rs.Instances {
if oldIS.Current == nil {
// Not interested in instances that only have deposed objects
continue
}
addr := rs.Addr.Instance(key)
newIS := newState.ResourceInstance(addr)

schema, _ := schemas.ResourceTypeConfig(
provider,
addr.Resource.Resource.Mode,
addr.Resource.Resource.Type,
)
if schema == nil {
return fmt.Errorf("no schema found for %s (in provider %s)", addr, provider)
}
ty := schema.ImpliedType()

oldObj, err := oldIS.Current.Decode(ty)
if err != nil {
return fmt.Errorf("failed to decode previous run data for %s: %s", addr, err)
}

var newObj *states.ResourceInstanceObject
if newIS != nil && newIS.Current != nil {
newObj, err = newIS.Current.Decode(ty)
if err != nil {
return fmt.Errorf("failed to decode refreshed data for %s: %s", addr, err)
}
}

var oldVal, newVal cty.Value
oldVal = oldObj.Value
if newObj != nil {
newVal = newObj.Value
} else {
newVal = cty.NullVal(ty)
}

if oldVal.RawEquals(newVal) {
// No drift if the two values are semantically equivalent
continue
}

// We can only detect updates and deletes as drift.
action := plans.Update
if newVal.IsNull() {
action = plans.Delete
}

change := &plans.ResourceInstanceChangeSrc{
Addr: addr,
ChangeSrc: plans.ChangeSrc{
Action: action,
},
}
v.view.ResourceDrift(json.NewResourceInstanceChange(change))
}
}
}
return nil
}

func (v *OperationJSON) PlannedChange(change *plans.ResourceInstanceChangeSrc) {
if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
// Avoid rendering data sources on deletion
Expand Down
Loading