Exclude setting attribute if related value is null

Trying to create a kubernetes_manifest for a CRD.

It is specifically for the Cloud SQL Auth Proxy Operator for GCP. The manifest sets up a cloud sql auth proxy sidecar that gets injected into matching k8s pods.

The problem I am facing is how to not set the proxy port attribute in the manifest, if it is omitted in the object variable…

The reason I want to do this is that the optional value is null, it is still recognized as a change in TF which causes an update to the resource.

variable looks like this:

variable "sidecar_definitions" {
  description = "Map of workloads to add Cloud SQL Proxy sidecar to, with configuration details..."
  type = map(object({
    workload_namespace      = string
    workload_selector_kind  = string
    workload_selector_name  = string
    connections = list(object({
      connection_string     = string
      port                  = optional(number)
      hostEnvName           = optional(string, "CSAP_HOSTNAME")
      portEnvName           = optional(string, "CSAP_PORT")
    }))
    telemetry               = bool
  }))
  validation {
    condition = length(var.sidecar_definitions) > 0
    error_message = "must provide at least 1 entry"
  }
  nullable = false
}```

data looks like this:
"application_with_port" = { 
  workload_namespace = "namespace1",
  workload_selector_kind = "StatefulSet",
  workload_selector_name = "application_1",
  connections = [
    { connection_string = "sample_project_abc:northamerica-northeast1:sample_db_1",
      hostEnvName = "DBHOST",
      portEnvName = "DBPORT",
  port = 54320,
    } 
  ],
  telemetry = false
},
"application_without_port" = { 
  workload_namespace = "namespace1",
  workload_selector_kind = "StatefulSet",
  workload_selector_name = "application_2",
  connections = [
    { connection_string = "sample_project_abc:northamerica-northeast1:sample_db_1",
      hostEnvName = "DBHOST",
      portEnvName = "DBPORT",
    } 
  ],
  telemetry = false
},

At the moment, my locals and resource definition looks like this:

```locals {
  # flatten ensures that this local value is a flat list of objects, rather
  # than a list of lists of objects.
  flattened_sidecar_defn = flatten([
    for sidecar_key, sidecar in var.sidecar_definitions : [
      for connection_key, connection in sidecar.connections : {
        sidecar_key = sidecar_key
        connection_key  = connection_key
        connection_string  = connection.connection_string
        connection_port = connection.port
        hostEnvName = connection.hostEnvName
        portEnvName = connection.portEnvName
      }
    ]
  ])
}

resource "kubernetes_manifest" "auth_proxy_workload" {
  for_each = var.sidecar_definitions
  manifest = {
    "apiVersion" = "cloudsql.cloud.google.com/v1"
    "kind" = "AuthProxyWorkload"
    "metadata" = {
      "name" = "cloud-sql-auth-proxy-operator--workload--${each.key}"
      "namespace" = each.value["workload_namespace"]
    }
    "spec" = {
      "instances" = [
/*          
          "autoIAMAuthN" = true 
          "connectionString" = each.value["db_connection_string"] 
          "hostEnvName" = each.value["hostEnvName"] 
          "portEnvName" = each.value["portEnvName"] 
          "privateIP" = true
*/
          for conn_key, conn in local.flattened_sidecar_defn : {
            "autoIAMAuthN" = true
            "privateIP" = true         
            "connectionString" = conn.connection_string
            "hostEnvName" = conn.hostEnvName
            "portEnvName" = conn.portEnvName
#            "port" = conn.connection_port   
            "port" = (conn.connection_port == null || conn.connection_port == 0) ? null : conn.connection_port
#            %{ if conn.connection_port != null }"port" = conn.connection_port %{ endif }
          } if conn.sidecar_key == each.key         
      ]
      "workloadSelector" = {
        "kind" = each.value["workload_selector_kind"]
        "name" = each.value["workload_selector_name"]
      }
    }
  }
}

The error I see in TF plan is this:

  ~ resource "kubernetes_manifest" "auth_proxy_workload" {
      ~ object   = {
          ~ spec       = {
              ~ instances          = [
                  ~ {
                      + port                  = (known after apply)
                        # (7 unchanged attributes hidden)
                    },
                ]
                # (2 unchanged attributes hidden)
            }
            # (3 unchanged attributes hidden)
        }
        # (1 unchanged attribute hidden)
    }

because port is being recognized as being changed, TF is pushing the new value (ie null) into the resource definition and recreating it…

+ port = (known after apply)

I need a hint how to make the port attribute optional if the value of conn.connection_port is null…

