Inconsistency problem

During terraform provider development I encountered the following problems.

=== First question ===
There is a plan to implement resource for RAID volume creation.
Server endpoint supports parameter capacity in bytes which behaves in the following manner:

  1. value can be not provided by user (=optional) - then volume is created out of maximum size
  2. value can be provided by user - then volume is created with requested size, but it’s not exactly same as requested - for example: user might request 100GB volume, but capacity returned by the server will be 99.9GB in bytes, so it will be not exactly same as requested.

Inside of Terraform resource schema, the property has been declared by me as Optional and Computed.
Such server behavior leads to problems with plugin development because of inconsistency between plan and state in some scenarios.
Read() function IMO cannot rely on plan value, should rely on real value from the server, but it will be different from the real plan leading to warnings / errors raised by provider during normal operation or during acceptance tests. From other side I cannot resign of using no value in the plan to support full volume creation…

Using lifecycle::ignore_changes mechanism does not solve the problem.
Does anyone has an idea? I’m rather beginner in provider development and hope the question is not stupid but I can`t find working solution.

=== Second question ===
Is there a mechanism inside of schema which can be used to handle the following situations:

  1. resource parameter which can be used only during resource creation, but cannot be modified when resource is already created - such parameters can be filtered out in resource Update implementation to not take part, but maybe it can be solved on schema layer already?
  2. resource parameter which can be modified once resource is created, but cannot be set up during resource creation - so if someone will apply a value in plan during resource creation which will not be equal to e.g.: default value taken for resource creation, it might lead to conflicts as well.

volume is created with requested size, but it’s not exactly same as requested

This might be a use case for a Custom Type which implements Semantic Equality.

resource parameter which can be used only during resource creation, but cannot be modified

This sounds like a RequiresReplace() Attribute Plan Modifier could be included in the schema. Rather than “cannot be modified”, think of this as “resource must be re-created to accommodate modification of this attribute”.

resource parameter which can be modified once resource is created, but cannot be set up during resource creation

I don’t think I understand the constraint here. An example would be helpful.

Thanks for hints! I will double check them.

Regarding last question:
I figured out right now that there is nothing against applying some parameters of the plan during resource creation request to backend and then apply a change for all remaining ones which are not acceptable in initial request.

I’ve found that it’s important to set the id attribute (resp.State.Set()) after making the first API call in cases where my provider has to do stuff like this.

This way, if you encounter an error on the 2nd request, the terraform state will know that the resource got created, and can fix/update it when the user tries again.

Hi,
I’m struggling with semantics equal mechanism…
It looks like code implemented as Int64SemanticEquals() is called and the function returns true, but for some reason terraform still returns “inconsistent result after apply” when planned value for capacity_bytes is different than the one returned from real volume after creation.
I intentionally put warning to confirm that the call is done. If warning is removed, it does not change anything. I tried to return true in every function which could be called from these implemented below - nothing has changed. Do you maybe have an idea what is missing? Implementation follows (IMO) example for string in official documentation.

2024-08-16T11:49:07.956+0200 [TRACE] statemgr.Filesystem: state has changed since last snapshot, so incrementing serial to 47
2024-08-16T11:49:07.956+0200 [TRACE] statemgr.Filesystem: writing snapshot at terraform.tfstate
╷
│ Warning: Int64SemanticsEquals
│
│   with irmc-redfish_storage_volume.volume["batman"],
│   on resource.tf line 1, in resource "irmc-redfish_storage_volume" "volume":
│    1: resource "irmc-redfish_storage_volume" "volume" {
│
│ Difference is ok!
╵
╷
│ Error: Provider produced inconsistent result after apply
│
│ When applying changes to irmc-redfish_storage_volume.volume["batman"], provider "provider[\"hashicorp/fujitsu/irmc-redfish\"]" produced an unexpected new value: .capacity_bytes: was cty.NumberIntVal(1e+08), but now cty.NumberIntVal(9.961472e+07).
│
│ This is a bug in the provider, which should be reported in the provider's own issue tracker.
╵
2024-08-16T11:49:07.957+0200 [TRACE] statemgr.Filesystem: removing lock metadata file .terraform.tfstate.lock.info
2024-08-16T11:49:07.957+0200 [TRACE] statemgr.Filesystem: unlocking terraform.tfstate using fcntl flock
2024-08-16T11:49:07.958+0200 [DEBUG] provider.stdio: received EOF, stopping recv loop: err="rpc error: code = Unavailable desc = error reading from server: EOF"
2024-08-16T11:49:07.962+0200 [INFO]  provider: plugin process exited: plugin=/home/andrzej/go/bin/terraform-provider-irmc-redfish id=1308882
2024-08-16T11:49:07.962+0200 [DEBUG] provider: plugin exited```

How the custom type and semantics logic are implemented.

package models

import (
“context”
“fmt”

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-go/tftypes"

)

type CapacityByteValue struct {
basetypes.Int64Value
}

var _ basetypes.Int64Valuable = CapacityByteValue{}
var _ basetypes.Int64ValuableWithSemanticEquals = CapacityByteValue{}
var _ basetypes.Int64Typable = CapacityByteType{}

type CapacityByteType struct {
basetypes.Int64Type
}

func (t CapacityByteType) Equal(o attr.Type) bool {
return true
}

func (t CapacityByteType) String() string {
return “CapacityByteType”
}

func (t CapacityByteType) ValueFromInt64(ctx context.Context, in basetypes.Int64Value) (basetypes.Int64Valuable, diag.Diagnostics) {
value := CapacityByteValue{
Int64Value: in,
}

return value, nil

}

func (t CapacityByteType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
attrValue, err := t.Int64Type.ValueFromTerraform(ctx, in)

if err != nil {
    return nil, err
}

stringValue, ok := attrValue.(basetypes.Int64Value)

if !ok {
    return nil, fmt.Errorf("unexpected value type of %T", attrValue)
}

stringValuable, diags := t.ValueFromInt64(ctx, stringValue)

if diags.HasError() {
    return nil, fmt.Errorf("unexpected error converting Int64Value to IntValuable: %v", diags)
}

return stringValuable, nil

}

func (v CapacityByteType) ValueType(ctx context.Context) attr.Value {
return CapacityByteValue{}
}

func (v CapacityByteValue) Int64SemanticEquals(_ context.Context, newValueable basetypes.Int64Valuable) (bool, diag.Diagnostics) {
var diags diag.Diagnostics
newValue, ok := newValueable.(CapacityByteValue)
if !ok {
diags.AddError(“Semantics equality check error”, “”)
return false, diags
}

diff := v.Int64Value.ValueInt64() - newValue.ValueInt64()
if (diff < 50000000) {
    diags.AddWarning("Int64SemanticsEquals", "Difference is ok!")
    return true, diags
}

diags.AddError("Int64SemanticsEquals", "Difference too big")
return false, diags

}

func (v CapacityByteValue) Equal(o attr.Value) bool {
newValue, ok := o.(CapacityByteValue)
if !ok {
return false
}

diff := v.Int64Value.ValueInt64() - newValue.ValueInt64()
if (diff < 50000000) {
    return true
}

return false

}

func (v CapacityByteValue) Type(ctx context.Context) attr.Type {
return CapacityByteType{}
}

// VirtualMediaResourceModel describes the resource data model.
type StorageVolumeResourceModel struct {
Id types.String tfsdk:"id"
StorageId types.String tfsdk:"storage_controller_id"
RedfishServer RedfishServer tfsdk:"server"

RaidType             types.String `tfsdk:"raid_type"`
CapacityBytes        CapacityByteValue `tfsdk:"capacity_bytes"`
VolumeName           types.String `tfsdk:"name"`
InitMode             types.String `tfsdk:"init_mode"`
PhysicalDrives       types.List `tfsdk:"physical_drives"`
OptimumIOSizeBytes   types.Int64 `tfsdk:"optimum_io_size_bytes"`
ReadMode             types.String `tfsdk:"read_mode"`
WriteMode            types.String `tfsdk:"write_mode"`

// CacheMode types.String tfsdk:"cache_mode"
DriveCacheMode types.String tfsdk:"drive_cache_mode"
}


part of schema for the resource
	"capacity_bytes": schema.Int64Attribute{
        CustomType: models.CapacityByteType{},
		Description:         "Volume capacity in bytes.",
		MarkdownDescription: "Volume capacity in bytes. If not specified during creation, volume will have maximum size calculated from chosen disks.",
		Optional:            true,
		Computed:            true,
	},

I will rephrase the question:

Implemented semantic equality for custom type seems to be fired but and my provider logic stores value of the custom type property from the backend, not from the plan.
So it seems that values are compared, semantic comparison returns true (value are semantically same), but still terraform returns an error Provider produced inconsistent result after apply.

Reading spec for semantic equality seems to cover this case
When creating or updating a resource, the response new state value from the Create or Update method logic is compared to the request plan value.

But is it really covered or I expect something what is not implemented in Terraform…

Hey there @andruszamojski :wave:, your understanding of semantic equality is correct but I believe there is a separate bug in that CapacityByteValue code that’s causing the prior value to not be preserved.

The implementation of Int64SemanticEquals looks correct, since it’s main job is to determine if two values are semantically equal.

The implementation of Equal in CapacityByteValue should be checking that the two values are exactly equal, which is typically just checking the underlying value type, then calling the base type’s equal method. The docs mention this in a small note block and there is an example you can see with our normalized JSON custom type (which is just a string).

I think adjusting your Equal method to something like this should get you closer to what you’re looking for:

func (v CapacityByteValue) Equal(o attr.Value) bool {
	other, ok := o.(CapacityByteValue)
	if !ok {
		return false
	}

	return v.Int64Value.Equal(other.Int64Value)
}

If you’re interested why this actually matters, plugin framework use the Equal method internally to determine if it needs to perform certain pieces of logic. In this case, the framework is not preserving the prior value because the result of the Equal method is indicating the two values are exactly equal, so there is no reason to set the state value to the prior value.

Here is the piece of code I’m referring to:

Seems to help, such a small change…

Thank you for your help!

1 Like