Dynamic block getting created despite empty list

I am trying to create a single dynamic block if there are values, but it tries to create multiple copies with the dynamic block given the error below. I am probably doing something wrong that is obvious, but I cannot see it at the moment.

│ Error: Too many monitor_threshold_windows blocks
│
│   on main.tf line 225, in resource "datadog_monitor" "monitor":
│  225:     content {
│
│ No more than 1 "monitor_threshold_windows" blocks are allowed

The resource I am trying to create is datadog_monitor. I fetch a JSON using https://api.datadoghq.com/api/v1/monitor (ref). A snippet of this can looks like this:

[
  {
    "id": 123456789,
    "org_id": 123456,
    "type": "query alert",
    "name": "METRIC_CHECK",
    "message": "something is not good",
    "tags": [
      "service:mydatabase"
    ],
    "query": "some_query_goes_here",
    "options": {
      "thresholds": {
        "critical": 0.99,
        "critical_recovery": 0.0,
        "warning": 0.6
      },
      "notify_audit": false,
      "require_full_window": false,
      "notify_no_data": false,
      "renotify_interval": 0,
      "threshold_windows": {
        "trigger_window": "last_1d",
        "recovery_window": "last_15m"
      },
      "include_tags": false,
      "new_host_delay": 300,
      "silenced": {}
    },
    "restricted_roles": [
      "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    ],
    "priority": 5,
  }
]

This was the resource I created:

locals {
    monitors = jsondecode(file("${path.module}/tfvars/${var.org_workspace}.monitors.json"))
}

resource "datadog_monitor" "monitor" {
  for_each = { for monitor in local.monitors : monitor.name => monitor }

  name               = each.value.name
  type               = each.value.type
  message            = each.value.message
  escalation_message = try(each.value.options.escalation_message, null) # invalid for metrics 

  query = each.value.query

  monitor_thresholds {
    ok                = try(each.value.options.thresholds.ok, null)
    warning           = try(each.value.options.thresholds.warning, null)
    critical          = try(each.value.options.thresholds.critical, null)
    critical_recovery = try(each.value.options.thresholds.critical_recovery, null)
  }

  include_tags = each.value.options.include_tags
  tags         = each.value.tags

  priority                 = each.value.priority
  new_group_delay          = try(each.value.options.new_group_delay, null)
  new_host_delay           = try(each.value.options.new_host_delay, null) # provider defaults to 300 regardless of null
  no_data_timeframe        = try(each.value.options.no_data_timeframe, null)
  notification_preset_name = try(each.value.options.notification_preset_name, null)
  notify_audit             = try(each.value.options.notify_audit, null)
  notify_by                = try(each.value.options.notify_by, null)
  notify_no_data           = try(each.value.options.notify_no_data, null)
  on_missing_data          = try(each.value.options.on_missing_data, null)

  renotify_interval    = try(each.value.options.renotify_interval, null)
  renotify_occurrences = try(each.value.options.renotify_occurrences, null)
  renotify_statuses    = try(each.value.options.renotify_statuses, null)
  require_full_window  = try(each.value.options.require_full_window, false) # null = true, so defaulting to false
  restricted_roles     = try(each.value.restricted_roles, null)

  enable_logs_sample       = try(each.value.options.enable_logs_sample, null)

  # getting error 'No more than 1 "monitor_threshold_windows" blocks are allowed'
  dynamic "monitor_threshold_windows" {
    for_each = each.value.options.threshold_windows

    content {
      recovery_window = monitor_threshold_windows.value.recovery_window
      trigger_window  = monitor_threshold_windows.value.trigger_window
    }
  }
}

Most monitor resources will not have the monitor_threshold_windows, but this particular metric does.

This is my final solution. I wish there was function that tested if the key exists, but this works:

 for_each = try(each.value.options.threshold_windows, null) != null ? [each.value.options.threshold_windows] : []

When I do this though, I get at least one empty monitor_thresholds block, which I don’t want.

  # datadog_monitor.monitor["[Synthetics] GB enrollment 2"] will be updated in-place
  ~ resource "datadog_monitor" "monitor" {
        id                       = "123456789"
        name                     = "my_monitor"
        tags                     = [
            "check_status:live",
            "check_type:browser",
            "ci_execution_rule:blocking"
        ]
        # (21 unchanged attributes hidden)

      + monitor_thresholds {}
    }

@ExtelligenceIT Any ideas for this one? I don’t want an empty block. I thought if the block was empty it wouldn’t be included.

I also found this existing issue, it seems they did not want to fix it.

Hi @darkn3rd,

Again, given the nested-block structure of this resource, you will need to handle optional blocks as a dynamic block if you don’t want those blocks to appear at all if there are no attributes being set within them.
If I’ve understood you correctly you will need the following. Which will prevent the monitor_thresholds block being created in the config if the thresholds object does not appear within the options object in the JSON.

    "options": {
      "thresholds": {...  # <-- Handle if this object does not exist.
  dynamic monitor_thresholds {
    for_each = try(each.value.options.thresholds, null) != null ? [each.value.options.thresholds] : []
    content {
    ok                = try(monitor_thresholds.value.ok, null)
    warning           = try(monitor_thresholds.value.warning, null)
    critical          = try(monitor_thresholds.value.critical, null)
    critical_recovery = try(monitor_thresholds.value.critical_recovery, null)
    }
  }

Hope that helps,

Happy Terraforming!