Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strange behavior with Computed attributes after v0.4.2 upgrade #187

Closed
ewbankkit opened this issue Sep 29, 2021 · 6 comments · Fixed by #213
Closed

Strange behavior with Computed attributes after v0.4.2 upgrade #187

ewbankkit opened this issue Sep 29, 2021 · 6 comments · Fixed by #213
Assignees
Labels
bug Something isn't working

Comments

@ewbankkit
Copy link
Contributor

ewbankkit commented Sep 29, 2021

I upgraded to terraform-plugin-framework@v0.4.2 and am now seeing some strange behavior.
A resource has an attribute

"function_name": {
	Description: "The name of the Lambda function, up to 64 characters in length. If you don't specify a name, AWS CloudFormation generates one.",
	Type:        types.StringType,
	Optional:    true,
	Computed:    true,
	Validators: []tfsdk.AttributeValidator{
		validate.StringLenAtLeast(1),
	},
	PlanModifiers: []tfsdk.AttributePlanModifier{
		tfsdk.RequiresReplace(),
	},
},

When terraform plan shows a difference in another attribute (description in this case which is Optional and is now getting a value):

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # cloud_service_thing.test must be replaced
-/+ resource "cloud_service_thing" "test" {
      ~ arn                 = "..." -> (known after apply)
      + description         = "test"
      ~ function_name       = "1WlhknThPCtZOiZjwt283Olmq-0A8jrKEtMBpJ" -> (known after apply) # forces replacement
      ~ id                  = "1WlhknThPCtZOiZjwt283Olmq-0A8jrKEtMBpJ" -> (known after apply)
        # (9 unchanged attributes hidden)
    }

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

The attribute function_name (which hasn't changed) is showing a change (and a replacement as it's ForceNew).
arn and id are also both Computed attributes and their values have not changed.
This behavior was not observed with v0.4.0.

@bflad
Copy link
Contributor

bflad commented Oct 1, 2021

This behavior is (unfortunately) expected with the current implementation of RequiresReplace(), which will unilaterally mark an attribute for replacement any time there is any plan difference, even outside the scope of the current attribute.

As shown in hashicorp/terraform-provider-awscc#222 this behavior can be improved with the following workaround:

tfsdk.RequiresReplaceIf(func(ctx context.Context, state, config attr.Value, path *tftypes.AttributePath) (bool, diag.Diagnostics) {
	// TODO: Use config.IsNull() when available
	// Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/193
	if config.Equal(state) || config.Equal(types.String{Null: true}) {
		return false, nil
	}

	return true, nil
}, "require replacement if configuration value changes", "require replacement if configuration value changes"),

Whether either or both parts of the conditional there should be part of the RequiresReplace() implementation is an interesting design consideration. I would posit that most folks would expect it to work similarly to ForceNew in the older Terraform Plugin SDK, which only marked the path for replacement when configuration values changed.

@paddycarver
Copy link
Contributor

So the solution we came up with to fix this is to:

  1. only set RequiresReplace to true if the value for that attribute, specifically, is actually changing. This seems reasonable to me.
  2. only set RequiresReplace to true if the config is not null.

As I'm writing tests and leaving a comment for why that exception is carved out, I'm struggling here. Why did we decide that was important? Is it only because we're trying to control for the scenario where Optional+Computed attributes aren't set, and the provider changes them, in which case the practitioner likely doesn't expect the resource to be destroyed and recreated? If so, should that conditional only apply to computed attributes?

@paddycarver paddycarver self-assigned this Oct 27, 2021
paddycarver added a commit that referenced this issue Oct 27, 2021
Fixes #187, supercedes #203.

This changes the logic on our RequiresReplace function in the following
ways:

* if the attribute being modified has not changed, RequiresReplace isn't
  changed. This prevents the resource from being destroyed and recreated
  when _any_ attribute changes, limiting it only to the attribute with
  RequiresReplace set on it.
