Issue with SetNestedAttribute in Terraform version 1.4.x

Below is the schema for the resource.

resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"id": schema.StringAttribute{
				Optional:            true,
				Computed:            true,
				Validators: []validator.String{
					stringvalidator.LengthAtLeast(1),
				},
			},
			"name": schema.StringAttribute{
				Optional:            true,
				Computed:            true,
				Validators: []validator.String{
					stringvalidator.LengthAtLeast(1),
					stringvalidator.ExactlyOneOf(path.MatchRoot("id")),
				},
			},
			"demo_list": schema.SetNestedAttribute{
				Computed:            true,
				Optional:            true,
				NestedObject: schema.NestedAttributeObject{
					Attributes: map[string]schema.Attribute{
						"demo_id": schema.StringAttribute{
							Optional:            true,
							Computed:            true,
							Validators: []validator.String{
								stringvalidator.LengthAtLeast(1),
							},
						},
						"demo_name": schema.StringAttribute{
							Computed:            true,
							Optional:            true,
							Validators: []validator.String{
								stringvalidator.LengthAtLeast(1),
								stringvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("demo_id")),
							},
						},
						"demo_iops": schema.Int64Attribute{
							Optional:            true,
							Computed:            true,
							PlanModifiers: []planmodifier.Int64{
								int64planmodifier.UseStateForUnknown(),
							},
						},
						"demo_bw_in_mbps": schema.Int64Attribute{
							Optional:            true,
							Computed:            true,
							PlanModifiers: []planmodifier.Int64{
								int64planmodifier.UseStateForUnknown(),
							},
						},
						"demo_mode": schema.StringAttribute{
							Computed:            true,
							Optional:            true,
							Validators: []validator.String{stringvalidator.OneOf(
								"ReadOnly",
								"ReadWrite",
								"NoAccess",
							)},
							PlanModifiers: []planmodifier.String{
								stringDefault("ReadOnly"),
								stringplanmodifier.UseStateForUnknown(),
							},
						},
					},
				},
			},
		},
	}

The above schema is working fine for both create/update operations in Terraform 1.3.2.
Ran the same config in 1.4.6. Create operation is working fine but the update is not working as expected.
In create operation, there are 2 objects in the demo_list. Both objects got added.
In the update operation, changed one of the demo_name. The expected behavior is it should remove one entity and add a new one.
In 1.4.6 for the plan, the newly added entity is showing correctly but for the other for which there is no change, its showing demo_id and demo_name as known after apply.

Terraform-Plugin_Framework Version: 1.1.1
Terraform-Plugin-Framework-Validators Version: v0.9.0

Welcome to the forum - please reformat your message - All of the indentation has been removed from your code, making it very hard to read

and

Guide to asking for help in this forum - Your question would be much more approachable if you included actual terraform plan output showing the problem

Hi @maxb Please find the plan for the update operation.

In create operation, vol1 and vol2 got added. For update operation, changed vol2 to vol3. So ideally it should remove vol2 and add vol3. But the plan produced itself is wrong.

~ resource "test_demo" "test-resource" {
        id          = "id"
        name        = "name"
      ~ demo_list = [
          - {
              - demo_mode      = "ReadOnly" -> null
              - demo_bw_in_mbps = 40 -> null
              - demo_iops       = 20 -> null
              - demo_id        = "id2" -> null
              - demo_name      = "vol2" -> null
            },
          - {
              - demo_mode      = "ReadWrite" -> null
              - demo_bw_in_mbps = 25 -> null
              - demo_iops       = 17 -> null
              - demo_id        = "id1" -> null
              - demo_name      = "vol1" -> null
            },
          + {
              + demo_mode      = "ReadOnly"
              + demo_bw_in_mbps = 25
              + demo_iops       = 17
              + demo_id        = (known after apply)
              + demo_name      = (known after apply)
            },
          + {
              + demo_mode      = "ReadOnly"
              + demo_bw_in_mbps = 40
              + demo_iops       = 20
              + demo_id        = "id3"
              + demo_name      = "vol3"
            },
        ]
    }

For the element at index 2 (starting from 0), both demo_id and demo_name are unknown.

{
              + demo_mode      = "ReadOnly"
              + demo_bw_in_mbps = 25
              + demo_iops       = 17
              + demo_id        = (known after apply)
              + demo_name      = (known after apply)
},

Your demo_list is actually defined as a set - SetNestedAttribute. That’s misleading.

Also, sets that contain computed values are vulnerable to behaviours which can cause considerable surprise to users, if they’re not very familiar with Terraform internals. Carefully consider whether you actually want this data to be a list, set, or map. I suggest a map keyed by name is likely to provide the best semantics.

terraform-plugin-framework assumes that by default, the provider might want to recompute the value of optional-computed attributes that are not specified in the user’s configuration on every change. As a result it defaults to marking them all (known after apply). You need to use a UseStateForUnknown plan-modifier on every such attribute if you are committing to them retaining their original computed value after initial resource creation.

demo_id and demo_name specify the entity that needs to be added. So user can provide either demo_id or demo_name. So I am not sure the map keyed by name will work as the user can provide demo_id as well.

I am attaching the plan executed in Terraform 1.3.9 which is working perfectly fine for the same workflow.

~ resource "test_demo" "test-resource" {
        id          = "id"
        name        = "name"
      ~ demo_list = [
          + {
              + demo_mode      = "ReadOnly"
              + demo_bw_in_mbps = 40
              + demo_iops        = 20
              + demo_id        = "id3"
              + demo_name      = "vol3"
            },
          - {
              - demo_mode      = "ReadOnly" -> null
              - demo_bw_in_mbps = 40 -> null
              - demo_iops        = 20 -> null
              - demo_id        = "id2" -> null
              - demo_name      = "vol2" -> null
            },
            # (1 unchanged element hidden)
        ]
    }

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

I am not sure why its not working in Terraform 1.4.x

If you have discovered a case where Terraform 1.4 does something wrong when Terraform 1.3 behaves correctly, you should open a GitHub issue to report it.

Be sure to provide sufficient Terraform configuration, and provider source code, so that someone else can reproduce the problem on their own computer.

Hi @akashgs,

The demo_id and demo_name object attributes are both computed, and not using UseStateForUnknown, so the framework by default is going to mark those as unknown during plan if they are not set in the configuration.

Can you show the exact configuration and change which is causing this plan?

Hi @jbardin

Create time configuration:

resource "test_demo" "test-resource" {
	name = "name"
	demo_list = [
		{
			demo_name 		= "vol1"
			demo_mode 		= "ReadWrite"
			demo_bw_in_mbps = 25 
            demo_iops       = 17
		},
		{
			demo_name 		= "vol2"
			demo_mode 		= "ReadOnly"
			demo_bw_in_mbps = 40 
            demo_iops       = 20 
		},
	]
}

The create operation is working fine. For update, the vol2 gets changed to vol3.
Update time configuration:

resource "test_demo" "test-resource" {
	name = "name"
	demo_list = [
		{
			demo_name 		= "vol1"
			demo_mode 		= "ReadWrite"
			demo_bw_in_mbps = 25 
            demo_iops       = 17
		},
		{
			demo_name 		= "vol3"
			demo_mode 		= "ReadOnly"
			demo_bw_in_mbps = 40 
            demo_iops       = 20 
		},
	]
}

Thanks @akashgs,

Given your example schema, Terraform is going to propose the change you expected. This means it looks like the framework is not able to correlate the existing "vol1" object, and thinks it’s being replaced marking the computed attributes as unknown. The fact that you are not using UseStateForUnknown may have something to do with this, but the demo_name is shown as unknown and does exist in the configuration, so I’m not sure if that’s a mistake in your reproduction or part of the same framework issue.

Also since you have no non-computed attributes for the object, there is no fixed set of attributes which could be used to try and correlate objects between changes, meaning that there would always be ambiguous cases like what you have run into if even there were no bug. If things like demo_name are only meant to be user supplied, they should never be Computed in the schema.

Please provide the source for your custom plan modifier - it may be relevant:

Actually - please provide full source code of a demo provider that allows for this problem to be reproduced using Terraform 1.4 - as I attempted to simulate this scenario with a test provider built around the information provided in this topic so far, and I got the desired version of the plan every time.

I was using the latest version of terraform-plugin-framework in each test, so perhaps upgrade to that first, just in case it’s a bug fixed since the relatively old version you are using.