terraform-plugin-framework
terraform-plugin-framework copied to clipboard
Default values within sets cause crashes and/or incorrect behaviour
Module version
v1.3.1
Relevant provider source code
This is the entire source code of a single .go file provider that demonstrates the issues:
package main
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
)
func main() {
err := providerserver.Serve(
context.Background(),
func() provider.Provider {
return new(bugProvider)
},
providerserver.ServeOpts{
Address: "bug/bug/bug",
},
)
if err != nil {
panic(err)
}
}
type bugProvider struct{}
func (p *bugProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "bug"
}
func (p *bugProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
}
func (p *bugProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
}
func (p *bugProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return nil
}
func (p *bugProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
func() resource.Resource {
return new(bugResource)
},
}
}
type bugResource struct{}
func (r bugResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_bug"
}
func (r bugResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"set": schema.SetNestedAttribute{
Required: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"fruit": schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.StaticString("orange"),
},
"other": schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.StaticString("other"),
},
},
},
},
},
}
}
func (r bugResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
}
func (r bugResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
resp.State.Raw = req.Plan.Raw
}
func (r bugResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
resp.State.Raw = req.Plan.Raw
}
func (r bugResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
}
Terraform Configuration Files
terraform {
required_providers {
bug = {
source = "bug/bug/bug"
}
}
}
resource "bug_bug" "this" {
set = [
{ fruit = "apple" },
{ fruit = "banana" },
{ fruit = "kumquat" },
]
}
Debug Output
https://gist.github.com/maxb/f0b606530531a1f7e89c1ac122f141da
Steps to Reproduce
- Copy the included provider source code to a file, add a
go.mod
including the latest version of terraform-plugin-framework (no other dependencies needed), build the provider. - Set up
dev_overrides
so you can test the provider. - Copy the included Terraform configuration to a file
- Run
terraform apply -auto-approve
(no initial state is needed) - Run
terraform apply -auto-approve
again ... the provider panics/crashes
Secondary related bug:
- Remove any
terraform.tfstate
from the above reproduction - Reduce the number of items in the set in the Terraform configuration from 3 to 1
- Repeatedly run
terraform apply -auto-approve
... observe that provider repeatedly changes the value back and forth on each run, oscillating between the value actually written in the configuration, and the value set as a default in the code.
Partial diagnosis
The terraform-plugin-framework appears to use a bafflingly complex algorithm to apply defaults, involving correlating the state and the configuration.
This is a victim of its own complexity when dealing with sets of objects, as the identity of an object within a set incorporates its own value ... a value that may itself have defaults. This means the identity of a set member in the config may omit unspecified attributes, whilst the identity of the same set member in the state will include unspecified attributes, now set to their default values.