SetNestedAttribute,Executing terraform apply gets an error "Error: Provider produced inconsistent result after apply"

  • Terraform v1.8.4 darwin_arm64
  • go1.22.1
  • terraform-plugin-framework v1.9.0

Hi team, I tried to develop a provider using SetNestedAttribute, but when I executed terraform apply, I got an error. I tried for a long time but couldn’t figure out why this happened.

Error: Provider produced inconsistent result after apply

When applying changes to example_set_nested.test01, provider "provider[\"test.com/test/example\"]" produced an unexpected new value:
.set_nested: planned set element cty.ObjectVal(map[string]cty.Value{
  "enable_gateway":cty.False, 
  "fixed_ip":cty.StringVal("127.0.0.1"),
  "fixed_ip_v4":cty.NullVal(cty.String), 
  "mac":cty.NullVal(cty.String), 
  "port":cty.NullVal(cty.String), 
  "uuid":cty.StringVal("123")
}) 
does not correlate with any element in actual.

This is a bug in the provider, which should be reported in the provider's own issue tracker.

Sample code using SetNestedAttribute

type (
	SetNestedResourceModel struct {
		Id        types.String `tfsdk:"id"`
		SetNested types.Set    `tfsdk:"set_nested"`
	}

	SetNestedModel struct {
		Uuid          types.String `tfsdk:"uuid"`
		FixedIp       types.String `tfsdk:"fixed_ip"`
		FixedIpV4     types.String `tfsdk:"fixed_ip_v4"`
		Port          types.String `tfsdk:"port"`
		Mac           types.String `tfsdk:"mac"`
		EnableGateway types.Bool   `tfsdk:"enable_gateway"`
	}
)

var (
	setNestedModelTypeMap = map[string]attr.Type{
		"uuid":           types.StringType,
		"fixed_ip":       types.StringType,
		"fixed_ip_v4":    types.StringType,
		"port":           types.StringType,
		"mac":            types.StringType,
		"enable_gateway": types.BoolType,
	}
)

func (r *SetNestedResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"id": schema.StringAttribute{
				MarkdownDescription: "ID",
				Computed:            true,
				PlanModifiers: []planmodifier.String{
					stringplanmodifier.UseStateForUnknown(),
				},
			},
			"set_nested": schema.SetNestedAttribute{
				MarkdownDescription: "Example configurable attribute",
				Optional:            true,
				NestedObject: schema.NestedAttributeObject{
					Attributes: map[string]schema.Attribute{
						"uuid": schema.StringAttribute{
							MarkdownDescription: "经典网络ID",
							Required:            true,
						},
						"fixed_ip": schema.StringAttribute{
							MarkdownDescription: "指定IP地址",
							Optional:            true,
						},
						"fixed_ip_v4": schema.StringAttribute{
							MarkdownDescription: "指定IPv4地址",
							Computed:            true,
						},
						"port": schema.StringAttribute{
							MarkdownDescription: "网卡端口ID",
							Computed:            true,
						},
						"mac": schema.StringAttribute{
							MarkdownDescription: "MAC地址",
							Computed:            true,
						},
						"enable_gateway": schema.BoolAttribute{
							MarkdownDescription: "是否启用网关",
							Optional:            true,
							Computed:            true,
							Default:             booldefault.StaticBool(false),

							//Required:            true,
						},
					},
				},
			},
		},
	}
}

func (r *SetNestedBlockResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
	// .........

	data.Id = types.StringValue("example-id")
	diags := data.fnConvert(ctx)

	// .........
}

func (r *SetNestedBlockResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
	// .........

	diags := data.fnConvert(ctx)

	// .........
}

func (s *SetNestedBlockResourceModel) fnConvert(ctx context.Context) diag.Diagnostics {
	var diags diag.Diagnostics

	sSetNested := s.SetNested
	var sSetNestedBlockModels []SetNestedBlockModel
	sSetNested.ElementsAs(ctx, &sSetNestedBlockModels, false)

	processedSetNestedBlockModels := make([]SetNestedBlockModel, 0)

	for i, model := range sSetNestedBlockModels {
		model.Port = types.StringValue(fmt.Sprintf("port_id_%d", i))
		model.Mac = types.StringValue(fmt.Sprintf("mac_address_%d", i))

		mFixedIp := model.FixedIp
		if mFixedIp.IsNull() || mFixedIp.IsUnknown() {
			model.FixedIpV4 = types.StringValue(fmt.Sprintf("fixed_ip_%d", i))
		} else {
			model.FixedIpV4 = mFixedIp
		}

		processedSetNestedBlockModels = append(processedSetNestedBlockModels, model)
	}

	if len(processedSetNestedBlockModels) > 0 {
		sets, diags1 := types.SetValueFrom(ctx, types.ObjectType{
			AttrTypes: SetNestedBlockModelTypeMap,
		}, processedSetNestedBlockModels)

		diags.Append(diags1...)

		s.SetNested = sets
	}

	return diags
}

main.tf.json

{
  "resource": {
    "example_set_nested": {
      "test01": {
        "set_nested": [
          {
            "uuid": "123",
            "fixed_ip": "127.0.0.1"
          }
        ]
      },
      "test02": {
        "set_nested": [
          {
            "uuid": "234",
            "enable_gateway": true
          }
        ]
      }
    }
  }
}

Command line output of executing terraform apply, I found that the output of “test01” and “test02” are different, “test01” does not display “fixed_ip_v4”, “mac”, “port”, I think “test01” should display these contents. Although the execution failed, the data was correctly written into the tfstate file.


And then, I modified the main.tf.json file, added " “enable_gateway”: true " in “test01”, and executed terraform apply again and got the correct result.


I am very confused now. I think whether I add “enable_gateway” in “main.tf.json” or not, the execution result should be the same.

Can someone answer this question? Thanks.

Sample code:set_nested_resourcemain.tf.json

Hey there @hyfly233, thanks for posting a complete example and sorry you’re running into trouble here.

This is actually a known bug with SetNestedAttributes and underlying default values, if you’d like to upvote + subscribe to the GH issue for past/future updates, it’s here → Default values within sets cause crashes and/or incorrect behaviour · Issue #783 · hashicorp/terraform-plugin-framework · GitHub

The root problem is that sets in Terraform derive their identity from the actual value (rather than an index for lists, or key for maps). If it’s possible to build your resource using a different collection, like a list, that’d probably be preferable as there are a few awkward bugs with set implementations that usually boil down to the identity/lack of control in plan algorithms when dealing with sets.