Skip to content

Default values within sets cause crashes and/or incorrect behaviourΒ #783

@maxb

Description

@maxb

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.

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