hashicorp / hashicorp/terraform-plugin-sdk

mapping nested structure with computed attribute to state fails with outer TypeSet but works with TypeList

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

Description

### Terraform Version
```
Terraform v0.11.8
```

### Terraform Configuration Files
```hcl
"ports": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"internal": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: true,
},

"external": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},

"ip": &schema.Schema{
Type: schema.TypeString,
Default: "0.0.0.0",
Optional: true,
ForceNew: true,
},

"protocol": &schema.Schema{
Type: schema.TypeString,
Default: "tcp",
Optional: true,
ForceNew: true,
},
},
},
},
```

```go
func resourceDockerContainerRead(d *schema.ResourceData, meta interface{}) error {
// ...
if err := d.Set("ports", flattenContainerPorts(container.NetworkSettings.Ports)); err != nil {
log.Printf("[WARN] failed to set ports from API: %s", err)
}
// ...
}

func flattenContainerPorts(in nat.PortMap) *schema.Set {
var out = make([]interface{}, 0)
for port, portBindings := range in {
m := make(map[string]interface{})
for _, portBinding := range portBindings {
portProtocolSplit := strings.Split(string(port), "/")
convertedInternal, _ := strconv.Atoi(portProtocolSplit[0])
convertedExternal, _ := strconv.Atoi(portBinding.HostPort)
m["internal"] = convertedInternal
m["external"] = convertedExternal
m["ip"] = portBinding.HostIP
m["protocol"] = portProtocolSplit[1]
out = append(out, m)
}
}
portsSpecResource := resourceDockerContainer().Schema["ports"].Elem.(*schema.Resource)
f := schema.HashResource(portsSpecResource)
return schema.NewSet(f, out)
}
```

### Working solution
Change of outer structure to `schema.TypeList`
```hcl
"ports": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"internal": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: true,
},

"external": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},

"ip": &schema.Schema{
Type: schema.TypeString,
Default: "0.0.0.0",
Optional: true,
ForceNew: true,
},

"protocol": &schema.Schema{
Type: schema.TypeString,
Default: "tcp",
Optional: true,
ForceNew: true,
},
},
},
},
```

Changing return type to `[]interface{}` and adapting logic
```go
func flattenContainerPorts(in nat.PortMap) []interface{} {
var out = make([]interface{}, 0)
for port, portBindings := range in {
m := make(map[string]interface{})
for _, portBinding := range portBindings {
portProtocolSplit := strings.Split(string(port), "/")
convertedInternal, _ := strconv.Atoi(portProtocolSplit[0])
convertedExternal, _ := strconv.Atoi(portBinding.HostPort)
m["internal"] = convertedInternal
m["external"] = convertedExternal
m["ip"] = portBinding.HostIP
m["protocol"] = portProtocolSplit[1]
out = append(out, m)
}
}
return out
```

### Debug Output
From Travis: https://travis-ci.org/terraform-providers/terraform-provider-docker/builds/442146026

```
--- FAIL: TestAccDockerContainer_port_internal (1.40s)
testing.go:434: Step 0 error: After applying this step, the plan was not empty:

DIFF:

DESTROY/CREATE: docker_container.foo
bridge: "" => ""
gateway: "172.17.0.1" => ""
image: "sha256:dbfc48660aeb7ef0ebd74b4a7e0822520aba5416556ee43acb9a6350372e516f" => "sha256:dbfc48660aeb7ef0ebd74b4a7e0822520aba5416556ee43acb9a6350372e516f"
ip_address: "172.17.0.2" => ""
ip_prefix_length: "16" => ""
log_driver: "json-file" => "json-file"
must_run: "true" => "true"
name: "tf-test" => "tf-test"
ports.#: "1" => "1"
ports.1347368482.internal: "80" => "0" (forces new resource)
ports.1347368482.ip: "0.0.0.0" => "" (forces new resource)
ports.1347368482.protocol: "tcp" => "" (forces new resource)
ports.2861306838.external: "" => "" (forces new resource)
ports.2861306838.internal: "" => "80" (forces new resource)
ports.2861306838.ip: "" => "0.0.0.0" (forces new resource)
ports.2861306838.protocol: "" => "tcp" (forces new resource)
restart: "no" => "no"

STATE:

docker_container.foo:
ID = 90153bc2bf68ccc051983bea72a157cd9948d853538d6185b2ab732c689cb1fe
bridge =
gateway = 172.17.0.1
image = sha256:dbfc48660aeb7ef0ebd74b4a7e0822520aba5416556ee43acb9a6350372e516f
ip_address = 172.17.0.2
ip_prefix_length = 16
log_driver = json-file
must_run = true
name = tf-test
ports.# = 1
ports.1347368482.external = 32768
ports.1347368482.internal = 80
ports.1347368482.ip = 0.0.0.0
ports.1347368482.protocol = tcp
restart = no

Dependencies:
docker_image.foo
docker_image.foo:
ID = sha256:dbfc48660aeb7ef0ebd74b4a7e0822520aba5416556ee43acb9a6350372e516fnginx:latest
keep_locally = true
latest = sha256:dbfc48660aeb7ef0ebd74b4a7e0822520aba5416556ee43acb9a6350372e516f
name = nginx:latest

```

### Expected Behavior
*flattening* to the state should work with `TypeSet` as outer structure as well and not cause an non-empty `plan` after a successful `apply`

### Actual Behavior
After a successful `apply` a further `plan` with no changes made, causes a non-empty plan. This means although there were no changes `terraform` thinks the structure has changes. This only occurs in the nested structure if the outer structure is a `TypeSet` and it is a `TypeList`

Making the outer structure `Computed: true` did not solve the problem. See failing [build](https://travis-ci.org/terraform-providers/terraform-provider-docker/builds/441860987#L1799)

### Steps to Reproduce
See commits + changes and the failing and working solution above
- https://github.com/terraform-providers/terraform-provider-docker/pull/103/commits/26ea5c60cd852d0385822087583055d460aae0ac
- https://github.com/terraform-providers/terraform-provider-docker/pull/103/commits/0b7d64fd994bb4160c6ad98cf994791a4b7b9ba7

If it is desired I can also make a seprate branch which make the reproduction easier.

### Additional Context
- Terraform Provider Docker

### References
- https://github.com/terraform-providers/terraform-provider-docker/pull/103/
- Failing build: https://travis-ci.org/terraform-providers/terraform-provider-docker/builds/441860987
- Working build after fix: https://travis-ci.org/terraform-providers/terraform-provider-docker/builds/442175818

Contributor guide

Open the contributing guide

Research direction

Start with resourceDockerContainerRead and flattenContainerPorts, then run TestAccDockerContainer_port_internal using the TypeSet configuration shown in the issue. Compare the failing TypeSet behavior with the working TypeList changes and the referenced pull request commits. Done means applying the configuration produces an empty subsequent plan without changing the outer structure to TypeList.

Written by the indexing model from the issue text.

Assessment

Tech stack
docker, go
Domain
backend, devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.