terraform-plugin-framework icon indicating copy to clipboard operation
terraform-plugin-framework copied to clipboard

Default values within sets cause crashes and/or incorrect behaviour

Open maxb opened this issue 1 year ago • 3 comments

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

  1. 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.
  2. Set up dev_overrides so you can test the provider.
  3. Copy the included Terraform configuration to a file
  4. Run terraform apply -auto-approve (no initial state is needed)
  5. Run terraform apply -auto-approve again ... the provider panics/crashes

Secondary related bug:

  1. Remove any terraform.tfstate from the above reproduction
  2. Reduce the number of items in the set in the Terraform configuration from 3 to 1
  3. 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.

maxb avatar Jun 18 '23 16:06 maxb