terraformterraform-provider

Terraform plugin framework, prevent value check on specific field after resource creation


I am implementing a Terraform provider using the Terraform Plugin Framewok.

I have an issue with a resource. Here is a dummy example :

resource "dummy_resource" "dummy" {
  category                 = "A"      // Can be A, B or C
  performance              = "high"   // Can be high, medium or low
}

The API to create a Dummy resource have 2 configurable fields, category and performance. In this API (that I can't modify), there is the following rule :

Note : In my real use case, there is multiple rules that can change over time so I can't know exactly what the rules will be.

The issue I have here is that, if a user uses the following configuration

resource "dummy_resource" "dummy" {
  category                 = "A"    
  performance              = "low" 
}

The API will not use the given performance field and set performance to high resulting in the following error :

Error: Provider produced inconsistent result after apply

        When applying changes to dummy_resource.dummy, provider
        "provider[\"registry.terraform.io/hashicorp/my_provider\"]" produced an
        unexpected new value: .performance: was cty.StringVal("low"), but now
        cty.StringVal("high").

        This is a bug in the provider, which should be reported in the provider's own
        issue tracker.

In order to avoid this error, I would like to prevent Terraform from checking the performance field value as I can never be sure the API will use the given value.

Is it possible to do that? And how ?


Solution

  • Because Terraform allows using the attributes of one resource instance to populate the configuration of another, and because Terraform wants to promise that applying the plan will either perform the actions as described or return an error explaining that it cannot, Terraform requires that providers follow a number of consistency rules that constrain how values may change between the plan phase and the apply phase.

    The full set of requirements is documented in Resource Instance Change Lifecycle, but the specific constraints relevant here are the ones listed under ApplyResourceChange:

    The New State object returned from the provider must meet the following constraints:

    • Any attribute that had a known value in the Final Planned State must have an identical value in the new state. In particular, if the remote API returned a different serialization of the same value then the provider must preserve the form the user wrote in the configuration, and must not return the normalized form produced by the provider.
    • Any attribute that had an unknown value in the Final Planned State must take on a known value whose type conforms to the type constraint of the unknown value. No unknown values are permitted in the New State.

    The first bullet point is the one directly related to the error you encountered: the "new state" you returned has a different value for performance than was returned in the final planned state. It was also different to what was written in the resource configuration, and so subject to the last part of that paragraph even though this isn't strictly a "normalization" situation.


    If the specific rules were documented as a fixed set of constraints for this API then I would typically have suggested replicating those rules as a validation check in the provider logic.

    For example, if the rule were exactly what you stated then it would be reasonable for the provider to check whether category = "A" and if so require that performance be set to "high", returning an error if not. That would then force the configuration author to match how the remote system would interpret the configuration, and thus allow Terraform to reach the goal of having the desired state (described by the configuration) match the actual state (derived from the remote system).

    However, you mentioned that these rules are arbitrary and subject to change at any time and so it would not be appropriate to encode them in the provider. Therefore this strategy would not work in your case.


    For a situation where the remote system decides an outcome based on a combination of arguments, often the most effective solution is to split the arguments that the author should provide from the values that the remote system will decide, using separate attributes.

    For example, in your case you might define an Optional: true or Required: true attribute named performance where the author would specify their desired performance setting, but then also offer a separate Computed: true attribute effective_performance which the provider sets to match what the remote API decided.

    That would then make it explicit that referring to the performance attribute always returns what the configuration set, while referring to the effective_performance attribute always returns what the remote API decided.

    In the scenario you described in your question then, the resulting object would have the following attributes (possibly among others)

    {
      category              = "A"
      performance           = "low"
      effective_performance = "high"
    }
    

    This design conforms to Terraform's consistency rules by making it explicit in the schema that this remote API sometimes ignores the performance argument. The effective_performance argument cannot be set in configuration at all, and so your provider is free to return any value for it as long as it matches the type constraint in the schema.

    As a bonus, a user of your provider could potentially add their own postcondition to treat the situation as an error, if they'd prefer to make the configuration match the effective result in all cases:

    resource "dummy_resource" "dummy" {
      category    = "A"    
      performance = "low" 
    
      lifecycle {
        postcondition {
          condition     = self.effective_performance == self.performance
          error_message = "For category ${self.category}, performance must be set to ${self.effective_performance}."
        }
      }
    }
    

    Note that since effective_performance would (presumably) be decided only during the apply phase, this postcondition would also only be checked during the apply phase. If the error were raised then it would be after the remote object was already created.

    You could potentially improve on that by returning a known value for effective_performance during the planning phase, but again that would be possible only if the rules were guaranteed by the remote API so you could replicate the logic in the provider, and so I don't think that would be possible in your case. (If it were then you could've used the first approach I described in this answer, where the provider itself checks for consistency during the validation phase.)