terraform-plugin-sdk
terraform-plugin-sdk copied to clipboard
mapping nested structure with computed attribute to state fails with outer TypeSet but works with TypeList
Terraform Version
Terraform v0.11.8
Terraform Configuration Files
"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,
},
},
},
},
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
"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
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: "" => "<computed>"
gateway: "172.17.0.1" => "<computed>"
image: "sha256:dbfc48660aeb7ef0ebd74b4a7e0822520aba5416556ee43acb9a6350372e516f" => "sha256:dbfc48660aeb7ef0ebd74b4a7e0822520aba5416556ee43acb9a6350372e516f"
ip_address: "172.17.0.2" => "<computed>"
ip_prefix_length: "16" => "<computed>"
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: "" => "<computed>" (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
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