Thanks for any suggestions!

I haven’t even got to the port bit yet, I got distracted trying to understand what on earth is going on with

What was the point in creating a flattened list, only to then select out of it exactly the data you could have got more simply directly from each.value.connections ?!

I think this should do what you’re looking for, as well as allowing you to delete flattened_sidecar_defn entirely:

      instances = [
        for conn in each.value.connections : merge(
          {
            autoIAMAuthN     = true
            privateIP        = true
            connectionString = conn.connection_string
            hostEnvName      = conn.hostEnvName
            portEnvName      = conn.portEnvName
          },
          conn.connection_port == null ? {} : { port = conn.connection_port },
        )
      ]

Thanks @maxb - It’s a fair question. My example data was not fully fleshed out.

The cloud sql proxy allows multiple database connections to be defined, that are translated to individual instances in the k8s manifest. Here is a better sample that needs to be accommodated:

source data:

"application_with_many_db" = { 
  workload_namespace = "namespace1",
  workload_selector_kind = "StatefulSet",
  workload_selector_name = "application_1",
  connections = [
    { connection_string = "sample_project_abc:northamerica-northeast1:sample_db_1",
      hostEnvName = "DBHOST",
      portEnvName = "DBPORT",
      port = 54320,
    } ,
    { connection_string = "sample_project_abc:northamerica-northeast1:sample_db_2",
      hostEnvName = "DBHOST",
      portEnvName = "DBPORT",
      port = 54321,
    } ,
    { connection_string = "sample_project_abc:northamerica-northeast1:sample_db_3",
      hostEnvName = "DBHOST",
      portEnvName = "DBPORT",
      port = 54322,
    } 
  ],
  telemetry = false
},
"application_with_many_db_without_port_2" = { 
  workload_namespace = "namespace1",
  workload_selector_kind = "StatefulSet",
  workload_selector_name = "application_2",
  connections = [
    { connection_string = "sample_project_abc:northamerica-northeast1:sample_db_1",
      hostEnvName = "DBHOST",
      portEnvName = "DBPORT",
    } ,
    { connection_string = "sample_project_abc:northamerica-northeast1:sample_db_2",
      hostEnvName = "DBHOST",
      portEnvName = "DBPORT",
    } ,
    { connection_string = "sample_project_abc:northamerica-northeast1:sample_db_3",
      hostEnvName = "DBHOST",
      portEnvName = "DBPORT",
    } 
  ],
  telemetry = false
},

each top level object can have multiple connection instances…

desired output:

---
apiVersion: cloudsql.cloud.google.com/v1
kind: AuthProxyWorkload
metadata:
  name: application_with_many_db_2
  namespace: namespace1
spec:
  instances:
  - autoIAMAuthN: true
    connectionString: sample_project_abc:northamerica-northeast1:sample_db_1
    hostEnvName: DBHOST
    port: 54320
    portEnvName: DBPORT
    privateIP: true
  - autoIAMAuthN: true
    connectionString: sample_project_abc:northamerica-northeast1:sample_db_2
    hostEnvName: DBHOST
    port: 54321
    portEnvName: DBPORT
    privateIP: true
  - autoIAMAuthN: true
    connectionString: csample_project_abc:northamerica-northeast1:sample_db_3
    hostEnvName: DBHOST
    port: 54322
    portEnvName: DBPORT
    privateIP: true
  workloadSelector:
    kind: StatefulSet
    name: application_1
---
apiVersion: cloudsql.cloud.google.com/v1
kind: AuthProxyWorkload
metadata:
  name: application_with_many_db_without_port_2
  namespace: namespace1
spec:
  instances:
  - autoIAMAuthN: true
    connectionString: sample_project_abc:northamerica-northeast1:sample_db_1
    hostEnvName: DBHOST
    portEnvName: DBPORT
    privateIP: true
  - autoIAMAuthN: true
    connectionString: sample_project_abc:northamerica-northeast1:sample_db_2
    hostEnvName: DBHOST
    portEnvName: DBPORT
    privateIP: true
  - autoIAMAuthN: true
    connectionString: csample_project_abc:northamerica-northeast1:sample_db_3
    hostEnvName: DBHOST
    portEnvName: DBPORT
    privateIP: true
  workloadSelector:
    kind: StatefulSet
    name: application_2

Hope this helps clarify. :slight_smile:

@maxb Thanks a lot for your code…

My mind never arrived at the idea of creating the final object by conditionally merging two objects/maps, and that works beautifully for my multiple instance data structure. I did not come across an example like your suggestion in my research.

cheers :clap: