Error: produced unexpected new value for Computed attribute

I have a user reporting an error that I have to summarize and sanitize, but essentially:

Error: Provided produced inconsistent result after apply
… produced an unexpected new value: .foo
was null, but now cty.ObjectValue(map[string]cty.Value{…})

Note that the logs show expected and accurate values within the cty.ObjectValue(map[string]cty.Value{...}) so the Read is apparently functioning correctly. In fact I would posit the provider plugin code is all functioning as expected since this attribute and all of its field member attributes in the schema are all Computed. This error seems completely counter-intuitive to me as I would fully expect this behavior of a Computed attribute to be null prior to a Read, and then afterwards to be populated with the correct values which is what occurs.

I have scoured through about fifteen or so errors similar to this one, but all but one were for values disappearing unexpectedly, and the remaining error was for some red herring in the Consul provider from 2019.

Is there a documented explanation as to why a Computed attribute must be populated with some sort of “dummy” value prior to a Read and a recommendation on how best to execute that functionality? Thank you.

Hi there @mschuchard :wave:, thanks for posting and sorry you’re running into trouble here.

I’m assuming based on the error message that the provider code is written in terraform-plugin-framework, please correct me if that’s not accurate.

The core of the problem is that the plan is declaring that the object will be null, when really (based on your description), it should be declaring the object is unknown. This usually happens automatically for Computed attributes, and the relevant documentation that might help shed some light on that is here: Plugin Development - Framework: Plan Modification | Terraform | HashiCorp Developer

When the provider receives a request to generate the plan for a resource change via the framework, the following occurs:

  1. Set any attributes with a null configuration value to the default value.
  2. If the plan differs from the current resource state, the framework marks computed attributes that are null in the configuration as unknown in the plan. This is intended to prevent unexpected Terraform errors. Providers can later enter any values that may be known.
  3. Run attribute plan modifiers.
  4. Run resource plan modifiers.

So if your object attribute is marked as Computed and is not set in the Read, (null state), then the resulting Plan should automatically mark it as Unknown to prevent the error you’re describing.

One thing you could check is that the steps 3 + 4 of the above process are not overwriting the unknown marking for the affected attribute that happens in step 2, say with something like: types.ObjectNull(attrTypes)

If that doesn’t help, could you provide the relevant object’s resource schema and maybe any plan modifiers/Read implementation? That would be helpful if I need to re-create your scenario to see if it’s a potential bug.

Thanks!

Thanks for the prompt reply.

Ok I realize now why I had missed the default attribute schema field member: I was always looking at schema package - github.com/hashicorp/terraform-plugin-framework/resource/schema - Go Packages instead of e.g. schema package - github.com/hashicorp/terraform-plugin-framework/resource/schema - Go Packages. Ok good job me moving on…

Just to clarify this was a situation where the resource did not exist in the user’s state, and therefore this is your second described “null state” situation where Read is not invoked, but instead Create is invoked first.

I checked and my attribute PlanModifiers were within other parts of the schema and seem to be behaving as expected as they are primarily stringplanmodifier.UseStateForUnknown(). I have no resource plan modifiers anywhere to my knowledge.

Unfortunately the plugin and the related SDK are both closed source and proprietary that I each developed for a very large company, and so I have no initial idea how that would be possible. Any recommendations for what would qualify as a MCVE?

Basic execution flow for the plugin was:

  1. user declared resource in config
  2. mapstructure/tfsdk schema → tftypes → human_to_api → tf go → sdk go → sdk → api → sdk → sdk go → tf go → api_to_human → tftypes → mapstructure/tfsdk schema (hooray models and converters)
  3. resource did not exist, and therefore Create invoked successfully utilizing SDK (verified in ui); above conversions successful
  4. Read then invoked successfully utilizing SDK (verified in ui); above conversions successful again
  5. error as described above

If it is also helpful then note that the analogous data source for this resource is functioning 100% as expected.

Also what would you recommend with the Default attribute field member as a workaround in the interim for computed struct / SingleNestedAttribute / types.Object?

I just noticed https://github.com/hashicorp/terraform-plugin-testing/pull/154 when inspecting the CHANGELOG because 1.5.0 has some backwards incompatible change breaking my acceptance tests. I will pursue that shortly to attempt to pare down the possibilities here.

Update:

ConfigPlanChecks: resource.ConfigPlanChecks{
					PreApply: []plancheck.PlanCheck{
						plancheck.ExpectUnknownValue("<resource>.test", tfjsonpath.New("foo")),
					},
				},

passes, but if I change the path to id to validate the outcome then it mysteriously throws:

