How to handle unknown plan ? [custom provider]

we have connected resources in our custom provider, something like :

resource "server" "this" {
  name = "for-test"
  data = "tableids" # this has a force new
  address = "reg1" 
 }

resource "server_custom_ip" "this" {
  server_id = server.this.id          # this has a custom diff, which forces a new resource on certain criteria, this is optional parameter
}

when server is forcing a new resource, the server_custom_ip is showing an update in place. After server creation its becoming a forcenew for server_custom_ip which is throwing an error

│ Error: Provider produced inconsistent final plan
│ 
│ When expanding the plan for server_custom_ip.this to include new values learned so far during apply, provider "registry.terraform.io/hashicorp/clib" changed the planned action from Update to
│ DeleteThenCreate.
│ 
│ This is a bug in the provider, which should be reported in the provider's own issue tracker.

Hi @ramuklawjju,

You cannot change whether a resource will be updated or replaced between the plan and apply stages. Because the server.this.id value is not known during planning, the server_custom_ip resource must conservatively assume that it will cause replacement in order to create a valid plan.

I don’t think you can get away with it in the legacy SDK due to the implicit handling of id, but once the new plugin framework becomes available, I believe you may be able to transparently replace a resource by planning an update in place while reporting all computed fields as being unknown. This of course depends on whether than makes sense to do so with the logical resource represented by server_custom_ip, since the details would be hidden from the user.

1 Like

Thank you for replying @jbardin

For server_custom_ip resource, in my schema I have a customdiff.Sequence, which has a logic in place which determines whether a force new is applicable.
So when I have both the resources server and server-custom_ip in separate configuration/folder, I don’t run into this specific case.

resource "server_custom_ip" "this" {
  server_id = server.this.id          # this has a custom diff, which forces a new resource on certain criteria, this is optional parameter
}

But when I have both in the same configuration the terraform plan says

 # server_custom_ip.this will be updated in-place
  ~ resource "server_custom_ip" "this" {
            id            = "j111-5400001"
         ~  server_id     = "o677-2e0000f" -> (known after apply)
        # (9 unchanged attributes hidden)
    }

  # server.this must be replaced
-/+ resource "server" "this" {
         name             = "for-test"
      ~ data              = "a" -> "b" # forces replacement
 # (2 unchanged attributes hidden)
}

when both are clubbed as part of the same configuration, a force new in parent resource is not enough to decide if force new is applicable for the dependent resource and I am getting the error

│ Error: Provider produced inconsistent final plan

Hi @ramuklawjju,

Without seeing the full source code of your CustomizeDiff it’s hard to guess what might be going on here, but the requirement @jbardin described would be met by making your CustomizeDiff function mark this argument as forcing replacement if the new server_id value is different than the old in a way that matches your rule or if the new server_id value is unknown.

That would then cause the initial plan to say something like this:

  # server_custom_ip.this must be replaced
-/+ resource "server_custom_ip" "this" {
        id            = "j111-5400001"
     ~  server_id     = "o677-2e0000f" -> (known after apply) # forces replacement
        # (9 unchanged attributes hidden)
    }

As @jbardin noted, this rule is conservative in the sense that there isn’t enough information to decide whether the change actually requires replacement, and so we must assume it does because that’s the most severe of the possible outcomes.

Terraform’s promise to the user is that applying the plan will either do what the plan said or produce an error explaining why not, and so this rule of being conservative is a consequence of that, even though in some special situations like this it does unfortunately lead to replacing an object that might not have needed replacing if the objects had both changed as separate steps.

There might be other ways to design your provider to mitigate this, but as a general rule CustomizeDiff with arguments that are typically populated from Computed: true attributes on other resources will often need to make this sort of conservative assumption in order to avoid taking an action more severe than the one reported in the plan.

1 Like

Hi @apparentlymart , thank you for replying

The CustomizeDiff function is like this, it internally calls a checkIfTablesChanged which makes a DB call to see if tables changed and returns a bool.

func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error {
					if diff.HasChange("server_id") {
						old, new := diff.GetChange("server_id")
						serviceFramework, err := sClient(v)
						id := diff.Id()
						if err != nil {
							return err
						}
						if checkIfTablesChanged(old.(string), new.(string), id, serviceFramework) {
							diff.ForceNew("server_id")
						}
					}
					return nil
				},

Hi @ramuklawjju,

To implement what I was describing in my comment, I think you could add the following extra lines immediately after your line if diff.HasChange("server_id") {:

if !diff.NewValueKnown("server_id") {
    // If server_id isn't known yet then we must
    // conservatively assume it'll require replacement.
    diff.ForceNew("server_id")
    return nil
}

I’ve not actually tested this so I may not have got it exactly right, but hopefully you can try this out and see what it does. If you run into any problems you’re not sure how to solve, feel free to share the error messages or other strange behavior here and I’ll do my best to explain what it means.

1 Like