Loops in templates with yamlencode

Hi @apparentlymart .

I have the original definition using a TF provider for launching K8s clusters


        infrastructure_config {
          aws {
            networks {
                vpc {
                  cidr = var.vnetcidr
                }
                dynamic "zones" {
                  for_each = var.subnets
                  content {
                    name    = zones.value.name
                    workers = zones.value.workers
                    public = zones.value.public
                    internal = zones.value.internal
                  }
                }
            }
          }
        }

Which works well. However i trying to use a plain k8s yaml manfiest using the following provider

github.com/gavinbunney/kubectl

terraform {
  required_version = ">= 0.13.0"
  required_providers {
    kubectl = {
      source = "github.com/gavinbunney/kubectl"
      version = "1.5.1"
    }
    kubernetes = "~> 1.11.4"
  }
}

provider "kubectl" {
  config_path      = "/path/k8s.kubeconfig"
  load_config_file = true
}

resource "kubectl_manifest" "gardener_shoot" {
   yaml_body = templatefile("${path.module}/templates/gardener-shoot.yaml.tmpl", {
          shoot_cluster_name = var.shoot_cluster_name,
          project_name = var.project_name,
        })

The template has the following for now.

${yamlencode({
            "infrastructureConfig": {
                "apiVersion": "aws.provider.extensions.gardener.cloud/v1alpha1",
                "kind": "InfrastructureConfig",
                "networks": {
                    "vpc": {
                        "cidr": vnetcidr,
                    },
                    "zones": [
                        {
                            "internal": "10.250.48.0/20",
                            "name": "eu-central-1a",
                            "public": "10.250.32.0/20",
                            "workers": "10.250.0.0/19"
                        }
                    ]
                }
            },
})}

How would i use a loop in the yamlencode tempalte if i have the following variables as input ?

variable "subnets" {
    type = list
    description = "Subnets"
    default = [
    {
      name   = "eu-central-1a"
      workers = "10.250.0.0/26"
      public = "10.250.96.0/26"
      internal = "10.250.112.0/26"
    },
    {
      name   = "eu-central-1b"
      workers = "10.250.0.64/26"
      public = "10.250.96.64/26"
      internal = "10.250.112.64/26"
    },
    {
      name   = "eu-central-1c"
      workers = "10.250.0.128/26"
      public = "10.250.96.128/26"
      internal = "10.250.112.128/26"
    }
    ]
}

variable "zones" {
    type = list
    description = "Zones"
    default = [ "eu-central-1a",  "eu-central-1b",  "eu-central-1c"]
}

Kevin

Assuming you’ve passed that list of subnets in the second argument to the templatefile function as subnets, I don’t think you really need any sort of repetition construct here because the structure of that variable already seems to be the right shape for the output you want:

${yamlencode({
            "infrastructureConfig": {
                "apiVersion": "aws.provider.extensions.gardener.cloud/v1alpha1",
                "kind": "InfrastructureConfig",
                "networks": {
                    "vpc": {
                        "cidr": vnetcidr,
                    },
                    "zones": subnets
                }
            },
})}

However, I’ll also show a version with a for expression, even though it’s redundant in this case, because this pattern will probably be useful to you in other situations where the shape of your desired YAML data structure doesn’t exactly match the shape of the input data:

${yamlencode({
            "infrastructureConfig": {
                "apiVersion": "aws.provider.extensions.gardener.cloud/v1alpha1",
                "kind": "InfrastructureConfig",
                "networks": {
                    "vpc": {
                        "cidr": vnetcidr,
                    },
                    "zones": [
                        for subnet in subnets : {
                            internal = subnet.internal
                            name     = subnet.name
                            public   = subnet.public
                            workers  = subnet.workers
                        }
                    ]
                }
            },
})}

Hi @apparentlymart,

The 2nd snippet was what i needed. It worked. I have only one question. Is there a way to do something like a if condition in a template file ? I would like to selectively load a yaml definition when some variables are set to true ?

For eg

        if dns_enabled = "true"
        "dns": {
            "domain": "test.ondemand.com",
            "providers": [
                {
                    "domains": {
                        "include": [
                            "abc.domain1.com",
                            "xyz.domain1.com"
                        ]
                    },
                    "secretName": "secret_name",
                    "type": "aws-route53"
                }
            ]
        },
       if extensions_enabled = "true"
        "extensions": [
            {
                "type": "shoot-dns-service"
            },
            {
                "providerConfig": {
                    "apiVersion": "service.cert.extensions.test.cloud/v1alpha1",
                    "issuers": [
                        {
                            "email": "test@domain.com",
                            "name": "domain1.com",
                            "server": "https://acme-v02.api.letsencrypt.org/directory"
                        }
                    ]
                },
                "type": "shoot-cert-service"
            }
        ],
        */

Also how would i make the variables optional ? I only want them to be used when the dns_enabled or extensions_enabled is set to true. The remainder of the yaml stays the same.

Kevin

Hi @linuxbsdfreak,

I think the least complex answer to that would be to use conditional expressions to set a different value for each of those attributes depending on the condition, although that approach doesn’t exactly meet the requirements you stated because those attributes would still both be present, just potentially set to null when not needed:

  "dns": dns_enabled ? {
    "domain": "test.ondemand.com",
    # etc
  } : null,
  "extensions": extensions_enabled ? [
    {
      "type": "shoot-dns-service",
    },
    # etc
  ] : null,

Whether this solution will be acceptable will depend on whether the system you’re sending this YAML to will tolerate those attributes being present but being set to null, like this:

dns: null
extensions: null

If omitting them entirely is the requirement then unfortunately the problem gets a little harder because the Terraform language doesn’t have a built-in operator to conditionally omit an attribute entirely. It’s possible to do it with some clever use of merge to combine together multiple objects with different properties set, which I think might be the best answer for you here if you have this requirement, but I’d encourage you to try the approach above first and see if that works, and then consider merge to be a more complicated fallback if the remote system rejects those null values with an error.

Hi @apparentlymart,

Thanks for the reply. I would try that tomorrow. I think the 2nd option with merge would be necessary, since the system accepting the yaml is a K8s custom resource definition and the null has to be removed. Do you have any example with merge and conditionals to accomplish it?

Kevin

Hi @linuxbsdfreak,

I can’t write out a full example right now because it’s almost the end of my work day, but the general pattern would be like the following, which hopefully you can adapt to the specific structure you are working with:

${yamlencode(merge(
  {
    # put the attributes that will be unconditional
    # in this object.
  },
  dns_enabled ? {
    "dns": {
      "domain": "test.ondemand.com",
    }
  } : {},
  extensions_enabled ? {
    "extensions": {
      "type": "shoot-dns-service",
    }
  } : {},
))}

The idea here is that each of the subsequent arguments to merge (after the first unconditional one) use a conditional expression to choose between either an object with one additional attribute to merge in, or an object that has no attributes at all and will thus cause merge to make no changes because there is nothing additional to merge in.

Hi @apparentlymart,

Thanks for the reply. I am have the following template defintion

${yamlencode(merge(
    "apiVersion": "core.gardener.cloud/v1beta1",
    "kind": "Shoot",
    "metadata": {
        "name": shoot_cluster_name,
        "namespace": project_name,
    },
    "timeouts": {
        "create": create_timeout,
        "update": update_timeout,
        "delete": delete_timeout,
    }
    "spec": {
        "addons": {
            "kubernetesDashboard": {
                "authenticationMode": "token",
                "enabled": dashboard_enabled,
            },
            "nginxIngress": {
                "enabled": nginx_enabled,
                "externalTrafficPolicy": "Cluster",
            }
        },
        "cloudProfileName": target_profile,
        "hibernation": {
            "enabled": false,
        },
        "kubernetes": {
            "allowPrivilegedContainers": true,
            "version": kubernetes_version,
        },
        "maintenance": {
            "autoUpdate": {
                "kubernetesVersion": maintenance_k8s_version_enabled,
                "machineImageVersion": maintenance_machine_image_version_enabled,
            },
            "timeWindow": {
                "begin": "210000+0000",
                "end": "220000+0000",
            }
        },
        "networking": {
            "nodes": networking_nodes,
            "pods": networking_pods,
            "services": networking_services,
            "type": networking_type,
        },
        "provider": {
            "infrastructureConfig": {
                "apiVersion": "aws.provider.extensions.gardener.cloud/v1alpha1",
                "kind": "InfrastructureConfig",
                "networks": {
                    "vpc": {
                        "cidr": vnetcidr,
                    },
                    "zones": [
                        for subnet in subnets : {
                            internal = subnet.internal
                            name     = subnet.name
                            public   = subnet.public
                            workers  = subnet.workers
                        }
                    ]
                }
            },
            "type": cloud_provider,
            "workers": [
                {
                    "machine": {
                        "image":{
                            "name": machine_image_name,
                            "version": machine_image_version,
                        },
                        "type": machine_type,
                    },
                    "maxSurge": worker_max_surge,
                    "maxUnavailable": worker_max_unavailable,
                    "maximum": worker_maximum,
                    "minimum": worker_minimum,
                    "name": worker_name,
                    "volume": {
                        "size": disk_size,
                        "type": disk_type,
                    },
                    "zones": zones,
                }
            ]
        },
        "purpose": "evaluation",
        "region": location,
        "secretBindingName": target_secret,
    },
    gardener_dns_management? {
    "spec": {
        "dns": {
            "domain": "testcluster.ondemand.com",
            "providers": [
                {
                    "domains": {
                        "include": [
                            "domain1.ondemand.com",
                            "domain12.ondemand.com",
                        ]
                    },
                    "secretName": "mysecret",
                    "type": "aws-route53",
                }
            ]
        },
        "extensions": [
            {
                "type": "shoot-dns-service",
            }
      ]
     },
    } : {},
    gardener_dns_ssl_management ? {
    "spec": {
      "dns": {
            "domain": "testcluster.ondemand.com",
            "providers": [
                {
                    "domains": {
                        "include": [
                            "domain1.ondemand.com",
                            "domain12.ondemand.com",
                        ],
                    },
                    "secretName": "my-secret",
                    "type": "aws-route53",
                }
            ]
      },
      "extensions": [
          {
                "type": "shoot-dns-service",
          },
          {
                "providerConfig": {
                    "apiVersion": "service.cert.extensions.gardener.cloud/v1alpha1",
                    "issuers": [
                        {
                            "email": "test@example.com",
                            "name": "ondemand.com",
                            "server": "https://acme-v02.api.letsencrypt.org/directory",
                        }
                    ],
                },
                "type": "shoot-cert-service",
          }
       ]
     },
   } : {},
))}

I added the following in the variables

variables.tf

variable "gardener_dns_management" {
    type = bool
    default = true
}

variable "gardener_dns_ssl_management" {
    type = bool
    default = false
}

In main.tf

resource "kubectl_manifest" "gardener_shoot" {
   yaml_body = templatefile("${path.module}/templates/gardener-shoot.yaml_wip.tmpl", {
          shoot_cluster_name = var.shoot_cluster_name,
          project_name = var.project_name,
          create_timeout = var.create_timeout,
          update_timeout = var.update_timeout,
          delete_timeout = var.delete_timeout,
          dashboard_enabled = var.dashboard_enabled,
          nginx_enabled = var.nginx_enabled,
          target_profile = var.target_profile,
          kubernetes_version = var.kubernetes_version,
          maintenance_k8s_version_enabled = var.maintenance_k8s_version_enabled,
          maintenance_machine_image_version_enabled = var.maintenance_machine_image_version_enabled,
          networking_nodes = var.networking_nodes,
          networking_pods = var.networking_pods,
          networking_services = var.networking_services,
          networking_type = var.networking_type,
          vnetcidr = var.vnetcidr,
          cloud_provider = var.cloud_provider,
          machine_image_name = var.machine_image_name,
          machine_image_version = var.machine_image_version,
          machine_type = var.machine_type,
          worker_max_surge = var.worker_max_surge,
          worker_max_unavailable = var.worker_max_unavailable,
          worker_maximum = var.worker_maximum,
          worker_minimum = var.worker_minimum,
          worker_name = var.worker_name,
          disk_size = var.disk_size,
          disk_type = var.disk_type,
          location = var.location,
          target_secret = var.target_secret,
          zones = var.zones,
          subnets = var.subnets,
          gardener_dns_management = var.gardener_dns_management,
          gardener_dns_ssl_management = var.gardener_dns_ssl_management,
        })
}

I get an error with merge

 on main.tf line 18, in resource "kubectl_manifest" "gardener_shoot":
  18:    yaml_body = templatefile("${path.module}/templates/gardener-shoot.yaml_wip.tmpl", {
  19:           shoot_cluster_name = var.shoot_cluster_name,
  20:           project_name = var.project_name,
  21:           create_timeout = var.create_timeout,
  22:           update_timeout = var.update_timeout,
  23:           delete_timeout = var.delete_timeout,
  24:           dashboard_enabled = var.dashboard_enabled,
  25:           nginx_enabled = var.nginx_enabled,
  26:           target_profile = var.target_profile,
  27:           kubernetes_version = var.kubernetes_version,
  28:           maintenance_k8s_version_enabled = var.maintenance_k8s_version_enabled,
  29:           maintenance_machine_image_version_enabled = var.maintenance_machine_image_version_enabled,
  30:           networking_nodes = var.networking_nodes,
  31:           networking_pods = var.networking_pods,
  32:           networking_services = var.networking_services,
  33:           networking_type = var.networking_type,
  34:           vnetcidr = var.vnetcidr,
  35:           cloud_provider = var.cloud_provider,
  36:           machine_image_name = var.machine_image_name,
  37:           machine_image_version = var.machine_image_version,
  38:           machine_type = var.machine_type,
  39:           worker_max_surge = var.worker_max_surge,
  40:           worker_max_unavailable = var.worker_max_unavailable,
  41:           worker_maximum = var.worker_maximum,
  42:           worker_minimum = var.worker_minimum,
  43:           worker_name = var.worker_name,
  44:           disk_size = var.disk_size,
  45:           disk_type = var.disk_type,
  46:           location = var.location,
  47:           target_secret = var.target_secret,
  48:           zones = var.zones,
  49:           subnets = var.subnets,
  50:           gardener_dns_management = var.gardener_dns_management,
  51:           gardener_dns_ssl_management = var.gardener_dns_ssl_management,
  52:         })
    |----------------
    | path.module is "."

Call to function "templatefile" failed:
./templates/gardener-shoot.yaml_wip.tmpl:2,17-18: Missing argument separator;
A comma is required to separate each function argument from the next..

As you see i would like to add the relevant snippet under the spec section of the yaml if the boolean value is set.

What am i doing incorrect ?

Kevin

Hi @apparentlymart,

I tried to debug the template along with the merge information online. I had no success.

Kevin

It looks like you are missing the braces around the first argument to merge.

Hi @apparentlymart,

Hmmm ok so ${yamlencode(merge( … ))} is incorrect. What kind of brackets have i missed? Could you please provide me a Sample snippet to understand ?

Kevin

Hi @apparentlymart,

Thanks for the hint. I went a little ahead but not satisfied with the merge . It is not doing as you had mentioned. TF is trying to remove the attributes that would be unconditional in the object. For eg

${yamlencode(merge({
    "apiVersion": "core.gardener.cloud/v1beta1",
    "kind": "Shoot",
    "metadata": {
        "name": shoot_cluster_name,
        "namespace": project_name,
    },
    "timeouts": {
        "create": create_timeout,
        "update": update_timeout,
        "delete": delete_timeout,
    }
    "spec": {
        "addons": {
            "kubernetesDashboard": {
                "authenticationMode": "token",
                "enabled": dashboard_enabled,
            },
            "nginxIngress": {
                "enabled": nginx_enabled,
                "externalTrafficPolicy": "Cluster",
            }
        },
        "cloudProfileName": target_profile,
        "hibernation": {
            "enabled": false,
        },
        "kubernetes": {
            "allowPrivilegedContainers": true,
            "version": kubernetes_version,
        },
        "maintenance": {
            "autoUpdate": {
                "kubernetesVersion": maintenance_k8s_version_enabled,
                "machineImageVersion": maintenance_machine_image_version_enabled,
            },
            "timeWindow": {
                "begin": "210000+0000",
                "end": "220000+0000",
            }
        },
        "networking": {
            "nodes": networking_nodes,
            "pods": networking_pods,
            "services": networking_services,
            "type": networking_type,
        },
        "provider": {
            "infrastructureConfig": {
                "apiVersion": "aws.provider.extensions.gardener.cloud/v1alpha1",
                "kind": "InfrastructureConfig",
                "networks": {
                    "vpc": {
                        "cidr": vnetcidr,
                    },
                    "zones": [
                        for subnet in subnets : {
                            internal = subnet.internal
                            name     = subnet.name
                            public   = subnet.public
                            workers  = subnet.workers
                        }
                    ]
                }
            },
            "type": cloud_provider,
            "workers": [
                {
                    "machine": {
                        "image":{
                            "name": machine_image_name,
                            "version": machine_image_version,
                        },
                        "type": machine_type,
                    },
                    "maxSurge": worker_max_surge,
                    "maxUnavailable": worker_max_unavailable,
                    "maximum": worker_maximum,
                    "minimum": worker_minimum,
                    "name": worker_name,
                    "volume": {
                        "size": disk_size,
                        "type": disk_type,
                    },
                    "zones": zones,
                }
            ]
        },
        "purpose": "evaluation",
        "region": location,
        "secretBindingName": target_secret,
    }},
    gardener_dns_management ? {
    "spec": {
        "dns": {
            "domain" : "${shoot_cluster_name}.${element(split("-",project_name),1)}.${gardenerdomain}",
            "providers": [
                {
                    "domains": {
                        "include": included_dns_domains,
                    },
                    "secretName": dns_secret_name,
                    "type": dns_type,
                }
            ]
        },
        "extensions": [
            {
                "type": "shoot-dns-service",
            }
      ]
     },
    } : {},
    gardener_dns_ssl_management ? {
    "spec": {
      "dns": {
            "domain" : "${shoot_cluster_name}.${element(split("-",project_name),1)}.${gardenerdomain}",
            "providers": [
                {
                    "domains": {
                        "include": included_dns_domains,
                    },
                    "secretName": dns_secret_name,
                    "type": dns_type,
                }
            ]
      },
      "extensions": [
          {
                "type": "shoot-dns-service",
          },
          {
                "providerConfig": {
                    "apiVersion": "service.cert.extensions.gardener.cloud/v1alpha1",
                    "issuers": [
                        {
                            "email": email_id,
                            "name": subdomain,
                            "server": "https://acme-v02.api.letsencrypt.org/directory",
                        }
                    ],
                },
                "type": "shoot-cert-service"
          }
       ]
     },
   } : {},
))}

The output is as follows

~ yaml_body               = <<~EOT
            "apiVersion": "core.gardener.cloud/v1beta1"
            "kind": "Shoot"
            "metadata":
              "name": "testcluster"
              "namespace": "garden-abap"
            "spec":
          -   "addons":
          -     "kubernetesDashboard":
          -       "authenticationMode": "token"
          -       "enabled": true
          -     "nginxIngress":
          -       "enabled": true
          -       "externalTrafficPolicy": "Cluster"
          -   "cloudProfileName": "aws"
          -   "hibernation":
          -     "enabled": false
          -   "kubernetes":
          -     "allowPrivilegedContainers": true
          -     "version": "1.18.8"
          -   "maintenance":
          -     "autoUpdate":
          -       "kubernetesVersion": true
          -       "machineImageVersion": true
          -     "timeWindow":
          -       "begin": "210000+0000"
          -       "end": "220000+0000"
          -   "networking":
          -     "nodes": "10.250.0.0/16"
          -     "pods": "100.96.0.0/11"
          -     "services": "100.64.0.0/13"
          -     "type": "calico"
          -   "provider":
          -     "infrastructureConfig":
          -       "apiVersion": "aws.provider.extensions.gardener.cloud/v1alpha1"
          -       "kind": "InfrastructureConfig"
          -       "networks":
          -         "vpc":
          -           "cidr": "10.250.0.0/16"
          -         "zones":
          -         - "internal": "10.250.112.0/26"
          -           "name": "eu-central-1a"
          -           "public": "10.250.96.0/26"
          -           "workers": "10.250.0.0/26"
          -         - "internal": "10.250.112.64/26"
          -           "name": "eu-central-1b"
          -           "public": "10.250.96.64/26"
          -           "workers": "10.250.0.64/26"
          -         - "internal": "10.250.112.128/26"
          -           "name": "eu-central-1c"
          -           "public": "10.250.96.128/26"
          -           "workers": "10.250.0.128/26"
          -     "type": "aws"
          -     "workers":
          -     - "machine":
          -         "image":
          -           "name": "linux"
          -           "version": "27.1.0"
          -         "type": "m5.large"
          -       "maxSurge": 2
          -       "maxUnavailable": 2
          -       "maximum": 2
          -       "minimum": 1
          -       "name": "workertest"
          -       "volume":
          -         "size": "30Gi"
          -         "type": "gp2"
          -       "zones":
          -       - "eu-central-1a"
          -       - "eu-central-1b"
          -       - "eu-central-1c"
          -   "purpose": "evaluation"
          -   "region": "eu-central-1"
          -   "secretBindingName": "mysecret"
          +   "dns":
          +     "domain": "mydomain"
          +     "providers":
          +     - "domains":
          +         "include":
          +         - "domain1.ondemand.com"
          +         - "domain2.ondemand.com"
          +       "secretName": "mysecret"
          +       "type": "aws-route53"
          +   "extensions":
          +   - "type": "shoot-dns-service"
          +   - "type": "shoot-cert-service"
          +   - "providerConfig":
          +       "apiVersion": "service.cert.extensions.gardener.cloud/v1alpha1"
          +       "issuers":
          +       - "email": "test@ondeman.com"
          +         "name": "subdomain.com"
          +         "server": "https://acme-v02.api.letsencrypt.org/directory"
            "timeouts":
              "create": "30m0s"
              "delete": "20m0s"
              "update": "30m0s"

Curious what i am missing with the merge? I expected that it would merge the common part along with the conditional part when i set via the boolean variable.

Kevin

merge only works for one level of mapping at a time, so your conditional override of spec overrides the entire spec value.

If you want to conditionally add elements to the spec object then you’ll need to put this merge pattern as the expression for spec itself instead of for the top-level object. Then the presence of the dns and extensions nested attributes can be what is conditional, rather than the presence of the spec object as a whole.

Hi @apparentlymart,

Thanks for the feedback. Could you give me small sample snippet to solve my use case? I never worked with merge in depth in terraform . Thanks once again.

Kevin

Hi @apparentlymart,

I tried as per your suggestions by moving the merge pattern under spec.

${yamlencode({
    "apiVersion": "core.gardener.cloud/v1beta1",
    "kind": "Shoot",
    "metadata": {
        "name": shoot_cluster_name,
        "namespace": project_name,
    },
    "timeouts": {
        "create": create_timeout,
        "update": update_timeout,
        "delete": delete_timeout,
    },
    merge({
    "spec": {
        "addons": {
            "kubernetesDashboard": {
                "authenticationMode": "token",
                "enabled": dashboard_enabled,
            },
            "nginxIngress": {
                "enabled": nginx_enabled,
                "externalTrafficPolicy": "Cluster",
            }
        },
        "cloudProfileName": target_profile,
        "hibernation": {
            "enabled": false,
        },
        "kubernetes": {
            "allowPrivilegedContainers": true,
            "version": kubernetes_version,
        },
        "maintenance": {
            "autoUpdate": {
                "kubernetesVersion": maintenance_k8s_version_enabled,
                "machineImageVersion": maintenance_machine_image_version_enabled,
            },
            "timeWindow": {
                "begin": "210000+0000",
                "end": "220000+0000",
            }
        },
        "networking": {
            "nodes": networking_nodes,
            "pods": networking_pods,
            "services": networking_services,
            "type": networking_type,
        },
        "provider": {
            "infrastructureConfig": {
                "apiVersion": "aws.provider.extensions.gardener.cloud/v1alpha1",
                "kind": "InfrastructureConfig",
                "networks": {
                    "vpc": {
                        "cidr": vnetcidr,
                    },
                    "zones": [
                        for subnet in subnets : {
                            internal = subnet.internal
                            name     = subnet.name
                            public   = subnet.public
                            workers  = subnet.workers
                        }
                    ]
                }
            },
            "type": cloud_provider,
            "workers": [
                {
                    "machine": {
                        "image":{
                            "name": machine_image_name,
                            "version": machine_image_version,
                        },
                        "type": machine_type,
                    },
                    "maxSurge": worker_max_surge,
                    "maxUnavailable": worker_max_unavailable,
                    "maximum": worker_maximum,
                    "minimum": worker_minimum,
                    "name": worker_name,
                    "volume": {
                        "size": disk_size,
                        "type": disk_type,
                    },
                    "zones": zones,
                }
            ]
        },
        "purpose": "evaluation",
        "region": location,
        "secretBindingName": target_secret,
    }
    gardener_dns_management ? {
    "spec": {
        "dns": {
            "domain" : "${shoot_cluster_name}.${element(split("-",project_name),1)}.${gardenerdomain}",
            "providers": [
                {
                    "domains": {
                        "include": included_dns_domains,
                    },
                    "secretName": dns_secret_name,
                    "type": dns_type,
                }
            ]
        },
        "extensions": [
            {
                "type": "shoot-dns-service",
            }
      ]
    },
    } : {},    ---->  ERROR on this line. 
    gardener_dns_ssl_management ? {
    "spec": {
      "dns": {
            "domain" : "${shoot_cluster_name}.${element(split("-",project_name),1)}.${gardenerdomain}",
            "providers": [
                {
                    "domains": {
                        "include": included_dns_domains,
                    },
                    "secretName": dns_secret_name,
                    "type": dns_type,
                }
            ]
      },
      "extensions": [
          {
                "type": "shoot-dns-service",
          },
          {
                "providerConfig": {
                    "apiVersion": "service.cert.extensions.gardener.cloud/v1alpha1",
                    "issuers": [
                        {
                            "email": email_id,
                            "name": subdomain,
                            "server": "https://acme-v02.api.letsencrypt.org/directory",
                        }
                    ],
                },
                "type": "shoot-cert-service",
          }
       ]
    },
    } : {},
   })
})}

Error as follows

Call to function "templatefile" failed:
./templates/gardener-shoot.yaml_wip.tmpl:114,11-12: Missing attribute value;
Expected an attribute value, introduced by an equals sign ("=")..

I am not confident if i am doing it correct .

Kevin

Hi @linuxbsdfreak,

It looks like you were almost there… just some things in a slightly different order. I find it hard to work with these long examples because it’s hard to replicate all of the settings you are including when I’m not familiar with your system, but here’s an abbreviated example that hopefully shows the relevant parts in a way you can incorporate into your larger template:

${yamlencode({
  "apiVersion": "core.gardener.cloud/v1beta1",
  "kind": "Shoot",
  # (and all of the other unconditional top-level attributes)
  "spec": merge(
    {
      "addons": { /* ... */ },
      "cloudProfileName": target_profile,
      # (and all of the other unconditional "spec" attributes)
    },
    gardener_dns_management || gardener_dns_ssl_management ? {
      "dns": {
        "domain" : "${shoot_cluster_name}.${element(split("-",project_name),1)}.${gardenerdomain}",
        # ...
      }
    } : {},
    gardener_dns_management ? {
      "extensions": [
        {
          "type": "shoot-dns-service",
        },
      ]
    } : {},
    gardener_dns_ssl_management ? {
      "extensions": [
        {
          "type": "shoot-dns-service"
        },
        {
          "type": "shoot-cert-service",
          # and "providerConfig"
        },
      ]
    } : {},
  ),
})}

Hi @apparentlymart,

Thanks for the feedback. It works partially but the yaml data structure is incorrect in the terraform plan

${yamlencode({
  "apiVersion": "core.gardener.cloud/v1beta1",
  "kind": "Shoot",
  # (and all of the other unconditional top-level attributes)
  "spec": merge(
    {
      "addons": { /* ... */ },
      "cloudProfileName": target_profile,
      # (and all of the other unconditional "spec" attributes)
    },
    gardener_dns_management || gardener_dns_ssl_management ? {
      "dns": {
        "domain" : "${shoot_cluster_name}.${element(split("-",project_name),1)}.${gardenerdomain}",
        # ...
      }
    } : {},
    gardener_dns_management ? {
      "extensions": [
        {
          "type": "shoot-dns-service",
        },
      ]
    } : {},
    gardener_dns_ssl_management ? {
      "extensions": [
        {
          "type": "shoot-dns-service"
        },
        {
          "type": "shoot-cert-service",
          # and "providerConfig"
        },
      ]
    } : {},
  ),
})}

With the above snippet definition i get the following error

Call to function "templatefile" failed:
./templates/gardener-shoot.yaml_wip.tmpl:94,31-112,11: Inconsistent
conditional result types; The true and false result expressions must have
consistent types. The given expressions are object and object, respectively.,
and 1 other diagnostic(s).

For the TF plan to work i need to add the following under the
gardener_dns_management and gardener_dns_ssl_management conditional section

"spec": {

}

However the dns or extensions do not get appended under the original spec definitions
Example output

      ~ yaml_body               = <<~EOT
            "apiVersion": "core.gardener.cloud/v1beta1"
            "kind": "Shoot"
            "metadata":
              "name": "testcluster"
              "namespace": "test-garden"
            "spec":
              "addons":
                "kubernetesDashboard":
                  "authenticationMode": "token"
                  "enabled": true
                "nginxIngress":
                  "enabled": true
                  "externalTrafficPolicy": "Cluster"
              "cloudProfileName": "aws"
              "hibernation":
                "enabled": false
              "kubernetes":
                "allowPrivilegedContainers": true
                "version": "1.18.8"
              "maintenance":
                "autoUpdate":
                  "kubernetesVersion": true
                  "machineImageVersion": true
                "timeWindow":
                  "begin": "210000+0000"
                  "end": "220000+0000"
              "networking":
                "nodes": "10.250.0.0/16"
                "pods": "100.96.0.0/11"
                "services": "100.64.0.0/13"
                "type": "calico"
              "provider":
                "infrastructureConfig":
                  "apiVersion": "aws.provider.extensions.gardener.cloud/v1alpha1"
                  "kind": "InfrastructureConfig"
                  "networks":
                    "vpc":
                      "cidr": "10.250.0.0/16"
                    "zones":
                    - "internal": "10.250.112.0/26"
                      "name": "eu-central-1a"
                      "public": "10.250.96.0/26"
                      "workers": "10.250.0.0/26"
                    - "internal": "10.250.112.64/26"
                      "name": "eu-central-1b"
                      "public": "10.250.96.64/26"
                      "workers": "10.250.0.64/26"
                    - "internal": "10.250.112.128/26"
                      "name": "eu-central-1c"
                      "public": "10.250.96.128/26"
                      "workers": "10.250.0.128/26"
                "type": "aws"
                "workers":
                - "machine":
                    "image":
                      "name": "linux"
                      "version": "27.1.0"
                    "type": "m5.large"
                  "maxSurge": 2
                  "maxUnavailable": 2
                  "maximum": 2
                  "minimum": 1
                  "name": "workertest"
                  "volume":
                    "size": "30Gi"
                    "type": "gp2"
                  "zones":
                  - "eu-central-1a"
                  - "eu-central-1b"
                  - "eu-central-1c"
              "purpose": "evaluation"
              "region": "eu-central-1"
              "secretBindingName": "test-gardener"
          +   "spec":  -->  NOT NEEDED
          +     "dns":   --> SHOULD be aligned direct under the secretBindingName
          +       "domain": "testcluster.ondemand.com"
          +       "providers":
          +       - "domains":
          +           "include":
          +           - "domain1.k8s-testcluster-eu10.example.com"
          +           - "domain1-support.k8s-testcluster-eu10.example.com"
          +         "secretName": "mysecret"
          +         "type": "aws-route53"
          +     "extensions":
          +     - "type": "shoot-dns-service"
            "timeouts":
              "create": "30m0s"
              "delete": "20m0s"
              "update": "30m0s"

I would like to have the dns direct under the secretBindingName key and without the spec.

Kevin

Hi @apparentlymart,

Do you have any clue how i can fix that with merge ?

Kevin