Skip to content

Use moved block with target used in output adds Note: Objects have changed outside of Terraform to plan #1109

@EspenAlbert

Description

@EspenAlbert

tl;dr: When implementing the moved block support you get an extra Note: Objects have changed outside of Terraform when the terraform configuration uses output variables referencing the moved.to resource or an attribute not mapped by MoveState, for example:

output "items" {
  value = hashicups_order.edu.items
}

Theory: Some part of the internal Terraform graph is updated when using an output variable, therefore, the mapped state after MoveState is called creates a diff with the state returned from Read.

What consequence can this have?
If you don't map all fields in MoveState implementation (sometimes impossible since Source.Schema != Target.Schema), users sees the Note: Objects have changed outside of Terraform and become afraid to complete the moved.

Module version

github.com/hashicorp/terraform-plugin-framework v1.14.1

Relevant provider source code

  • Full source code in this branch
  • Summary: only map id from the source type, as the rest of the attributes are found in the Read
func (r *orderResource) MoveState(context.Context) []resource.StateMover {
	return []resource.StateMover{{StateMover: stateMover}}
}
func stateMover(ctx context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) {
	if req.SourceTypeName != "hashicups_order_legacy" || !strings.HasSuffix(req.SourceProviderAddress, "/hashicups"){
		return
	}
	// Use always new sharding config when moving from cluster to adv_cluster
	rawStateValue, err := req.SourceRawState.UnmarshalWithOpts(tftypes.Object{
		AttributeTypes: map[string]tftypes.Type{
			"id": tftypes.String,
		},
	}, tfprotov6.UnmarshalOpts{ValueFromJSONOpts: tftypes.ValueFromJSONOpts{IgnoreUndefinedAttributes: true}})
	diags := &resp.Diagnostics
	if err != nil {
		diags.AddError("Unable to Unmarshal state", err.Error())
		return
	}
	var stateObj map[string]tftypes.Value
	if err := rawStateValue.As(&stateObj); err != nil {
		diags.AddError("Unable to Parse state", err.Error())
		return
	}
	var id *string
	if err := stateObj["id"].As(&id); err != nil {
		diags.AddError("Unable to Parse id from state", err.Error())
		return
	}
	model := orderResourceModel{
		ID: types.StringPointerValue(id),
	}
	diags.Append(resp.TargetState.Set(ctx, model)...)
}

Terraform Configuration Files

terraform {
  required_providers {
    hashicups = {}
  }
  required_version = ">1.10"
}

provider "hashicups" {
  host     = "http://localhost:19090"
  username = "education"
  password = "test123"
}

# LEGACY RESOURCE
# resource "hashicups_order_legacy" "edu" {
#   items = [{
#     coffee = {
#       id = 3
#     }
#     quantity = 2
#     },
#     {
#       coffee = {
#         id = 2
#       }
#       quantity = 3
#   }]
# }

# NEW RESOURCE
moved {
  from = hashicups_order_legacy.edu
  to   = hashicups_order.edu
}

resource "hashicups_order" "edu" {
  items = [{
    coffee = {
      id = 3
    }
    quantity = 2
    },
    {
      coffee = {
        id = 2
      }
      quantity = 3
  }]
}


# LEGACY OUTPUT VAR
# output "edu_order_legacy" {
#   value = hashicups_order_legacy.edu
# }

# NEW OUTPUT VAR
output "edu_order" {
  value = hashicups_order.edu
}

Debug Output (relevant parts see comment for full logs)

2025-03-11T11:22:28.157Z [INFO]  backend/local: plan operation completed

# START UNEXPECTED PLAN OUTPUT #
Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the last "terraform apply" which may have affected this plan:

  # hashicups_order.edu has changed
  # (moved from hashicups_order_legacy.edu)
  ~ resource "hashicups_order" "edu" {
        id    = "3"
      + items = [
          + {
              + coffee   = {
                  + id          = 3
                  + image       = "/vault.png"
                  + name        = "Vaulatte"
                  + price       = 200
                  + teaser      = "Nothing gives you a safe and secure feeling like a Vaulatte"
                    # (1 unchanged attribute hidden)
                }
              + quantity = 2
            },
          + {
              + coffee   = {
                  + id          = 2
                  + image       = "/packer.png"
                  + name        = "Packer Spiced Latte"
                  + price       = 350
                  + teaser      = "Packed with goodness to spice up your images"
                    # (1 unchanged attribute hidden)
                }
              + quantity = 3
            },
        ]
    }


Unless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes.

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
# END UNEXPECTED PLAN OUTPUT #

Terraform will perform the following actions:

  # hashicups_order_legacy.edu has moved to hashicups_order.edu
    resource "hashicups_order" "edu" {
        id    = "3"
        # (1 unchanged attribute hidden)
    }

Plan: 0 to add, 0 to change, 0 to destroy.

Expected Behavior

The Note: Objects have changed outside of Terraform section should not be there

Actual Behavior

Full Note: Objects have changed outside of Terraform

Steps to Reproduce

  1. git clone https://github.com/EspenAlbert/terraform-provider-hashicups-1.git
  2. git checkout moved-with-output-refs-changes-outside-tf
  3. cd 12-final
  4. docker-compose up -d
  5. make build
  6. configure $TF_CLI_CONFIG_FILE to the $(pwd)/bin
  7. cd examples/moved
  8. uncomment # LEGACY RESOURCE section and comment # NEW RESOURCE sections
  9. terraform apply
  10. comment # LEGACY RESOURCE section and uncomment # NEW RESOURCE sections
  11. terraform plan### References

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions