TypeSet is not picking up correct changes

Continuing from a previous post, I am writing a custom provider that creates a project resource with many nested but unique role resources defined as role blocks.

resource "project" "foo" {
  name = "foo"

  role {
    name         = "owner"
    description  = "Owner of foo"
  }
}

The role blocks should actually be unique by name, so they are more like a map, but complex definitions like the above that are not supported for TypeMap so it is defined as a TypeSet like so:

"role": {
	Type: schema.TypeSet,
	Elem: &schema.Resource{
		Schema: map[string]*schema.Schema{
			"name":        {Type: schema.TypeString, Required: true},
			"description": {Type: schema.TypeString, Required: true},
		},
	},
},

When I make a change to the description for editor , I end up with the following diff:

  # project.foo will be updated in-place
  ~ resource "project" "foo" {
        id    = "1"
        name  = "foo"

      - role {
          - description  = "Editor of foo" -> null
          - name         = "editor" -> null
        }
      + role {
          + description  = "Editor of foo (additional  info)
          + name         = "editor"
        }
        role {
            description  = "Owner of foo"
            name         = "owner"
        }
    }

This is not what I want at all. I just want to see that the description has changed for editor. Is there a way to do this with the current SDK?

Hi @bschaeffer,

Inside a set, elements are identified by their own content alone, so there isn’t really any concept of editing an existing object: there is no stable identifier to use to determine which object was changed. Removing the existing object and adding a new one is therefore the correct interpretation of this change.

The other option available todayis schema.TypeList, which uses the position of the item in the sequence as the identifier, and thus allows recognizing the difference between adding a new item and editing an existing one by correlating by index.

It sounds like in your system the name argument is a unique key and so you want to use that argument as the unique key to recognize objects. As you’ve seen, that isn’t currently possible for schema.TypeMap with the current SDK, although Terraform Core itself (since Terraform v0.12) does support a structure like this, so I understand it’s frustrating that the SDK is not exposing that functionality today.

A block backed by a map, were it possible to represent in the SDK, would expect a syntax like the following to make it clear what the unique identifier for each block is:

  role "owner" {
    description = "Owner of foo"
  }

This functionality is available via the lower-level utility library terraform-plugin-go, but that is at a considerably less convenient level of abstraction since it’s really just a thin wrapper around Terraform’s wire protocol. :confounded: There is a replacement for terraform-plugin-sdk coming, named terraform-plugin-framework, but at the time I’m writing this it’s only in the design phase and isn’t usable yet.

Given all of this, I would suggest continuing with TypeSet for now and accepting the non-ideal diffs as a cosmetic quirk that won’t otherwise affect the behavior – as the developer of the resource type, you can choose to implement it as an update-in-place if you wish, regardless of what the rendered plan might show. Later you might be able to improve on this using the new SDK framework, but it’ll still be some time before that is in a stable, usable state.

@apparentlymart Thanks again for a thorough reply.

as the developer of the resource type, you can choose to implement it as an update-in-place if you wish

What does that look like? Is there a way I can merge the changes? I have a feeling it might be something obvious I am just not thinking of…

In your Update function you can call either d.Get("role") to get a data structure representing all of the blocks in the new configuration or d.GetChange("role") to get both the old blocks (from the prior state) and the new blocks (from the configuration) at once.

Once you have that data structure you can process it in any way you need. One way you could do it is to iterate over the set and transform the two d.GetChange values into a single map containing the old and new values for each distinct name value. I don’t have a real provider handy to test this right now so the following probably isn’t completely right but hopefully it’s some useful pseudocode:

type RoleChange struct {
    Old, New map[string]interface{}
}

o, n := d.GetChange("role")
vals := make(map[string]*RoleChange)
for _, raw := range o.(*schema.Set).List() {
    obj := raw.(map[string]interface{})
    k := obj["name"].(string)
    vals[k] = &Change{ Old: obj }
}
for _, raw := range o.(*schema.Set).List() {
    obj := raw.(map[string]interface{})
    k := obj["name"].(string)
    if _, ok := vals[k]; !ok {
        vals[k] = &Change{}
    }
    vals[k].New = obj
}

for name, change := range vals {
    // A few different situations to deal with in here:
    // - change.Old is nil: create a new role
    // - change.New is nil: remove an existing role
    // - both are set: test if New is different than Old and update if not
}

For this I’m assuming that yours is an API where adding, removing, and updating individual roles for a project are all distinct operations from adding, removing, and updating the project itself, and so you’d presumably be doing an API operation within each iteration of that last loop I included above as a placeholder. If your API just treats roles as a directly property of projects, writing all of them together as part of create/update on the project object, then you probably wouldn’t need anything like this because you can just use the d.Get("role") result directly to populate the new set of roles, without worrying about the old values.

Hi @apparentlymart , I am facing the same issue. Has the set diff display improved in terraform-plugin-framework? I am especially interested in a Set of complex objects.

Hi @instaclustr-wenbodu,

The presentation for diffs is handled by the Terraform CLI, and not related to the sdk used for provider development. The initial issue here is logically inherent to a set data structure, so I would expect the same results with the framework. If you have a specific question about the data handling in sets, please start a new topic.

Thanks!

Hi @jbardin, thank you for your prompt response!

I’m aware that in the Terraform SDK, TypeMap can only handle primitive types. Does this limitation still apply in the terraform-plugin-framework?

In our use case, we have a Terraform resource that we’d like to represent as a set or map. Each element in the map is a complex, mutable object with multiple attributes. In the Terraform plan, we want to display changes to individual objects, rather than showing them as removed and added. If TypeSet can’t display an element as changed, then a TypeMap capable of handling non-primitive types would be ideal.