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

Consider MatchElementStateForUnknown() Plan Modifiers #717

Open
bflad opened this issue Apr 5, 2023 · 0 comments
Open

Consider MatchElementStateForUnknown() Plan Modifiers #717

bflad opened this issue Apr 5, 2023 · 0 comments
Labels
enhancement New feature or request

Comments

@bflad
Copy link
Contributor

bflad commented Apr 5, 2023

Module version

v1.2.0

Use-cases

Over in #709, it was discovered that the UseStateForUnknown() plan modifier (amongst other custom ones) can return unexpected values when a parent list or set has elements rearranged or removed. In particular:

  • Attribute-based plan modifiers which attempt to read prior state (such as UseStateForUnknown())
  • Implementing those attribute-based plan modifiers on attributes underneath a schema.ListNestedAttribute, schema.ListNestedBlock, schema.SetNestedAttribute, or schema.SetNestedBlock
  • The resource is planning/applying an in-place update which rearranges and/or removes those elements within the list/set

This is because the framework's list and set data structures are array index based and the planning implementation will align configuration, plan, and prior state values based on that index. The implementation itself was an design choice where other choices introduced their own potential tradeoffs, such as requiring provider developers to implement much more explicit logic into their schemas. It was particularly desirable to avoid the prior terraform-plugin-sdk model of set and plan difference handling, which consistently had bug reports which were difficult or not possible to fix without breaking changes. In any event, the framework's data handling for attribute plan modifiers cannot potentially be changed until a theoretical next major version, so that is not an option in this case.

This leaves the framework maintainers in an awkward position where UseStateForUnknown() can introduce unexpected plans or Terraform data consistency errors, which framework-native functionality should never do. It is possible to detect when the plan modifier is being called while under a list or set, which means that it can raise its own implementation error to tell developers to remove the plan modifier. This does mean however, that previously working cases such as unchanged lists/sets or ones where only new list/set elements were added will lose their ability to display a more known plan.

Attempted Solutions

Provider developers implementing custom attribute plan modifiers with fairly complex data handling logic.

Proposal

Parent Plan Modifier

One possible proposal would be a new list/set attribute plan modifier that enables provider developers to input tracking attributes which the plan modifier logic would realign the prior state values of any unknown computed values. As a design sketch:

schema.ListNestedAttribute{
  NestedObject: schema.NestedAttributeObject{
    Attributes: map[string]schema.Attribute{
      "attr1": schema.StringAttribute{
         Required: true,
       },
       "attr2": schema.StringAttribute{
         Computed: true,
       },
       // potentially other configurable or non-configurable attributes to handle
    },
  },
  PlanModifiers: []planmodifier.List{
    listplanmodifier.MatchElementStateForUnknown(
      path.MatchRelative().AtAnyListIndex().AtName("attr1"),
      // potentially other configurable attributes to handle
    ),
  },
},

While requiring less provider developer implementation details especially in the case of many computed attributes and potentially being a slightly easier framework implementation, this approach does have a few immediate downsides:

  • Action at a distance: Provider developers need to know this implementation goes in a different location than the affected attribute.
  • Lack of ability to choose which element attributes it effects: Unless separate function parameters are introduced or a separate plan modifier is created, there's no ability to have the value preservation apply to a subset of computed attribute values. That could introduce Terraform data consistency errors between plan and apply depending on the provider implementation details.

Nested Plan Modifiers

Another possible proposal would be new typed attribute plan modifiers, such as in the resource/schema/stringplanmodifer package, where the provider developer can optionally realign the prior state based on identifying attributes. As a design sketch:

schema.ListNestedAttribute{
  NestedObject: schema.NestedAttributeObject{
    Attributes: map[string]schema.Attribute{
      "attr1": schema.StringAttribute{
         Required: true,
       },
       "attr2": schema.StringAttribute{
         Computed: true,
         PlanModifiers: []planmodifier.String{
           stringplanmodifier.MatchElementStateForUnknown(
             path.MatchRelative().AtParent().AtName("attr1"),
             // potentially other configurable attributes to handle
           ),
         },
       },
       // potentially other configurable or non-configurable attributes to handle
    },
  },
  // ...
},

This means that each underlying attribute can opt into the behavior as appropriate (action where its implemented and explicit choice of effect). There is slightly less discoverability concerns since the implementation would be next to UseStateForUnknown().

The plan modifier logic will need to implement some rigorous validation where it raises errors if:

  • The plan modifier is not implemented on an attribute below a list/set.
  • It receives 0 path expressions.
  • A given path expression goes outside the parent list/set.
  • A given path expression goes deeper than nesting of the current path. (Provider developers likely need to implement their own custom plan modifiers for this level of planning complexity.)

