Custom Provider: How to reference computed attribute of TypeMap/List/Set defined as nested block

I am writing a custom provider that, for simplicity’s sake, creates a project resource with many nested but unique role resources. Each role has a computed attribute id that I would like to reference in other project resources. Example ideal terraform:

resource "custom_project" "foo" {
  name = "foo"
  role {
    name = "owner"
  }
  role {
    name = "editor"
  }
}

resource "custom_project" "foo-internal" {
  name         = "foo-internal"
  approverRole = custom_project.foo.role["owner"].id
   
  # ...
}

I have tried making role a TypeSet but when I test interpolation it says I cannot reference items in a set by key (which I totally expected). Example:

"role": {
	Type: schema.TypeSet,
	Set: func(v interface{}) int {
		m := v.(map[string]interface{})
		return schema.HashString(m["name"])
	},
	Elem: &schema.Resource{
		Schema: map[string]*schema.Schema{
			"name": {Type: schema.TypeString Required: true},
			"id": {Type: schema.TypeString Computed: true},
		},
	},
}

I know TypeList is not what I want because I don’t want to reference roles by index and I am having trouble understanding how I might do it with TypeMap.

If I could use block labels I would, but I am not seeing how I can configure the role attribute to take in required labels. Is that possible? Something like:

role "owner" { }
role "viewer" { }

Is there a way I can enable the ability to reference these nested role’s computed attributes by keys like owner without defining a new resource type like custom_project_role?

Hi @bschaeffer,

What you’ve encountered here is unfortunately a typical modelling challenge when it comes to mapping existing APIs to Terraform. The problem here is a mixture of necessary complexity and of some remaining limitations of the Terraform SDK, so I’ll try to explain both here first and then discuss some patterns that can work within those current constraints.

As you’ve seen, it doesn’t really make sense to put Computed: true attributes in a “set” collection, because the elements of a set are identified only by their values and so having part of the object be decided at apply time by the provider means that there’s no way to predict ahead of time what the value would be.

It looks like you already noticed the intended answer to this, which is to allow a block type to be represented as a map using block labels as keys. Unfortunately that’s where we get into the limitations of the current SDK: this SDK was originally designed around the capabilities of Terraform v0.11 and earlier, and so unfortunately it lacks support for having a block type backed by a map even though modern Terraform itself supports it in principle. :confounded:

A new SDK built around the capabilities of modern Terraform is in progress, with the lowest-level component already available in experimental form and a higher-level version with a similar abstraction level as the current SDK to follow later, but I know that’s not a very satisfying answer when you want to develop a provider right now.

With that in mind, I’ll share a typical design pattern that originates from the Terraform v0.11 era, which is within the modelling capabilities of the current Terraform SDK. The idea of this is to separate the blocks that the user configures from the data that the provider generates, so that you’ll have two separate items in your schema. For example:

	"role": {
		Type: schema.TypeSet,
		Elem: &schema.Resource{
			Schema: map[string]*schema.Schema{
				"name": {Type: schema.TypeString, Required: true},
				// (and whatever other arguments you need)
			},
		},
		Optional: true,
	},
	"role_ids": {
		Type: schema.TypeMap,
		Elem: &schema.Schema{
			Type: schema.TypeString,
		},
		Computed: true,
	},

With this approach, the configuration of the resource itself still looks the same as you showed it, but the references to the role ids from elsewhere in the configuration will look a bit different:

custom_project.foo.role_ids["owner"]
custom_project.foo.role_ids["editor"]

This structure works because the SDK (and Terraform v0.11) supported maps of primitive types, but not maps of objects, due to some limitations of the wire protocol. Therefore we can export the ids into a separate map of strings, while leaving the role block type to still be represented by a set.

Note also that you now don’t need to include an explicit Set function, because the SDK knows how to generate a suitable hash value automatically when none of the arguments in the block are Computed: true.

You can see an example of this pattern in real-world use in the consul_keys data source implemented by the hashicorp/consul provider. This is quite an old design so it follows an older naming convention where its map is named as singular var rather than plural vars, but you can see in the implementation that its schema is structured just like the example I showed above:

I hope that helps!

@apparentlymart,

Thanks for your reply. That was one of the best responses to a forum post I have ever received and definitely keeps me from spinning wheels trying to solve the problem, so it really helps!

I didn’t realize there was a new SDK in the works. Really excited to start following its progress. In the meantime, it is nice to see we have a pattern we can follow.