Skip to content

Value of optional attributes with defaults is mishandled when multiple exist in the same block #1256

@positiveEV

Description

@positiveEV

Module version

github.com/hashicorp/terraform-plugin-framework v1.17.0

Relevant provider source code

func (r IncusNetworkLBResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            "network": schema.StringAttribute{
                Required: true,
                PlanModifiers: []planmodifier.String{
                    stringplanmodifier.RequiresReplace(),
                },
            },

            "listen_address": schema.StringAttribute{
                Required: true,
                PlanModifiers: []planmodifier.String{
                    stringplanmodifier.RequiresReplace(),
                },
            },
            "description": schema.StringAttribute{
                Optional: true,
                Computed: true,
                Default:  stringdefault.StaticString(""),
            },

            "project": schema.StringAttribute{
                Optional: true,
                PlanModifiers: []planmodifier.String{
                    stringplanmodifier.RequiresReplace(),
                },
                Validators: []validator.String{
                    stringvalidator.LengthAtLeast(1),
                },
            },

            "remote": schema.StringAttribute{
                Optional: true,
                PlanModifiers: []planmodifier.String{
                    stringplanmodifier.RequiresReplace(),
                },
            },

            "config": schema.MapAttribute{
                Optional:    true,
                Computed:    true,
                ElementType: types.StringType,
            },
        },

        Blocks: map[string]schema.Block{
            "backend": schema.SetNestedBlock{
                Description: "Network load balancer backend",
                NestedObject: schema.NestedBlockObject{
                    Attributes: map[string]schema.Attribute{
                        "name": schema.StringAttribute{
                            Required:    true,
                            Description: "LB backend name",
                        },

                        "description": schema.StringAttribute{
                            Optional:    true,
                            Computed:    true,
                            Description: "LB backend description",
                            Default:     stringdefault.StaticString(""),
                        },

                        "target_address": schema.StringAttribute{
                            Required:    true,
                            Description: "LB backend target address",
                        },

                        "target_port": schema.StringAttribute{
                            Optional:    true,
                            Description: "LB backend target port",
                        },
                    },
                },
            },
            "port": schema.SetNestedBlock{
                Description: "Network load balancer port",
                NestedObject: schema.NestedBlockObject{
                    Attributes: map[string]schema.Attribute{
                        "description": schema.StringAttribute{
                            Optional:    true,
                            Computed:    true,
                            Default:     stringdefault.StaticString(""),
                            Description: "Port description",
                        },

                        "protocol": schema.StringAttribute{
                            Optional:    true,
                            Computed:    true,
                            Default:     stringdefault.StaticString("tcp"),
                            Description: "Port protocol",
                            Validators: []validator.String{
                                stringvalidator.OneOf("tcp", "udp"),
                            },
                        },

                        "listen_port": schema.StringAttribute{
                            Required:    true,
                            Description: "Port to listen to",
                        },

                        "target_backend": schema.SetAttribute{
                            Required:    true,
                            Description: "List of target LB backends",
                            ElementType: types.StringType,
                            Validators: []validator.Set{
                                setvalidator.SizeAtLeast(1),
                            },
                        },
                    },
                },
            },
        },
    }
}

Terraform Configuration Files

resource "incus_network_lb" "test_v4_debug" {
  network = incus_network.ovn-test.name
  listen_address = "10.10.10.50"
  port {
    protocol    = "udp"
    listen_port = "100"
    target_backend = [
      "toto_udp",
    ]
  }
   backend {
     description = "myback"
     name = "toto_udp"
     target_address = "10.10.10.150"
   }
}

Debug Output

{"@caller":"/home/admin/go/pkg/mod/github.com/hashicorp/terraform-plugin-framework@v1.17.0/internal/fwschemadata/data_default.go:370","@level":"trace","@message":"setting attribute port[Value({\"description\":\"\",\"listen_port\":\"100\",\"protocol\":\"udp\",\"target_backend\":[\"toto_udp\"]})].protocol to default value: \"tcp\"","@mo
dule":"sdk.framework","@timestamp":"2026-01-17T17:03:25.869120+01:00","tf_provider_addr":"registry.opentofu.org/lxc/incus","tf_req_id":"e81a5e95-bcca-1cf8-d36f-457cfb8ed825","tf_resource_type":"incus_network_lb","tf_rpc":"PlanResourceChange"}

Expected Behavior

The port protocol should remain UDP and not be set to the default value

Actual Behavior

The port protocol is changed to TCP

Steps to Reproduce

  1. tofu apply a first time; the port will be correctly created in UDP.
  2. tofu plan or tofu apply ; the port protocol will be changed to TCP

Workaround

  • Setting a description (the only other optional attribute with a default in the same block) prevents the bug.

References

Issue location

Using Delve, I was able to locate the issue at github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata.Data.ValueAtPath
When we are not setting the description attribute in the port block, the framework will set it to its default value at internal/fwschemadata/data_default.go:350. This behavior is expected.
Then Terraform will look for the protocol attribute of the port in internal/fwschemadata.Data.ValueAtPath()
Line 64 tfValue, err := d.TerraformValueAtTerraformPath(ctx, tftypesPath) returns nil and ErrInvalidStep.
Since tfValue is nil it will cause the protocol attribute to be set to his default.

Cause of the issue

This error is raised by https://github.com/hashicorp/terraform-plugin-go/blob/dfb1155c79e13595a8c70a4c487e1a0ba9a478f5/tftypes/value.go#L202
This is caused by stepValue (the state of the port block as initially defined, who got its value from the d arg in the function internal/fwschemadata.Data.ValueAtPath()) differing from sl (who represents the state of the port as it is now, meaning after Terraform sets description to its default value. The sl value comes from tftypesPath

tftypesPath, tftypesPathDiags := totftypes.AttributePath(ctx, schemaPath)
)
This means that in stepValue, the description attribute of the port block is nil, while in sl it is an empty string "".
Therefore these 2 variables are not equal, and this interrupts the function TerraformValueAtTerraformPath() before it can retrieve the value of the protocol attribute.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions