Skip to content

RequiresReplace does not consider SemanticEquals #1168

Open
@oscarhermoso

Description

@oscarhermoso

Currently, the RequiresReplace planmodifier doesn't respect StringSemanticEquals.

...this may not be a bug, it may be working as intended. If so, would be nice to have a new plan modifier - something like stringplanmodifier.RequiresReplaceIfSemanticallyDifferent()

Thankyou!

Module version

github.com/hashicorp/terraform-plugin-framework v1.15.0

Relevant provider source code

I have defined a custom value TrimmedStringValue which is essentially identical to StringValue, but it ignores leading/trailing whitespace when semantic equality is checked.

// trimmed_string_custom_type.go

var _ basetypes.StringValuable = TrimmedStringValue{}
var _ basetypes.StringValuableWithSemanticEquals = TrimmedStringValue{}

type TrimmedStringValue struct {
	basetypes.StringValue
}

// ... 

func (v TrimmedStringValue) StringSemanticEquals(ctx context.Context, newValuable basetypes.StringValuable) (bool, diag.Diagnostics) {
	var diags diag.Diagnostics

	// The framework should always pass the correct value type, but always check
	newValue, ok := newValuable.(TrimmedStringValue)
	if !ok {
		diags.AddError("Semantic Equality Check Error", "...")
		return false, diags
	}

	// ignore leading/training whitespace
	priorString := strings.TrimSpace(v.StringValue.ValueString())
	newString := strings.TrimSpace(newValue.ValueString())

	// If the strings are equivalent, keep the prior value
	return priorString == newString, diags
}

I then implement in in my schema with CustomType: TrimmedStringType{} and plan modifier stringplanmodifier.RequiresReplace().

// ssh_key_resource.go

func (r *sshKeyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
	resp.Schema = resources.SshKeyResourceSchema(ctx)

	// ...

	publicKeyDescription := "The public key in OpenSSH \"authorized_keys\" format. This should be a valid public key, " +
		"such as one generated by `ssh-keygen`."
	resp.Schema.Attributes["public_key"] = schema.StringAttribute{
		Required:            true,
		Description:         publicKeyDescription,
		MarkdownDescription: publicKeyDescription,
		PlanModifiers:       []planmodifier.String{stringplanmodifier.RequiresReplace()},
		CustomType:          TrimmedStringType{}, // Ignore leading/trailing whitespace
	}
}

Terraform Configuration Files

I then test this in my unit tests

First test step:

resource "binarylane_ssh_key" "test" {
  name       = "tf-test-key-resource-test"
  public_key = "` + publicKey + `"
}

Second test step:

resource "binarylane_ssh_key" "test" {
  name  = "tf-test-key-resource-test"
	# Test public key with a newline to ensure it does not get recreated
  public_key = <<EOT
` + publicKey + `
EOT
}`

I am expecting an empty plan, with

	ConfigPlanChecks: resource.ConfigPlanChecks{
		PreApply: []plancheck.PlanCheck{
			plancheck.ExpectEmptyPlan(),
		},
	},

Debug Output

Not a full debug output, but should sufficient explain the issue:

=== NAME  TestSshKeyResource
    ssh_key_resource_test.go:22: Step 4/4 error: Pre-apply plan check(s) failed:
        expected empty plan, but binarylane_ssh_key.test has planned action(s): [delete create]

Expected Behavior

Plan is empty due to semantic equality.

Actual Behavior

Plan wants to delete & create resource.

Steps to Reproduce

  1. Implement custom type and value with StringSemanticEquals
  2. Define schema with plan modifier RequiresReplace
  3. Modify string while maintaining semantic equality

References

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