* if the attribute being modified is computed and is null in the config,
  RequiresReplace isn't changed. This prevents the resource from being
  destroyed and recreated when the provider returns an unknown value in
  the plan, or even a new, provider-controlled value, because it's
  unlikely the practitioners expect their resource to be destroyed and
  recreated when an attribute out of their control changes. This has the
  unfortunate side-effect that practitioners removing an
  Optional+Computed field from their config will not prompt the default
  RequiresReplace behavior, but providers can specify their own, special
  plan modifier that has a better understanding of practitioner intent
  in that case.
* if the resource is being created or destroyed, RequiresReplace isn't
  changed. This prevents the resource from being marked for destruction
  and recreation when it's just now being created or destroyed, which
  would be nonsensical.

RequiresReplaceIf gets all these changes and an additional change that
it will only set RequiresReplace to true, never to false, as its Modify
method's GoDoc indicated. This means that RequiresReplaceIf functions
can be chained, and that RequiresReplaceIf won't overwrite previous plan
modifiers' determinations on whether or not the resource should be
destroyed and recreated.

Tests were added for RequiresReplace and RequiresReplaceIf to test these
invariants.
@bflad
Copy link
Contributor

bflad commented Oct 27, 2021

Indeed, the second rule:

only set RequiresReplace to true if the config is not null.

Should probably only apply to Computed attributes. For strictly Optional attributes, removing the configuration of an attribute should likely be seen as a change that requires replacement.

@ewbankkit
Copy link
Contributor Author

Yes - see hashicorp/terraform-provider-awscc#222.

@paddycarver
Copy link
Contributor

I think #213 meets these needs. Tests that I added should illustrate its behavior in various scenarios and explain what those scenarios are/why we decided to behave that way in those scenarios.

paddycarver added a commit that referenced this issue Oct 28, 2021
Fixes #187, supercedes #203.

This changes the logic on our RequiresReplace function in the following
ways:

* if the attribute being modified has not changed, RequiresReplace isn't
  changed. This prevents the resource from being destroyed and recreated
  when _any_ attribute changes, limiting it only to the attribute with
  RequiresReplace set on it.
* if the attribute being modified is computed and is null in the config,
  RequiresReplace isn't changed. This prevents the resource from being
  destroyed and recreated when the provider returns an unknown value in
  the plan, or even a new, provider-controlled value, because it's
  unlikely the practitioners expect their resource to be destroyed and
  recreated when an attribute out of their control changes. This has the
  unfortunate side-effect that practitioners removing an
  Optional+Computed field from their config will not prompt the default
  RequiresReplace behavior, but providers can specify their own, special
  plan modifier that has a better understanding of practitioner intent
  in that case.
* if the resource is being created or destroyed, RequiresReplace isn't
  changed. This prevents the resource from being marked for destruction
  and recreation when it's just now being created or destroyed, which
  would be nonsensical.

RequiresReplaceIf gets all these changes and an additional change that
it will only set RequiresReplace to true, never to false, as its Modify
method's GoDoc indicated. This means that RequiresReplaceIf functions
can be chained, and that RequiresReplaceIf won't overwrite previous plan
modifiers' determinations on whether or not the resource should be
destroyed and recreated.

Tests were added for RequiresReplace and RequiresReplaceIf to test these
invariants.
paddycarver added a commit that referenced this issue Nov 3, 2021
Fixes #187, supercedes #203.

This changes the logic on our RequiresReplace function in the following
ways:

* if the attribute being modified has not changed, RequiresReplace isn't
  changed. This prevents the resource from being destroyed and recreated
  when _any_ attribute changes, limiting it only to the attribute with
  RequiresReplace set on it.
* if the attribute being modified is computed and is null in the config,
  RequiresReplace isn't changed. This prevents the resource from being
  destroyed and recreated when the provider returns an unknown value in
  the plan, or even a new, provider-controlled value, because it's
  unlikely the practitioners expect their resource to be destroyed and
  recreated when an attribute out of their control changes. This has the
  unfortunate side-effect that practitioners removing an
  Optional+Computed field from their config will not prompt the default
  RequiresReplace behavior, but providers can specify their own, special
  plan modifier that has a better understanding of practitioner intent
  in that case.
