diff --git a/CHANGELOG.md b/CHANGELOG.md index 057e38a..b9522db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.0.1 - 2026-09-16 + +- Validate module inputs and access rules before provider API requests. +- Reject invalid permissions and conflicting settings without changing resource addresses. +- Preserve provider defaults for optional values. + ## 1.0.0 - 2026-09-09 - Declare the module contract stable. The inputs, outputs, resource addresses, and provider diff --git a/README.md b/README.md index 5021dee..ccd59a6 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The resource addresses above are part of the DataTF import contract. Do not rena ```hcl module "instance_pool" { source = "536tech/instance-pool/databricks" - version = "1.0.0" + version = "1.0.1" name = "shared-pool" node_type_id = "Standard_DS3_v2" @@ -70,6 +70,19 @@ The workspace pattern module checks the complete DataTF contract and its integra [Apache-2.0](LICENSE). +## Input safeguards + +The module rejects blank required names and invalid access inputs during the plan. +Cross-input preconditions preserve the Terraform 1.5 minimum and existing resource addresses. +A null or zero `max_capacity` keeps the provider's unlimited-capacity behavior. +Null remains valid for inputs where the provider supplies a default. +Provider and API checks still apply. These checks do not prove complete permission visibility. + +Each permission needs exactly one nonblank principal and a supported resource-specific permission level. +`databricks_permissions` manages the object's permission set. Keep one state owner for that set. +Empty permissions omit the permission resource. +See [provider permission semantics](https://github.com/databricks/terraform-provider-databricks/blob/v1.130.0/docs/resources/permissions.md). + ## Requirements diff --git a/main.tf b/main.tf index 6e07804..653d679 100644 --- a/main.tf +++ b/main.tf @@ -1,4 +1,11 @@ resource "databricks_instance_pool" "this" { + lifecycle { + precondition { + condition = var.max_capacity == null ? true : (var.max_capacity == 0 || var.max_capacity >= coalesce(var.min_idle_instances, 0)) + error_message = "A positive max_capacity must be at least min_idle_instances." + } + } + instance_pool_name = var.name node_type_id = var.node_type_id min_idle_instances = var.min_idle_instances diff --git a/tests/module.tftest.hcl b/tests/module.tftest.hcl index 5c942c9..a89b7d3 100644 --- a/tests/module.tftest.hcl +++ b/tests/module.tftest.hcl @@ -47,3 +47,76 @@ run "without_access" { error_message = "Empty access must omit the access resources." } } + +run "reject_blank_name" { + command = plan + variables { + name = " " + } + expect_failures = [var.name] +} + +run "reject_missing_principal" { + command = plan + variables { + permissions = [{ permission_level = "CAN_ATTACH_TO" }] + } + expect_failures = [var.permissions] +} + +run "reject_multiple_principals" { + command = plan + variables { + permissions = [{ permission_level = "CAN_ATTACH_TO", user_name = "user@example.com", group_name = "readers" }] + } + expect_failures = [var.permissions] +} + +run "reject_blank_principal" { + command = plan + variables { + permissions = [{ permission_level = "CAN_ATTACH_TO", group_name = " " }] + } + expect_failures = [var.permissions] +} + +run "reject_invalid_permission" { + command = plan + variables { + permissions = [{ permission_level = "INVALID", group_name = "readers" }] + } + expect_failures = [var.permissions] +} + +run "reject_negative_idle" { + command = plan + variables { + min_idle_instances = -1 + } + expect_failures = [var.min_idle_instances] +} + +run "reject_capacity_below_idle" { + command = plan + variables { + max_capacity = 1 + min_idle_instances = 2 + } + expect_failures = [databricks_instance_pool.this] +} + +run "accept_unlimited_zero_capacity" { + command = plan + variables { + max_capacity = 0 + min_idle_instances = 2 + } +} + +run "accept_provider_defaults" { + command = plan + variables { + min_idle_instances = null + enable_elastic_disk = null + } +} diff --git a/variables.tf b/variables.tf index 315c458..32edfc0 100644 --- a/variables.tf +++ b/variables.tf @@ -1,21 +1,44 @@ variable "name" { description = "Instance pool name." type = string + nullable = false + + validation { + condition = try(length(trimspace(var.name)) > 0, false) + error_message = "name must not be empty or blank." + } } variable "node_type_id" { description = "Azure VM size for the pool, for example Standard_DS3_v2." type = string + nullable = false + + validation { + condition = try(length(trimspace(var.node_type_id)) > 0, false) + error_message = "node_type_id must not be empty or blank." + } } variable "min_idle_instances" { description = "Instances the pool keeps ready." type = number + + validation { + condition = var.min_idle_instances == null ? true : try(var.min_idle_instances >= 0 && floor(var.min_idle_instances) == var.min_idle_instances, false) + error_message = "min_idle_instances must be an integer of at least 0." + } } variable "idle_instance_autotermination_minutes" { description = "Minutes an idle instance stays in the pool above min_idle_instances." type = number + nullable = false + + validation { + condition = try(var.idle_instance_autotermination_minutes >= 0 && floor(var.idle_instance_autotermination_minutes) == var.idle_instance_autotermination_minutes, false) + error_message = "idle_instance_autotermination_minutes must be an integer of at least 0." + } } variable "enable_elastic_disk" { @@ -32,6 +55,10 @@ variable "max_capacity" { description = "Maximum number of instances in the pool." type = number default = null + validation { + condition = var.max_capacity == null ? true : try(var.max_capacity >= 0 && floor(var.max_capacity) == var.max_capacity, false) + error_message = "max_capacity must be null or a nonnegative integer; zero means no limit." + } } variable "custom_tags" { @@ -61,5 +88,18 @@ variable "permissions" { service_principal_name = optional(string) })) - default = [] + default = [] + nullable = false + + validation { + condition = try(alltrue([for permission in var.permissions : + length([for principal in [permission.group_name, permission.user_name, permission.service_principal_name] : + principal if principal != null + ]) == 1 && + alltrue([for principal in [permission.group_name, permission.user_name, permission.service_principal_name] : + principal == null ? true : length(trimspace(principal)) > 0 + ]) && contains(["CAN_ATTACH_TO", "CAN_MANAGE"], permission.permission_level) + ]), false) + error_message = "Each permission needs exactly one nonblank principal and a supported level: CAN_ATTACH_TO, CAN_MANAGE." + } }