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

Detect when provider and resource/module have identical for_each #2186

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

cam72cam
Copy link
Member

@cam72cam cam72cam commented Nov 19, 2024

As discussed in #300, users can accidentally shoot themselves in the foot by using the same expressions for provider for_each and module/resource for_each. It creates a situation where removing a key from the for_each value will orphan resources without a corresponding provider.

Resolves #300

Target Release

1.9.0

Checklist

  • I have read the contribution guide.
  • I have not used an AI coding assistant to create this PR.
  • I have written all code in this PR myself OR I have marked all code I have not written myself (including modified code, e.g. copied from other places and then modified) with a comment indicating where it came from.
  • I (and other contributors to this PR) have not looked at the Terraform source code while implementing this PR.

Go checklist

  • I have run golangci-lint on my change and receive no errors relevant to my code.
  • I have run existing tests to ensure my code doesn't break anything.
  • I have added tests for all relevant use cases of my code, and those tests are passing.
  • I have only exported functions, variables and structs that should be used from other packages.
  • I have added meaningful comments to all exported functions, variables, and structs.

Website/documentation checklist

  • I have locally started the website as described here and checked my changes.

Copy link

Reminder for the PR assignee: If this is a user-visible change, please update the changelog as part of the PR.

@cam72cam cam72cam marked this pull request as ready for review November 20, 2024 15:00
@cam72cam cam72cam requested a review from a team as a code owner November 20, 2024 15:00
Copy link
Contributor

@apparentlymart apparentlymart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this seems like it would work!

My feedback inline is highly subjective and so you might disagree with it. The most substantial inline comment suggests implementing this in a later phase so that we could emit the warning in fewer situations.

internal/configs/provider_validation.go Outdated Show resolved Hide resolved
internal/configs/provider_validation.go Show resolved Hide resolved
internal/configs/provider_validation.go Outdated Show resolved Hide resolved
internal/configs/provider_validation.go Outdated Show resolved Hide resolved
internal/configs/provider_validation.go Outdated Show resolved Hide resolved
cam72cam and others added 4 commits November 25, 2024 09:06
Signed-off-by: Christian Mesh <[email protected]>
Co-authored-by: Martin Atkins <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
// resembles HCL native syntax, and is suitable for display in the UI.
//
// This was copied (and simplified) from internal/command/views/json/diagnostic.go.
func TraversalStr(traversal hcl.Traversal) string {
Copy link
Contributor

@apparentlymart apparentlymart Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't suggest this originally because I was suggesting to use reference-based comparison instead, but if we're going to continue to compare purely on a raw traversal basis (accepting that this won't deal with any normalization that addrs.ParseRef would perform) then I think it's worth writing an actual comparison function that doesn't rely on a string representation, since string representation is a little messy for numbers in particular.

For example

func TraversalsEquivalent(a, b hcl.Traversal) bool {
    if len(a) != len(b) {
        return false 
    }

    for idx, stepA := range a {
        stepB := b[idx]

        switch stepA := step.(type) {
        case hcl.TraverseRoot:
            stepB, ok := stepB.(hcl.TraverseRoot)
            if !ok || stepA.Name != stepB.Name {
                return false
            }
        case hcl.TraverseAttr:
            stepB, ok := stepB.(hcl.TraverseAttr)
            if !ok || stepA.Name != stepB.Name {
                return false
            }
        case hcl.TraverseIndex:
            stepB, ok := stepB.(hcl.TraverseIndex)
            if !ok || stepA.Key.Equals(stepB.Key) != cty.True {
                return false
            }
        default:
            // The above should be exhaustive for all traversal
            // step types that HCL can possibly generate. We'll
            // treat any unsupported stepA types as non-equal
            // because that matches what would happen if
            // any stepB were unsupported: the type assertions
            // in the above cases would fail.
            return false
        }
    }

    return true
}

Compared to comparing by string representation this only significantly changes the handling of hcl.TraverseIndex, performing the equality test using cty's definition of equal, which therefore matches HCL's == operator. cty's equality test for number and string types both carefully handle some subtle situations that arise from number being a big float and string comparison honoring unicode normalization rules.

This addition documents the warning we plan to introduce to help module
authors avoid a trap where they would be unable to successfully remove a
dynamically-declared provider instance at the same time as managed resource
instances that belong to it.

Signed-off-by: Martin Atkins <[email protected]>
@@ -852,8 +856,8 @@ func providerIterationIdenticalWarning(blockType string, sourceExpr, instanceExp
Severity: hcl.DiagWarning,
Summary: "Provider configuration for_each matches " + blockType,
Detail: fmt.Sprintf(
"This provider configuration uses the same for_each expression as a %s, which means that subsequent removal of elements from this collection would cause a planning error.\n\nOpenTofu relies on a provider instance to destroy resource instances that are associated with it, and so the provider instance must outlive all of its resource instances by at least one plan/apply round. For removal of instances to succeed in future you must structure the configuration so that the provider block's for_each expression can produce a superset of the instances of the resources associated with the provider configuration. Refer to the OpenTofu documentation for specific suggestions.",
blockType,
"This provider configuration uses the same for_each expression as a %s, which means that subsequent removal of elements from this collection would cause a planning error.\n\nOpenTofu relies on a provider instance to destroy resource instances that are associated with it, and so the provider instance must outlive all of its resource instances by at least one plan/apply round. For removal of instances to succeed in future you must structure the configuration so that the provider block's for_each expression can produce a superset of the instances of the resources associated with the provider configuration. Refer to the OpenTofu documentation for specific suggestions.\n\nIf you are stuck trying to remove a configured provider instance, targeting may be used: tofu apply -destroy %s",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"This provider configuration uses the same for_each expression as a %s, which means that subsequent removal of elements from this collection would cause a planning error.\n\nOpenTofu relies on a provider instance to destroy resource instances that are associated with it, and so the provider instance must outlive all of its resource instances by at least one plan/apply round. For removal of instances to succeed in future you must structure the configuration so that the provider block's for_each expression can produce a superset of the instances of the resources associated with the provider configuration. Refer to the OpenTofu documentation for specific suggestions.\n\nIf you are stuck trying to remove a configured provider instance, targeting may be used: tofu apply -destroy %s",
"This provider configuration uses the same for_each expression as a %s, which means that subsequent removal of elements from this collection would cause a planning error.\n\nOpenTofu relies on a provider instance to destroy resource instances that are associated with it, and so the provider instance must outlive all of its resource instances by at least one plan/apply round. For removal of instances to succeed in future you must structure the configuration so that the provider block's for_each expression can produce a superset of the instances of the resources associated with the provider configuration. Refer to the OpenTofu documentation for specific suggestions.\n\nTo destroy this object before removing the provider configuration, consider first performing a targeted destroy:\n tofu apply -destroy %s",

The existing "error message" voice avoids using passive voice and tends to put example commands to run indented on their own line so that they are easier to copy-paste correctly.

This function returns true if the two given traversals are "equivalent",
which means that HCL would consider them to represent the same information
even if they aren't using exactly the same syntax.

In particular it deals with some quirks in how HCL (via cty) decides
whether two index values are equal.

Signed-off-by: Martin Atkins <[email protected]>
We now have a function directly intended for comparing two traversals for
equivalence, so we no longer need to cheat and use comparison of the
string representations that were intended for use only in the UI.

Signed-off-by: Martin Atkins <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Dynamic provider configuration assignment
2 participants