diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index fedc218..e30c184 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,3 +1,11 @@ +## When writing or updating Terraform code +Use the instructions [here](./github/instructions/devbox-tf.instructions.md) for writing/updating Terraform code. + +## When updating code +Document the changes made in the [CHANGELOG.md](CHANGELOG.md) file, including: +- A brief description of the change. +- If it is a bug fix, feature, or improvement. +- Include assessment if this is a breaking change or not. ## MCP Server Instructions If the respective MCP server exists, follow these instructions: - Terraform MCP Server: provides seamless integration with Terraform Registry APIs, enabling advanced automation and interaction capabilities for Infrastructure as Code (IaC) development. diff --git a/.github/instructions/devbox-tf.instructions.md b/.github/instructions/devbox-tf.instructions.md new file mode 100644 index 0000000..8e6ebbc --- /dev/null +++ b/.github/instructions/devbox-tf.instructions.md @@ -0,0 +1,311 @@ +# Devfactory Project - Terraform Implementation Guidelines + +## Quick Reference Summary + +- **Provider:** AzAPI v2.4.0 only +- **Run Location:** Always from project root +- **Sensitive Data:** Never hardcode credentials or subscription IDs +- **Module Verification:** Always check resource arguments against latest provider docs +- **Variable Typing:** Use strong types, descriptions, and constraints +- **Examples:** Every resource/module must have an example in `/examples/` +- **Validation:** Run `terraform fmt` and `terraform validate` before commit + +--- + +## DO +- Use only AzAPI provider version 2.4.0 +- Place all resource modules in `/modules/` and examples in `/examples/` +- Use dynamic blocks for optional/flexible config +- Use nested maps and strongly-typed objects for variables +- Use `locals` for preprocessing and complex parameter objects +- Use `try()` for error handling and parameter fallbacks +- Merge tags (resource-specific + global) +- Use `azurecaf_name` for naming conventions +- Add input validation in `variables.tf` +- Add a working example for every resource/module +- Update module README.md with usage and examples +- Reference provider docs for every resource: https://registry.terraform.io/providers/Azure/azapi/2.4.0/docs/resources/ +- Use the Azure MCP server to find the latest API version, detailed schema, and attributes for each resource implemented. + +## DO NOT +- Do not embed subscription IDs or credentials in code/config +- Do not use untyped or weakly-typed variables +- Do not skip example creation for new/changed resources +- Do not commit without running `terraform fmt` and `terraform validate` +- Do not use provider versions other than 2.4.0 + +--- + +## Repository Structure +- `/modules/`: Resource-specific modules (storage, networking, compute, etc.) +- `/examples/`: Example implementations/configurations for each module +- `/docs/`: Project documentation and conventions + +--- + +## Key Module Patterns +- Each Azure resource type in its own module folder +- Use dynamic blocks for optional/flexible config +- Input variables: nested maps, strongly-typed objects + +--- + +## Code Conventions +- Each module: `module.tf`, `variables.tf`, `output.tf` +- Use `locals` for preprocessing/complex objects +- Use `try()` for optional/defaulted params +- Merge tags (resource + global) +- Use `azurecaf_name` for naming + +--- + +## Common Patterns + +**Resource Creation:** +```hcl +resource "azurecaf_name" "this" { + name = var.name + resource_type = "general" + prefixes = var.global_settings.prefixes + random_length = var.global_settings.random_length + clean_input = true + passthrough = var.global_settings.passthrough + use_slug = var.global_settings.use_slug +} + +resource "azapi_resource" "this" { + name = azurecaf_name.this.result + location = var.location + parent_id = var.parent_id + type = var.resource_type + api_version = var.api_version + tags = local.tags + # Resource-specific properties +} +``` + +**Variable Structure and Typing:** +```hcl +variable "resource" { + description = "Configuration object for the resource" + type = object({ + name = string + description = optional(string) + location = optional(string) + tags = optional(map(string)) + # Resource-specific properties + sku = object({ + name = string + tier = string + capacity = optional(number) + }) + security = optional(object({ + enable_rbac = optional(bool, false) + network_acls = optional(list(object({ + default_action = string + bypass = string + ip_rules = optional(list(string)) + }))) + })) + }) +} + +variable "global_settings" { + description = "Global settings object for naming conventions and standard parameters" + type = object({ + prefixes = list(string) + random_length = number + passthrough = bool + use_slug = bool + environment = string + regions = map(string) + }) +} +``` + +**Module Integration with Strong Typing:** +```hcl +module "resource" { + source = "./modules/resource" + for_each = try(var.settings.resources, {}) + global_settings = var.global_settings + settings = each.value + resource_group_name = var.resource_group_name + location = try(each.value.location, var.location) + tags = try(each.value.tags, {}) + depends_on = [ + module.resource_groups + ] +} +``` + +**Variable Validation:** +```hcl +variable "environment_type" { + description = "The type of environment to deploy (dev, test, prod)" + type = string + validation { + condition = contains(["dev", "test", "prod"], var.environment_type) + error_message = "Environment type must be one of: dev, test, prod." + } +} + +variable "allowed_ip_ranges" { + description = "List of allowed IP ranges in CIDR format" + type = list(string) + validation { + condition = alltrue([for ip in var.allowed_ip_ranges : can(cidrhost(ip, 0))]) + error_message = "All elements must be valid CIDR notation IP addresses." + } +} +``` + +**Dynamic Blocks Implementation:** +```hcl +resource "azurerm_key_vault" "kv" { + # ... other properties ... + dynamic "network_acls" { + for_each = try(var.settings.network, null) != null ? [var.settings.network] : [] + content { + default_action = network_acls.value.default_action + bypass = network_acls.value.bypass + ip_rules = try(network_acls.value.ip_rules, []) + virtual_network_subnet_ids = try(network_acls.value.subnets, []) + } + } +} +``` + +--- + +## Example Patterns +- Add an example for each feature under `/examples/_feature_name/simple_case/configuration.tfvars` +- Include a `global_settings` block for naming +- Define resources in nested map structure +- Link dependent resources using parent key reference + +--- + +## Execution Instructions +- Run from root: + ```bash + terraform init + terraform plan -var-file=examples/_feature_name/simple_case/configuration.tfvars + terraform apply -var-file=examples/_feature_name/simple_case/configuration.tfvars + ``` +- Destroy resources: + ```bash + terraform destroy -var-file=examples/_feature_name/simple_case/configuration.tfvars + ``` +- Set authentication via environment variables (never in code): + ```bash + export ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv) + export ARM_CLIENT_ID="your-client-id" + export ARM_CLIENT_SECRET="your-client-secret" + export ARM_TENANT_ID="your-tenant-id" + ``` + +--- + +## Testing & Validation +- Add input validation in `variables.tf` +- Add a working example in `/examples/` +- Update module README.md with usage and examples +- Run `terraform validate` and `terraform fmt` before commit + +--- + +## Common Helper Patterns +- `try(var.settings.property, default_value)` for fallbacks +- `lookup(map, key, default)` for map access +- `can(tostring(var.something))` for conditional evaluation +- `for_each = toset(var.subnet_names)` for multiple resources +- `coalesce(var.custom_name, local.default_name)` for first non-null value + +--- + +## Azure API Property Naming and Data Type Conventions + +### DevCenter API Specifics (API Version 2025-04-01-preview) +When working with Azure DevCenter resources, be aware of these critical naming and data type requirements: + +**Property Naming Convention:** +- Azure DevCenter API requires camelCase property names in the request body +- Terraform variables use snake_case for consistency +- Always map snake_case variable names to camelCase API properties + +**Common Property Mappings:** +```hcl +# Variable (snake_case) → API Property (camelCase) +install_azure_monitor_agent_enable_installation → installAzureMonitorAgentEnableStatus +microsoft_hosted_network_enable_status → microsoftHostedNetworkEnableStatus +catalog_item_sync_enable_status → catalogItemSyncEnableStatus +``` + +**Data Type Requirements:** +- Many DevCenter "enable" properties expect string values, not booleans +- Use `"Enabled"` or `"Disabled"` instead of `true`/`false` +- Always verify expected data types in Azure API documentation + +**Example Implementation:** +```hcl +# Variable definition (snake_case, string type) +variable "dev_box_provisioning_settings" { + type = object({ + install_azure_monitor_agent_enable_installation = optional(string, "Enabled") + }) +} + +# API body mapping (camelCase) +body = { + properties = { + devBoxProvisioningSettings = { + installAzureMonitorAgentEnableStatus = try(var.settings.dev_box_provisioning_settings.install_azure_monitor_agent_enable_installation, "Enabled") + } + } +} +``` + +**Validation Approach:** +- Always run `terraform plan` to validate API compatibility +- Check Azure API documentation for exact property names and types +- Use Azure MCP server tools to verify latest API schemas +- Test with actual API calls when implementing new resource properties +- Also test changes with TFLint, `terraform fmt` and `terraform validate` + +--- + +## Security Best Practices +- Use `sensitive = true` for secret variables +- Never hardcode credentials +- Use least privilege IAM roles +- Use NSGs and private endpoints +- Store state files securely with locking +- Use key vaults for sensitive values + +--- + +## Documentation Reference +- See README.md in each module +- See `/examples/` for implementation +- See `docs/conventions.md` for standards +- See `docs/module_guide.md` for module development +- Always verify resource arguments at: https://registry.terraform.io/providers/Azure/azapi/2.4.0/docs/resources/ + +--- + +## AI Assistant Prompt Guidance +- When asked to generate Terraform code, always: + - Use AzAPI provider v2.4.0 + - Use strong typing and validation for variables + - Add an example in `/examples/` + - Reference provider documentation for all arguments + - Never include credentials or subscription IDs in code + - Use dynamic blocks and locals as shown above + - Follow naming conventions with `azurecaf_name` + - Add input validation and documentation + - Use only patterns and helpers listed above + +--- + +This dev container includes the Azure CLI, GitHub CLI, Terraform CLI, TFLint, and Terragrunt pre-installed and available on the PATH, along with the Terraform and Azure extensions for development. diff --git a/.gitignore b/.gitignore index 27d5c01..7db4396 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ # Environment Variables .env +# User generated configuration files (the configuration directory) +/configurations + # User-specific files *.rsuser *.suo diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 936753e..fa8c7f4 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -19,6 +19,10 @@ "server", "start" ] + }, + "github": { + "type": "http", + "url": "https://api.githubcopilot.com/mcp/" } } } \ No newline at end of file diff --git a/CHANGES_SUMMARY.md b/CHANGES_SUMMARY.md index 61dfbb5..f1c2a4b 100644 --- a/CHANGES_SUMMARY.md +++ b/CHANGES_SUMMARY.md @@ -4,6 +4,49 @@ This document summarizes the updates made to the Azure DevCenter module to implement the 2025-04-01-preview API version and fix the identity block placement. +## Latest Changes (June 19, 2025) + +### Merge Conflict Resolution +- **Fixed**: Resolved merge conflicts in PR #24 (devboxpools branch) +- **Conflict Location**: `tests/run_tests.sh` - between dynamic test discovery and hardcoded test list +- **Resolution**: Preserved enhanced dynamic test discovery functionality while merging upstream changes +- **Merged Changes**: Updated from upstream main: + - `.devcontainer/devcontainer.json` - DevContainer configuration updates + - `.vscode/mcp.json` - MCP server configuration + - `README.md` - Documentation improvements + - `docs/getting_started.md` - Getting started guide updates +- **Type**: Bug fix and merge resolution +- **Breaking Change**: No + +### TFLint Compliance Fixes +- **Fixed**: Added missing `required_version = ">= 1.9.0"` to Terraform blocks + - `modules/dev_center_project_pool/module.tf` + - `modules/dev_center_project_pool_schedule/module.tf` +- **Fixed**: Added TFLint ignore comment for unused but documented variable + - `modules/dev_center_project_pool/variables.tf` - `resource_group_id` variable +- **Result**: All modules now pass TFLint validation without warnings +- **Type**: Code quality improvement +- **Breaking Change**: No + +### Test Runner Update +- **Fixed**: Updated `tests/run_tests.sh` to include all missing test directories +- **Enhanced**: Made test discovery dynamic instead of hardcoded + - Automatically discovers unit test directories in `tests/unit/` + - Automatically discovers integration test directories in `tests/integration/` + - Validates directories contain test files (*.tftest.hcl or *.tf) + - Shows discovered test directories before execution +- **Fixed**: Added proper root configuration initialization + - Initializes root Terraform configuration before running tests + - Properly handles module dependencies for tests that reference root configuration + - Cleans and re-initializes test directories when needed +- **Added**: Missing unit test directories: + - `dev_center_project_pool` + - `dev_center_project_pool_schedule` + - `key_vault` +- **Result**: All 9 test suites now pass (41 individual test cases) +- **Type**: Improvement and bug fix +- **Breaking Change**: No + ## Issues Addressed 1. **API Version Update**: Updated from `2025-02-01` to `2025-04-01-preview` diff --git a/dev_center_project_pool_schedules.tf b/dev_center_project_pool_schedules.tf new file mode 100644 index 0000000..4c0f424 --- /dev/null +++ b/dev_center_project_pool_schedules.tf @@ -0,0 +1,19 @@ +# DevCenter Project Pool Schedules Configuration +# This file instantiates the dev_center_project_pool_schedule module +# Schedules are managed separately from pools for better reusability + +# Create DevCenter project pool schedules +module "dev_center_project_pool_schedules" { + source = "./modules/dev_center_project_pool_schedule" + + for_each = var.dev_center_project_pool_schedules + + # Reference the pool ID - use provided ID or reference from pool module + dev_center_project_pool_id = lookup(each.value, "dev_center_project_pool_id", null) != null ? each.value.dev_center_project_pool_id : module.dev_center_project_pools[each.value.dev_center_project_pool.key].id + + # Schedule configuration + schedule = each.value.schedule + + # Pass through global settings + global_settings = var.global_settings +} diff --git a/dev_center_project_pools.tf b/dev_center_project_pools.tf new file mode 100644 index 0000000..1e65d3e --- /dev/null +++ b/dev_center_project_pools.tf @@ -0,0 +1,15 @@ +# DevCenter Project Pools module instantiation +module "dev_center_project_pools" { + source = "./modules/dev_center_project_pool" + for_each = try(var.dev_center_project_pools, {}) + + global_settings = var.global_settings + pool = { + name = each.value.name + dev_box_definition_name = module.dev_center_dev_box_definitions[each.value.dev_box_definition_name].name + description = lookup(each.value, "description", null) + } + dev_center_project_id = lookup(each.value, "dev_center_project_id", null) != null ? each.value.dev_center_project_id : module.dev_center_projects[each.value.dev_center_project.key].id + resource_group_id = lookup(each.value, "resource_group_id", null) != null ? each.value.resource_group_id : module.resource_groups[each.value.resource_group.key].id + location = lookup(each.value, "region", null) != null ? each.value.region : module.resource_groups[each.value.resource_group.key].location +} diff --git a/examples/dev_center_project_pool/enhanced_case/configuration.tfvars b/examples/dev_center_project_pool/enhanced_case/configuration.tfvars new file mode 100644 index 0000000..c99eb9a --- /dev/null +++ b/examples/dev_center_project_pool/enhanced_case/configuration.tfvars @@ -0,0 +1,185 @@ +global_settings = { + prefixes = ["enterprise"] + random_length = 5 + passthrough = false + use_slug = true + tags = { + environment = "production" + created_by = "terraform" + cost_center = "engineering" + project = "devfactory" + } +} + +resource_groups = { + rg_core = { + name = "devfactory-core-enhanced" + region = "eastus" + tags = { + workload = "core-infrastructure" + } + } + rg_dev = { + name = "devfactory-development-enhanced" + region = "eastus" + tags = { + workload = "development-environments" + } + } +} + +dev_centers = { + enterprise_devcenter = { + name = "enterprise-devcenter" + resource_group = { + key = "rg_core" + } + identity = { + type = "SystemAssigned" + } + display_name = "Enterprise Development Center" + dev_box_provisioning_settings = { + install_azure_monitor_agent_enable_installation = "Enabled" + } + tags = { + tier = "enterprise" + } + } +} + +dev_center_dev_box_definitions = { + standard_dev = { + name = "standard-developer-vm" + dev_center = { + key = "enterprise_devcenter" + } + display_name = "Standard Developer Machine" + image_reference = { + id = "/subscriptions/12345678-1234-5678-9012-123456789012/resourceGroups/rg-images/providers/Microsoft.Compute/galleries/enterpriseGallery/images/vs2022-enterprise/versions/latest" + } + sku = { + name = "general_i_8c32gb256ssd_v2" + } + os_storage_type = "ssd_512gb" + hibernate_support = "Enabled" + } + + premium_dev = { + name = "premium-developer-vm" + dev_center = { + key = "enterprise_devcenter" + } + display_name = "Premium Developer Machine" + image_reference = { + id = "/subscriptions/12345678-1234-5678-9012-123456789012/resourceGroups/rg-images/providers/Microsoft.Compute/galleries/enterpriseGallery/images/vs2022-enterprise-premium/versions/latest" + } + sku = { + name = "general_i_16c64gb512ssd_v2" + } + os_storage_type = "ssd_1024gb" + hibernate_support = "Enabled" + } +} + +dev_center_projects = { + frontend_team = { + name = "frontend-development" + dev_center = { + key = "enterprise_devcenter" + } + resource_group = { + key = "rg_dev" + } + display_name = "Frontend Development Team" + description = "Development environment for frontend team with multiple pool configurations" + maximum_dev_boxes_per_user = 3 + + tags = { + team = "frontend" + tier = "premium" + } + } + + backend_team = { + name = "backend-development" + dev_center = { + key = "enterprise_devcenter" + } + resource_group = { + key = "rg_dev" + } + display_name = "Backend Development Team" + description = "Development environment for backend team" + maximum_dev_boxes_per_user = 2 + + tags = { + team = "backend" + tier = "standard" + } + } +} + +dev_center_project_pools = { + frontend_standard = { + name = "frontend-standard-pool" + dev_center_project = { + key = "frontend_team" + } + dev_box_definition_name = "standard-developer-vm" + display_name = "Frontend Standard Development Pool" + local_administrator_enabled = true + network_connection_name = "default" + stop_on_disconnect_grace_period_minutes = 90 + license_type = "Windows_Client" + virtual_network_type = "Managed" + single_sign_on_status = "Enabled" + + tags = { + module = "dev_center_project_pool" + tier = "standard" + team = "frontend" + } + } + + frontend_premium = { + name = "frontend-premium-pool" + dev_center_project = { + key = "frontend_team" + } + dev_box_definition_name = "premium-developer-vm" + display_name = "Frontend Premium Development Pool" + local_administrator_enabled = true + network_connection_name = "default" + stop_on_disconnect_grace_period_minutes = 120 + license_type = "Windows_Client" + virtual_network_type = "Managed" + single_sign_on_status = "Enabled" + + tags = { + module = "dev_center_project_pool" + tier = "premium" + team = "frontend" + } + } + + backend_standard = { + name = "backend-development-pool" + dev_center_project = { + key = "backend_team" + } + dev_box_definition_name = "standard-developer-vm" + display_name = "Backend Development Pool" + local_administrator_enabled = false + network_connection_name = "default" + stop_on_disconnect_grace_period_minutes = 60 + license_type = "Windows_Client" + virtual_network_type = "Managed" + single_sign_on_status = "Disabled" + + tags = { + module = "dev_center_project_pool" + tier = "standard" + team = "backend" + } + } +} diff --git a/examples/dev_center_project_pool/simple_case/configuration.tfvars b/examples/dev_center_project_pool/simple_case/configuration.tfvars new file mode 100644 index 0000000..8ae4292 --- /dev/null +++ b/examples/dev_center_project_pool/simple_case/configuration.tfvars @@ -0,0 +1,87 @@ +global_settings = { + prefixes = ["dev"] + random_length = 3 + passthrough = false + use_slug = true + tags = { + environment = "demo" + created_by = "terraform" + } +} + +resource_groups = { + rg1 = { + name = "devfactory-pool-simple" + region = "eastus" + } +} + +dev_centers = { + devcenter1 = { + name = "simple-devcenter-pool" + resource_group = { + key = "rg1" + } + identity = { + type = "SystemAssigned" + } + } +} + +dev_center_dev_box_definitions = { + definition1 = { + name = "simple-definition" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + image_reference = { + id = "/subscriptions/12345678-1234-5678-9012-123456789012/resourceGroups/rg-images/providers/Microsoft.Compute/galleries/myGallery/images/myImage/versions/1.0.0" + } + sku = { + name = "general_i_8c32gb256ssd_v2" + } + os_storage_type = "ssd_1024gb" + } +} + +dev_center_projects = { + project1 = { + name = "simple-project-pool" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + description = "Simple development project for pool testing" + maximum_dev_boxes_per_user = 2 + } +} + +dev_center_project_pools = { + pool1 = { + name = "development-pool" + dev_center_project = { + key = "project1" + } + resource_group = { + key = "rg1" + } + dev_box_definition_name = "definition1" + display_name = "Development Pool" + local_administrator_enabled = false + network_connection_name = "default" + stop_on_disconnect_grace_period_minutes = 60 + license_type = "Windows_Client" + virtual_network_type = "Managed" + single_sign_on_status = "Disabled" + + tags = { + module = "dev_center_project_pool" + tier = "basic" + } + } +} diff --git a/examples/dev_center_project_pool_schedule/enhanced_case/configuration.tfvars b/examples/dev_center_project_pool_schedule/enhanced_case/configuration.tfvars new file mode 100644 index 0000000..6a8e3d8 --- /dev/null +++ b/examples/dev_center_project_pool_schedule/enhanced_case/configuration.tfvars @@ -0,0 +1,151 @@ +global_settings = { + prefixes = ["enterprise"] + random_length = 5 + passthrough = false + use_slug = true + tags = { + environment = "production" + created_by = "terraform" + cost_center = "engineering" + project = "devfactory" + } +} + +resource_groups = { + rg_core = { + name = "devfactory-core-schedule" + region = "eastus" + tags = { + workload = "core-infrastructure" + } + } + rg_dev = { + name = "devfactory-development-schedule" + region = "eastus" + tags = { + workload = "development-environments" + } + } +} + +dev_centers = { + enterprise_devcenter = { + name = "enterprise-devcenter-schedule" + resource_group = { + key = "rg_core" + } + identity = { + type = "SystemAssigned" + } + display_name = "Enterprise Development Center with Scheduling" + dev_box_provisioning_settings = { + install_azure_monitor_agent_enable_installation = "Enabled" + } + } +} + +dev_center_dev_box_definitions = { + standard_dev = { + name = "standard-developer-vm" + dev_center = { + key = "enterprise_devcenter" + } + display_name = "Standard Developer Machine" + image_reference = { + id = "/subscriptions/12345678-1234-5678-9012-123456789012/resourceGroups/rg-images/providers/Microsoft.Compute/galleries/enterpriseGallery/images/vs2022-enterprise/versions/latest" + } + sku = { + name = "general_i_8c32gb256ssd_v2" + } + os_storage_type = "ssd_512gb" + hibernate_support = "Enabled" + } +} + +dev_center_projects = { + development_team = { + name = "development-team" + dev_center = { + key = "enterprise_devcenter" + } + resource_group = { + key = "rg_dev" + } + display_name = "Development Team with Auto-Scheduling" + description = "Development environment with comprehensive auto-scheduling policies" + maximum_dev_boxes_per_user = 2 + } +} + +dev_center_project_pools = { + development_pool = { + name = "development-pool" + dev_center_project = { + key = "development_team" + } + dev_box_definition_name = "standard-developer-vm" + display_name = "Development Pool with Scheduling" + local_administrator_enabled = true + network_connection_name = "default" + stop_on_disconnect_grace_period_minutes = 90 + license_type = "Windows_Client" + virtual_network_type = "Managed" + single_sign_on_status = "Enabled" + } +} + +dev_center_project_pool_schedules = { + weekday_shutdown = { + name = "weekday-auto-shutdown" + dev_center_project_pool = { + key = "development_pool" + } + type = "StopDevBox" + frequency = "Daily" + time = "18:00" + time_zone = "W. Europe Standard Time" + state = "Enabled" + + tags = { + module = "dev_center_project_pool_schedule" + schedule = "weekday-shutdown" + policy = "cost-optimization" + } + } + + morning_startup = { + name = "morning-auto-startup" + dev_center_project_pool = { + key = "development_pool" + } + type = "StartDevBox" + frequency = "Daily" + time = "08:00" + time_zone = "W. Europe Standard Time" + state = "Enabled" + + tags = { + module = "dev_center_project_pool_schedule" + schedule = "morning-startup" + policy = "productivity-optimization" + } + } + + weekend_shutdown = { + name = "weekend-extended-shutdown" + dev_center_project_pool = { + key = "development_pool" + } + type = "StopDevBox" + frequency = "Weekly" + time = "16:00" + time_zone = "W. Europe Standard Time" + state = "Enabled" + + tags = { + module = "dev_center_project_pool_schedule" + schedule = "weekend-shutdown" + policy = "weekend-cost-optimization" + } + } +} diff --git a/examples/dev_center_project_pool_schedule/simple_case/configuration.tfvars b/examples/dev_center_project_pool_schedule/simple_case/configuration.tfvars new file mode 100644 index 0000000..e931901 --- /dev/null +++ b/examples/dev_center_project_pool_schedule/simple_case/configuration.tfvars @@ -0,0 +1,95 @@ +global_settings = { + prefixes = ["dev"] + random_length = 3 + passthrough = false + use_slug = true + tags = { + environment = "demo" + created_by = "terraform" + } +} + +resource_groups = { + rg1 = { + name = "devfactory-schedule-simple" + region = "eastus" + } +} + +dev_centers = { + devcenter1 = { + name = "simple-devcenter-schedule" + resource_group = { + key = "rg1" + } + identity = { + type = "SystemAssigned" + } + } +} + +dev_center_dev_box_definitions = { + definition1 = { + name = "simple-definition" + dev_center = { + key = "devcenter1" + } + image_reference = { + id = "/subscriptions/12345678-1234-5678-9012-123456789012/resourceGroups/rg-images/providers/Microsoft.Compute/galleries/myGallery/images/myImage/versions/1.0.0" + } + sku = { + name = "general_i_8c32gb256ssd_v2" + } + os_storage_type = "ssd_1024gb" + } +} + +dev_center_projects = { + project1 = { + name = "simple-project-schedule" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + description = "Simple development project for schedule testing" + maximum_dev_boxes_per_user = 2 + } +} + +dev_center_project_pools = { + pool1 = { + name = "development-pool" + dev_center_project = { + key = "project1" + } + dev_box_definition_name = "simple-definition" + display_name = "Development Pool" + local_administrator_enabled = false + network_connection_name = "default" + stop_on_disconnect_grace_period_minutes = 60 + license_type = "Windows_Client" + virtual_network_type = "Managed" + single_sign_on_status = "Disabled" + } +} + +dev_center_project_pool_schedules = { + schedule1 = { + name = "auto-shutdown" + dev_center_project_pool = { + key = "pool1" + } + type = "StopDevBox" + frequency = "Daily" + time = "18:00" + time_zone = "W. Europe Standard Time" + state = "Enabled" + + tags = { + module = "dev_center_project_pool_schedule" + tier = "basic" + } + } +} diff --git a/modules/dev_center_project_pool/README.md b/modules/dev_center_project_pool/README.md new file mode 100644 index 0000000..30e3ddf --- /dev/null +++ b/modules/dev_center_project_pool/README.md @@ -0,0 +1,133 @@ +# DevCenter Project Pool Module + +This module creates Azure DevCenter project pools using the AzAPI provider, following the official Microsoft documentation for API version `2025-04-01-preview`. + +## Features + +- **DevCenter Project Pools**: Creates project pools with DevBox definitions +- **Network Configuration**: Supports both Microsoft-hosted and custom network connections +- **Security Settings**: Configurable local administrator access and single sign-on +- **Stop on Disconnect**: Automatic shutdown when users disconnect for specified period +- **Flexible Licensing**: Support for Windows Client and Server licensing +- **Tag Management**: Comprehensive tagging with global and resource-specific tags +- **Modular Design**: Separate schedule management through dedicated schedule module + +## Usage + +```hcl +module "dev_center_project_pool" { + source = "./modules/dev_center_project_pool" + + global_settings = var.global_settings + pool = { + name = "my-dev-pool" + display_name = "My Development Pool" + dev_box_definition_name = "windows-vs-definition" + local_administrator_enabled = true + stop_on_disconnect_grace_period_minutes = 60 + + tags = { + environment = "dev" + purpose = "development" + } + } + + dev_center_project_id = "/subscriptions/.../projects/my-project" + location = "westeurope" + resource_group_id = "/subscriptions/.../resourceGroups/my-rg" +} + +# Separate schedule management +module "daily_shutdown_schedule" { + source = "./modules/dev_center_project_pool_schedule" + + dev_center_project_pool_id = module.dev_center_project_pool.id + + schedule = { + name = "daily-shutdown" + time = "22:00" + time_zone = "W. Europe Standard Time" + type = "StopDevBox" + state = "Enabled" + } + + global_settings = var.global_settings +} +``` +``` + +## Requirements + +| Name | Version | +|------|---------| +| azapi | ~> 2.4.0 | + +## Resources + +| Name | Type | +|------|------| +| [azapi_resource.dev_center_project_pool](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/azapi_resource) | resource | +| [azapi_resource.dev_center_project_pool_schedule](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/azapi_resource) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| dev_center_project_id | The ID of the DevCenter project | `string` | n/a | yes | +| global_settings | Global settings for the module | `object({...})` | `{}` | no | +| location | The Azure region where the pool will be deployed | `string` | n/a | yes | +| pool | DevCenter project pool configuration | `object({...})` | n/a | yes | +| resource_group_id | The ID of the resource group | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| dev_box_definition_name | The DevBox definition name used by this pool | +| id | The ID of the DevCenter project pool | +| local_administrator_enabled | Whether local administrator is enabled | +| name | The name of the DevCenter project pool | +| network_connection_name | The network connection name used by this pool | +| properties | The properties of the DevCenter project pool | +| resource_id | The full resource ID of the DevCenter project pool | +| schedule_ids | Map of schedule names to their resource IDs | +| tags | The tags applied to the DevCenter project pool | + +## Configuration Options + +### Pool Configuration + +- **name**: (Required) Name of the project pool +- **display_name**: (Optional) Display name shown in Azure portal +- **dev_box_definition_name**: (Required) Reference to the DevBox definition +- **local_administrator_enabled**: (Optional) Enable local admin rights (default: false) +- **network_connection_name**: (Optional) Network connection (default: "default" for Microsoft-hosted) +- **stop_on_disconnect_grace_period_minutes**: (Optional) Auto-shutdown delay (60-480 minutes, default: 60) +- **license_type**: (Optional) Windows licensing type (default: "Windows_Client") +- **virtual_network_type**: (Optional) Network type (default: "Managed") +- **single_sign_on_status**: (Optional) SSO configuration (default: "Disabled") + +### Schedule Configuration + +- **name**: (Required) Schedule name +- **type**: (Optional) Schedule type (default: "StopDevBox") +- **frequency**: (Optional) Schedule frequency (default: "Daily") +- **time**: (Required) Time in HH:MM format +- **time_zone**: (Required) IANA timezone (e.g., "Europe/Brussels") +- **state**: (Optional) Schedule state (default: "Enabled") + +## Best Practices + +1. **Cost Optimization**: Use separate schedule modules to automatically stop DevBoxes during non-working hours +2. **Security**: Enable local administrator only when necessary +3. **Network**: Use Microsoft-hosted network for simplicity unless custom networking is required +4. **Grace Period**: Set appropriate disconnect grace period (minimum 60 minutes) +5. **Tagging**: Apply consistent tags for cost tracking and resource management +6. **Modularity**: Use the separate schedule module for better reusability and management + +## Notes + +- This module uses the preview API version `2025-04-01-preview` +- Pool schedules are managed separately through the `dev_center_project_pool_schedule` module +- The module automatically handles tag merging between global and resource-specific tags +- Lifecycle rules ignore computed properties like `healthStatus` and `provisioningState` diff --git a/modules/dev_center_project_pool/module.tf b/modules/dev_center_project_pool/module.tf new file mode 100644 index 0000000..2267629 --- /dev/null +++ b/modules/dev_center_project_pool/module.tf @@ -0,0 +1,86 @@ +# DevCenter Project Pool Module +# This module creates Azure DevCenter project pools using the AzAPI provider +# Following Microsoft.DevCenter/projects/pools@2025-04-01-preview schema + +terraform { + required_version = ">= 1.9.0" + required_providers { + azapi = { + source = "Azure/azapi" + version = "~> 2.4.0" + } + azurecaf = { + source = "aztfmod/azurecaf" + version = "~> 1.2.0" + } + } +} + +locals { + tags = merge( + try(var.global_settings.tags, {}), + try(var.pool.tags, {}), + { + terraform-azapi-resource-type = "Microsoft.DevCenter/projects/pools" + terraform-azapi-version = "2025-04-01-preview" + } + ) +} + +resource "azurecaf_name" "project_pool" { + name = var.pool.name + resource_type = "azurerm_dev_center_project" + prefixes = var.global_settings.prefixes + random_length = var.global_settings.random_length + clean_input = true + passthrough = var.global_settings.passthrough + use_slug = var.global_settings.use_slug +} + +# DevCenter Project Pool Resource +resource "azapi_resource" "dev_center_project_pool" { + type = "Microsoft.DevCenter/projects/pools@2025-04-01-preview" + name = azurecaf_name.project_pool.result + parent_id = var.dev_center_project_id + location = var.location + + body = { + properties = { + devBoxDefinitionName = var.pool.dev_box_definition_name + displayName = try(var.pool.display_name, var.pool.name) + + # Local administrator settings + localAdministrator = try(var.pool.local_administrator_enabled, false) ? "Enabled" : "Disabled" + + # Network connection (use "default" for Microsoft-hosted network) + networkConnectionName = try(var.pool.network_connection_name, "default") + + # Stop on disconnect settings + stopOnDisconnect = { + status = "Enabled" + gracePeriodMinutes = try(var.pool.stop_on_disconnect_grace_period_minutes, 60) + } + + # Licensing type (required property) + licenseType = try(var.pool.license_type, "Windows_Client") + + # Virtual network type and regions for managed networks + virtualNetworkType = try(var.pool.virtual_network_type, "Managed") + managedVirtualNetworkRegions = [var.location] + } + } + + # Conditional tags - merge global tags with resource-specific tags + tags = local.tags + + # Ignore changes to certain computed properties and Azure-managed tags + lifecycle { + ignore_changes = [ + body.properties.healthStatus, + body.properties.provisioningState, + tags["hidden-title"] + ] + } +} + + diff --git a/modules/dev_center_project_pool/output.tf b/modules/dev_center_project_pool/output.tf new file mode 100644 index 0000000..e62b645 --- /dev/null +++ b/modules/dev_center_project_pool/output.tf @@ -0,0 +1,42 @@ +# DevCenter Project Pool Module Outputs + +output "id" { + description = "The ID of the DevCenter project pool" + value = azapi_resource.dev_center_project_pool.id +} + +output "name" { + description = "The name of the DevCenter project pool" + value = azapi_resource.dev_center_project_pool.name +} + +output "resource_id" { + description = "The full resource ID of the DevCenter project pool" + value = azapi_resource.dev_center_project_pool.id +} + +output "properties" { + description = "The properties of the DevCenter project pool" + value = azapi_resource.dev_center_project_pool.output + sensitive = false +} + +output "dev_box_definition_name" { + description = "The DevBox definition name used by this pool" + value = var.pool.dev_box_definition_name +} + +output "local_administrator_enabled" { + description = "Whether local administrator is enabled for DevBoxes in this pool" + value = try(var.pool.local_administrator_enabled, false) +} + +output "network_connection_name" { + description = "The network connection name used by this pool" + value = try(var.pool.network_connection_name, "default") +} + +output "tags" { + description = "The tags applied to the DevCenter project pool" + value = azapi_resource.dev_center_project_pool.tags +} diff --git a/modules/dev_center_project_pool/variables.tf b/modules/dev_center_project_pool/variables.tf new file mode 100644 index 0000000..5e54c20 --- /dev/null +++ b/modules/dev_center_project_pool/variables.tf @@ -0,0 +1,66 @@ +# DevCenter Project Pool Module Variables + +variable "global_settings" { + description = "Global settings for the module" + type = object({ + prefixes = optional(list(string), []) + random_length = optional(number, 0) + passthrough = optional(bool, false) + use_slug = optional(bool, true) + tags = optional(map(string), {}) + }) + default = {} +} + +variable "pool" { + description = "DevCenter project pool configuration" + type = object({ + name = string + display_name = optional(string) + dev_box_definition_name = string + local_administrator_enabled = optional(bool, false) + network_connection_name = optional(string, "default") + stop_on_disconnect_grace_period_minutes = optional(number, 60) + license_type = optional(string, "Windows_Client") + virtual_network_type = optional(string, "Managed") + managed_virtual_network_regions = optional(list(string)) + single_sign_on_status = optional(string, "Disabled") + tags = optional(map(string), {}) + }) + + validation { + condition = var.pool.stop_on_disconnect_grace_period_minutes >= 60 && var.pool.stop_on_disconnect_grace_period_minutes <= 480 + error_message = "Stop on disconnect grace period must be between 60 and 480 minutes." + } + + validation { + condition = contains(["Windows_Client", "Windows_Server"], var.pool.license_type) + error_message = "License type must be either 'Windows_Client' or 'Windows_Server'." + } + + validation { + condition = contains(["Managed", "Unmanaged"], var.pool.virtual_network_type) + error_message = "Virtual network type must be either 'Managed' or 'Unmanaged'." + } + + validation { + condition = contains(["Enabled", "Disabled"], var.pool.single_sign_on_status) + error_message = "Single sign-on status must be either 'Enabled' or 'Disabled'." + } +} + +variable "dev_center_project_id" { + description = "The ID of the DevCenter project that will contain this pool" + type = string +} + +variable "location" { + description = "The Azure region where the pool will be deployed" + type = string +} + +#tflint-ignore: terraform_unused_declarations +variable "resource_group_id" { + description = "The ID of the resource group" + type = string +} diff --git a/modules/dev_center_project_pool_schedule/README.md b/modules/dev_center_project_pool_schedule/README.md new file mode 100644 index 0000000..06bb44f --- /dev/null +++ b/modules/dev_center_project_pool_schedule/README.md @@ -0,0 +1,137 @@ +# DevCenter Project Pool Schedule Module + +This Terraform module creates Azure DevCenter project pool schedules using the AzAPI provider, following the `Microsoft.DevCenter/projects/pools/schedules@2025-04-01-preview` resource schema. + +## Features + +- **Flexible Schedule Types**: Supports StopDevBox and StartDevBox schedule types +- **Time Zone Support**: Configure schedules with specific time zones +- **State Management**: Enable or disable schedules as needed +- **Strong Typing**: Comprehensive variable validation for time format, types, and states +- **Tag Management**: Automatic merging of global and resource-specific tags +- **AzAPI Integration**: Uses the latest Azure API preview version for enhanced functionality + +## Usage + +```hcl +module "dev_center_project_pool_schedule" { + source = "./modules/dev_center_project_pool_schedule" + + dev_center_project_pool_id = module.dev_center_project_pool.id + + schedule = { + name = "daily-shutdown" + type = "StopDevBox" + frequency = "Daily" + time = "22:00" + time_zone = "W. Europe Standard Time" + state = "Enabled" + tags = { + purpose = "auto-shutdown" + } + } + + global_settings = { + tags = { + environment = "dev" + project = "contoso-devbox" + } + } +} +``` + +## Requirements + +| Name | Version | +|------|---------| +| azapi | ~> 2.4.0 | + +## Resources + +| Name | Type | +|------|------| +| [azapi_resource.dev_center_project_pool_schedule](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/azapi_resource) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| dev_center_project_pool_id | The resource ID of the DevCenter project pool | `string` | n/a | yes | +| schedule | Configuration for the DevCenter project pool schedule | `object({...})` | n/a | yes | +| global_settings | Global settings for all resources | `object({...})` | `{ tags = {} }` | no | + +### Schedule Object Structure + +```hcl +schedule = { + name = string # Schedule name + type = optional(string, "StopDevBox") # StopDevBox, StartDevBox + frequency = optional(string, "Daily") # Daily, Weekly + time = string # HH:MM format (24-hour) + time_zone = string # Time zone (e.g., "W. Europe Standard Time") + state = optional(string, "Enabled") # Enabled, Disabled + tags = optional(map(string), {}) # Resource-specific tags +} +``` + +## Outputs + +| Name | Description | +|------|-------------| +| id | The resource ID of the DevCenter project pool schedule | +| name | The name of the DevCenter project pool schedule | +| properties | The properties of the DevCenter project pool schedule | +| type | The schedule type (StopDevBox, StartDevBox) | +| time | The schedule time in HH:MM format | +| time_zone | The schedule time zone | +| state | The schedule state (Enabled, Disabled) | +| parent_pool_id | The resource ID of the parent DevCenter project pool | + +## Schedule Types + +- **StopDevBox**: Automatically stops dev boxes at the specified time +- **StartDevBox**: Automatically starts dev boxes at the specified time + +## Time Zones + +Common time zones for European deployments: +- `W. Europe Standard Time` (West Europe) +- `Central Europe Standard Time` (Central Europe) +- `GMT Standard Time` (Greenwich Mean Time) + +## Validation + +The module includes comprehensive validation for: +- Time format (HH:MM in 24-hour format) +- Schedule types (StopDevBox, StartDevBox) +- Frequency values (Daily, Weekly) +- State values (Enabled, Disabled) +- DevCenter project pool resource ID format + +## Examples + +### Daily Auto-Shutdown Schedule + +```hcl +schedule = { + name = "daily-shutdown" + type = "StopDevBox" + frequency = "Daily" + time = "22:00" + time_zone = "W. Europe Standard Time" + state = "Enabled" +} +``` + +### Morning Auto-Start Schedule + +```hcl +schedule = { + name = "morning-start" + type = "StartDevBox" + frequency = "Daily" + time = "08:00" + time_zone = "W. Europe Standard Time" + state = "Enabled" +} +``` diff --git a/modules/dev_center_project_pool_schedule/module.tf b/modules/dev_center_project_pool_schedule/module.tf new file mode 100644 index 0000000..0c0edc8 --- /dev/null +++ b/modules/dev_center_project_pool_schedule/module.tf @@ -0,0 +1,60 @@ +# DevCenter Project Pool Schedule Module +# This module creates Azure DevCenter project pool schedules using the AzAPI provider +# Following Microsoft.DevCenter/projects/pools/schedules@2025-04-01-preview schema + +terraform { + required_version = ">= 1.9.0" + required_providers { + azapi = { + source = "Azure/azapi" + version = "~> 2.4.0" + } + } +} + +locals { + tags = merge( + try(var.global_settings.tags, {}), + try(var.schedule.tags, {}), + { + terraform-azapi-resource-type = "Microsoft.DevCenter/projects/pools/schedules" + terraform-azapi-version = "2025-04-01-preview" + } + ) +} + +# DevCenter Project Pool Schedule Resource +resource "azapi_resource" "dev_center_project_pool_schedule" { + type = "Microsoft.DevCenter/projects/pools/schedules@2025-04-01-preview" + name = "default" + parent_id = var.dev_center_project_pool_id + + body = { + properties = { + # Schedule type (StopDevBox, StartDevBox, etc.) + type = try(var.schedule.type, "StopDevBox") + + # Frequency (Daily, Weekly, etc.) + frequency = try(var.schedule.frequency, "Daily") + + # Time in HH:MM format (24-hour) + time = var.schedule.time + + # Time zone (e.g., "W. Europe Standard Time") + timeZone = var.schedule.time_zone + + # Schedule state (Enabled, Disabled) + state = try(var.schedule.state, "Enabled") + + # Tags within properties for schedules + tags = local.tags + } + } + + # Ignore changes to certain computed properties + lifecycle { + ignore_changes = [ + body.properties.provisioningState + ] + } +} diff --git a/modules/dev_center_project_pool_schedule/output.tf b/modules/dev_center_project_pool_schedule/output.tf new file mode 100644 index 0000000..826bf9a --- /dev/null +++ b/modules/dev_center_project_pool_schedule/output.tf @@ -0,0 +1,42 @@ +# DevCenter Project Pool Schedule Module Outputs +# Outputs for the DevCenter project pool schedule module + +output "id" { + description = "The resource ID of the DevCenter project pool schedule" + value = azapi_resource.dev_center_project_pool_schedule.id +} + +output "name" { + description = "The name of the DevCenter project pool schedule" + value = azapi_resource.dev_center_project_pool_schedule.name +} + +output "properties" { + description = "The properties of the DevCenter project pool schedule" + value = azapi_resource.dev_center_project_pool_schedule.body.properties +} + +output "type" { + description = "The schedule type (StopDevBox, StartDevBox)" + value = azapi_resource.dev_center_project_pool_schedule.body.properties.type +} + +output "time" { + description = "The schedule time in HH:MM format" + value = azapi_resource.dev_center_project_pool_schedule.body.properties.time +} + +output "time_zone" { + description = "The schedule time zone" + value = azapi_resource.dev_center_project_pool_schedule.body.properties.timeZone +} + +output "state" { + description = "The schedule state (Enabled, Disabled)" + value = azapi_resource.dev_center_project_pool_schedule.body.properties.state +} + +output "parent_pool_id" { + description = "The resource ID of the parent DevCenter project pool" + value = var.dev_center_project_pool_id +} diff --git a/modules/dev_center_project_pool_schedule/variables.tf b/modules/dev_center_project_pool_schedule/variables.tf new file mode 100644 index 0000000..abbee1d --- /dev/null +++ b/modules/dev_center_project_pool_schedule/variables.tf @@ -0,0 +1,57 @@ +# DevCenter Project Pool Schedule Module Variables +# Variables for creating Azure DevCenter project pool schedules + +variable "global_settings" { + description = "Global settings for the module" + type = object({ + prefixes = optional(list(string), []) + random_length = optional(number, 0) + passthrough = optional(bool, false) + use_slug = optional(bool, true) + tags = optional(map(string), {}) + }) + default = {} +} + +variable "dev_center_project_pool_id" { + description = "The resource ID of the DevCenter project pool" + type = string + + validation { + condition = can(regex("^/subscriptions/[0-9a-f-]+/resourceGroups/[^/]+/providers/Microsoft.DevCenter/projects/[^/]+/pools/[^/]+$", var.dev_center_project_pool_id)) + error_message = "The dev_center_project_pool_id must be a valid Azure DevCenter project pool resource ID." + } +} + +variable "schedule" { + description = "Configuration for the DevCenter project pool schedule" + type = object({ + name = string + type = optional(string, "StopDevBox") # StopDevBox, StartDevBox + frequency = optional(string, "Daily") # Daily, Weekly + time = string # HH:MM format (24-hour) + time_zone = string # Time zone (e.g., "W. Europe Standard Time") + state = optional(string, "Enabled") # Enabled, Disabled + tags = optional(map(string), {}) + }) + + validation { + condition = can(regex("^[0-2][0-9]:[0-5][0-9]$", var.schedule.time)) + error_message = "The time must be in HH:MM format (24-hour)." + } + + validation { + condition = contains(["StopDevBox", "StartDevBox"], var.schedule.type) + error_message = "The schedule type must be either 'StopDevBox' or 'StartDevBox'." + } + + validation { + condition = contains(["Daily", "Weekly"], var.schedule.frequency) + error_message = "The schedule frequency must be either 'Daily' or 'Weekly'." + } + + validation { + condition = contains(["Enabled", "Disabled"], var.schedule.state) + error_message = "The schedule state must be either 'Enabled' or 'Disabled'." + } +} diff --git a/tests/run_tests.sh b/tests/run_tests.sh index ef547f9..c3e5aab 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -21,7 +21,11 @@ run_test() { # Initialize the test directory if needed if [ ! -d "${test_dir}/.terraform" ]; then echo -e " Initializing ${test_dir}..." - terraform -chdir="${test_dir}" init -input=false > /dev/null + if ! terraform -chdir="${test_dir}" init -input=false > /dev/null 2>&1; then + echo -e " ${RED}✗ Failed to initialize ${test_dir}${NC}" + echo -e " ${RED}✗ ${test_name} tests failed (initialization failed)${NC}" + return 1 + fi fi # Run the test and capture output @@ -54,14 +58,69 @@ ROOT_DIR="$(dirname "$SCRIPT_DIR")" # Change to the root directory cd "$ROOT_DIR" +# Initialize the root configuration first (required for tests that reference root modules) +echo -e "${YELLOW}Initializing root configuration...${NC}" +if [ ! -d ".terraform" ] || [ ! -f ".terraform/modules/modules.json" ]; then + echo -e " Initializing root directory..." + if ! terraform init -input=false > /dev/null 2>&1; then + echo -e " ${RED}✗ Failed to initialize root configuration${NC}" + echo -e " This may cause some tests to fail" + else + echo -e " ${GREEN}✓ Root configuration initialized${NC}" + fi +else + echo -e " ${GREEN}✓ Root configuration already initialized${NC}" +fi +echo "" + echo -e "${BOLD}Running Unit Tests${NC}" echo -e "----------------\n" # Create an array to store failed tests failed_tests=() -# Run all unit tests -unit_test_dirs=("tests/unit/resource_group" "tests/unit/dev_center" "tests/unit/dev_center_dev_box_definition" "tests/unit/dev_center_environment_type" "tests/unit/dev_center_project" "tests/unit/dev_center_catalog") +# Dynamically discover unit test directories +echo -e "${YELLOW}Discovering test directories...${NC}" +unit_test_dirs=() +if [ -d "tests/unit" ]; then + while IFS= read -r -d '' dir; do + if [ -d "$dir" ] && [ -n "$(find "$dir" -name "*.tftest.hcl" -o -name "*.tf" 2>/dev/null)" ]; then + unit_test_dirs+=("$dir") + fi + done < <(find tests/unit -mindepth 1 -maxdepth 1 -type d -print0 | sort -z) +fi + +echo -e "Found ${#unit_test_dirs[@]} unit test directories" +for dir in "${unit_test_dirs[@]}"; do + echo -e " - $(basename "$dir")" +done +echo "" + +# Initialize all test directories first +echo -e "${YELLOW}Initializing all test directories...${NC}" +for dir in "${unit_test_dirs[@]}"; do + if [ ! -d "${dir}/.terraform" ]; then + echo -e " Initializing $(basename "$dir")..." + if ! terraform -chdir="${dir}" init -input=false > /dev/null 2>&1; then + echo -e " ${RED}✗ Failed to initialize ${dir}${NC}" + else + echo -e " ${GREEN}✓ Initialized ${dir}${NC}" + fi + else + # Check if modules.json exists and is recent + if [ ! -f "${dir}/.terraform/modules/modules.json" ] || [ "${dir}/.terraform/modules/modules.json" -ot "../../../.terraform/modules/modules.json" ] 2>/dev/null; then + echo -e " Re-initializing $(basename "$dir") (modules may be outdated)..." + if ! terraform -chdir="${dir}" init -input=false > /dev/null 2>&1; then + echo -e " ${RED}✗ Failed to re-initialize ${dir}${NC}" + else + echo -e " ${GREEN}✓ Re-initialized ${dir}${NC}" + fi + else + echo -e " ${GREEN}✓ $(basename "$dir") already initialized${NC}" + fi + fi +done +echo "" for dir in "${unit_test_dirs[@]}"; do test_name=$(basename "$dir") @@ -74,8 +133,26 @@ done echo -e "${BOLD}Running Integration Tests${NC}" echo -e "---------------------\n" -# Run integration tests -integration_test_dirs=("tests/integration") +# Dynamically discover integration test directories +integration_test_dirs=() +if [ -d "tests/integration" ]; then + while IFS= read -r -d '' dir; do + if [ -d "$dir" ] && [ -n "$(find "$dir" -name "*.tftest.hcl" -o -name "*.tf" 2>/dev/null)" ]; then + integration_test_dirs+=("$dir") + fi + done < <(find tests/integration -mindepth 1 -maxdepth 1 -type d -print0 | sort -z) + + # If no subdirectories with tests found, check if integration directory itself has tests + if [ ${#integration_test_dirs[@]} -eq 0 ] && [ -n "$(find tests/integration -maxdepth 1 -name "*.tftest.hcl" -o -name "*.tf" 2>/dev/null)" ]; then + integration_test_dirs+=("tests/integration") + fi +fi + +echo -e "Found ${#integration_test_dirs[@]} integration test directories" +for dir in "${integration_test_dirs[@]}"; do + echo -e " - $(basename "$dir")" +done +echo "" for dir in "${integration_test_dirs[@]}"; do test_name=$(basename "$dir") diff --git a/tests/unit/dev_center_project_pool/pool_test.tftest.hcl b/tests/unit/dev_center_project_pool/pool_test.tftest.hcl new file mode 100644 index 0000000..fc18df2 --- /dev/null +++ b/tests/unit/dev_center_project_pool/pool_test.tftest.hcl @@ -0,0 +1,216 @@ +variables { + global_settings = { + prefixes = ["dev"] + random_length = 3 + passthrough = false + use_slug = true + } + + resource_groups = { + rg1 = { + name = "test-resource-group" + region = "eastus" + tags = { + environment = "test" + } + } + } + + dev_centers = { + devcenter1 = { + name = "test-dev-center" + resource_group = { + key = "rg1" + } + tags = { + environment = "test" + module = "dev_center" + } + } + } + + dev_center_dev_box_definitions = { + definition1 = { + name = "test-definition" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + image_reference = { + id = "/subscriptions/12345678-1234-5678-9012-123456789012/resourceGroups/rg-images/providers/Microsoft.Compute/galleries/myGallery/images/myImage/versions/1.0.0" + } + sku = { + name = "general_i_8c32gb256ssd_v2" + } + os_storage_type = "ssd_1024gb" + } + } + + dev_center_projects = { + project1 = { + name = "test-project" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + description = "Test project description" + maximum_dev_boxes_per_user = 3 + tags = { + environment = "test" + module = "dev_center_project" + } + } + } + + dev_center_project_pools = { + pool1 = { + name = "test-pool" + dev_center_project = { + key = "project1" + } + resource_group = { + key = "rg1" + } + dev_box_definition_name = "definition1" + display_name = "Test Pool" + local_administrator_enabled = false + network_connection_name = "default" + stop_on_disconnect_grace_period_minutes = 60 + license_type = "Windows_Client" + virtual_network_type = "Managed" + single_sign_on_status = "Disabled" + + tags = { + environment = "test" + module = "dev_center_project_pool" + } + } + } + + // Empty variables required by the root module + dev_center_galleries = {} + dev_center_environment_types = {} + dev_center_project_environment_types = {} + dev_center_network_connections = {} + dev_center_catalogs = {} + shared_image_galleries = {} + dev_center_project_pool_schedules = {} +} + +mock_provider "azapi" { + mock_data "azapi_client_config" { + defaults = { + subscription_id = "12345678-1234-1234-1234-123456789012" + tenant_id = "12345678-1234-1234-1234-123456789012" + client_id = "12345678-1234-1234-1234-123456789012" + } + } +} + +mock_provider "azurecaf" {} + +// Test for basic pool creation +run "test_basic_pool" { + command = plan + + module { + source = "../../../" + } + + assert { + condition = module.dev_center_project_pools["pool1"] != null + error_message = "Pool should exist" + } +} + +// Test for pool with custom properties +run "test_custom_pool" { + command = plan + + variables { + dev_center_project_pools = { + custom_pool = { + name = "custom-pool" + dev_center_project = { + key = "project1" + } + resource_group = { + key = "rg1" + } + dev_box_definition_name = "definition1" + display_name = "Custom Pool with Special Settings" + local_administrator_enabled = true + network_connection_name = "custom-network" + stop_on_disconnect_grace_period_minutes = 120 + license_type = "Windows_Server" + virtual_network_type = "Unmanaged" + single_sign_on_status = "Enabled" + + tags = { + environment = "test" + module = "dev_center_project_pool" + custom = "yes" + } + } + } + } + + module { + source = "../../../" + } + + assert { + condition = module.dev_center_project_pools["custom_pool"] != null + error_message = "Custom pool should exist" + } +} + +// Test multiple pools +run "test_multiple_pools" { + command = plan + + variables { + dev_center_project_pools = { + pool1 = { + name = "pool-one" + dev_center_project = { + key = "project1" + } + resource_group = { + key = "rg1" + } + dev_box_definition_name = "definition1" + license_type = "Windows_Client" + } + pool2 = { + name = "pool-two" + dev_center_project = { + key = "project1" + } + resource_group = { + key = "rg1" + } + dev_box_definition_name = "definition1" + license_type = "Windows_Server" + } + } + } + + module { + source = "../../../" + } + + assert { + condition = module.dev_center_project_pools["pool1"] != null + error_message = "Pool1 should exist" + } + + assert { + condition = module.dev_center_project_pools["pool2"] != null + error_message = "Pool2 should exist" + } +} diff --git a/tests/unit/dev_center_project_pool/pool_test_simple.tftest.hcl b/tests/unit/dev_center_project_pool/pool_test_simple.tftest.hcl new file mode 100644 index 0000000..fc18df2 --- /dev/null +++ b/tests/unit/dev_center_project_pool/pool_test_simple.tftest.hcl @@ -0,0 +1,216 @@ +variables { + global_settings = { + prefixes = ["dev"] + random_length = 3 + passthrough = false + use_slug = true + } + + resource_groups = { + rg1 = { + name = "test-resource-group" + region = "eastus" + tags = { + environment = "test" + } + } + } + + dev_centers = { + devcenter1 = { + name = "test-dev-center" + resource_group = { + key = "rg1" + } + tags = { + environment = "test" + module = "dev_center" + } + } + } + + dev_center_dev_box_definitions = { + definition1 = { + name = "test-definition" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + image_reference = { + id = "/subscriptions/12345678-1234-5678-9012-123456789012/resourceGroups/rg-images/providers/Microsoft.Compute/galleries/myGallery/images/myImage/versions/1.0.0" + } + sku = { + name = "general_i_8c32gb256ssd_v2" + } + os_storage_type = "ssd_1024gb" + } + } + + dev_center_projects = { + project1 = { + name = "test-project" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + description = "Test project description" + maximum_dev_boxes_per_user = 3 + tags = { + environment = "test" + module = "dev_center_project" + } + } + } + + dev_center_project_pools = { + pool1 = { + name = "test-pool" + dev_center_project = { + key = "project1" + } + resource_group = { + key = "rg1" + } + dev_box_definition_name = "definition1" + display_name = "Test Pool" + local_administrator_enabled = false + network_connection_name = "default" + stop_on_disconnect_grace_period_minutes = 60 + license_type = "Windows_Client" + virtual_network_type = "Managed" + single_sign_on_status = "Disabled" + + tags = { + environment = "test" + module = "dev_center_project_pool" + } + } + } + + // Empty variables required by the root module + dev_center_galleries = {} + dev_center_environment_types = {} + dev_center_project_environment_types = {} + dev_center_network_connections = {} + dev_center_catalogs = {} + shared_image_galleries = {} + dev_center_project_pool_schedules = {} +} + +mock_provider "azapi" { + mock_data "azapi_client_config" { + defaults = { + subscription_id = "12345678-1234-1234-1234-123456789012" + tenant_id = "12345678-1234-1234-1234-123456789012" + client_id = "12345678-1234-1234-1234-123456789012" + } + } +} + +mock_provider "azurecaf" {} + +// Test for basic pool creation +run "test_basic_pool" { + command = plan + + module { + source = "../../../" + } + + assert { + condition = module.dev_center_project_pools["pool1"] != null + error_message = "Pool should exist" + } +} + +// Test for pool with custom properties +run "test_custom_pool" { + command = plan + + variables { + dev_center_project_pools = { + custom_pool = { + name = "custom-pool" + dev_center_project = { + key = "project1" + } + resource_group = { + key = "rg1" + } + dev_box_definition_name = "definition1" + display_name = "Custom Pool with Special Settings" + local_administrator_enabled = true + network_connection_name = "custom-network" + stop_on_disconnect_grace_period_minutes = 120 + license_type = "Windows_Server" + virtual_network_type = "Unmanaged" + single_sign_on_status = "Enabled" + + tags = { + environment = "test" + module = "dev_center_project_pool" + custom = "yes" + } + } + } + } + + module { + source = "../../../" + } + + assert { + condition = module.dev_center_project_pools["custom_pool"] != null + error_message = "Custom pool should exist" + } +} + +// Test multiple pools +run "test_multiple_pools" { + command = plan + + variables { + dev_center_project_pools = { + pool1 = { + name = "pool-one" + dev_center_project = { + key = "project1" + } + resource_group = { + key = "rg1" + } + dev_box_definition_name = "definition1" + license_type = "Windows_Client" + } + pool2 = { + name = "pool-two" + dev_center_project = { + key = "project1" + } + resource_group = { + key = "rg1" + } + dev_box_definition_name = "definition1" + license_type = "Windows_Server" + } + } + } + + module { + source = "../../../" + } + + assert { + condition = module.dev_center_project_pools["pool1"] != null + error_message = "Pool1 should exist" + } + + assert { + condition = module.dev_center_project_pools["pool2"] != null + error_message = "Pool2 should exist" + } +} diff --git a/tests/unit/dev_center_project_pool_schedule/schedule_test.tftest.hcl b/tests/unit/dev_center_project_pool_schedule/schedule_test.tftest.hcl new file mode 100644 index 0000000..4afca92 --- /dev/null +++ b/tests/unit/dev_center_project_pool_schedule/schedule_test.tftest.hcl @@ -0,0 +1,234 @@ +variables { + global_settings = { + prefixes = ["dev"] + random_length = 3 + passthrough = false + use_slug = true + } + + resource_groups = { + rg1 = { + name = "test-resource-group" + region = "eastus" + tags = { + environment = "test" + } + } + } + + dev_centers = { + devcenter1 = { + name = "test-dev-center" + resource_group = { + key = "rg1" + } + tags = { + environment = "test" + module = "dev_center" + } + } + } + + dev_center_dev_box_definitions = { + definition1 = { + name = "test-definition" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + image_reference = { + id = "/subscriptions/12345678-1234-5678-9012-123456789012/resourceGroups/rg-images/providers/Microsoft.Compute/galleries/myGallery/images/myImage/versions/1.0.0" + } + sku = { + name = "general_i_8c32gb256ssd_v2" + } + os_storage_type = "ssd_1024gb" + } + } + + dev_center_projects = { + project1 = { + name = "test-project" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + description = "Test project description" + maximum_dev_boxes_per_user = 3 + tags = { + environment = "test" + module = "dev_center_project" + } + } + } + + dev_center_project_pools = { + pool1 = { + name = "test-pool" + dev_center_project = { + key = "project1" + } + resource_group = { + key = "rg1" + } + dev_box_definition_name = "definition1" + display_name = "Test Pool" + local_administrator_enabled = false + network_connection_name = "default" + stop_on_disconnect_grace_period_minutes = 60 + license_type = "Windows_Client" + virtual_network_type = "Managed" + single_sign_on_status = "Disabled" + + tags = { + environment = "test" + module = "dev_center_project_pool" + } + } + } + + dev_center_project_pool_schedules = { + schedule1 = { + dev_center_project_pool = { + key = "pool1" + } + schedule = { + name = "test-schedule" + type = "StopDevBox" + frequency = "Daily" + time = "18:00" + time_zone = "W. Europe Standard Time" + state = "Enabled" + tags = { + environment = "test" + module = "dev_center_project_pool_schedule" + } + } + } + } + + // Empty variables required by the root module + dev_center_galleries = {} + dev_center_environment_types = {} + dev_center_project_environment_types = {} + dev_center_network_connections = {} + dev_center_catalogs = {} + shared_image_galleries = {} +} + +mock_provider "azapi" { + mock_data "azapi_client_config" { + defaults = { + subscription_id = "12345678-1234-1234-1234-123456789012" + tenant_id = "12345678-1234-1234-1234-123456789012" + client_id = "12345678-1234-1234-1234-123456789012" + } + } +} + +mock_provider "azurecaf" {} + +// Test for basic schedule creation +run "test_basic_schedule" { + command = plan + + module { + source = "../../../" + } + + assert { + condition = module.dev_center_project_pool_schedules["schedule1"] != null + error_message = "Schedule should exist" + } +} + +// Test for schedule with custom properties +run "test_custom_schedule" { + command = plan + + variables { + dev_center_project_pool_schedules = { + custom_schedule = { + dev_center_project_pool = { + key = "pool1" + } + schedule = { + name = "custom-schedule" + type = "StartDevBox" + frequency = "Weekly" + time = "08:30" + time_zone = "Eastern Standard Time" + state = "Disabled" + tags = { + environment = "test" + module = "dev_center_project_pool_schedule" + custom = "yes" + } + } + } + } + } + + module { + source = "../../../" + } + + assert { + condition = module.dev_center_project_pool_schedules["custom_schedule"] != null + error_message = "Custom schedule should exist" + } +} + +// Test multiple schedules for the same pool +run "test_multiple_schedules" { + command = plan + + variables { + dev_center_project_pool_schedules = { + morning_start = { + dev_center_project_pool = { + key = "pool1" + } + schedule = { + name = "morning-start" + type = "StartDevBox" + frequency = "Daily" + time = "08:00" + time_zone = "W. Europe Standard Time" + state = "Enabled" + } + } + evening_stop = { + dev_center_project_pool = { + key = "pool1" + } + schedule = { + name = "evening-stop" + type = "StopDevBox" + frequency = "Daily" + time = "18:00" + time_zone = "W. Europe Standard Time" + state = "Enabled" + } + } + } + } + + module { + source = "../../../" + } + + assert { + condition = module.dev_center_project_pool_schedules["morning_start"] != null + error_message = "Morning start schedule should exist" + } + + assert { + condition = module.dev_center_project_pool_schedules["evening_stop"] != null + error_message = "Evening stop schedule should exist" + } +} diff --git a/tests/unit/dev_center_project_pool_schedule/schedule_test_simple.tftest.hcl b/tests/unit/dev_center_project_pool_schedule/schedule_test_simple.tftest.hcl new file mode 100644 index 0000000..4afca92 --- /dev/null +++ b/tests/unit/dev_center_project_pool_schedule/schedule_test_simple.tftest.hcl @@ -0,0 +1,234 @@ +variables { + global_settings = { + prefixes = ["dev"] + random_length = 3 + passthrough = false + use_slug = true + } + + resource_groups = { + rg1 = { + name = "test-resource-group" + region = "eastus" + tags = { + environment = "test" + } + } + } + + dev_centers = { + devcenter1 = { + name = "test-dev-center" + resource_group = { + key = "rg1" + } + tags = { + environment = "test" + module = "dev_center" + } + } + } + + dev_center_dev_box_definitions = { + definition1 = { + name = "test-definition" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + image_reference = { + id = "/subscriptions/12345678-1234-5678-9012-123456789012/resourceGroups/rg-images/providers/Microsoft.Compute/galleries/myGallery/images/myImage/versions/1.0.0" + } + sku = { + name = "general_i_8c32gb256ssd_v2" + } + os_storage_type = "ssd_1024gb" + } + } + + dev_center_projects = { + project1 = { + name = "test-project" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + description = "Test project description" + maximum_dev_boxes_per_user = 3 + tags = { + environment = "test" + module = "dev_center_project" + } + } + } + + dev_center_project_pools = { + pool1 = { + name = "test-pool" + dev_center_project = { + key = "project1" + } + resource_group = { + key = "rg1" + } + dev_box_definition_name = "definition1" + display_name = "Test Pool" + local_administrator_enabled = false + network_connection_name = "default" + stop_on_disconnect_grace_period_minutes = 60 + license_type = "Windows_Client" + virtual_network_type = "Managed" + single_sign_on_status = "Disabled" + + tags = { + environment = "test" + module = "dev_center_project_pool" + } + } + } + + dev_center_project_pool_schedules = { + schedule1 = { + dev_center_project_pool = { + key = "pool1" + } + schedule = { + name = "test-schedule" + type = "StopDevBox" + frequency = "Daily" + time = "18:00" + time_zone = "W. Europe Standard Time" + state = "Enabled" + tags = { + environment = "test" + module = "dev_center_project_pool_schedule" + } + } + } + } + + // Empty variables required by the root module + dev_center_galleries = {} + dev_center_environment_types = {} + dev_center_project_environment_types = {} + dev_center_network_connections = {} + dev_center_catalogs = {} + shared_image_galleries = {} +} + +mock_provider "azapi" { + mock_data "azapi_client_config" { + defaults = { + subscription_id = "12345678-1234-1234-1234-123456789012" + tenant_id = "12345678-1234-1234-1234-123456789012" + client_id = "12345678-1234-1234-1234-123456789012" + } + } +} + +mock_provider "azurecaf" {} + +// Test for basic schedule creation +run "test_basic_schedule" { + command = plan + + module { + source = "../../../" + } + + assert { + condition = module.dev_center_project_pool_schedules["schedule1"] != null + error_message = "Schedule should exist" + } +} + +// Test for schedule with custom properties +run "test_custom_schedule" { + command = plan + + variables { + dev_center_project_pool_schedules = { + custom_schedule = { + dev_center_project_pool = { + key = "pool1" + } + schedule = { + name = "custom-schedule" + type = "StartDevBox" + frequency = "Weekly" + time = "08:30" + time_zone = "Eastern Standard Time" + state = "Disabled" + tags = { + environment = "test" + module = "dev_center_project_pool_schedule" + custom = "yes" + } + } + } + } + } + + module { + source = "../../../" + } + + assert { + condition = module.dev_center_project_pool_schedules["custom_schedule"] != null + error_message = "Custom schedule should exist" + } +} + +// Test multiple schedules for the same pool +run "test_multiple_schedules" { + command = plan + + variables { + dev_center_project_pool_schedules = { + morning_start = { + dev_center_project_pool = { + key = "pool1" + } + schedule = { + name = "morning-start" + type = "StartDevBox" + frequency = "Daily" + time = "08:00" + time_zone = "W. Europe Standard Time" + state = "Enabled" + } + } + evening_stop = { + dev_center_project_pool = { + key = "pool1" + } + schedule = { + name = "evening-stop" + type = "StopDevBox" + frequency = "Daily" + time = "18:00" + time_zone = "W. Europe Standard Time" + state = "Enabled" + } + } + } + } + + module { + source = "../../../" + } + + assert { + condition = module.dev_center_project_pool_schedules["morning_start"] != null + error_message = "Morning start schedule should exist" + } + + assert { + condition = module.dev_center_project_pool_schedules["evening_stop"] != null + error_message = "Evening stop schedule should exist" + } +} diff --git a/variables.tf b/variables.tf index b4e9ef4..02d22ef 100644 --- a/variables.tf +++ b/variables.tf @@ -6,6 +6,7 @@ variable "global_settings" { passthrough = optional(bool) use_slug = optional(bool) tags = optional(map(string)) + regions = optional(map(string)) # Ensure downstream modules accept this field }) } @@ -253,24 +254,6 @@ variable "dev_center_environment_types" { default = {} } -# tflint-ignore: terraform_unused_declarations -variable "dev_center_project_environment_types" { - description = "Dev Center Project Environment Types configuration objects" - type = map(object({ - name = string - project_id = optional(string) - project = optional(object({ - key = string - })) - environment_type_id = optional(string) - environment_type = optional(object({ - key = string - })) - tags = optional(map(string), {}) - })) - default = {} -} - # tflint-ignore: terraform_unused_declarations variable "dev_center_network_connections" { description = "Dev Center Network Connections configuration objects" @@ -311,3 +294,57 @@ variable "shared_image_galleries" { })) default = {} } + +variable "dev_center_project_pools" { + description = "DevCenter Project Pools configuration objects" + type = map(object({ + name = string + display_name = optional(string) + dev_box_definition_name = string + dev_center_project_id = optional(string) + dev_center_project = optional(object({ + key = string + })) + resource_group_id = optional(string) + resource_group = optional(object({ + key = string + })) + region = optional(string) + local_administrator_enabled = optional(bool, false) + network_connection_name = optional(string, "default") + stop_on_disconnect_grace_period_minutes = optional(number, 60) + license_type = optional(string, "Windows_Client") + virtual_network_type = optional(string, "Managed") + single_sign_on_status = optional(string, "Disabled") + tags = optional(map(string), {}) + })) + default = {} + + validation { + condition = alltrue([ + for pool_key, pool in var.dev_center_project_pools : + pool.stop_on_disconnect_grace_period_minutes >= 60 && pool.stop_on_disconnect_grace_period_minutes <= 480 + ]) + error_message = "Stop on disconnect grace period must be between 60 and 480 minutes for all pools." + } +} + +variable "dev_center_project_pool_schedules" { + description = "DevCenter Project Pool Schedules configuration objects" + type = map(object({ + dev_center_project_pool_id = optional(string) + dev_center_project_pool = optional(object({ + key = string + })) + schedule = object({ + name = string + type = optional(string, "StopDevBox") + frequency = optional(string, "Daily") + time = string + time_zone = string + state = optional(string, "Enabled") + tags = optional(map(string), {}) + }) + })) + default = {} +}