hashicorp / hashicorp/terraform-plugin-sdk

SetNew in CustomizeDiff does not trigger diff on computed field when map value is set to empty string

Open
#371 3 comments 0 reactions 0 assignees View on GitHub
bug subsystem/types
Dominant language
Go
Stars
485
Forks
244
Avg merge
19h 57m
Merged PRs (30d)
4

Description

### SDK version

```
{
"Path": "github.com/hashicorp/terraform-plugin-sdk",
"Version": "v1.9.0"
}

```

### Relevant provider source code

```go
package main

import (
"log"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/plugin"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
)

func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: Provider})
}

func Provider() terraform.ResourceProvider {
return &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
"mapdiff_test": resourceTest(),
},
}
}

func resourceTest() *schema.Resource {
return &schema.Resource{
Create: resourceTestCreate,
Read: resourceTestRead,
Delete: resourceTestDelete,
Update: resourceTestCreate,
CustomizeDiff: resourceTestDiff,
Schema: map[string]*schema.Schema{
"input": &schema.Schema{
Type: schema.TypeMap,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"result": &schema.Schema{
Type: schema.TypeMap,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
}
}

func resourceTestCreate(d *schema.ResourceData, m interface{}) error {
d.SetId("dooo")

return resourceTestRead(d, m)
}

func resourceTestRead(d *schema.ResourceData, m interface{}) error {
return nil
}

func resourceTestDelete(d *schema.ResourceData, m interface{}) error {
return nil
}

func resourceTestDiff(d *schema.ResourceDiff, m interface{}) error {
d.SetNew("result", d.Get("input"))

o, n := d.GetChange("result")

log.Printf(cmp.Diff(o, n))

return nil
}
```

### Terraform Configuration Files
```hcl
// Example to reproduce.
resource "mapdiff_test" "foo" {
input = {
"baz" = "hahn"
"a" = ""
}
}

// Real world example.
provider "tls" {
version = "~> 2.1"
}

resource "tls_private_key" "example" {
algorithm = "ECDSA"
ecdsa_curve = "P384"
}

resource "mapdiff_test" "foo" {
input = {
"foo" = tls_private_key.example.private_key_pem
"baz" = "hahn"
}
}
```

### Debug Output

https://gist.github.com/invidian/4f1d9d0b6f74f352aad892dab857a8fa

### Expected Behavior

When using `SetNew` from `schema.ResourceDiff` on fields which are `TypeMap` and `Computed`, Terraform should properly write them and show them in diff, not ignore them.

### Actual Behavior

Field `input`, containing user input properly shows diff and gets updated in the state, but field `result` gets ignored and it's never corrected. This might be problematic, when one needs a Map, where the value can actually be an empty string.

Related upstream issue https://github.com/flexkube/libflexkube/issues/48#issuecomment-605352824.

### Steps to Reproduce

1. Save provider code to `main.go` file.
2. Build using `go build -o terraform-provider-diffmap`.
3. Create `main.tf` file with following content:
```hcl
resource "mapdiff_test" "foo" {
input = {
"baz" = "hahn"
"a" = ""
}
}
```
4. Run `terraform init`.
5. Run `terraform apply -auto-approve`.
6. Inspect created Terraform state with `cat terraform.tfstate`. Example output:
```json
{
"version": 4,
"terraform_version": "0.12.24",
"serial": 38,
"lineage": "9f56ba63-345b-ca92-75c4-7e9cbd2d5da1",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "mapdiff_test",
"name": "foo",
"provider": "provider.mapdiff",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "dooo",
"input": {
"a": "",
"baz": "hahn"
},
"result": {
"baz": "hahn"
}
},
"private": "bnVsbA=="
}
]
}
]
}
```
7. What actual output should be:
```json
{
"version": 4,
"terraform_version": "0.12.24",
"serial": 38,
"lineage": "9f56ba63-345b-ca92-75c4-7e9cbd2d5da1",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "mapdiff_test",
"name": "foo",
"provider": "provider.mapdiff",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "dooo",
"input": {
"a": "",
"baz": "hahn"
},
"result": {
"a": "",
"baz": "hahn"
}
},
"private": "bnVsbA=="
}
]
}
]
}
```

### Context

With [terraform-provider-flexkube](https://github.com/flexkube/libflexkube/tree/master/cmd/terraform-provider-flexkube), I take user configuration, including TLS certificates and then transpile it to more complex structure, which is written to Terraform state to provide user friendly diff. Unfortunately, currently re-generating the certificates triggers inconsistent state, as certificate content is not known while planning. I would expect computed fields which has value set to empty string (`""`) to be seen in diff as "Known after apply", instead of just being ignored. Using `SetNewComputed` on sub-field produces error too.

With "real world example", running Terraform twice or with proper `-target` serves as a workaround. To trigger inconsistent plan issue, one needs to run `terraform taint tls_private_key.example` and then `terraform apply`.

If this is like this by design, how can I avoid getting inconsistent plan in such case?

### References
- Might be related to #282

Contributor guide

Open the contributing guide

Research direction

Start with the ResourceDiff.SetNew and SetNewComputed behavior in the SDK's helper/schema package, then reproduce the issue using main.go and main.tf. Compare the generated terraform.tfstate with the expected result and add coverage showing that a computed TypeMap retains the key whose value is an empty string.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, terraform
Domain
tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.