Argument must not be null, inside dynamic block

I am using kubernetes_network_policy resource. I have around ten network_poilicyand each of them are different. One policy has only ingress, another one has only egrees, few of them have both ingress and egress. I am getting below error when I have a null value in the dynamic block, is there a way to overcome this error. like only execute the dynamic block if the variable(ingress_number) has some value?

 Error: Invalid function argument
│
│   on main.tf line 16, in resource "kubernetes_network_policy" "example-policy":
│   16:       for_each = range(length(each.value.ingress_number))
│     ├────────────────
│     │ each.value.ingress_number is null
│
│ Invalid value for "value" parameter: argument must not be null.

My Resource

resource "kubernetes_network_policy" "example-policy" {
  for_each = var.inputs
  metadata {
    name      = each.value.name
    namespace = each.value.namespace
  }
  spec {
    pod_selector {
      match_labels = {
        app = each.value.selector
      }
    }
    policy_types = each.value.policy
    dynamic "ingress" {
        
        for_each = range(length(each.value.ingress_number))
        
        content {
            ports {
                port     = each.value.ingress_number[ingress.value]
                protocol = each.value.ingress_protocol[ingress.value]
            }
            
            to {
                namespace_selector {
                    match_labels = {
                        app = each.value.ingress_label
                    }
                }
           } 
      }       
    }      
    dynamic "egress" {
        
        for_each = range(length(each.value.egress_number))
        
        content {
            ports {
                port     = each.value.egress_number[egress.value]
                protocol = each.value.egress_protocol[egress.value]
            }
            
            to {
                namespace_selector {
                    match_labels = {
                        app = each.value.egress_label
                    }
                }
           } 
      }       
    }    
  }
}

My varibale.tf

variable "inputs" {
  type = map(object({
    name            = string
    namespace       = string
    selector        = string
    policy          = list(string)
    ingress_number   = optional(list(string))
    ingress_protocol = optional(list(string))
    ingress_label    = optional(string)
    egress_number   = optional(list(string))
    egress_protocol = optional(list(string))
    egress_label    = optional(string)
  }))
  default = {}
}

My tfvars

  inputs = {

    app1 = {
      name           = "apache"
      namespace       = "default"
      selector        = "apache-app"
      policy          = ["ingrees", "Egress"]
      egress_label    = "example"
      egress_number   = ["443", "8080"]
      egress_protocol = ["TCP", "TCP"]
    }
    app2 = {
      name           = "nignx"
      namespace       = "default"
      selector        = "nignix-app"
      policy          = ["ingrees", "Egress"]
      ingress_label    = "play"
      ingress_number   = ["9080", "9320"]
      ingress_protocol = ["TCP", "TCP"]
      egress_label    = "example"
      egress_number   = ["443", "8080"]
      egress_protocol = ["TCP", "TCP"]
    }
  }

Hi @Eva,

I think we can find an answer here if we look at the requirements and behaviors of smaller parts of your example.

First there’s the for_each inside a dynamic block (which I assume is where this error was from, although I don’t see a dynamic block in your full example).

A dynamic block acts much like a for expression, but produces nested blocks instead of a complex typed value. It iterates over a given complex value, and generates a nested block for each element of that complex value.

So if the goal is to generate zero blocks of the given type in some situation, then the way we can achieve that is to make sure that for_each is given an empty collection or structure in that case.

Then we can consider the range function:

The resulting list is created by starting with the given start value and repeatedly adding step to it until the result is equal to or beyond limit.

Since you are passing only one function to range, the documentation says that that single argument is treated as limit, so start will be 0 and step will be 1 by default.

So the way to get range to produce an empty list as its result would be to set the limit argument to 0 too, so that the result will immediately be “equal to or beyond limit” before generating any elements.

Finally, we have the length function, which determines the length of the given list, map, or string. To make this function return zero then will require passing it a zero-length value.

Putting that all together, it seems like your requirement is to use each.value.ingress_number unless it is null, and otherwise to use an empty value of the same type. Since ingress_number is a list of strings, you’ll need to use an empty list in place of the null.

With Terraform as it exists today (Terraform v1.2), you can handle this by using the coalesce function, which takes one or more arguments and returns the first one that isn’t null:

       for_each = range(length(coalesce(each.value.ingress_number, [])))

That extra coalesce call means that if ingress_number is null then the result will be an empty list instead, and so length will return 0 and range will return an empty list and so for_each will have zero elements and therefore the dynamic block will generate zero blocks.


I see that you are using the experimental feature for declaring optional attributes here, and so it might interest you to learn that in the forthcoming Terraform v1.3 release a new iteration of that experiment is due to become stabilized, and the final design for that feature includes built-in support for declaring default values, and so once v1.3 is released and you’re ready to use it you will be able to eliminate the coalesce call in favor of having Terraform perform the same operation automatically as part of the input variable type conversion:

# NOTE: This example is for Terraform v1.3, which is
# not released at the time I'm writing it so details
# could potentially change before final release.

variable "inputs" {
  type = map(object({
    name            = string
    namespace       = string
    selector        = string
    policy          = list(string)
    ingress_number   = optional(list(string), [])
    ingress_protocol = optional(list(string), [])
    ingress_label    = optional(string)
    egress_number   = optional(list(string), [])
    egress_protocol = optional(list(string), [])
    egress_label    = optional(string)
  }))
  default = {}
}

Notice that for each of the optional(list(string)) attributes I’ve added the extra argument [], which specifies a value to use whenever that attribute would’ve been null. In effect then, Terraform is automatically running coalesce(v, []) where v is whatever value the caller assigned to that attribute, and so you will no longer need to do that explicitly elsewhere in your module.

@apparentlymart apologize, I have updated my main.tf now. dynamic block now has for_each

@apparentlymart
I get this error when I updated my variables to optional(list(string), [])

│ Error: Invalid type specification
│
│   on variables.tf line 70, in variable "inputs":
│   70:     egress_protocol     = optional(list(string), [])
│
│ Optional attribute modifier expects only one argument: the attribute type.

Hi @Eva,

That part of my answer was describing a feature of a future version of Terraform that hasn’t been released yet. Try what I described in the first part of my answer for now. You will not be able to follow the second part of the answer until Terraform v1.3 is released and you have upgraded to it.