Skip to content

computed field producing spurious plan changes with framework but not SDK v2 #628

@asaba-hashi

Description

@asaba-hashi

Module version

github.com/hashicorp/terraform-plugin-framework v1.0.1

Relevant provider source code

PR for updating the provider to framework: https://github.com/JupiterOne/terraform-provider-jupiterone/pull/136/files#diff-516571d421fdf72766d23e79357696f48d9949132cfd700d30df5a26fa0dc8aa

These errors are occurred in the tests when moving from SDK v2 to Framework:

Framework:

		        "version": schema.Int64Attribute{
				Description: "Computed current version of the rule. Incremented each time the rule is updated.",
				Computed:    true,
			},

SDK:

			"version": {
				Type:        schema.TypeInt,
				Description: "Computed current version of the rule. Incremented each time the rule is updated.",
				Computed:    true,
			},

Terraform Configuration Files

This happens as part of the acceptance tests.

func TestRuleInstance_Basic(t *testing.T) {
	ctx := context.TODO()

	recorder, cleanup := setupCassettes(t.Name())
	defer cleanup(t)
	testHttpClient := cleanhttp.DefaultClient()
	testHttpClient.Transport = logging.NewTransport("JupiterOne", recorder)
	// testJ1Client is used for direct calls for CheckDestroy/etc.
	testJ1Client, err := (&JupiterOneProviderModel{httpClient: testHttpClient}).Client(ctx)
	if err != nil {
		t.Fatal("error configuring check client", err)
	}

	ruleName := "tf-provider-rule"
	resourceName := "jupiterone_rule.test"
	operations := getValidOperations()
	operationsUpdate := getValidOperationsWithoutConditions()
	resource.Test(t, resource.TestCase{
		PreCheck:                 func() { testAccPreCheck(t) },
		ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(testJ1Client),
		CheckDestroy:             testAccCheckRuleInstanceDestroy(ctx, resourceName, testJ1Client),
		Steps: []resource.TestStep{
			{
				Config: testRuleInstanceBasicConfigWithOperations(ruleName, operations),
				Check: resource.ComposeTestCheckFunc(
				),
			},
			{
				Config: testRuleInstanceBasicConfigWithOperations(ruleName, operationsUpdate),
				Check: resource.ComposeTestCheckFunc(
				),
			},
		},
	})
}

func testRuleInstanceBasicConfigWithOperations(rName string, operations string) string {
	return fmt.Sprintf(`
		resource "jupiterone_rule" "test" {
			name = %q
			description = "Test"
			spec_version = 1
			polling_interval = "ONE_WEEK"
			tags = ["tag1","tag2"]

			question {
				queries {
					name = "query0"
					query = "Find DataStore with classification=('critical' or 'sensitive' or 'confidential' or 'restricted') and encrypted!=true"
					version = "v1"
				}
			}

			outputs = [
				"queries.query0.total",
				"alertLevel"
			]

			operations = %q
		}
	`, rName, operations)
}

Debug Output

https://gist.github.com/asaba-hashi/275dd1d9fc6f774a2ae5629143acde30

Expected Behavior

The tests should have passed because no changes were detected in the plan for just the computed field.

Actual Behavior

The test failed with:

=== RUN   TestRuleInstance_Basic
    resource_rule_test.go:36: Step 1/2 error: After applying this test step and performing a `terraform refresh`, the plan was not empty.
        stdout
        
        
        Terraform used the selected providers to generate the following execution
        plan. Resource actions are indicated with the following symbols:
          ~ update in-place
        
        Terraform will perform the following actions:
        
          # jupiterone_rule.test will be updated in-place
          ~ resource "jupiterone_rule" "test" {
                id               = "f2ad0219-549e-4f36-9569-35f8979c38fa"
                name             = "tf-provider-rule"
                tags             = [
                    "tag1",
                    "tag2",
                ]
              ~ version          = 1 -> (known after apply)
                # (5 unchanged attributes hidden)
        
                # (1 unchanged block hidden)
            }
        
        Plan: 0 to add, 1 to change, 0 to destroy.

Adding UseStateForUnknown() produces the following error:

			"version": schema.Int64Attribute{
				Description: "Computed current version of the rule. Incremented each time the rule is updated.",
				Computed:    true,
				PlanModifiers: []planmodifier.Int64{
					int64planmodifier.UseStateForUnknown(),
				},
			},
=== RUN   TestRuleInstance_Basic
    resource_rule_test.go:36: Step 2/2 error: Error running apply: exit status 1
        
        Error: Provider produced inconsistent result after apply
        
        When applying changes to jupiterone_rule.test, provider
        "provider[\"registry.terraform.io/hashicorp/jupiterone\"]" produced an
        unexpected new value: .version: was cty.NumberIntVal(1), but now
        cty.NumberIntVal(2).
        
        This is a bug in the provider, which should be reported in the provider's own
        issue tracker.
--- FAIL: TestRuleInstance_Basic (2.08s)

A full workaround can be implemented by providing a ModifyPlan() that will run after the field plan modifier:

// ModifyPlan is a workaround for unexpected behavior in the framework around
// the `computed: true` `version` field to make sure that it is only part of
// the plan if there is some other change in the resource.
//
// Based on the implementation of the Time resource:
// https://github.com/hashicorp/terraform-provider-time/blob/main/internal/provider/resource_time_rotating.go#L189-L234
func (*QuestionRuleResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
	// Plan does not need to be modified when the resource is being destroyed.
	if req.Plan.Raw.IsNull() {
		return
	}

	// Plan only needs modifying if the resource already exists as the purpose of
	// the plan modifier is to show updated attribute values on CLI.
	if req.State.Raw.IsNull() {
		return
	}

	var plan, state *RuleModel

	resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
	resp.Diagnostics.Append(req.State.Get(ctx, &state)...)

	if resp.Diagnostics.HasError() {
		return
	}

	if !reflect.DeepEqual(plan, state) {
		resp.Diagnostics.Append(resp.Plan.SetAttribute(ctx, path.Root("version"),
			types.Int64Unknown())...)
	}
}

Steps to Reproduce

  1. Remove or comment out the workaround:
    1. (*RuleResource) ModifyPlan()
    2. UseStateForUnknown on the version field schema
  2. make test.

References

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions