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

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

Open mavogel opened this issue 7 years ago • 0 comments

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

mavogel avatar Oct 16 '18 16:10 mavogel