SetNestedBlock with custom custom type for the nested object doesn't seem to work as expceted

I’m trying to migrate my resource definition from Terraform Plugin SDK v2 to Plugin Framework. I need to maintain (at least for now) backwards compatibility for the users of the provider and I need to stick to blocks for objects, rather than attributes.

So far, the road has been bumpy, to say the least, however, I’ve been stuck with a peculiar problem for days now and would appreciate any suggestions/help.

What I’m trying to achieve?

I have an object, called “objective”, it’s a block set (and has to stay this way for backwards compatibility). The elements of the set are fairly complex, they have some computed fields. Previously (in the SDK) there was this very hand schema.NewSet function, which also accepted a hashing function and thus let me only focus on a couple of attributes which uniquely defined each element.
Now, there’s no such option out of the box, so I decided to implement a custom equality function for these elements, and in order to do that I had to create a custom type:

return schema.SetNestedBlock{
	Description:         description,
	MarkdownDescription: description,
	Validators: []validator.Set{
		setvalidator.IsRequired(),
		setvalidator.SizeAtLeast(1),
	},
	NestedObject: schema.NestedBlockObject{
		CustomType: objectiveType,
		Attributes: objectiveAttributes,
		Blocks:     objectiveBlocks,
	},
}

I can link the full type definition, along with a corresponding value definition, but these two seem to be working well.

What I’m getting now, is when Terraform is reading the config, it drops the following error:

While creating a Set value, an invalid element was detected. A Set must use
the single, given element type. This is always an issue with the provider and
should be reported to the provider developers.
        
Set Element Type: frameworkprovider.sloObjectiveType
Set Index (0) Element Type:
types.ObjectType[...

This was unexpected, but I figured maybe I’ve missed adding CustomType to the set schema? So I added this:

CustomType: types.SetType{
	ElemType: objectiveType,
},

To no avail.
And thus I wonder, what I’m missing here? Is this even the right path? Or maybe there’s some other, easier way to implement custom set elements uniqueness constraints?


Framework version: v1.15.0

Hi @nieomylnieja,

The error isn’t coming from Terraform, but rather the provider framework, but you said the error happens when reading the config so it would be useful to see exactly how you are reaching the error. If Terraform can decode the config but the framework throws an error there may be a bug somewhere in the framework. Nested blocks in the framework haven’t gotten a ton of use yet since they are mainly there to help legacy providers migrate off the old SDK.

Without a reproducible example i can only guess, but does objectiveType contain any dynamic types?

Note that the legacy SDK set func can’t be used to force equality of different values, because doing so would disagree with the Terraform type system. It must always return a unique value for different elements. The same goes for elements of sets under the new type system, you can’t avoid the fact that Terraform knows nothing about a custom equality function, different values within a set will always be different values, and they must always be of the same type. Legacy providers were given some leeway here because the SDK could not follow the protocol correctly, and you could check if you were violating some of these constraints by looking for warnings in the Terraform logs during plan and apply.

Sorry, I wasn’t very precise, when I said it comes from Terraform, I know it comes from the framework :slight_smile:

objectiveType does not contain any dynamic types, it does however contain computed fields, it’s a fairly complex structure, the computed fields are the reason I went on with these custom types, otherwise I was having a messy plan with each update.

I can share the branch I’m working on, rather than pasting all that code, the type and schema definitions are defined here: terraform-provider-nobl9/internal/frameworkprovider/slo_resource_schema.go at f5207aca06e97b4e555ef34e79381b9955b6cb8f · nobl9/terraform-provider-nobl9 · GitHub.

Yeah, I figured that about legacy SDK, the hash func was returning an int, so it only really conveyed that a certain set elements were equal and nothing more.

Overall, I think I solved the issue, after deeper debugging, I pinned down the problem to terraform-plugin-framework/internal/fwserver/block_plan_modification.go at acbc06a5e7a43fdc192a5995bc5ad7d855a37be2 · hashicorp/terraform-plugin-framework · GitHub.

I missed ValueFromObject interface implementation in my custom type :man_facepalming:

Overall, the process of writing custom objects is rather involved, is there a chance for a documentation update which would contain an example for that? There are only custom string examples, which vastly simpler.

Great! I’m glad you find the problem. Complex set types can be tricky in Terraform for a variety of reasons, so one thing that I recommend if possible is to make sure the set elements are individually identifiable by only their non-computed attributes. For example, if elements only differ by computed values, it can be hard or impossible for the framework to know which individual elements are the ones changing during a plan because the unknown values could correlate to any prior computed field. This results in the users seeing a more conservative plan indicating that all similar elements might change, which could cause unexpected changes for them in the overall Terraform plan.

I’ll ping that team about the docs, but they’ll probably see this anyway :wink:

Thanks for pinging the docs team!

Could you explain what “identifiable” means in the context of set elements? Is the Equals function responsible for that? I don’t think so, right? I have a couple of computed fields in objective schema, but I marked them as required instead, just to test the plan, and I still have the whole set element listed:

Since the whole point of creating this custom object was to give it the custom Equals function it looks like I’ve went the wrong rabbit hole :stuck_out_tongue:
Is there a way for me to produce a better looking plan for sets, one which only highlights changes to the specific field, and no the whole set element?
Since it makes no difference in the config definition, maybe I should just switch to lists and sort them on my own?

By “identifiable” I only mean that the non-computed attributes alone should make the object unique. It has nothing to do with actual equality, just that when Terraform or the framework tried to calculate the change it could use non-computed attributes which have not changed to help correlate the objects. It’s not a hard and fast rule, just something that must be considered if you need more precise plans.

Yeah, the custom Equals function there is just satisfy the interface, because some types can’t natively support comparison. SemanticEquality can be used to alter the planning behavior in order to suppress unwanted changes, but that’s also not going to affect the plan format.

What you’re showing here is a result of the plan renderer interpreting the change to display in the UI. Because each element of a set is identified by it’s entire value, the underlying change in data is that the old element is removed and a new element with a different value is added, and that is what the renderer is showing. Your plan output is correct here, so there’s nothing you can change in that plan, the rendering it outside of your control.

There can be benefits to using a list, and a lot of providers default to that now anyway. That might take some semantic equality work to ensure that reordering doesn’t show up in the plan too often, because while you can sort things for comparison, you can’t change the order of the objects from what the user wrote in the configuration. This means if the user wrote (using the reference syntax) objective[0].display_name is “OK1” in the config, that must be written to state in the 0 index or the apply will fail. You can suppress later semantically equal changes from the prior state, like reordering, but once the user changes a value then you must now show all the reordered changes to write the new state (technically they will show up as each individual index having a change to the new index value, since there’s no way to diff index changes alone)

That makes sense, thanks for the in-depth explanation!

Yeah, I understand that the tradeoff for lists would be showing plan changes If the users change the order of the elements in their config, which I think is overall more user friendly than showing the diff every time users modify any attribute of the nested set object.

Overall, consider the issue closed and resolved :slight_smile:
Thanks again for the quick responses, helped me a lot!