Panic: SetValueMust received error(s): Error | Invalid Set Element Type -- but is it?

I’m fooling around with two new custom types with semantic equality in my provider:

StringWithAltValues is a custom string type which contains semantically equal alternate values:

type StringWithAltValues struct {
	basetypes.StringValue
	altValues []attr.Value
}

func NewStringWithAltValuesValue(value string, alt ...string) StringWithAltValues {
	altValues := make([]attr.Value, len(alt))
	for i, a := range alt {
		altValues[i] = StringWithAltValues{StringValue: basetypes.NewStringValue(a)}
	}

	return StringWithAltValues{
		StringValue: basetypes.NewStringValue(value),
		altValues:   altValues,
	}
}

The idea is that values passed to Terraform during Read() might wind up like this:

someVal := NewStringWithAltValuesValue("Space Cowboy", "Gangster of Love", "Maurice")

This way, I won’t have to care which exact string is in the configuration.

I’ve also got this other custom type:

type SetWithSemanticEquals struct {
	basetypes.SetValue
	ignoreLength bool   // currently unused
}

This thing expects elements to implement semantic equality and relies on it in SetSemanticEquals()

The idea, then is that these two sets might be considered semantically equal:

["Maurice", "red"]
["#ff0000", "Midnight Toker", "rojo", "rouge"]

I’m probably doing something obviously wrong, but I can’t see it right now. terraform validate is telling me:

panic: SetValueMust received error(s): Error | Invalid Set Element Type | 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:           types.ObjectType["access_ids":types.SetType[customtypes.StringWithAltValues], "leaf_id":customtypes.StringWithAltValues, "vlan_id":basetypes.Int64Type]
Set Index (0) Element Type: types.ObjectType["access_ids":types.SetType[customtypes.StringWithAltValues], "leaf_id":customtypes.StringWithAltValues, "vlan_id":basetypes.Int64Type]

…which… Those types match, so what’s the problem?

Steps to reproduce:

  1. git clone https://github.com/Juniper/terraform-provider-apstra.git
  2. cd terraform-provider-apstra
  3. git checkout 1373852
  4. go install
  5. curl https://pastebin.com/raw/NTEhgAK7 > main.tf
  6. Set up ~/.terraformrc (below), and then run terraform validate:
# ~/.terraformrc example
provider_installation {
  dev_overrides {
    "Juniper/apstra" = "/path/to/go/bin"
  }
  direct {}
}
1 Like

Hey @hQnVyLRx :wave:, thanks for providing the super easy to run example!

A very confusing error! The cause of these is almost always when a type or value has a bug in the Equals implementation. Which in general, just needs to confirm the value or type is the same struct implementation as the custom type or value, then move on.

In your case, the SetWithSemanticEqualsType isn’t implementing Equals at all: terraform-provider-apstra/apstra/custom_types/string_with_alt_values_type.go at 137385299433faeaee4047c7ea63177daebdbbb4 · Juniper/terraform-provider-apstra · GitHub, so it’s using the embedded Equals implementation from SetType: terraform-plugin-framework/types/basetypes/set_type.go at c126fe413fcc7fe9668a298ffe53c09bd6f99e64 · hashicorp/terraform-plugin-framework · GitHub

Since the your custom type SetWithSemanticEqualsType is not a SetType implementation, it returns false from the Equals, causing SetValueMust to panic. The error message is confusing, but there isn’t really a way for it to know exactly why the Equals method returned false.


For any future travelers, it’s always required for custom types to implement Equals and a couple other methods found here: Plugin Development - Framework: Handling Data - Custom Types | Terraform | HashiCorp Developer

1 Like

Thank you, @austin.valle.

In your case, the SetWithSemanticEqualsType isn’t implementing Equals at all, so it’s using the embedded Equals implementation

I thought I’d found a shortcut :slight_smile:

I’m off to read more about required methods from the linked document.

edit: “With type embedding, the following attr.Type methods must be overridden by the custom type to prevent confusing errors” ← It’s like somebody saw me coming!

1 Like

I’ve gotten around to implementing Equal() on my custom set type…

One of the first things basetypes.Set does in Equal() is compare the element types:

	// A set with no elementType is an invalid state
	if s.elementType == nil || other.elementType == nil {
		return false
	}

	if !s.elementType.Equal(other.elementType) {
		return false
	}

Because I’ve chosen to use an embedded basetypes.Set, the non-exported elementType attribute is out of reach.

I can get to the element type by invoking the ElementTypes() method, but that thing requires me to send a context.Context, which I don’t have in Equal(). Though it doesn’t really require that context.Context. The full text of the function (today, anyway) is:

func (s SetValue) ElementType(_ context.Context) attr.Type {
	return s.elementType
}

So, I need to make a decision here: How will I make it possible for my set’s Equal() method to determine it’s own element type?

Options which have occurred to me:

  1. Invoke the embedded type’s ElementType() method, sending it context.Background().
  2. Invoke the embedded type’s ElementType() method, sending it nil instead of an actual context.Context.
  3. Add an elementType attr.Type struct element to my custom Set. Carry the type around alongside the embedded basetypes.Set
  4. Abandon embedding the basetypes.Set altogether, implement everything.

For #2 to be safe/reasonable, I need to predict the future of a 3rd party package.

#4 Seems like a bunch of unnecessary work along with opportunites to write new bugs.

So, I guess I’m on the fence between #1 (pulling a context.Context out of thin air`) and #3 (storing two copies of the same piece of data). Both options feel bad. Which one should I feel less badly about?

Hmm :thinking:, since you’re embedding SetValue already, I’d say that it should be safe to just verify the other value is the same custom value type and then call the embedded Equal directly, rather than re-implementing the logic:

func (s SetWithSemanticEquals) Equal(o attr.Value) bool {
	other, ok := o.(SetWithSemanticEquals)

	if !ok {
		return false
	}

	return s.SetValue.Equal(other.SetValue)
}

Example (with a string custom type, but I believe it should be relevant): terraform-plugin-framework-jsontypes/jsontypes/normalized_value.go at 25cab516ed484bbe466add5ae70925df98dc4240 · hashicorp/terraform-plugin-framework-jsontypes · GitHub

1 Like

Yes, this is so obviously the answer. :man_facepalming:

Thank you for spelling it out for me.

1 Like

To be fair, I don’t think it’s obvious :laughing:. I’m hoping to get a chance to update our custom type documentation this year, perhaps with a more detailed tutorial of “which methods are important, what they are used for, etc”. :pray: