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

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!