References

@bflad bflad added the enhancement New feature or request label Apr 5, 2023
bflad added a commit that referenced this issue Apr 6, 2023
…ing implementation error when under a list or set

Reference: #709
Reference: #717

This change will adjust the `UseStateForUnknown` plan modifiers to return an implementation error if defined on an attribute below a list or set. In any cases where the list or set elements were rearranged or removed, the plan modifier result could cause invalid data being proposed in the plan or Terraform data consistency errors. These situations require additional value identifying information from the provider developer to ensure that the prior state for an element is correctly realigned, however the `UseStateForUnknown` plan modifier takes no parameters for such information and adding those parameters would represent a breaking change to the function signature. Future framework proposals will investigate whether a second plan modifier can be introduced to cover this situation.

The new plan modification unit tests reproduced the issue prior to the new implementation error:

```
--- FAIL: TestAttributeModifyPlan (0.00s)
    --- FAIL: TestAttributeModifyPlan/attribute-set-nested-nested-usestateforunknown-elements-rearranged (0.00s)
        attribute_plan_modification_test.go:1627: Unexpected response (-wanted, +got):   fwserver.ModifyAttributePlanResponse{
            - 	AttributePlan:   s`[{"nested_computed":"statevalue2","nested_required":"testvalue2"},{"nested_computed":"statevalue1","nested_required":"testvalue1"}]`,
            + 	AttributePlan:   s`[{"nested_computed":"statevalue1","nested_required":"testvalue2"},{"nested_computed":"statevalue2","nested_required":"testvalue1"}]`,
              	Diagnostics:     nil,
              	RequiresReplace: s"[]",
              	Private:         nil,
              }
    --- FAIL: TestAttributeModifyPlan/attribute-set-nested-nested-usestateforunknown-elements-removed (0.00s)
        attribute_plan_modification_test.go:1627: Unexpected response (-wanted, +got):   fwserver.ModifyAttributePlanResponse{
            - 	AttributePlan:   s`[{"nested_computed":"statevalue2","nested_required":"testvalue2"}]`,
            + 	AttributePlan:   s`[{"nested_computed":"statevalue1","nested_required":"testvalue2"}]`,
              	Diagnostics:     nil,
              	RequiresReplace: s"[]",
              	Private:         nil,
              }
    --- FAIL: TestAttributeModifyPlan/attribute-list-nested-nested-usestateforunknown-elements-removed (0.00s)
        attribute_plan_modification_test.go:1627: Unexpected response (-wanted, +got):   fwserver.ModifyAttributePlanResponse{
            - 	AttributePlan:   s`[{"nested_computed":"statevalue2","nested_required":"testvalue2"}]`,
            + 	AttributePlan:   s`[{"nested_computed":"statevalue1","nested_required":"testvalue2"}]`,
              	Diagnostics:     nil,
              	RequiresReplace: s"[]",
              	Private:         nil,
              }
    --- FAIL: TestAttributeModifyPlan/attribute-list-nested-nested-usestateforunknown-elements-rearranged (0.00s)
        attribute_plan_modification_test.go:1627: Unexpected response (-wanted, +got):   fwserver.ModifyAttributePlanResponse{
            - 	AttributePlan:   s`[{"nested_computed":"statevalue2","nested_required":"testvalue2"},{"nested_computed":"statevalue1","nested_required":"testvalue1"}]`,
            + 	AttributePlan:   s`[{"nested_computed":"statevalue1","nested_required":"testvalue2"},{"nested_computed":"statevalue2","nested_required":"testvalue1"}]`,
              	Diagnostics:     nil,
              	RequiresReplace: s"[]",
              	Private:         nil,
              }
--- FAIL: TestBlockModifyPlan (0.00s)
    --- FAIL: TestBlockModifyPlan/block-list-nested-usestateforunknown-elements-removed (0.00s)
        block_plan_modification_test.go:2858: Unexpected response (+wanted, -got):   fwserver.ModifyAttributePlanResponse{
            - 	AttributePlan:   s`[{"nested_computed":"statevalue2","nested_required":"testvalue2"}]`,
            + 	AttributePlan:   s`[{"nested_computed":"statevalue1","nested_required":"testvalue2"}]`,
              	Diagnostics:     nil,
              	RequiresReplace: s"[]",
              	Private:         nil,
              }
    --- FAIL: TestBlockModifyPlan/block-set-nested-usestateforunknown-elements-removed (0.00s)
        block_plan_modification_test.go:2858: Unexpected response (+wanted, -got):   fwserver.ModifyAttributePlanResponse{
            - 	AttributePlan:   s`[{"nested_computed":"statevalue2","nested_required":"testvalue2"}]`,
            + 	AttributePlan:   s`[{"nested_computed":"statevalue1","nested_required":"testvalue2"}]`,
              	Diagnostics:     nil,
              	RequiresReplace: s"[]",
              	Private:         nil,
              }
    --- FAIL: TestBlockModifyPlan/block-list-nested-usestateforunknown-elements-rearranged (0.00s)
        block_plan_modification_test.go:2858: Unexpected response (+wanted, -got):   fwserver.ModifyAttributePlanResponse{
            - 	AttributePlan:   s`[{"nested_computed":"statevalue2","nested_required":"testvalue2"},{"nested_computed":"statevalue1","nested_required":"testvalue1"}]`,
            + 	AttributePlan:   s`[{"nested_computed":"statevalue1","nested_required":"testvalue2"},{"nested_computed":"statevalue2","nested_required":"testvalue1"}]`,
              	Diagnostics:     nil,
              	RequiresReplace: s"[]",
              	Private:         nil,
              }
    --- FAIL: TestBlockModifyPlan/block-set-nested-usestateforunknown-elements-rearranged (0.00s)
        block_plan_modification_test.go:2858: Unexpected response (+wanted, -got):   fwserver.ModifyAttributePlanResponse{
            - 	AttributePlan:   s`[{"nested_computed":"statevalue2","nested_required":"testvalue2"},{"nested_computed":"statevalue1","nested_required":"testvalue1"}]`,
            + 	AttributePlan:   s`[{"nested_computed":"statevalue1","nested_required":"testvalue2"},{"nested_computed":"statevalue2","nested_required":"testvalue1"}]`,
              	Diagnostics:     nil,
              	RequiresReplace: s"[]",
              	Private:         nil,
              }
```
bflad added a commit that referenced this issue Apr 6, 2023
…ing implementation error when under a list or set (#711)

Reference: #709
Reference: #717

This change will adjust the `UseStateForUnknown` plan modifiers to return an implementation error if defined on an attribute below a list or set. In any cases where the list or set elements were rearranged or removed, the plan modifier result could cause invalid data being proposed in the plan or Terraform data consistency errors. These situations require additional value identifying information from the provider developer to ensure that the prior state for an element is correctly realigned, however the `UseStateForUnknown` plan modifier takes no parameters for such information and adding those parameters would represent a breaking change to the function signature. Future framework proposals will investigate whether a second plan modifier can be introduced to cover this situation.

The new plan modification unit tests reproduced the issue prior to the new implementation error:

```
--- FAIL: TestAttributeModifyPlan (0.00s)
    --- FAIL: TestAttributeModifyPlan/attribute-set-nested-nested-usestateforunknown-elements-rearranged (0.00s)
        attribute_plan_modification_test.go:1627: Unexpected response (-wanted, +got):   fwserver.ModifyAttributePlanResponse{
            - 	AttributePlan:   s`[{"nested_computed":"statevalue2","nested_required":"testvalue2"},{"nested_computed":"statevalue1","nested_required":"testvalue1"}]`,
            + 	AttributePlan:   s`[{"nested_computed":"statevalue1","nested_required":"testvalue2"},{"nested_computed":"statevalue2","nested_required":"testvalue1"}]`,
              	Diagnostics:     nil,
              	RequiresReplace: s"[]",
              	Private:         nil,
              }
    --- FAIL: TestAttributeModifyPlan/attribute-set-nested-nested-usestateforunknown-elements-removed (0.00s)
        attribute_plan_modification_test.go:1627: Unexpected response (-wanted, +got):   fwserver.ModifyAttributePlanResponse{
            - 	AttributePlan:   s`[{"nested_computed":"statevalue2","nested_required":"testvalue2"}]`,
            + 	AttributePlan:   s`[{"nested_computed":"statevalue1","nested_required":"testvalue2"}]`,
              	Diagnostics:     nil,
              	RequiresReplace: s"[]",
              	Private:         nil,
              }
    --- FAIL: TestAttributeModifyPlan/attribute-list-nested-nested-usestateforunknown-elements-removed (0.00s)
        attribute_plan_modification_test.go:1627: Unexpected response (-wanted, +got):   fwserver.ModifyAttributePlanResponse{
            - 	AttributePlan:   s`[{"nested_computed":"statevalue2","nested_required":"testvalue2"}]`,
            + 	AttributePlan:   s`[{"nested_computed":"statevalue1","nested_required":"testvalue2"}]`,
              	Diagnostics:     nil,
              	RequiresReplace: s"[]",
              	Private:         nil,
              }
    --- FAIL: TestAttributeModifyPlan/attribute-list-nested-nested-usestateforunknown-elements-rearranged (0.00s)
        attribute_plan_modification_test.go:1627: Unexpected response (-wanted, +got):   fwserver.ModifyAttributePlanResponse{
            - 	AttributePlan:   s`[{"nested_computed":"statevalue2","nested_required":"testvalue2"},{"nested_computed":"statevalue1","nested_required":"testvalue1"}]`,
            + 	AttributePlan:   s`[{"nested_computed":"statevalue1","nested_required":"testvalue2"},{"nested_computed":"statevalue2","nested_required":"testvalue1"}]`,
              	Diagnostics:     nil,
              	RequiresReplace: s"[]",
              	Private:         nil,
              }
--- FAIL: TestBlockModifyPlan (0.00s)
    --- FAIL: TestBlockModifyPlan/block-list-nested-usestateforunknown-elements-removed (0.00s)
        block_plan_modification_test.go:2858: Unexpected response (+wanted, -got):   fwserver.ModifyAttributePlanResponse{
            - 	AttributePlan:   s`[{"nested_computed":"statevalue2","nested_required":"testvalue2"}]`,
            + 	AttributePlan:   s`[{"nested_computed":"statevalue1","nested_required":"testvalue2"}]`,
              	Diagnostics:     nil,
              	RequiresReplace: s"[]",
              	Private:         nil,
              }
    --- FAIL: TestBlockModifyPlan/block-set-nested-usestateforunknown-elements-removed (0.00s)
        block_plan_modification_test.go:2858: Unexpected response (+wanted, -got):   fwserver.ModifyAttributePlanResponse{
            - 	AttributePlan:   s`[{"nested_computed":"statevalue2","nested_required":"testvalue2"}]`,
            + 	AttributePlan:   s`[{"nested_computed":"statevalue1","nested_required":"testvalue2"}]`,
              	Diagnostics:     nil,
              	RequiresReplace: s"[]",
              	Private:         nil,
              }
    --- FAIL: TestBlockModifyPlan/block-list-nested-usestateforunknown-elements-rearranged (0.00s)
        block_plan_modification_test.go:2858: Unexpected response (+wanted, -got):   fwserver.ModifyAttributePlanResponse{
            - 	AttributePlan:   s`[{"nested_computed":"statevalue2","nested_required":"testvalue2"},{"nested_computed":"statevalue1","nested_required":"testvalue1"}]`,
            + 	AttributePlan:   s`[{"nested_computed":"statevalue1","nested_required":"testvalue2"},{"nested_computed":"statevalue2","nested_required":"testvalue1"}]`,
              	Diagnostics:     nil,
              	RequiresReplace: s"[]",
              	Private:         nil,
              }
    --- FAIL: TestBlockModifyPlan/block-set-nested-usestateforunknown-elements-rearranged (0.00s)
        block_plan_modification_test.go:2858: Unexpected response (+wanted, -got):   fwserver.ModifyAttributePlanResponse{
            - 	AttributePlan:   s`[{"nested_computed":"statevalue2","nested_required":"testvalue2"},{"nested_computed":"statevalue1","nested_required":"testvalue1"}]`,
            + 	AttributePlan:   s`[{"nested_computed":"statevalue1","nested_required":"testvalue2"},{"nested_computed":"statevalue2","nested_required":"testvalue1"}]`,
              	Diagnostics:     nil,
              	RequiresReplace: s"[]",
              	Private:         nil,
              }
```

Co-authored-by: Austin Valle <austinvalle@gmail.com>
bflad added a commit that referenced this issue Apr 7, 2023
Reference: #711
Reference: #717

This change implements a new plan modifier to cover cases where `UseStateForUnknown` will now raise an implementation error when under a list or set nested attribute or block. Provider developers can supply all configurable and identifying nested object attributes which can be used to align and preserve prior state data for this attribute.

An example schema implementation:

```go
schema.SetNestedAttribute{
	NestedObject: schema.NestedAttributeObject{
		Attributes: map[string]schema.Attribute{
			"computed_attr": schema.StringAttribute{
				Computed: true,
				PlanModifiers: []planmodifier.String{
					// Preseve this computed value during updates.
					stringplanmodifier.MatchElementStateForUnknown(
						// Identify matching prior state value based on configurable_attr
						path.MatchRelative().AtParent().AtName("configurable_attr"),
						// ... potentially others ...
					),
				},
			},
			"configurable_attr": schema.StringAttribute{
				Required: true,
			},
		},
	},
	Optional: true,
}
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
1 participant