* if the resource is being created or destroyed, RequiresReplace isn't
  changed. This prevents the resource from being marked for destruction
  and recreation when it's just now being created or destroyed, which
  would be nonsensical.

RequiresReplaceIf gets all these changes and an additional change that
it will only set RequiresReplace to true, never to false, as its Modify
method's GoDoc indicated. This means that RequiresReplaceIf functions
can be chained, and that RequiresReplaceIf won't overwrite previous plan
modifiers' determinations on whether or not the resource should be
destroyed and recreated.

Tests were added for RequiresReplace and RequiresReplaceIf to test these
invariants.
paddycarver added a commit that referenced this issue Nov 3, 2021
Fixes #187, supercedes #203.

This changes the logic on our RequiresReplace function in the following
ways:

* if the attribute being modified has not changed, RequiresReplace isn't
  changed. This prevents the resource from being destroyed and recreated
  when _any_ attribute changes, limiting it only to the attribute with
  RequiresReplace set on it.
* if the attribute being modified is computed and is null in the config,
  RequiresReplace isn't changed. This prevents the resource from being
  destroyed and recreated when the provider returns an unknown value in
  the plan, or even a new, provider-controlled value, because it's
  unlikely the practitioners expect their resource to be destroyed and
  recreated when an attribute out of their control changes. This has the
  unfortunate side-effect that practitioners removing an
  Optional+Computed field from their config will not prompt the default
  RequiresReplace behavior, but providers can specify their own, special
  plan modifier that has a better understanding of practitioner intent
  in that case.
* if the resource is being created or destroyed, RequiresReplace isn't
  changed. This prevents the resource from being marked for destruction
  and recreation when it's just now being created or destroyed, which
  would be nonsensical.

RequiresReplaceIf gets all these changes and an additional change that
it will only set RequiresReplace to true, never to false, as its Modify
method's GoDoc indicated. This means that RequiresReplaceIf functions
can be chained, and that RequiresReplaceIf won't overwrite previous plan
modifiers' determinations on whether or not the resource should be
destroyed and recreated.

Tests were added for RequiresReplace and RequiresReplaceIf to test these
invariants.
paddycarver added a commit that referenced this issue Nov 3, 2021
Fixes #187, supercedes #203.

This changes the logic on our RequiresReplace function in the following
ways:

* if the attribute being modified has not changed, RequiresReplace isn't
  changed. This prevents the resource from being destroyed and recreated
  when _any_ attribute changes, limiting it only to the attribute with
  RequiresReplace set on it.
* if the attribute being modified is computed and is null in the config,
  RequiresReplace isn't changed. This prevents the resource from being
  destroyed and recreated when the provider returns an unknown value in
  the plan, or even a new, provider-controlled value, because it's
  unlikely the practitioners expect their resource to be destroyed and
  recreated when an attribute out of their control changes. This has the
  unfortunate side-effect that practitioners removing an
  Optional+Computed field from their config will not prompt the default
  RequiresReplace behavior, but providers can specify their own, special
  plan modifier that has a better understanding of practitioner intent
  in that case.
* if the resource is being created or destroyed, RequiresReplace isn't
  changed. This prevents the resource from being marked for destruction
  and recreation when it's just now being created or destroyed, which
  would be nonsensical.

RequiresReplaceIf gets all these changes and an additional change that
it will only set RequiresReplace to true, never to false, as its Modify
method's GoDoc indicated. This means that RequiresReplaceIf functions
can be chained, and that RequiresReplaceIf won't overwrite previous plan
modifiers' determinations on whether or not the resource should be
destroyed and recreated.

Tests were added for RequiresReplace and RequiresReplaceIf to test these
invariants.

Co-authored-by: Brian Flad <bflad417@gmail.com>
@github-actions
Copy link

github-actions bot commented Dec 4, 2021

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues.
If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Dec 4, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug Something isn't working
Projects
None yet
4 participants