From 687dc73c4ccc089637d4c104327821f680cb086d Mon Sep 17 00:00:00 2001 From: Jayant Date: Wed, 3 Jun 2026 18:52:49 +0530 Subject: [PATCH] fix: lookup-safe access for optional cluster_addons + additional_node_policies (RULE-015) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Optional spec fields must be read via lookup(var.instance.spec, "field", default) rather than var.instance.spec.field directly. The Facets exec path does not reliably apply the optional() defaults declared in variables.tf, so a blueprint that omits an optional field crashes at plan/apply on the direct attribute access (RULE-015). Two crash sites fixed: 1. kubernetes_cluster/eks_standard/1.0/main.tf — cluster_addons is optional (only cluster_version is required). The locals read var.instance.spec.cluster_addons and nested .vpc_cni / .kube_proxy / .coredns directly, so a blueprint omitting cluster_addons crashed. Added a cluster_addons_spec = lookup(var.instance.spec, "cluster_addons", {}) local and routed every addon read through nested lookups with defaults. Omitting cluster_addons now enables all default addons (matching the schema/UI defaults). 2. karpenter/default/1.0/main.tf — additional_node_policies is optional but was read directly via values(var.instance.spec.additional_node_policies), crashing when omitted. Now values(lookup(var.instance.spec, "additional_node_policies", {})). Backward-compatible bugfix: behaviour is identical when the fields are present, and previously-crashing omissions now fall back to documented defaults. No version bump (RULE-023 reserves bumps for breaking changes). Co-Authored-By: Claude Opus 4.8 --- modules/karpenter/default/1.0/main.tf | 2 +- .../eks_standard/1.0/main.tf | 28 +++++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/modules/karpenter/default/1.0/main.tf b/modules/karpenter/default/1.0/main.tf index 79bea375..979ee52a 100644 --- a/modules/karpenter/default/1.0/main.tf +++ b/modules/karpenter/default/1.0/main.tf @@ -197,7 +197,7 @@ resource "aws_iam_role_policy_attachment" "karpenter_node_policies" { "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy", "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly", "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" - ], [for policy in values(var.instance.spec.additional_node_policies) : policy.arn])) + ], [for policy in values(lookup(var.instance.spec, "additional_node_policies", {})) : policy.arn])) role = aws_iam_role.karpenter_node.name policy_arn = each.value diff --git a/modules/kubernetes_cluster/eks_standard/1.0/main.tf b/modules/kubernetes_cluster/eks_standard/1.0/main.tf index f8b8e725..0fbb7599 100644 --- a/modules/kubernetes_cluster/eks_standard/1.0/main.tf +++ b/modules/kubernetes_cluster/eks_standard/1.0/main.tf @@ -52,31 +52,37 @@ locals { # Only the default system node group eks_managed_node_groups = local.default_system_node_group + # cluster_addons is an OPTIONAL spec field (only cluster_version is required). Default to {} + # so an omitted block doesn't crash on direct attribute access — RULE-015. The per-addon + # `enabled`/`version` defaults below then apply, so omitting cluster_addons enables all + # default addons (matching the schema/UI defaults). + cluster_addons_spec = lookup(var.instance.spec, "cluster_addons", {}) + # Check if EBS CSI driver addon is enabled (default: true) - ebs_csi_enabled = lookup(lookup(var.instance.spec.cluster_addons, "ebs_csi", {}), "enabled", true) + ebs_csi_enabled = lookup(lookup(local.cluster_addons_spec, "ebs_csi", {}), "enabled", true) # Build cluster addons configuration - default addons default_addons = { - vpc-cni = lookup(var.instance.spec.cluster_addons.vpc_cni, "enabled", true) ? { - addon_version = lookup(var.instance.spec.cluster_addons.vpc_cni, "version", "latest") == "latest" ? null : lookup(var.instance.spec.cluster_addons.vpc_cni, "version", null) + vpc-cni = lookup(lookup(local.cluster_addons_spec, "vpc_cni", {}), "enabled", true) ? { + addon_version = lookup(lookup(local.cluster_addons_spec, "vpc_cni", {}), "version", "latest") == "latest" ? null : lookup(lookup(local.cluster_addons_spec, "vpc_cni", {}), "version", null) resolve_conflicts = "OVERWRITE" service_account_role_arn = null } : null - kube-proxy = lookup(var.instance.spec.cluster_addons.kube_proxy, "enabled", true) ? { - addon_version = lookup(var.instance.spec.cluster_addons.kube_proxy, "version", "latest") == "latest" ? null : lookup(var.instance.spec.cluster_addons.kube_proxy, "version", null) + kube-proxy = lookup(lookup(local.cluster_addons_spec, "kube_proxy", {}), "enabled", true) ? { + addon_version = lookup(lookup(local.cluster_addons_spec, "kube_proxy", {}), "version", "latest") == "latest" ? null : lookup(lookup(local.cluster_addons_spec, "kube_proxy", {}), "version", null) resolve_conflicts = "OVERWRITE" service_account_role_arn = null } : null - coredns = lookup(var.instance.spec.cluster_addons.coredns, "enabled", true) ? { - addon_version = lookup(var.instance.spec.cluster_addons.coredns, "version", "latest") == "latest" ? null : lookup(var.instance.spec.cluster_addons.coredns, "version", null) + coredns = lookup(lookup(local.cluster_addons_spec, "coredns", {}), "enabled", true) ? { + addon_version = lookup(lookup(local.cluster_addons_spec, "coredns", {}), "version", "latest") == "latest" ? null : lookup(lookup(local.cluster_addons_spec, "coredns", {}), "version", null) resolve_conflicts = "OVERWRITE" service_account_role_arn = null } : null aws-ebs-csi-driver = local.ebs_csi_enabled ? { - addon_version = lookup(lookup(var.instance.spec.cluster_addons, "ebs_csi", {}), "version", "latest") == "latest" ? null : lookup(lookup(var.instance.spec.cluster_addons, "ebs_csi", {}), "version", null) + addon_version = lookup(lookup(local.cluster_addons_spec, "ebs_csi", {}), "version", "latest") == "latest" ? null : lookup(lookup(local.cluster_addons_spec, "ebs_csi", {}), "version", null) resolve_conflicts = "OVERWRITE" service_account_role_arn = aws_iam_role.ebs_csi_driver[0].arn } : null @@ -87,8 +93,8 @@ locals { service_account_role_arn = local.needs_cloudwatch_iam_policy ? aws_iam_role.cloudwatch_agent_irsa[0].arn : null } : null - metrics-server = lookup(lookup(var.instance.spec.cluster_addons, "metrics_server", {}), "enabled", true) ? { - addon_version = lookup(lookup(var.instance.spec.cluster_addons, "metrics_server", {}), "version", "latest") == "latest" ? null : lookup(lookup(var.instance.spec.cluster_addons, "metrics_server", {}), "version", null) + metrics-server = lookup(lookup(local.cluster_addons_spec, "metrics_server", {}), "enabled", true) ? { + addon_version = lookup(lookup(local.cluster_addons_spec, "metrics_server", {}), "version", "latest") == "latest" ? null : lookup(lookup(local.cluster_addons_spec, "metrics_server", {}), "version", null) resolve_conflicts = "OVERWRITE" service_account_role_arn = null } : null @@ -96,7 +102,7 @@ locals { # Build additional/custom addons configuration additional_addons = { - for addon_name, addon_config in lookup(var.instance.spec.cluster_addons, "additional_addons", {}) : + for addon_name, addon_config in lookup(local.cluster_addons_spec, "additional_addons", {}) : addon_name => lookup(addon_config, "enabled", true) ? { addon_version = lookup(addon_config, "version", "latest") == "latest" ? null : lookup(addon_config, "version", null) resolve_conflicts = "OVERWRITE"