Step 1/1 error: Pre-apply plan check(s) failed:
path not found: specified key id not found in map

so I am unsure of the validity of testing with plancheck.ExpectUnknownValue.

Hmm :thinking:, I think I’ll need to see more of a resource schema to really help investigate further. Maybe I can propose one and we can adjust it to get on the same page.

We’ll start simple and then can adjust to more closely reflect the schema you’re working with:

resp.Schema = schema.Schema{
	Attributes: map[string]schema.Attribute{
		"configure_me": schema.StringAttribute{
			Required: true,
		},
		"foo": schema.SingleNestedAttribute{
			Computed: true,
			Attributes: map[string]schema.Attribute{
				"bar": schema.StringAttribute{
					Computed: true,
				},
			},
		},
	},
}
resource "examplecloud_thing" "this" {
  configure_me = "hello"
}

output "foo" {
  value = examplecloud_thing.this.foo
}

First run

If I have the above schema, the expected behavior would be:

  1. User declares resource in config (not yet in state) and runs terraform apply
  2. Resource plans to create, foo is unknown (no plan modifiers are on the schema)
Terraform will perform the following actions:

  # examplecloud_thing.this will be created
  + resource "examplecloud_thing" "this" {
      + configure_me = "hello"
      + foo          = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + foo = (known after apply)
  1. Create method is called, foo is set to a value, saved to state, all good :+1:

Second run

  1. User changes config, runs terraform apply
resource "examplecloud_thing" "this" {
  configure_me = "hello123"
}

output "foo" {
  value = examplecloud_thing.this.foo
}
  1. Read is called, foo has changed, prior state is refreshed with the new value.
  2. Resource plans to update configure_me and foo is marked as unknown (no plan modifiers, it’s computed so the value could change)
Terraform will perform the following actions:

  # examplecloud_thing.this will be updated in-place
  ~ resource "examplecloud_thing" "this" {
      ~ configure_me = "hello" -> "hello123"
      ~ foo          = {
          ~ bar = "here is a value!" -> (known after apply)
        } -> (known after apply)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Changes to Outputs:
  ~ foo = {
      - bar = "here is a value!"
    } -> (known after apply)
  1. Update method is called, foo is set to a value, saved to state, all good :+1:

Read influences the prior state that is presented to the plan step, but in the example above, there is no plan modifier to suggest that foo should be populated, so it’s set to unknown.

All of this being said, the error you presented was complaining about the plan saying that .foo was null, which for a Computed value, should only be possible if a planmodifier or default sets it to null. If that’s not happening I think it’s a bug.

The only way I’ve been able to recreate your described error is by adding a default like so:

resp.Schema = schema.Schema{
	Attributes: map[string]schema.Attribute{
		"configure_me": schema.StringAttribute{
			Required: true,
		},
		"foo": schema.SingleNestedAttribute{
			Computed: true,
			Default: objectdefault.StaticValue(types.ObjectNull(map[string]attr.Type{
				"bar": types.StringType,
			})),
			Attributes: map[string]schema.Attribute{
				"bar": schema.StringAttribute{
					Computed: true,
				},
			},
		},
	},
}

Then you’ll get this on your next terraform apply

The Update method is setting the value of foo to something that is not null, which is invalid

Terraform will perform the following actions:

  # examplecloud_thing.this will be updated in-place
  ~ resource "examplecloud_thing" "this" {
      - foo          = {
          - bar = "here is an updated value!" -> null
        } -> null
        # (1 unchanged attribute hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Changes to Outputs:
  - foo = {
      - bar = "here is an updated value!"
    } -> null
examplecloud_thing.this: Modifying...
╷
│ Error: Provider produced inconsistent result after apply
│ 
│ When applying changes to examplecloud_thing.this, provider "provider[\"registry.terraform.io/austinvalle/sandbox\"]" produced an unexpected new value: .foo: was null, but now
│ cty.ObjectVal(map[string]cty.Value{"bar":cty.StringVal("here is an updated value!")}).
│ 
│ This is a bug in the provider, which should be reported in the provider's own issue tracker.

Apologies, I feel like I’m running us in circles, but hopefully some of this description can help. There is also documentation on how our RPCs map to the functions that you setup in your resources: Plugin Development - Framework: RPCs | Terraform | HashiCorp Developer

Just a note about this, data sources don’t implement planning, which I think is the main source of this issue. :+1: (This is also why data source schema’s don’t currently have plan modifiers or defaults)

Thanks so much for this. I will cross-reference this with my code to produce a MCVE for you in the next couple of business days.

1 Like

Here goes nothing for MCVE! The real usage is about a hundred times larger than this, but I believe this is a minimal reproducible. A quick side note is that I am also seeing this issue for all resources in the provider plugin with this type of model/schema of nested structs/object, so I am wondering if I am perhaps doing some kind of “early adoption” here.

// go.mod
go 1.20 // 1.20.7

require (
	"my-sdk-go"
	github.com/hashicorp/terraform-plugin-framework v1.3.5
	github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
	github.com/hashicorp/terraform-plugin-go v0.18.0
	github.com/hashicorp/terraform-plugin-log v0.9.0
	github.com/hashicorp/terraform-plugin-testing v1.5.1
	golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
)

// go sdk
// this is also the response body for later reference in provider plugin Create()/Read()
type Foo struct {
  Bar Bar `json:"bar"`
}

type Bar struct {
  Baz string `json:"baz"`
}

// provider plugin
// tf go models
type fooModel struct {
  Bar types.Object `tfsdk:"bar"`
}

type barModel struct {
  Baz types.String `tfsdk:"baz"`
}

// tf model
var barModelTF = map[string]attr.Type{
  "baz": types.StringType
}

// tf schema
// why the below line? because this is in a substantial internal side-util package streamlining and DRY'ing the many enormous models and converters between the five different required model types for a provider plugin
import resource "github.com/hashicorp/terraform-plugin-framework/resource/schema"
// and the above line also clarifies why below is "resource" and not "schema" for the package because the "data" schema exists here also

// this did not copy/paste very well prior to editing; if a syntax error exists here it does not exist in the actual code
Attributes: map[string]schema.Attribute{
  "foo": resource.SingleNestedAttribute{
		Computed:    true,
		Description: "The foo attributes.",
		Attributes: map[string]resource.Attribute{
			"bar": resource.StringAttribute{
				Computed:    true,
				Description: "The bar.",
			},
},
},
}

// create
func (resource *fooResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
	// read Terraform plan data into the model
	var data util.fooModel
	resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
	if resp.Diagnostics.HasError() {
		return
	}

	// assume below is executing sdk successfully and returning Foo type struct; note at this point data.ID is empty string
	responseBody, err := foo.DoFoo(resource.client, "POST", data.ID.ValueString())
	tflog.Info(ctx, "foo created", map[string]any{"success": true})
        // error handling etc. omitted

	// assign response fields to schema values
	// assign bar attr as id
	data.ID = types.StringValue(responseBody.Bar.BarAttr) // yes ID and barattr are omitted in mcve models for brevity
	// assign foo attributes
	var barConvertDiags, otherConvertDiags diag.Diagnostics
	data.Bar, barConvertDiags = util.BarGoToTerraform(ctx, responseBody.Bar)
	resp.Diagnostics.Append(barConvertDiags...)
	if resp.Diagnostics.HasError() {
		return
	}

	// more conversions here analogous to above, but the above alone is enough to trigger this error

	// set state
	resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

// read
func (resource *fooResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
	// determine input values
	var state util.fooModel
	resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
	if resp.Diagnostics.HasError() {
		return
	}

	// assume below is executing sdk successfully and returning Foo type struct
	responseBody, err := foo.DoFoo(resource.client, "GET", state.ID.ValueString())
        // error handling etc. omitted

	// assign response fields to schema values
	// assign foo attributes
	var barConvertDiags, otherConvertDiags diag.Diagnostics
	state.Bar, barConvertDiags = util.BarGoToTerraform(ctx, responseBody.Bar)
	resp.Diagnostics.Append(barConvertDiags...)
	if resp.Diagnostics.HasError() {
		return
	}

	// more conversions here analogous to above, but the above alone is enough to trigger this error

	// set state
	resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
	if !resp.Diagnostics.HasError() {
		tflog.Info(ctx, "Determined Foo information", map[string]any{"success": true})
	}
}

util.BarGoToTerraform(ctx, sdk.Bar) (types.Object, diag.Diagnostics) can be provided also, but it would probably be confusing and I think it is also unrelated to root cause here. At a very high level it converts sdk.Bar to barModel to barModelTF to types.Object for assignment to state.Bar. Communication to me in previous conversations about the plugin framework, and successful experiences thus far, is that the top level TF schema needs to correlate to a struct TF Go model, and the field members to tftype TF models.

I have full control over the Go SDK and TF provider plugin. I have no control over the API.

I’m not familiar with the use of a double-colon in the struct tag. What does it do?

That was a typo for the mapstructure tags; I fixed it.

1 Like

For future readers, the discussion has been moved to this GH issue: Provider produced unexpected value after apply for a Computed attribute · Issue #840 · hashicorp/terraform-plugin-framework · GitHub