From 69890f0c5a2e2c4bd1ddda8634eb29c773ca8a0f Mon Sep 17 00:00:00 2001 From: Shivam Sharma <192242476+ShivamSharma43@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:52:13 +0530 Subject: [PATCH 1/4] chore(terraform): move cross-variable validations to lifecycle preconditions Terraform v1.15 is stricter about variable validation blocks that reference other variables. Move the 10 cross-variable validations listed in #608 out of their variable blocks and into lifecycle preconditions on a new terraform_data "validate_inputs" resource in each affected component or module, keeping the conditions and error messages as they were: - 080-azureml: ml_workload_identity, network_security_group, nat_gateway - 100-cncf-cluster: both arc_onboarding_identity checks, key_vault, should_generate_cluster_server_token - 100-cncf-cluster/modules/ubuntu-k3s: should_generate_cluster_server_token - 036-managed-redis: private_endpoint_subnet - 031-fabric: fabric_capacity_admins In 036-managed-redis and 031-fabric the module that uses the checked value now depends on the guard, so a failed precondition is the only plan error. The 031-fabric and 100-cncf-cluster tests now expect the failure on terraform_data.validate_inputs instead of the variable, and the component READMEs are regenerated with terraform-docs. Fixes #608 Co-Authored-By: Claude Opus 5.5 --- src/000-cloud/031-fabric/terraform/README.md | 1 + src/000-cloud/031-fabric/terraform/main.tf | 13 +++++++++ .../terraform/tests/base.tftest.hcl | 2 +- .../031-fabric/terraform/variables.tf | 4 --- .../036-managed-redis/terraform/README.md | 7 ++--- .../036-managed-redis/terraform/main.tf | 17 ++++++++++++ .../terraform/variables.deps.tf | 5 ---- src/000-cloud/080-azureml/terraform/README.md | 8 +++--- src/000-cloud/080-azureml/terraform/main.tf | 23 ++++++++++++++++ .../080-azureml/terraform/variables.deps.tf | 12 --------- .../100-cncf-cluster/terraform/README.md | 1 + .../100-cncf-cluster/terraform/main.tf | 27 +++++++++++++++++++ .../terraform/modules/ubuntu-k3s/README.md | 12 +++++---- .../terraform/modules/ubuntu-k3s/main.tf | 11 ++++++++ .../terraform/modules/ubuntu-k3s/variables.tf | 4 --- .../terraform/tests/cluster.tftest.hcl | 2 +- .../terraform/variables.deps.tf | 15 ----------- .../100-cncf-cluster/terraform/variables.tf | 4 --- 18 files changed, 111 insertions(+), 57 deletions(-) diff --git a/src/000-cloud/031-fabric/terraform/README.md b/src/000-cloud/031-fabric/terraform/README.md index f94ce5d70..b79e7281a 100644 --- a/src/000-cloud/031-fabric/terraform/README.md +++ b/src/000-cloud/031-fabric/terraform/README.md @@ -25,6 +25,7 @@ Contains all the resources needed for Fabric based resources. | [terraform_data.defer_fabric_capacity_created](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | | [terraform_data.defer_fabric_capacity_existing](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | | [terraform_data.defer_fabric_workspace](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | +| [terraform_data.validate_inputs](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | | [fabric_capacity.created](https://registry.terraform.io/providers/microsoft/fabric/1.10.0/docs/data-sources/capacity) | data source | | [fabric_capacity.existing](https://registry.terraform.io/providers/microsoft/fabric/1.10.0/docs/data-sources/capacity) | data source | | [fabric_workspace.existing](https://registry.terraform.io/providers/microsoft/fabric/1.10.0/docs/data-sources/workspace) | data source | diff --git a/src/000-cloud/031-fabric/terraform/main.tf b/src/000-cloud/031-fabric/terraform/main.tf index dc7769481..3b68479a5 100644 --- a/src/000-cloud/031-fabric/terraform/main.tf +++ b/src/000-cloud/031-fabric/terraform/main.tf @@ -14,6 +14,17 @@ locals { workspace_id = try(module.fabric_workspace[0].workspace.id, data.fabric_workspace.existing[0].id, null) } +// Cross-variable checks run as preconditions instead of variable validation blocks, +// so every referenced variable is fully resolved when they are evaluated. +resource "terraform_data" "validate_inputs" { + lifecycle { + precondition { + condition = !var.should_create_fabric_capacity || length(var.fabric_capacity_admins) > 0 + error_message = "At least one administrator must be specified for Fabric capacity when creating a capacity." + } + } +} + // Defer computation to prevent `data` objects from querying for state on `terraform plan`. // Needed for testing and build system compatibility. resource "terraform_data" "defer_fabric_capacity_created" { @@ -58,6 +69,8 @@ module "fabric_capacity" { source = "./modules/capacity" + depends_on = [terraform_data.validate_inputs] + name = local.fabric_capacity_name resource_group_name = var.resource_group.name location = var.location diff --git a/src/000-cloud/031-fabric/terraform/tests/base.tftest.hcl b/src/000-cloud/031-fabric/terraform/tests/base.tftest.hcl index 857999b7a..b2f0cee76 100644 --- a/src/000-cloud/031-fabric/terraform/tests/base.tftest.hcl +++ b/src/000-cloud/031-fabric/terraform/tests/base.tftest.hcl @@ -163,6 +163,6 @@ run "create_capacity_without_admins" { } expect_failures = [ - var.fabric_capacity_admins, + terraform_data.validate_inputs, ] } diff --git a/src/000-cloud/031-fabric/terraform/variables.tf b/src/000-cloud/031-fabric/terraform/variables.tf index 94344f4f9..8b5b7901a 100644 --- a/src/000-cloud/031-fabric/terraform/variables.tf +++ b/src/000-cloud/031-fabric/terraform/variables.tf @@ -54,10 +54,6 @@ variable "fabric_capacity_admins" { EOT type = list(string) default = [] - validation { - condition = !var.should_create_fabric_capacity || length(var.fabric_capacity_admins) > 0 - error_message = "At least one administrator must be specified for Fabric capacity when creating a capacity." - } } variable "fabric_capacity_sku" { diff --git a/src/000-cloud/036-managed-redis/terraform/README.md b/src/000-cloud/036-managed-redis/terraform/README.md index 5db735dd1..09c50a2dd 100644 --- a/src/000-cloud/036-managed-redis/terraform/README.md +++ b/src/000-cloud/036-managed-redis/terraform/README.md @@ -19,9 +19,10 @@ Supports Microsoft Entra ID authentication and customer-managed key encryption. ## Resources -| Name | Type | -|----------------------------------------------------------------------------------------------------------------|----------| -| [terraform_data.defer](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | +| Name | Type | +|--------------------------------------------------------------------------------------------------------------------------|----------| +| [terraform_data.defer](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | +| [terraform_data.validate_inputs](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | ## Modules diff --git a/src/000-cloud/036-managed-redis/terraform/main.tf b/src/000-cloud/036-managed-redis/terraform/main.tf index 524bce657..74089a719 100644 --- a/src/000-cloud/036-managed-redis/terraform/main.tf +++ b/src/000-cloud/036-managed-redis/terraform/main.tf @@ -9,6 +9,21 @@ locals { redis_name = "redis-${var.resource_prefix}-${var.environment}-${var.instance}" } +/* + * Input Validation + */ + +// Cross-variable checks run as preconditions instead of variable validation blocks, +// so every referenced variable is fully resolved when they are evaluated. +resource "terraform_data" "validate_inputs" { + lifecycle { + precondition { + condition = var.should_enable_private_endpoint ? var.private_endpoint_subnet != null : true + error_message = "private_endpoint_subnet is required when should_enable_private_endpoint is true." + } + } +} + /* * Azure Managed Redis Cache */ @@ -17,6 +32,8 @@ module "managed_redis" { count = var.should_deploy_redis ? 1 : 0 source = "./modules/managed-redis" + depends_on = [terraform_data.validate_inputs] + // Core Configuration location = var.location name = local.redis_name diff --git a/src/000-cloud/036-managed-redis/terraform/variables.deps.tf b/src/000-cloud/036-managed-redis/terraform/variables.deps.tf index 9cb51a33b..5ec953423 100644 --- a/src/000-cloud/036-managed-redis/terraform/variables.deps.tf +++ b/src/000-cloud/036-managed-redis/terraform/variables.deps.tf @@ -16,11 +16,6 @@ variable "private_endpoint_subnet" { id = string }) default = null - - validation { - condition = var.should_enable_private_endpoint ? var.private_endpoint_subnet != null : true - error_message = "private_endpoint_subnet is required when should_enable_private_endpoint is true." - } } variable "resource_group" { diff --git a/src/000-cloud/080-azureml/terraform/README.md b/src/000-cloud/080-azureml/terraform/README.md index d178a02a3..8e5446bc5 100644 --- a/src/000-cloud/080-azureml/terraform/README.md +++ b/src/000-cloud/080-azureml/terraform/README.md @@ -16,14 +16,16 @@ existing cloud infrastructure including Key Vault, Storage Account, Application ## Providers -| Name | Version | -|---------|-------------------| -| azurerm | >= 5.3.0, < 6.0.0 | +| Name | Version | +|-----------|-------------------| +| azurerm | >= 5.3.0, < 6.0.0 | +| terraform | n/a | ## Resources | Name | Type | |-----------------------------------------------------------------------------------------------------------------------------------|-------------| +| [terraform_data.validate_inputs](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | | [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source | ## Modules diff --git a/src/000-cloud/080-azureml/terraform/main.tf b/src/000-cloud/080-azureml/terraform/main.tf index b85d524d4..ac6c77889 100644 --- a/src/000-cloud/080-azureml/terraform/main.tf +++ b/src/000-cloud/080-azureml/terraform/main.tf @@ -8,6 +8,29 @@ data "azurerm_client_config" "current" {} +/* + * Input Validation + */ + +// Cross-variable checks run as preconditions instead of variable validation blocks, +// so every referenced variable is fully resolved when they are evaluated. +resource "terraform_data" "validate_inputs" { + lifecycle { + precondition { + condition = !var.should_assign_ml_workload_identity_roles || var.ml_workload_identity != null + error_message = "ml_workload_identity must be provided when should_assign_ml_workload_identity_roles is true." + } + precondition { + condition = !var.should_associate_network_security_group || var.network_security_group != null + error_message = "network_security_group must be provided when should_associate_network_security_group is true." + } + precondition { + condition = !var.should_enable_nat_gateway || var.nat_gateway != null + error_message = "nat_gateway must be provided when should_enable_nat_gateway is true." + } + } +} + /* * Network Module for Azure ML Compute Cluster */ diff --git a/src/000-cloud/080-azureml/terraform/variables.deps.tf b/src/000-cloud/080-azureml/terraform/variables.deps.tf index 1398ec1da..f6d7bb60e 100644 --- a/src/000-cloud/080-azureml/terraform/variables.deps.tf +++ b/src/000-cloud/080-azureml/terraform/variables.deps.tf @@ -74,10 +74,6 @@ variable "ml_workload_identity" { }) description = "AzureML workload managed identity object from security identity containing id and principal_id." default = null - validation { - condition = (!var.should_assign_ml_workload_identity_roles) || (var.ml_workload_identity != null) - error_message = "ml_workload_identity must be provided when should_assign_ml_workload_identity_roles is true." - } } variable "network_security_group" { @@ -86,10 +82,6 @@ variable "network_security_group" { }) description = "Network security group from 050-networking component." default = null - validation { - condition = (!var.should_associate_network_security_group) || (var.network_security_group != null) - error_message = "network_security_group must be provided when should_associate_network_security_group is true." - } } variable "virtual_network" { @@ -108,8 +100,4 @@ variable "nat_gateway" { }) description = "NAT gateway object from the networking component for managed outbound access." default = null - validation { - condition = (!var.should_enable_nat_gateway) || (var.nat_gateway != null) - error_message = "nat_gateway must be provided when should_enable_nat_gateway is true." - } } diff --git a/src/100-edge/100-cncf-cluster/terraform/README.md b/src/100-edge/100-cncf-cluster/terraform/README.md index 51c3c3578..f79e3fed3 100644 --- a/src/100-edge/100-cncf-cluster/terraform/README.md +++ b/src/100-edge/100-cncf-cluster/terraform/README.md @@ -33,6 +33,7 @@ install extensions for cluster connect and custom locations. | [azurerm_role_assignment.arc_kubernetes_viewer_user](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | | [terraform_data.defer_azuread_user](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | | [terraform_data.defer_custom_locations](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | +| [terraform_data.validate_inputs](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | | [azapi_resource.arc_connected_cluster](https://registry.terraform.io/providers/Azure/azapi/latest/docs/data-sources/resource) | data source | | [azuread_service_principal.custom_locations](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/data-sources/service_principal) | data source | | [azuread_user.current](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/data-sources/user) | data source | diff --git a/src/100-edge/100-cncf-cluster/terraform/main.tf b/src/100-edge/100-cncf-cluster/terraform/main.tf index bcb76268c..66f40ae9d 100644 --- a/src/100-edge/100-cncf-cluster/terraform/main.tf +++ b/src/100-edge/100-cncf-cluster/terraform/main.tf @@ -16,6 +16,33 @@ locals { arc_onboarding_principal_ids = local.should_use_principal_ids ? var.arc_onboarding_principal_ids : [try(var.arc_onboarding_identity.principal_id, var.arc_onboarding_sp.object_id, null)] } +/* + * Input Validation + */ + +// Cross-variable checks run as preconditions instead of variable validation blocks, +// so every referenced variable is fully resolved when they are evaluated. +resource "terraform_data" "validate_inputs" { + lifecycle { + precondition { + condition = !var.should_assign_roles || anytrue([var.arc_onboarding_identity != null, var.arc_onboarding_sp != null, var.arc_onboarding_principal_ids != null]) + error_message = "Either 'arc_onboarding_identity', 'arc_onboarding_sp', or 'arc_onboarding_principal_ids' required when should_assign_roles is 'true'" + } + precondition { + condition = !var.should_assign_roles || (sum([var.arc_onboarding_identity != null ? 1 : 0, var.arc_onboarding_sp != null ? 1 : 0, var.arc_onboarding_principal_ids != null ? 1 : 0]) <= 1) + error_message = "Only one of 'arc_onboarding_identity', 'arc_onboarding_sp', or 'arc_onboarding_principal_ids' can be provided" + } + precondition { + condition = !var.should_upload_to_key_vault || var.key_vault != null + error_message = "'key_vault' is required when 'should_upload_to_key_vault' is true" + } + precondition { + condition = var.cluster_server_token != null ? !var.should_generate_cluster_server_token : true + error_message = "'should_generate_cluster_server_token' must be false if 'cluster_server_token' has been provided." + } + } +} + /* * Data Sources */ diff --git a/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/README.md b/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/README.md index 00401df17..5c38c35e5 100644 --- a/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/README.md +++ b/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/README.md @@ -15,11 +15,12 @@ along with installing extensions for cluster connect and custom locations. ## Providers -| Name | Version | -|---------|-------------------| -| azurerm | >= 5.3.0, < 6.0.0 | -| local | n/a | -| random | n/a | +| Name | Version | +|-----------|-------------------| +| azurerm | >= 5.3.0, < 6.0.0 | +| local | n/a | +| random | n/a | +| terraform | n/a | ## Resources @@ -30,6 +31,7 @@ along with installing extensions for cluster connect and custom locations. | [local_sensitive_file.cluster_node_setup_script](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/sensitive_file) | resource | | [local_sensitive_file.cluster_server_setup_script](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/sensitive_file) | resource | | [random_string.cluster_server_token](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | +| [terraform_data.validate_inputs](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | ## Inputs diff --git a/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/main.tf b/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/main.tf index d3c4bd30d..5b0d6bb74 100644 --- a/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/main.tf +++ b/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/main.tf @@ -84,6 +84,17 @@ locals { ], slice(local.script_file, 1, length(local.script_file)))) } +// Cross-variable checks run as preconditions instead of variable validation blocks, +// so every referenced variable is fully resolved when they are evaluated. +resource "terraform_data" "validate_inputs" { + lifecycle { + precondition { + condition = var.cluster_server_token != null ? !var.should_generate_cluster_server_token : true + error_message = "'should_generate_cluster_server_token' must be false if 'cluster_server_token' has been provided." + } + } +} + resource "random_string" "cluster_server_token" { count = var.should_generate_cluster_server_token ? 1 : 0 diff --git a/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/variables.tf b/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/variables.tf index d0b4955bb..0229e99dd 100644 --- a/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/variables.tf +++ b/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/variables.tf @@ -99,10 +99,6 @@ variable "cluster_server_token" { variable "should_generate_cluster_server_token" { type = bool description = "Should generate token used by the server. ('cluster_server_token' must be null if this is 'true')" - validation { - condition = var.cluster_server_token != null ? !var.should_generate_cluster_server_token : true - error_message = "'should_generate_cluster_server_token' must be false if 'cluster_server_token' has been provided." - } } variable "cluster_server_host_machine_username" { diff --git a/src/100-edge/100-cncf-cluster/terraform/tests/cluster.tftest.hcl b/src/100-edge/100-cncf-cluster/terraform/tests/cluster.tftest.hcl index d77662b2c..b805c7e1d 100644 --- a/src/100-edge/100-cncf-cluster/terraform/tests/cluster.tftest.hcl +++ b/src/100-edge/100-cncf-cluster/terraform/tests/cluster.tftest.hcl @@ -207,7 +207,7 @@ run "create_default_cluster_with_principal_id" { run "test_invalid_multiple_identities" { command = plan expect_failures = [ - var.arc_onboarding_identity + terraform_data.validate_inputs ] variables { diff --git a/src/100-edge/100-cncf-cluster/terraform/variables.deps.tf b/src/100-edge/100-cncf-cluster/terraform/variables.deps.tf index 98d54e87a..9bedd7d9d 100644 --- a/src/100-edge/100-cncf-cluster/terraform/variables.deps.tf +++ b/src/100-edge/100-cncf-cluster/terraform/variables.deps.tf @@ -52,16 +52,6 @@ variable "arc_onboarding_identity" { }) description = "The User Assigned Managed Identity that will be used for onboarding the cluster to Arc" default = null - - validation { - condition = !var.should_assign_roles || anytrue([var.arc_onboarding_identity != null, var.arc_onboarding_sp != null, var.arc_onboarding_principal_ids != null]) - error_message = "Either 'arc_onboarding_identity', 'arc_onboarding_sp', or 'arc_onboarding_principal_ids' required when should_assign_roles is 'true'" - } - - validation { - condition = !var.should_assign_roles || (sum([var.arc_onboarding_identity != null ? 1 : 0, var.arc_onboarding_sp != null ? 1 : 0, var.arc_onboarding_principal_ids != null ? 1 : 0]) <= 1) - error_message = "Only one of 'arc_onboarding_identity', 'arc_onboarding_sp', or 'arc_onboarding_principal_ids' can be provided" - } } variable "arc_onboarding_sp" { @@ -82,11 +72,6 @@ variable "key_vault" { }) description = "The Key Vault object containing id, name, and vault_uri properties" default = null - - validation { - condition = !var.should_upload_to_key_vault || var.key_vault != null - error_message = "'key_vault' is required when 'should_upload_to_key_vault' is true" - } } variable "private_key_pem" { diff --git a/src/100-edge/100-cncf-cluster/terraform/variables.tf b/src/100-edge/100-cncf-cluster/terraform/variables.tf index 7902eb467..59a9b3eb5 100644 --- a/src/100-edge/100-cncf-cluster/terraform/variables.tf +++ b/src/100-edge/100-cncf-cluster/terraform/variables.tf @@ -195,10 +195,6 @@ variable "should_generate_cluster_server_token" { type = bool description = "Should generate token used by the server. ('cluster_server_token' must be null if this is 'true')" default = false - validation { - condition = var.cluster_server_token != null ? !var.should_generate_cluster_server_token : true - error_message = "'should_generate_cluster_server_token' must be false if 'cluster_server_token' has been provided." - } } variable "cluster_server_host_machine_username" { From 37f35b5b3c1ca02175e10130b58b60039ade7f27 Mon Sep 17 00:00:00 2001 From: Shivam Sharma <192242476+ShivamSharma43@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:02:10 +0530 Subject: [PATCH 2/4] chore(terraform): address review feedback on cross-variable preconditions - move remaining cross-variable validations in 100-cncf-cluster (should_use_script_from_secrets_for_deploy, should_deploy_over_ssh) and 080-azureml (compute_cluster_min_nodes <= max) into validate_inputs - gate NAT gateway, NSG and ML workload identity wiring in 080-azureml on the object being set so the precondition is the only plan error - drop depends_on on validate_inputs in 031-fabric and 036-managed-redis - trim guard comments and add Input Validation headers - add plan tests for the moved 100-cncf-cluster rules Co-Authored-By: Claude Opus 5.5 Signed-off-by: Shivam Sharma <192242476+ShivamSharma43@users.noreply.github.com> --- src/000-cloud/031-fabric/terraform/main.tf | 9 +-- .../036-managed-redis/terraform/main.tf | 5 +- src/000-cloud/080-azureml/terraform/main.tf | 17 +++-- .../080-azureml/terraform/variables.tf | 4 +- .../100-cncf-cluster/terraform/main.tf | 15 +++- .../terraform/modules/ubuntu-k3s/main.tf | 7 +- .../terraform/tests/cluster.tftest.hcl | 71 +++++++++++++++++++ .../100-cncf-cluster/terraform/variables.tf | 15 ---- 8 files changed, 107 insertions(+), 36 deletions(-) diff --git a/src/000-cloud/031-fabric/terraform/main.tf b/src/000-cloud/031-fabric/terraform/main.tf index 3b68479a5..948500127 100644 --- a/src/000-cloud/031-fabric/terraform/main.tf +++ b/src/000-cloud/031-fabric/terraform/main.tf @@ -14,8 +14,11 @@ locals { workspace_id = try(module.fabric_workspace[0].workspace.id, data.fabric_workspace.existing[0].id, null) } -// Cross-variable checks run as preconditions instead of variable validation blocks, -// so every referenced variable is fully resolved when they are evaluated. +/* + * Input Validation + */ + +// Validates rules that span multiple input variables. resource "terraform_data" "validate_inputs" { lifecycle { precondition { @@ -69,8 +72,6 @@ module "fabric_capacity" { source = "./modules/capacity" - depends_on = [terraform_data.validate_inputs] - name = local.fabric_capacity_name resource_group_name = var.resource_group.name location = var.location diff --git a/src/000-cloud/036-managed-redis/terraform/main.tf b/src/000-cloud/036-managed-redis/terraform/main.tf index 74089a719..784cb7ea6 100644 --- a/src/000-cloud/036-managed-redis/terraform/main.tf +++ b/src/000-cloud/036-managed-redis/terraform/main.tf @@ -13,8 +13,7 @@ locals { * Input Validation */ -// Cross-variable checks run as preconditions instead of variable validation blocks, -// so every referenced variable is fully resolved when they are evaluated. +// Validates rules that span multiple input variables. resource "terraform_data" "validate_inputs" { lifecycle { precondition { @@ -32,8 +31,6 @@ module "managed_redis" { count = var.should_deploy_redis ? 1 : 0 source = "./modules/managed-redis" - depends_on = [terraform_data.validate_inputs] - // Core Configuration location = var.location name = local.redis_name diff --git a/src/000-cloud/080-azureml/terraform/main.tf b/src/000-cloud/080-azureml/terraform/main.tf index ac6c77889..086fe5f80 100644 --- a/src/000-cloud/080-azureml/terraform/main.tf +++ b/src/000-cloud/080-azureml/terraform/main.tf @@ -12,8 +12,7 @@ data "azurerm_client_config" "current" {} * Input Validation */ -// Cross-variable checks run as preconditions instead of variable validation blocks, -// so every referenced variable is fully resolved when they are evaluated. +// Validates rules that span multiple input variables. resource "terraform_data" "validate_inputs" { lifecycle { precondition { @@ -28,6 +27,10 @@ resource "terraform_data" "validate_inputs" { condition = !var.should_enable_nat_gateway || var.nat_gateway != null error_message = "nat_gateway must be provided when should_enable_nat_gateway is true." } + precondition { + condition = var.compute_cluster_min_nodes <= var.compute_cluster_max_nodes + error_message = "Minimum node count must be less than or equal to compute_cluster_max_nodes." + } } } @@ -52,9 +55,9 @@ module "network" { // Optional parameters default_outbound_access_enabled = var.default_outbound_access_enabled - should_associate_network_security_group = var.should_associate_network_security_group - should_enable_nat_gateway = var.should_enable_nat_gateway - nat_gateway_id = var.should_enable_nat_gateway ? var.nat_gateway.id : null + should_associate_network_security_group = var.should_associate_network_security_group && var.network_security_group != null + should_enable_nat_gateway = var.should_enable_nat_gateway && var.nat_gateway != null + nat_gateway_id = try(var.nat_gateway.id, null) subnet_address_prefixes_azureml = var.subnet_address_prefixes_azureml } @@ -80,7 +83,7 @@ module "workspace" { should_assign_current_user_workspace_roles = var.should_assign_current_user_workspace_roles current_user_object_id = var.should_assign_current_user_workspace_roles ? data.azurerm_client_config.current.object_id : null ml_workload_identity = var.ml_workload_identity - should_assign_ml_workload_identity_roles = var.should_assign_ml_workload_identity_roles + should_assign_ml_workload_identity_roles = var.should_assign_ml_workload_identity_roles && var.ml_workload_identity != null // Role assignment configuration should_assign_workspace_managed_identity_roles = var.should_assign_workspace_managed_identity_roles @@ -194,7 +197,7 @@ module "inference_cluster_integration" { ml_workload_identity = var.ml_workload_identity ml_workload_subjects = var.ml_workload_subjects - should_configure_ml_workload_identity = var.should_assign_ml_workload_identity_roles + should_configure_ml_workload_identity = var.should_assign_ml_workload_identity_roles && var.ml_workload_identity != null // App Configuration integration for volcano scheduler volcano_scheduler_configmap_name = try(var.kubernetes.app_configuration_configmap_name, null) diff --git a/src/000-cloud/080-azureml/terraform/variables.tf b/src/000-cloud/080-azureml/terraform/variables.tf index be4a188dd..3ce48d7c4 100644 --- a/src/000-cloud/080-azureml/terraform/variables.tf +++ b/src/000-cloud/080-azureml/terraform/variables.tf @@ -131,8 +131,8 @@ variable "compute_cluster_min_nodes" { default = 0 validation { - condition = var.compute_cluster_min_nodes >= 0 && var.compute_cluster_min_nodes <= var.compute_cluster_max_nodes - error_message = "Minimum node count must be greater than or equal to 0 and less than or equal to compute_cluster_max_nodes." + condition = var.compute_cluster_min_nodes >= 0 + error_message = "Minimum node count must be greater than or equal to 0." } } diff --git a/src/100-edge/100-cncf-cluster/terraform/main.tf b/src/100-edge/100-cncf-cluster/terraform/main.tf index 66f40ae9d..d58287436 100644 --- a/src/100-edge/100-cncf-cluster/terraform/main.tf +++ b/src/100-edge/100-cncf-cluster/terraform/main.tf @@ -20,8 +20,7 @@ locals { * Input Validation */ -// Cross-variable checks run as preconditions instead of variable validation blocks, -// so every referenced variable is fully resolved when they are evaluated. +// Validates rules that span multiple input variables. resource "terraform_data" "validate_inputs" { lifecycle { precondition { @@ -40,6 +39,18 @@ resource "terraform_data" "validate_inputs" { condition = var.cluster_server_token != null ? !var.should_generate_cluster_server_token : true error_message = "'should_generate_cluster_server_token' must be false if 'cluster_server_token' has been provided." } + precondition { + condition = var.should_upload_to_key_vault || !var.should_use_script_from_secrets_for_deploy + error_message = "'should_use_script_from_secrets_for_deploy' cannot be true when 'should_upload_to_key_vault' is false: no script would be present in Key Vault to fetch." + } + precondition { + condition = !var.should_deploy_over_ssh || var.should_deploy_arc_machines + error_message = "'should_deploy_over_ssh' requires 'should_deploy_arc_machines' to be true" + } + precondition { + condition = !var.should_deploy_over_ssh || (var.ssh_local_user != null && var.ssh_private_key_path != null) + error_message = "'ssh_local_user' and 'ssh_private_key_path' are required when 'should_deploy_over_ssh' is true" + } } } diff --git a/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/main.tf b/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/main.tf index 5b0d6bb74..b66682df3 100644 --- a/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/main.tf +++ b/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/main.tf @@ -84,8 +84,11 @@ locals { ], slice(local.script_file, 1, length(local.script_file)))) } -// Cross-variable checks run as preconditions instead of variable validation blocks, -// so every referenced variable is fully resolved when they are evaluated. +/* + * Input Validation + */ + +// Validates rules that span multiple input variables. resource "terraform_data" "validate_inputs" { lifecycle { precondition { diff --git a/src/100-edge/100-cncf-cluster/terraform/tests/cluster.tftest.hcl b/src/100-edge/100-cncf-cluster/terraform/tests/cluster.tftest.hcl index b805c7e1d..d9f71ecf0 100644 --- a/src/100-edge/100-cncf-cluster/terraform/tests/cluster.tftest.hcl +++ b/src/100-edge/100-cncf-cluster/terraform/tests/cluster.tftest.hcl @@ -225,3 +225,74 @@ run "test_invalid_multiple_identities" { } } + +# Test invalid configuration fetching scripts from Key Vault without uploading them +run "test_invalid_script_from_secrets_without_key_vault_upload" { + command = plan + expect_failures = [ + terraform_data.validate_inputs + ] + + variables { + resource_prefix = run.setup_tests.resource_prefix + environment = "dev" + resource_group = run.setup_tests.aio_resource_group + should_get_custom_locations_oid = false + should_deploy_script_to_vm = true + should_assign_roles = false + arc_onboarding_sp = run.setup_tests.mock_sp + custom_locations_oid = run.setup_tests.mock_custom_locations_oid + key_vault = run.setup_tests.key_vault + cluster_server_machine = run.setup_tests.cluster_server_machine + should_upload_to_key_vault = false + should_use_script_from_secrets_for_deploy = true + } +} + +# Test invalid configuration deploying over SSH without Arc machines +run "test_invalid_ssh_without_arc_machines" { + command = plan + expect_failures = [ + terraform_data.validate_inputs + ] + + variables { + resource_prefix = run.setup_tests.resource_prefix + environment = "dev" + resource_group = run.setup_tests.aio_resource_group + should_get_custom_locations_oid = false + should_deploy_script_to_vm = true + should_assign_roles = false + arc_onboarding_sp = run.setup_tests.mock_sp + custom_locations_oid = run.setup_tests.mock_custom_locations_oid + key_vault = run.setup_tests.key_vault + cluster_server_machine = run.setup_tests.cluster_server_machine + should_deploy_over_ssh = true + should_deploy_arc_machines = false + ssh_local_user = "azureuser" + ssh_private_key_path = "~/.ssh/id_rsa" + } +} + +# Test invalid configuration deploying over SSH without SSH credentials +run "test_invalid_ssh_without_credentials" { + command = plan + expect_failures = [ + terraform_data.validate_inputs + ] + + variables { + resource_prefix = run.setup_tests.resource_prefix + environment = "dev" + resource_group = run.setup_tests.aio_resource_group + should_get_custom_locations_oid = false + should_deploy_script_to_vm = true + should_assign_roles = false + arc_onboarding_sp = run.setup_tests.mock_sp + custom_locations_oid = run.setup_tests.mock_custom_locations_oid + key_vault = run.setup_tests.key_vault + cluster_server_machine = run.setup_tests.cluster_server_machine + should_deploy_over_ssh = true + should_deploy_arc_machines = true + } +} diff --git a/src/100-edge/100-cncf-cluster/terraform/variables.tf b/src/100-edge/100-cncf-cluster/terraform/variables.tf index 59a9b3eb5..2c98f6819 100644 --- a/src/100-edge/100-cncf-cluster/terraform/variables.tf +++ b/src/100-edge/100-cncf-cluster/terraform/variables.tf @@ -18,11 +18,6 @@ variable "should_use_script_from_secrets_for_deploy" { type = bool description = "Whether to use the deploy-script-secrets.sh script to fetch and execute deployment scripts from Key Vault" default = true - - validation { - condition = var.should_upload_to_key_vault || !var.should_use_script_from_secrets_for_deploy - error_message = "'should_use_script_from_secrets_for_deploy' cannot be true when 'should_upload_to_key_vault' is false: no script would be present in Key Vault to fetch." - } } variable "key_vault_script_secret_prefix" { @@ -71,16 +66,6 @@ variable "should_deploy_over_ssh" { type = bool description = "Should deliver the setup script over 'az ssh arc' instead of a CustomScript extension. Requires Azure CLI with the 'ssh' extension where Terraform runs." default = false - - validation { - condition = !var.should_deploy_over_ssh || var.should_deploy_arc_machines - error_message = "'should_deploy_over_ssh' requires 'should_deploy_arc_machines' to be true" - } - - validation { - condition = !var.should_deploy_over_ssh || (var.ssh_local_user != null && var.ssh_private_key_path != null) - error_message = "'ssh_local_user' and 'ssh_private_key_path' are required when 'should_deploy_over_ssh' is true" - } } variable "ssh_local_user" { From 7b71003593664470a0388886c37bb145787d4014 Mon Sep 17 00:00:00 2001 From: Shivam Sharma <192242476+ShivamSharma43@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:14:45 +0530 Subject: [PATCH 3/4] chore(terraform): keep cross-variable checks off managed resources The terraform_data guards in 080-azureml, 100-cncf-cluster and ubuntu-k3s were new managed resources inside existing depends_on boundaries. On the first plan after upgrading, their create action deferred data source reads behind those boundaries (for example the Arc connected cluster lookup that feeds the Arc RBAC scope), which can force role assignment replacement. - 080-azureml, 100-cncf-cluster: move the preconditions onto the existing data.azurerm_client_config.current, which is always read and adds no planned change - ubuntu-k3s: drop the nested guard; check the token rule as a precondition on random_string.cluster_server_token instead - 080-azureml: module.network depends on the data source so targeted plans (workspace and inference integration inherit it) still run the checks instead of silently skipping the NAT/NSG association - point the cluster plan tests at the data source and regenerate READMEs Co-Authored-By: Claude Opus 5.5 Signed-off-by: Shivam Sharma <192242476+ShivamSharma43@users.noreply.github.com> --- src/000-cloud/080-azureml/terraform/README.md | 8 +++---- src/000-cloud/080-azureml/terraform/main.tf | 8 ++++--- .../100-cncf-cluster/terraform/README.md | 1 - .../100-cncf-cluster/terraform/main.tf | 6 ++---- .../terraform/modules/ubuntu-k3s/README.md | 12 +++++------ .../terraform/modules/ubuntu-k3s/main.tf | 21 +++++++------------ .../terraform/tests/cluster.tftest.hcl | 8 +++---- 7 files changed, 26 insertions(+), 38 deletions(-) diff --git a/src/000-cloud/080-azureml/terraform/README.md b/src/000-cloud/080-azureml/terraform/README.md index 8e5446bc5..d178a02a3 100644 --- a/src/000-cloud/080-azureml/terraform/README.md +++ b/src/000-cloud/080-azureml/terraform/README.md @@ -16,16 +16,14 @@ existing cloud infrastructure including Key Vault, Storage Account, Application ## Providers -| Name | Version | -|-----------|-------------------| -| azurerm | >= 5.3.0, < 6.0.0 | -| terraform | n/a | +| Name | Version | +|---------|-------------------| +| azurerm | >= 5.3.0, < 6.0.0 | ## Resources | Name | Type | |-----------------------------------------------------------------------------------------------------------------------------------|-------------| -| [terraform_data.validate_inputs](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | | [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source | ## Modules diff --git a/src/000-cloud/080-azureml/terraform/main.tf b/src/000-cloud/080-azureml/terraform/main.tf index 086fe5f80..fcd38f53d 100644 --- a/src/000-cloud/080-azureml/terraform/main.tf +++ b/src/000-cloud/080-azureml/terraform/main.tf @@ -6,14 +6,13 @@ * existing cloud infrastructure including Key Vault, Storage Account, Application Insights, and networking. */ -data "azurerm_client_config" "current" {} - /* * Input Validation */ // Validates rules that span multiple input variables. -resource "terraform_data" "validate_inputs" { +// On a data source rather than a managed resource, so it adds no planned changes. +data "azurerm_client_config" "current" { lifecycle { precondition { condition = !var.should_assign_ml_workload_identity_roles || var.ml_workload_identity != null @@ -43,6 +42,9 @@ module "network" { source = "./modules/network" + // Keeps the input preconditions in targeted plans (workspace and inference integration inherit it). + depends_on = [data.azurerm_client_config.current] + // Resource dependencies first resource_group = var.resource_group network_security_group = var.network_security_group diff --git a/src/100-edge/100-cncf-cluster/terraform/README.md b/src/100-edge/100-cncf-cluster/terraform/README.md index f79e3fed3..51c3c3578 100644 --- a/src/100-edge/100-cncf-cluster/terraform/README.md +++ b/src/100-edge/100-cncf-cluster/terraform/README.md @@ -33,7 +33,6 @@ install extensions for cluster connect and custom locations. | [azurerm_role_assignment.arc_kubernetes_viewer_user](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | | [terraform_data.defer_azuread_user](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | | [terraform_data.defer_custom_locations](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | -| [terraform_data.validate_inputs](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | | [azapi_resource.arc_connected_cluster](https://registry.terraform.io/providers/Azure/azapi/latest/docs/data-sources/resource) | data source | | [azuread_service_principal.custom_locations](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/data-sources/service_principal) | data source | | [azuread_user.current](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/data-sources/user) | data source | diff --git a/src/100-edge/100-cncf-cluster/terraform/main.tf b/src/100-edge/100-cncf-cluster/terraform/main.tf index d58287436..263f30632 100644 --- a/src/100-edge/100-cncf-cluster/terraform/main.tf +++ b/src/100-edge/100-cncf-cluster/terraform/main.tf @@ -21,7 +21,8 @@ locals { */ // Validates rules that span multiple input variables. -resource "terraform_data" "validate_inputs" { +// On a data source rather than a managed resource, so it adds no planned changes. +data "azurerm_client_config" "current" { lifecycle { precondition { condition = !var.should_assign_roles || anytrue([var.arc_onboarding_identity != null, var.arc_onboarding_sp != null, var.arc_onboarding_principal_ids != null]) @@ -58,9 +59,6 @@ resource "terraform_data" "validate_inputs" { * Data Sources */ -data "azurerm_client_config" "current" { -} - resource "terraform_data" "defer_azuread_user" { count = var.should_add_current_user_cluster_admin ? 1 : 0 input = { diff --git a/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/README.md b/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/README.md index 5c38c35e5..00401df17 100644 --- a/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/README.md +++ b/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/README.md @@ -15,12 +15,11 @@ along with installing extensions for cluster connect and custom locations. ## Providers -| Name | Version | -|-----------|-------------------| -| azurerm | >= 5.3.0, < 6.0.0 | -| local | n/a | -| random | n/a | -| terraform | n/a | +| Name | Version | +|---------|-------------------| +| azurerm | >= 5.3.0, < 6.0.0 | +| local | n/a | +| random | n/a | ## Resources @@ -31,7 +30,6 @@ along with installing extensions for cluster connect and custom locations. | [local_sensitive_file.cluster_node_setup_script](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/sensitive_file) | resource | | [local_sensitive_file.cluster_server_setup_script](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/sensitive_file) | resource | | [random_string.cluster_server_token](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | -| [terraform_data.validate_inputs](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | ## Inputs diff --git a/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/main.tf b/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/main.tf index b66682df3..181909de7 100644 --- a/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/main.tf +++ b/src/100-edge/100-cncf-cluster/terraform/modules/ubuntu-k3s/main.tf @@ -84,20 +84,6 @@ locals { ], slice(local.script_file, 1, length(local.script_file)))) } -/* - * Input Validation - */ - -// Validates rules that span multiple input variables. -resource "terraform_data" "validate_inputs" { - lifecycle { - precondition { - condition = var.cluster_server_token != null ? !var.should_generate_cluster_server_token : true - error_message = "'should_generate_cluster_server_token' must be false if 'cluster_server_token' has been provided." - } - } -} - resource "random_string" "cluster_server_token" { count = var.should_generate_cluster_server_token ? 1 : 0 @@ -106,6 +92,13 @@ resource "random_string" "cluster_server_token" { numeric = true upper = true lower = true + + lifecycle { + precondition { + condition = var.cluster_server_token == null + error_message = "'should_generate_cluster_server_token' must be false if 'cluster_server_token' has been provided." + } + } } resource "local_sensitive_file" "cluster_server_setup_script" { diff --git a/src/100-edge/100-cncf-cluster/terraform/tests/cluster.tftest.hcl b/src/100-edge/100-cncf-cluster/terraform/tests/cluster.tftest.hcl index d9f71ecf0..238f7d57c 100644 --- a/src/100-edge/100-cncf-cluster/terraform/tests/cluster.tftest.hcl +++ b/src/100-edge/100-cncf-cluster/terraform/tests/cluster.tftest.hcl @@ -207,7 +207,7 @@ run "create_default_cluster_with_principal_id" { run "test_invalid_multiple_identities" { command = plan expect_failures = [ - terraform_data.validate_inputs + data.azurerm_client_config.current, ] variables { @@ -230,7 +230,7 @@ run "test_invalid_multiple_identities" { run "test_invalid_script_from_secrets_without_key_vault_upload" { command = plan expect_failures = [ - terraform_data.validate_inputs + data.azurerm_client_config.current, ] variables { @@ -253,7 +253,7 @@ run "test_invalid_script_from_secrets_without_key_vault_upload" { run "test_invalid_ssh_without_arc_machines" { command = plan expect_failures = [ - terraform_data.validate_inputs + data.azurerm_client_config.current, ] variables { @@ -278,7 +278,7 @@ run "test_invalid_ssh_without_arc_machines" { run "test_invalid_ssh_without_credentials" { command = plan expect_failures = [ - terraform_data.validate_inputs + data.azurerm_client_config.current, ] variables { From b0edf276c068048a2b0db2f3aabab9aa1d74a96e Mon Sep 17 00:00:00 2001 From: Shivam Sharma <192242476+ShivamSharma43@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:45:55 +0530 Subject: [PATCH 4/4] chore(terraform): gate Redis private endpoint on subnet and add AzureML guard tests - 036-managed-redis: pass should_enable_private_endpoint to the module only when private_endpoint_subnet is set, so a missing subnet reports just the precondition instead of also failing on the private endpoint's subnet_id. The module call depends on the guard so a plan targeting module.managed_redis still runs the check instead of silently skipping the endpoint; the module has no data sources, so no reads are deferred. - 080-azureml: add the cross-variable negative cases to the plan-level suite now on main: NAT gateway (full and module.network targeted), NSG (targeted), workload identity roles (full and module.workspace targeted), and min nodes above max nodes. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Shivam Sharma <192242476+ShivamSharma43@users.noreply.github.com> --- .../036-managed-redis/terraform/main.tf | 5 +- .../terraform/tests/base.tftest.hcl | 151 ++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) diff --git a/src/000-cloud/036-managed-redis/terraform/main.tf b/src/000-cloud/036-managed-redis/terraform/main.tf index 784cb7ea6..d11625aec 100644 --- a/src/000-cloud/036-managed-redis/terraform/main.tf +++ b/src/000-cloud/036-managed-redis/terraform/main.tf @@ -31,6 +31,9 @@ module "managed_redis" { count = var.should_deploy_redis ? 1 : 0 source = "./modules/managed-redis" + // Keeps the input precondition in targeted plans. The module has no data sources, so this defers no reads. + depends_on = [terraform_data.validate_inputs] + // Core Configuration location = var.location name = local.redis_name @@ -50,7 +53,7 @@ module "managed_redis" { private_endpoint_subnet_id = try(var.private_endpoint_subnet.id, null) private_dns_zone_id = var.private_dns_zone_id should_create_private_dns_zone = var.should_create_private_dns_zone - should_enable_private_endpoint = var.should_enable_private_endpoint + should_enable_private_endpoint = var.should_enable_private_endpoint && var.private_endpoint_subnet != null virtual_network_id = try(var.virtual_network.id, null) } diff --git a/src/000-cloud/080-azureml/terraform/tests/base.tftest.hcl b/src/000-cloud/080-azureml/terraform/tests/base.tftest.hcl index 097ba9171..b95f46250 100644 --- a/src/000-cloud/080-azureml/terraform/tests/base.tftest.hcl +++ b/src/000-cloud/080-azureml/terraform/tests/base.tftest.hcl @@ -294,3 +294,154 @@ run "reject_invalid_resource_prefix" { var.resource_prefix, ] } + +# Test 8: NAT gateway association requires a NAT gateway object +run "reject_nat_gateway_enabled_without_nat_gateway" { + command = plan + + variables { + resource_prefix = run.setup_tests.resource_prefix + environment = "test" + location = run.setup_tests.location + resource_group = run.setup_tests.resource_group + application_insights = run.setup_tests.application_insights + key_vault = run.setup_tests.key_vault + storage_account = run.setup_tests.storage_account + acr = run.setup_tests.acr + virtual_network = run.setup_tests.virtual_network + + should_enable_nat_gateway = true + } + + expect_failures = [ + data.azurerm_client_config.current, + ] +} + +# Test 9: A plan targeting only the network module still enforces the NAT gateway rule +run "reject_nat_gateway_enabled_without_nat_gateway_targeted" { + command = plan + + plan_options { + target = [module.network] + } + + variables { + resource_prefix = run.setup_tests.resource_prefix + environment = "test" + location = run.setup_tests.location + resource_group = run.setup_tests.resource_group + application_insights = run.setup_tests.application_insights + key_vault = run.setup_tests.key_vault + storage_account = run.setup_tests.storage_account + acr = run.setup_tests.acr + virtual_network = run.setup_tests.virtual_network + + should_enable_nat_gateway = true + } + + expect_failures = [ + data.azurerm_client_config.current, + ] +} + +# Test 10: A plan targeting only the network module still enforces the NSG rule +run "reject_nsg_association_without_nsg_targeted" { + command = plan + + plan_options { + target = [module.network] + } + + variables { + resource_prefix = run.setup_tests.resource_prefix + environment = "test" + location = run.setup_tests.location + resource_group = run.setup_tests.resource_group + application_insights = run.setup_tests.application_insights + key_vault = run.setup_tests.key_vault + storage_account = run.setup_tests.storage_account + acr = run.setup_tests.acr + virtual_network = run.setup_tests.virtual_network + + should_associate_network_security_group = true + } + + expect_failures = [ + data.azurerm_client_config.current, + ] +} + +# Test 11: Workload identity role assignment requires a workload identity object +run "reject_workload_identity_roles_without_identity" { + command = plan + + variables { + resource_prefix = run.setup_tests.resource_prefix + environment = "test" + location = run.setup_tests.location + resource_group = run.setup_tests.resource_group + application_insights = run.setup_tests.application_insights + key_vault = run.setup_tests.key_vault + storage_account = run.setup_tests.storage_account + acr = run.setup_tests.acr + virtual_network = run.setup_tests.virtual_network + + should_assign_ml_workload_identity_roles = true + } + + expect_failures = [ + data.azurerm_client_config.current, + ] +} + +# Test 12: A plan targeting only the workspace module still enforces the workload identity rule +run "reject_workload_identity_roles_without_identity_targeted" { + command = plan + + plan_options { + target = [module.workspace] + } + + variables { + resource_prefix = run.setup_tests.resource_prefix + environment = "test" + location = run.setup_tests.location + resource_group = run.setup_tests.resource_group + application_insights = run.setup_tests.application_insights + key_vault = run.setup_tests.key_vault + storage_account = run.setup_tests.storage_account + acr = run.setup_tests.acr + virtual_network = run.setup_tests.virtual_network + + should_assign_ml_workload_identity_roles = true + } + + expect_failures = [ + data.azurerm_client_config.current, + ] +} + +# Test 13: Minimum node count cannot exceed the maximum node count +run "reject_min_nodes_above_max_nodes" { + command = plan + + variables { + resource_prefix = run.setup_tests.resource_prefix + environment = "test" + location = run.setup_tests.location + resource_group = run.setup_tests.resource_group + application_insights = run.setup_tests.application_insights + key_vault = run.setup_tests.key_vault + storage_account = run.setup_tests.storage_account + acr = run.setup_tests.acr + virtual_network = run.setup_tests.virtual_network + + compute_cluster_min_nodes = 3 + compute_cluster_max_nodes = 2 + } + + expect_failures = [ + data.azurerm_client_config.current, + ] +}