Pass variable content to dynamic block verbatim in Terraform, or multiple dynamic sub blocks

126 Views Asked by At

Im trying to update my terraform proxmox module accoring to new disk formatting e.g.

https://github.com/TheGameProfi/terraform-provider-proxmox/blob/master/docs/resources/vm_qemu.md#disks-block

Edit: Original repository is this one https://github.com/Telmate/terraform-provider-proxmox - but original repo owners seem not to care anymore, so thus this fork was created. But the disks structure was intented to be this way unfortunaly even in original repo.

  disks {
    ide {
      //<arguments omitted for brevity...>
    }
    sata {
      //<arguments omitted for brevity...>
    }
    scsi {
      //<arguments omitted for brevity...>
    }
    virtio {
      //<arguments omitted for brevity...>
    }
  }
}

There can be virtio {virtio1,2,3,4...n}, scsi, ide.

What I tought I could do is to create variable like this in moule instance, and pass it to disk block e.g. the resource.

  vm_disk = {
    virtio = {
      virtio0 = {
        disk = {
          storage  = "lvm"
          size     = "30G"
          discard  = "on"
          iothread = 1
    }
      }
      virtio1 = {
        disk = {
          storage  = "lvm"
          size     = "150G"
          discard  = "on"
          iothread = 1
    }
      }
  
    }
  }

But boy, was I wrong. I've tried and tried (trial and error at this stage really)

I don't need to do any value sanitation, I don't care - I just need to pass the structure from variable to dynamic block inside of the module. I dont care if there is size, storage etc. defined if it would be too difficult

This is my latest creation and of course its not working at all and Im quite out of ideas.

  dynamic "disk" {
    for_each = var.vm_disk
    iterator = item
    content {
      dynamic "virtio" {
        for_each = item.value.virtio != null ? [item.value.virtio] : []
        content {
          disk = virtio.value
        }
      }
      dynamic "scsi" {
        for_each = item.value.scsi != null ? [item.value.scsi] : []
        content {
          disk = scsi.value
        }
      }
    }
  }

And this is variable deffinition:

variable "vm_disk" {
  description = "A disk block used to configure the disk devices"
  type        = map(any)
  default     = {}
}

Does anyone have any idea how to proceed? Any help is greatly appreciated!

EDIT2:

The final structure should look like this, but nothing I've tried seems to work:

  disks {
    ide {
      ide0 {
        disk {
          //<arguments omitted for brevity...>
        }
      }
      ide1 {
        disk {
          //<arguments omitted for brevity...>
        }
      }
    }
    //<arguments omitted for brevity...>
  }
}
1

There are 1 best solutions below

3
Martin Atkins On

The goal you've described suggests a map of objects, and so you should write out the object types you actually intend to use so that Terraform can give you better feedback when things aren't working:

variable "vm_disk" {
  type = map(object({
    ide = optional(object({
      # (whatever attributes are needed for ide disks)
    }))
    sata = optional(object({
      # (whatever attributes are needed for sata disks)
    }))
    scsi = optional(object({
      # (whatever attributes are needed for scsi disks)
    }))
    virtio = optional(object({
      # (whatever attributes are needed for virtio disks)
    }))
  }))
}

With the above type constraint, Terraform will ensure that each of these attributes is either an object or null, and that if it's an object then it has all of the attributes required for that nested object type.

You can then exploit this "definitely an object or null" guarantee to use the splat operator [*] in its Single Values as Lists variation, to simplify your for_each expressions:

  dynamic "disk" {
    for_each = var.vm_disk
    iterator = item
    content {
      dynamic "ide" {
        for_each = item.value.ide[*]
        content {
          disk {
            # ... argument assignments from ide.value
          }
        }
      }
      dynamic "sata" {
        for_each = item.value.sata[*]
        content {
          disk {
            # ... argument assignments from sata.value
          }
        }
      }
      dynamic "scsi" {
        for_each = item.value.scsi[*]
        content {
          disk {
            # ... argument assignments from scsi.value
          }
        }
      }
      dynamic "virtio" {
        for_each = item.value.virtio[*]
        content {
          disk {
            # ... argument assignments from virtio.value
          }
        }
      }
    }
  }

An expression like item.value.ide[*], when item.value.ide is not of a sequence-like type, evaluates to a single-element list when item.value.ide is not null, or to an empty list when it is null. This is therefore a shorthand for item.value.ide != null ? [item.value.ide] : [].

Because this provider is designed to use nested blocks for these structures, there is no alternative to writing out each argument individually. Providers designed in this way rely on Terraform to guarantee that the data structure is valid per the schema, which means that Terraform must statically verify the structure during the validation step.

For example, if you want to expose the format argument associated with IDE disks then you'd first declare that as part of the object type for the ide attribute:

    # ...
    ide = optional(object({
      format = optional(string)
    }))
    # ...

Then assign that attribute to the corresponding argument inside the configuration block:

      dynamic "ide" {
        for_each = item.value.ide[*]
        content {
          disk {
            format = ide.value.format
          }
        }
      }

Because this provider is not designed to take the entire configuration as a single argument with a data structure assigned to it, there is no alternative but to write out the rules for generating dynamic blocks with explicit arguments inside. If your module is not going to simplify this data structure in any way relative to the underlying provider, it's likely more productive to skip the module and have the caller declare a resource block directly with the needed settings, thus avoiding all of the complexity of dynamically generating configuration blocks.