Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The resource addresses above are part of the DataTF import contract. Do not rena
```hcl
module "warehouse" {
source = "536tech/sql-warehouse/databricks"
version = "1.0.0"
version = "1.0.1"

name = "Analytics WH"
cluster_size = "Small"
Expand Down Expand Up @@ -70,6 +70,18 @@ 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.
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).

<!-- BEGIN_TF_DOCS -->
## Requirements

Expand Down
7 changes: 7 additions & 0 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ locals {
}

resource "databricks_sql_endpoint" "this" {
lifecycle {
precondition {
condition = coalesce(var.max_num_clusters, 1) >= coalesce(var.min_num_clusters, 1)
error_message = "max_num_clusters must be at least min_num_clusters."
}
}

name = var.name
cluster_size = var.cluster_size
min_num_clusters = var.min_num_clusters
Expand Down
68 changes: 68 additions & 0 deletions tests/module.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,71 @@ 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_USE" }]
}
expect_failures = [var.permissions]
}

run "reject_multiple_principals" {
command = plan
variables {
permissions = [{ permission_level = "CAN_USE", user_name = "user@example.com", group_name = "readers" }]
}
expect_failures = [var.permissions]
}

run "reject_blank_principal" {
command = plan
variables {
permissions = [{ permission_level = "CAN_USE", 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_reversed_cluster_limits" {
command = plan
variables {
min_num_clusters = 4
}
expect_failures = [databricks_sql_endpoint.this]
}

run "reject_group_owner" {
command = plan
variables {
permissions = [{ permission_level = "IS_OWNER", group_name = "readers" }]
}
expect_failures = [var.permissions]
}

run "accept_provider_defaults" {
command = plan
variables {
min_num_clusters = null
max_num_clusters = null
auto_stop_mins = null
warehouse_type = null
enable_photon = null
enable_serverless_compute = null
}
}
57 changes: 56 additions & 1 deletion variables.tf
Original file line number Diff line number Diff line change
@@ -1,31 +1,63 @@
variable "name" {
description = "SQL warehouse name."
type = string
nullable = false

validation {
condition = try(length(trimspace(var.name)) > 0, false)
error_message = "name must not be empty or blank."
}
}

variable "cluster_size" {
description = "Warehouse size, for example 2X-Small, Small, or Medium."
type = string
nullable = false

validation {
condition = try(length(trimspace(var.cluster_size)) > 0, false)
error_message = "cluster_size must not be empty or blank."
}
}

variable "min_num_clusters" {
description = "Minimum number of clusters the warehouse runs."
type = number

validation {
condition = var.min_num_clusters == null ? true : try(var.min_num_clusters >= 1 && floor(var.min_num_clusters) == var.min_num_clusters, false)
error_message = "min_num_clusters must be an integer of at least 1."
}
}

variable "max_num_clusters" {
description = "Maximum number of clusters the warehouse scales to."
type = number

validation {
condition = var.max_num_clusters == null ? true : try(var.max_num_clusters >= 1 && floor(var.max_num_clusters) == var.max_num_clusters, false)
error_message = "max_num_clusters must be an integer of at least 1."
}
}

variable "auto_stop_mins" {
description = "Minutes of inactivity before the warehouse stops. 0 disables auto stop."
type = number

validation {
condition = var.auto_stop_mins == null ? true : try(var.auto_stop_mins >= 0 && floor(var.auto_stop_mins) == var.auto_stop_mins, false)
error_message = "auto_stop_mins must be an integer of at least 0."
}
}

variable "warehouse_type" {
description = "Warehouse type: CLASSIC or PRO."
type = string

validation {
condition = var.warehouse_type == null ? true : contains(["CLASSIC", "PRO"], var.warehouse_type)
error_message = "warehouse_type must be CLASSIC or PRO."
}
}

variable "enable_photon" {
Expand All @@ -42,6 +74,10 @@ variable "spot_instance_policy" {
description = "Spot policy: COST_OPTIMIZED or RELIABILITY_OPTIMIZED."
type = string
default = null
validation {
condition = var.spot_instance_policy == null ? true : contains(["COST_OPTIMIZED", "RELIABILITY_OPTIMIZED"], var.spot_instance_policy)
error_message = "spot_instance_policy must be COST_OPTIMIZED or RELIABILITY_OPTIMIZED."
}
}

variable "tags" {
Expand All @@ -60,5 +96,24 @@ 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_USE", "CAN_MONITOR", "CAN_MANAGE", "CAN_VIEW", "IS_OWNER"], permission.permission_level)
]), false)
error_message = "Each permission needs exactly one nonblank principal and a supported level: CAN_USE, CAN_MONITOR, CAN_MANAGE, CAN_VIEW, IS_OWNER."
}
validation {
condition = alltrue([for permission in var.permissions :
permission.permission_level != "IS_OWNER" || permission.group_name == null
]) && length([for permission in var.permissions : permission if permission.permission_level == "IS_OWNER"]) <= 1
error_message = "A warehouse can have at most one explicit owner; a group cannot own a warehouse."
}
}