terraform-plugin-framework icon indicating copy to clipboard operation
terraform-plugin-framework copied to clipboard

nil TypeMap permadiff

Open appilon opened this issue 4 years ago • 9 comments
trafficstars

Module version

0.4.2

Relevant provider source code

"permissions": {
				Type:        types.MapType{ElemType: types.BoolType},
				Optional:    true,
			},

Terraform Configuration Files

resource "sample_profile" "test" {
  name            = "test"
  description     = "test update"
  # permissions = {}
}

Expected Behavior

After applying the initial config, I expect a subsequent plan to be noop.

Actual Behavior

Terraform seems to think the current state has an empty set (but not nil) which causes a diff with the plan (nil).

- permissions     = {} -> null

Steps to Reproduce

References

appilon avatar Oct 29 '21 19:10 appilon

I attempted to massage the plan to match

type fixEmptyMap struct {
	emptyDescriptions
}

func (fixEmptyMap) Modify(_ context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) {
	if req.AttributeState == nil {
		return
	}
	state := req.AttributeState.(types.Map)
	plan := req.AttributePlan.(types.Map)
	if len(state.Elems) == 0 && !state.Null && plan.Null {
		plan.Null = false
		plan.Elems = make(map[string]attr.Value, 0)
		resp.AttributePlan = plan
	}
}

But got the following error on plan

╷
│ Error: Provider produced invalid plan
│
│ Provider "registry.terraform.io/hashicorp/salesforce" planned an invalid value for salesforce_profile.test.permissions: planned value cty.MapValEmpty(cty.Bool) for a non-computed attribute.
│
│ This is a bug in the provider, which should be reported in the provider's own issue tracker.
╵

appilon avatar Oct 29 '21 20:10 appilon

Would you be able to show how the attribute is being set into state (if it is) during Create and/or Read?

bflad avatar Oct 29 '21 20:10 bflad

The permissions go through a bit of an expansion from the API response but the logic is the following:

        // expand permissions
	permissions := make(map[string]attr.Value, len(includePermissions))
	for _, k := range includePermissions {
		v, ok := p[k]
		trimmedKey := strings.TrimPrefix(k, "Permissions")
		if ok {
			permissions[trimmedKey] = types.Bool{Value: v.(bool)}
		} else {
			// set to unknown, maybe we should panic?
			permissions[trimmedKey] = types.Bool{Unknown: true}
		}
	}
	if len(permissions) > 0 {
		data.Permissions = types.Map{ElemType: types.BoolType, Elems: permissions}
	}

I am certain len(includePermissions) == 0 therefore, data.Permissions should just be the zero value of that type. I can confirm

State setting code is very simple within Read

	d := pMap.ToStateData(data.PermissionKeys("Permissions")...)
	// copy the ID back over
	d.Id = data.Id

	resp.Diagnostics = resp.State.Set(ctx, &d)

appilon avatar Oct 29 '21 20:10 appilon

Confirmed, Permissions is whatever the zero value is

data := profileResourceData{
		Name:          p["Name"].(string),
		UserLicenseId: p["UserLicenseId"].(string),
		// Permissions is not touched
	}

appilon avatar Oct 29 '21 20:10 appilon

This may be similar to #201. Does explicitly setting the value Null: true help? e.g. something like:

if len(permissions) > 0 {
	data.Permissions = types.Map{ElemType: types.BoolType, Elems: permissions}
} else {
	data.Permissions = types.Map{ElemType: types.BoolType, Null: true}
}

Since types.Map uses Null bool under the hood, its zero-value would be false. This may however cause a different issue where if the configuration is permissions = {} that may now permadiff.

It might be possible to first manually passthrough the Config value (e.g. retrieved via GetAttribute) into Permissions then overwrite it after that length check.

bflad avatar Oct 29 '21 21:10 bflad

It may also be possible to use Permissions map[string]bool or something similar in the data type to workaround this.

bflad avatar Oct 29 '21 21:10 bflad

Hey again @appilon 👋 Did you find a resolution for this situation or does either of the above help?

bflad avatar Mar 16 '22 17:03 bflad

@bflad I will prioritize revisiting this this week.

appilon avatar Mar 16 '22 22:03 appilon

I encountered exactly the same problem today. The backend always provides a empty Map when no entries are set, no nil pointer. Now i have two options, either return the empty map, or when no entries are set set to Null:true. Both will always conflict with one of the scenarios in the terraform config.

// assuming someone generates a empty map
Label={}
// or not set at all
Label=null

Technically i dont care, both will always be empty for me as it is not computed. Not sure how to handle this, maybe something like a custom plan may help like "NullAreEmpty"

project0 avatar Apr 16 '22 11:04 project0

We're discussing a potential path forward here over in #447 with potentially modifying the zero-values of the types value types to be null, so I'm going to close this issue out so focus efforts and discussion in one place.

@Project0 if you didn't try it already, I think you may be able to do something like you propose by setting the map attribute as Computed: true and introducing the mentioned attribute plan modifier that converts a null configuration value to an empty map plan value.

bflad avatar Sep 23 '22 20:09 bflad

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues. If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

github-actions[bot] avatar Oct 24 '22 02:10 github-actions[bot]