From 497beb193bdb5875cd142428f3f1775e0325d223 Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Mon, 16 Jun 2025 19:41:29 +0800 Subject: [PATCH 01/10] Update provider versions and resource types for dev center modules --- modules/dev_center/README.md | 4 ++-- modules/dev_center/module.tf | 4 ++-- modules/dev_center_catalog/README.md | 4 ++-- modules/dev_center_catalog/module.tf | 4 ++-- modules/dev_center_environment_type/README.md | 4 ++-- modules/dev_center_environment_type/module.tf | 4 ++-- modules/dev_center_project/README.md | 4 ++-- modules/dev_center_project/module.tf | 4 ++-- modules/resource_group/README.md | 4 ++-- modules/resource_group/module.tf | 2 +- provider.tf | 11 +++++++++-- 11 files changed, 28 insertions(+), 21 deletions(-) diff --git a/modules/dev_center/README.md b/modules/dev_center/README.md index c76be0f..86f0031 100644 --- a/modules/dev_center/README.md +++ b/modules/dev_center/README.md @@ -92,14 +92,14 @@ For more examples including all possible configurations, see the [Dev Center exa |------|---------| | [terraform](#requirement\_terraform) | >= 1.9.0 | | [azapi](#requirement\_azapi) | ~> 2.4.0 | -| [azurecaf](#requirement\_azurecaf) | ~> 1.2.0 | +| [azurecaf](#requirement\_azurecaf) | ~> 1.2.29 | ## Providers | Name | Version | |------|---------| | [azapi](#provider\_azapi) | 2.4.0 | -| [azurecaf](#provider\_azurecaf) | 1.2.28 | +| [azurecaf](#provider\_azurecaf) | 1.2.29 | ## Modules diff --git a/modules/dev_center/module.tf b/modules/dev_center/module.tf index fed1abe..c6bedd4 100644 --- a/modules/dev_center/module.tf +++ b/modules/dev_center/module.tf @@ -3,7 +3,7 @@ terraform { required_providers { azurecaf = { source = "aztfmod/azurecaf" - version = "~> 1.2.0" + version = "~> 1.2.29" } azapi = { source = "Azure/azapi" @@ -25,7 +25,7 @@ locals { # Using resource instead of data source to ensure stable naming across plan/apply resource "azurecaf_name" "dev_center" { name = var.dev_center.name - resource_type = "general" + resource_type = "azurerm_dev_center" prefixes = var.global_settings.prefixes random_length = var.global_settings.random_length clean_input = true diff --git a/modules/dev_center_catalog/README.md b/modules/dev_center_catalog/README.md index ae91977..a4bb6c4 100644 --- a/modules/dev_center_catalog/README.md +++ b/modules/dev_center_catalog/README.md @@ -91,14 +91,14 @@ This module implements the [Microsoft.DevCenter/devcenters/catalogs](https://lea |------|---------| | [terraform](#requirement\_terraform) | >= 1.9.0 | | [azapi](#requirement\_azapi) | ~> 2.4.0 | -| [azurecaf](#requirement\_azurecaf) | ~> 1.2.0 | +| [azurecaf](#requirement\_azurecaf) | ~> 1.2.29 | ## Providers | Name | Version | |------|---------| | [azapi](#provider\_azapi) | 2.4.0 | -| [azurecaf](#provider\_azurecaf) | 1.2.28 | +| [azurecaf](#provider\_azurecaf) | 1.2.29 | ## Modules diff --git a/modules/dev_center_catalog/module.tf b/modules/dev_center_catalog/module.tf index fd1e839..88eebbb 100644 --- a/modules/dev_center_catalog/module.tf +++ b/modules/dev_center_catalog/module.tf @@ -3,7 +3,7 @@ terraform { required_providers { azurecaf = { source = "aztfmod/azurecaf" - version = "~> 1.2.0" + version = "~> 1.2.29" } azapi = { source = "Azure/azapi" @@ -21,7 +21,7 @@ locals { resource "azurecaf_name" "catalog" { name = var.catalog.name - resource_type = "general" + resource_type = "azurerm_dev_center_catalog" prefixes = var.global_settings.prefixes random_length = var.global_settings.random_length clean_input = true diff --git a/modules/dev_center_environment_type/README.md b/modules/dev_center_environment_type/README.md index 36ce944..0b3b94e 100644 --- a/modules/dev_center_environment_type/README.md +++ b/modules/dev_center_environment_type/README.md @@ -106,14 +106,14 @@ For more examples, see the [environment type examples](../../../examples/dev_cen |------|---------| | [terraform](#requirement\_terraform) | >= 1.9.0 | | [azapi](#requirement\_azapi) | ~> 2.4.0 | -| [azurecaf](#requirement\_azurecaf) | ~> 1.2.0 | +| [azurecaf](#requirement\_azurecaf) | ~> 1.2.29 | ## Providers | Name | Version | |------|---------| | [azapi](#provider\_azapi) | 2.4.0 | -| [azurecaf](#provider\_azurecaf) | 1.2.28 | +| [azurecaf](#provider\_azurecaf) | 1.2.29 | ## Modules diff --git a/modules/dev_center_environment_type/module.tf b/modules/dev_center_environment_type/module.tf index e1dacd2..524362e 100644 --- a/modules/dev_center_environment_type/module.tf +++ b/modules/dev_center_environment_type/module.tf @@ -3,7 +3,7 @@ terraform { required_providers { azurecaf = { source = "aztfmod/azurecaf" - version = "~> 1.2.0" + version = "~> 1.2.29" } azapi = { source = "Azure/azapi" @@ -22,7 +22,7 @@ locals { resource "azurecaf_name" "environment_type" { name = var.environment_type.name - resource_type = "general" + resource_type = "azurerm_dev_center_environment_type" prefixes = var.global_settings.prefixes random_length = var.global_settings.random_length clean_input = true diff --git a/modules/dev_center_project/README.md b/modules/dev_center_project/README.md index 0f359fe..3bdc9d0 100644 --- a/modules/dev_center_project/README.md +++ b/modules/dev_center_project/README.md @@ -111,14 +111,14 @@ For more examples, see the [Dev Center Project examples](../../../examples/dev_c |------|---------| | [terraform](#requirement\_terraform) | >= 1.9.0 | | [azapi](#requirement\_azapi) | ~> 2.4.0 | -| [azurecaf](#requirement\_azurecaf) | ~> 1.2.0 | +| [azurecaf](#requirement\_azurecaf) | ~> 1.2.29 | ## Providers | Name | Version | |------|---------| | [azapi](#provider\_azapi) | 2.4.0 | -| [azurecaf](#provider\_azurecaf) | 1.2.28 | +| [azurecaf](#provider\_azurecaf) | 1.2.29 | ## Modules diff --git a/modules/dev_center_project/module.tf b/modules/dev_center_project/module.tf index 868997c..b25866f 100644 --- a/modules/dev_center_project/module.tf +++ b/modules/dev_center_project/module.tf @@ -3,7 +3,7 @@ terraform { required_providers { azurecaf = { source = "aztfmod/azurecaf" - version = "~> 1.2.0" + version = "~> 1.2.29" } azapi = { source = "Azure/azapi" @@ -19,7 +19,7 @@ locals { resource "azurecaf_name" "project" { name = var.project.name - resource_type = "general" + resource_type = "azurerm_dev_center_project" prefixes = var.global_settings.prefixes random_length = var.global_settings.random_length clean_input = true diff --git a/modules/resource_group/README.md b/modules/resource_group/README.md index e7fcab0..b59a4de 100644 --- a/modules/resource_group/README.md +++ b/modules/resource_group/README.md @@ -84,14 +84,14 @@ For more examples, see the [Resource Group examples](../../../examples/resource_ |------|---------| | [terraform](#requirement\_terraform) | >= 1.9.0 | | [azapi](#requirement\_azapi) | ~> 2.4.0 | -| [azurecaf](#requirement\_azurecaf) | ~> 1.2.0 | +| [azurecaf](#requirement\_azurecaf) | ~> 1.2.29 | ## Providers | Name | Version | |------|---------| | [azapi](#provider\_azapi) | 2.4.0 | -| [azurecaf](#provider\_azurecaf) | 1.2.28 | +| [azurecaf](#provider\_azurecaf) | 1.2.29 | ## Modules diff --git a/modules/resource_group/module.tf b/modules/resource_group/module.tf index 56297a3..ff605fa 100644 --- a/modules/resource_group/module.tf +++ b/modules/resource_group/module.tf @@ -3,7 +3,7 @@ terraform { required_providers { azurecaf = { source = "aztfmod/azurecaf" - version = "~> 1.2.0" + version = "~> 1.2.29" } azapi = { source = "Azure/azapi" diff --git a/provider.tf b/provider.tf index afbf7bf..c93e40a 100644 --- a/provider.tf +++ b/provider.tf @@ -1,14 +1,21 @@ terraform { required_providers { - azapi = { source = "Azure/azapi" version = "~> 2.4.0" } azurecaf = { source = "aztfmod/azurecaf" - version = "~> 1.2.0" + version = "~> 1.2.29" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.26.0" } } required_version = ">= 1.12.1" } + +provider "azurerm" { + features {} +} From 80bb80d09a63be046ad7475e47cd2abf01cf3931 Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Tue, 17 Jun 2025 11:48:19 +0800 Subject: [PATCH 02/10] Add lifecycle block to ignore changes to system-managed tags in DevCenter resources --- modules/dev_center/module.tf | 7 +++++++ modules/dev_center_project/module.tf | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/modules/dev_center/module.tf b/modules/dev_center/module.tf index fed1abe..ab4e4d8 100644 --- a/modules/dev_center/module.tf +++ b/modules/dev_center/module.tf @@ -77,6 +77,13 @@ resource "azapi_resource" "dev_center" { tags = local.tags response_export_values = ["properties"] + + # Ignore changes to system-managed tags that Azure automatically adds + lifecycle { + ignore_changes = [ + tags["hidden-title"] + ] + } } data "azapi_client_config" "current" {} \ No newline at end of file diff --git a/modules/dev_center_project/module.tf b/modules/dev_center_project/module.tf index 868997c..4950bad 100644 --- a/modules/dev_center_project/module.tf +++ b/modules/dev_center_project/module.tf @@ -91,4 +91,11 @@ resource "azapi_resource" "project" { } : null } } + + # Ignore changes to system-managed tags that Azure automatically adds + lifecycle { + ignore_changes = [ + tags["hidden-title"] + ] + } } From 9c7004e1dd192cd945377668b7daa9febf0f27df Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Tue, 17 Jun 2025 13:22:03 +0800 Subject: [PATCH 03/10] feat: Update Dev Center Dev Box Definition module - Upgrade azurecaf provider version to 1.2.29. - Enhance SKU configuration to support both simple name and full object. - Introduce os_storage_type variable for OS disk configuration. - Simplify hibernate_support to a boolean value. - Enable schema validation for azapi resource. - Add new outputs for image validation status and details. - Create comprehensive unit tests for Dev Box Definition module. - Update README with test coverage and instructions. --- .../enhanced_case/configuration.tfvars | 69 +- .../simple_case/configuration.tfvars | 4 +- modules/dev_center/README.md | 2 +- .../dev_center_dev_box_definition/README.md | 614 +++++++++++++++++- .../dev_center_dev_box_definition/module.tf | 38 +- .../dev_center_dev_box_definition/output.tf | 25 + .../variables.tf | 61 +- modules/dev_center_project/README.md | 2 +- tests/run_tests.sh | 2 +- .../dev_center_dev_box_definition/README.md | 82 +++ .../devbox_definition_test.tftest.hcl | 286 +++++++- variables.tf | 18 +- 12 files changed, 1114 insertions(+), 89 deletions(-) create mode 100644 tests/unit/dev_center_dev_box_definition/README.md diff --git a/examples/dev_center_dev_box_definition/enhanced_case/configuration.tfvars b/examples/dev_center_dev_box_definition/enhanced_case/configuration.tfvars index 8fd77f9..ddb9831 100644 --- a/examples/dev_center_dev_box_definition/enhanced_case/configuration.tfvars +++ b/examples/dev_center_dev_box_definition/enhanced_case/configuration.tfvars @@ -70,9 +70,7 @@ dev_center_dev_box_definitions = { # Standard Windows 11 development environment image_reference_id = "galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win11-m365-gen2" sku_name = "general_i_8c32gb256ssd_v2" - hibernate_support = { - enabled = true # Enable hibernate for cost optimization - } + hibernate_support = true # Enable hibernate for cost optimization tags = { module = "dev_center_dev_box_definition" @@ -96,9 +94,10 @@ dev_center_dev_box_definitions = { # Standard Windows 11 development environment } sku_name = "general_i_32c128gb1024ssd_v2" # High-performance SKU - hibernate_support = { - enabled = false # Keep running for long-running AI training - } + hibernate_support = false # Keep running for long-running AI training + + # Test osStorageType property + os_storage_type = "ssd_1024gb" tags = { module = "dev_center_dev_box_definition" @@ -110,53 +109,65 @@ dev_center_dev_box_definitions = { # Standard Windows 11 development environment auto_delete = "disabled" } } - # Linux development environment - ubuntu_development = { - name = "ubuntu-development" + # Specialized environment for data science work + win11_data_science = { + name = "win11-datascience" dev_center = { key = "platform" } resource_group = { key = "rg_devbox" - } # Ubuntu development image (using built-in Ubuntu 22.04) - image_reference_id = "galleries/default/images/canonical_0001-com-ubuntu-server-jammy_22_04-lts-gen2" - sku_name = "general_i_16c64gb512ssd_v2" + } # Data science image (using built-in Windows 11 with development tools) + image_reference_id = "galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win11-m365-gen2" - hibernate_support = { - enabled = true - } + # Use simple SKU configuration + sku_name = "general_i_16c64gb512ssd_v2" + + hibernate_support = true + + # Test osStorageType property + os_storage_type = "ssd_512gb" tags = { module = "dev_center_dev_box_definition" - image_type = "ubuntu" - tier = "standard" - purpose = "linux-development" + image_type = "win11" + tier = "specialized" + purpose = "data-science" + tools = "python-r-jupyter" auto_delete = "enabled" } } - # Specialized environment for data science work - win11_data_science = { - name = "win11-datascience" + # Advanced SKU object configuration example + win11_enterprise_advanced = { + name = "win11-enterprise-advanced" dev_center = { key = "platform" } resource_group = { key = "rg_devbox" - } # Data science image (using built-in Windows 11 with development tools) - image_reference_id = "galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win11-m365-gen2" - sku_name = "general_i_16c64gb512ssd_v2" + } + # Enterprise development image + image_reference_id = "galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-pro-general-win11-m365-gen2" - hibernate_support = { - enabled = true + # Advanced SKU object configuration instead of simple sku_name + sku = { + name = "general_i_16c64gb512ssd_v2" + tier = "Standard" } + hibernate_support = true + + # Test osStorageType property + os_storage_type = "ssd_512gb" + tags = { module = "dev_center_dev_box_definition" image_type = "win11" - tier = "specialized" - purpose = "data-science" - tools = "python-r-jupyter" + tier = "enterprise" + purpose = "enterprise-development" + sku_type = "advanced_object" auto_delete = "enabled" + config_type = "advanced" } } } diff --git a/examples/dev_center_dev_box_definition/simple_case/configuration.tfvars b/examples/dev_center_dev_box_definition/simple_case/configuration.tfvars index 7a8e33b..e80696e 100644 --- a/examples/dev_center_dev_box_definition/simple_case/configuration.tfvars +++ b/examples/dev_center_dev_box_definition/simple_case/configuration.tfvars @@ -41,9 +41,7 @@ dev_center_dev_box_definitions = { image_reference_id = "galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win11-m365-gen2" sku_name = "general_i_8c32gb256ssd_v2" - hibernate_support = { - enabled = false - } + hibernate_support = false tags = { module = "dev_center_dev_box_definition" diff --git a/modules/dev_center/README.md b/modules/dev_center/README.md index e667ff3..437cfee 100644 --- a/modules/dev_center/README.md +++ b/modules/dev_center/README.md @@ -99,7 +99,7 @@ For more examples including all possible configurations, see the [Dev Center exa | Name | Version | |------|---------| | [azapi](#provider\_azapi) | 2.4.0 | -| [azurecaf](#provider\_azurecaf) | 1.2.28 | +| [azurecaf](#provider\_azurecaf) | 1.2.29 | ## Modules diff --git a/modules/dev_center_dev_box_definition/README.md b/modules/dev_center_dev_box_definition/README.md index 885c3a2..ccaae8b 100644 --- a/modules/dev_center_dev_box_definition/README.md +++ b/modules/dev_center_dev_box_definition/README.md @@ -6,16 +6,273 @@ This module creates an Azure DevCenter DevBox Definition using the AzAPI provide The DevBox Definition module enables the creation and management of DevBox definitions within Azure DevCenter. It leverages the AzAPI provider to ensure compatibility with the latest Azure features and APIs, following DevFactory standardization patterns. +## Quick Reference + +### ๐Ÿš€ **Most Common Configurations** + +```hcl +# Windows 11 + Visual Studio 2022 Enterprise (Recommended for .NET development) +image_reference_id = "galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win11-m365-gen2" +sku_name = "general_i_16c64gb512ssd_v2" # 16 vCPU, 64 GB RAM, 512 GB SSD + +# Windows 11 Enterprise with Microsoft 365 Apps (Recommended for general development) +image_reference_id = "galleries/default/images/microsoftwindowsdesktop_windows-ent-cpc_win11-24h2-ent-cpc-m365" +sku_name = "general_i_8c32gb256ssd_v2" # 8 vCPU, 32 GB RAM, 256 GB SSD + +# High-performance development (AI/ML, large projects) +image_reference_id = "galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win11-m365-gen2" +sku_name = "general_i_32c128gb1024ssd_v2" # 32 vCPU, 128 GB RAM, 1024 GB SSD +``` + +### ๐Ÿ”ง **Essential Discovery Commands** +```bash +# List available DevBox SKUs (subscription-wide) +az devcenter admin sku list --output table + +# List available images (requires DevCenter context) +az devcenter admin image list --dev-center-name "mydevcenter" --resource-group "myrg" --gallery-name "default" --query "[].name" -o table + +# List VM SKUs by location (alternative for region-specific sizing) +az vm list-skus --location "East US" --resource-type "virtualMachines" --query "[?contains(name, 'Standard_D')]" --output table + +# Validate your configuration +terraform plan -var-file="configuration.tfvars" +``` + ## Features - Uses AzAPI provider v2.4.0 for latest Azure features - Implements latest Azure DevCenter API (2025-04-01-preview) - Supports multiple image reference formats -- Configurable SKU and storage options +- Configurable SKU and storage options with full type safety - Hibernate support configuration - Integrates with azurecaf naming conventions - Manages resource tags (global + specific) -- Provides strong input validation +- Provides strong input validation and type checking +- Schema-aligned object types for Azure API compatibility + +## Finding DevBox Configuration Options + +### ๐Ÿ–ผ๏ธ **Finding Available Images** + +DevBox definitions support several image sources: + +#### **Built-in Gallery Images (Recommended)** +Use the relative path format for Microsoft's built-in images: + +```hcl +# Windows 11 with Visual Studio 2022 Enterprise +image_reference_id = "galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win11-m365-gen2" + +# Windows 10 with Visual Studio 2022 Professional +image_reference_id = "galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-pro-general-win10-m365-gen2" + +# Windows 11 Enterprise with Microsoft 365 Apps +image_reference_id = "galleries/default/images/microsoftwindowsdesktop_windows-ent-cpc_win11-24h2-ent-cpc-m365" +``` + +#### **Complete List of Available Built-in Images** +```hcl +# Visual Studio Images +"galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win11-m365-gen2" # VS 2022 Enterprise + Win11 + M365 +"galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-pro-general-win11-m365-gen2" # VS 2022 Professional + Win11 + M365 +"galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win10-m365-gen2" # VS 2022 Enterprise + Win10 + M365 +"galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-pro-general-win10-m365-gen2" # VS 2022 Professional + Win10 + M365 +"galleries/default/images/microsoftvisualstudio_visualstudio2019plustools_vs-2019-ent-general-win11-m365-gen2" # VS 2019 Enterprise + Win11 + M365 +"galleries/default/images/microsoftvisualstudio_visualstudio2019plustools_vs-2019-pro-general-win11-m365-gen2" # VS 2019 Professional + Win11 + M365 +"galleries/default/images/microsoftvisualstudio_visualstudio2019plustools_vs-2019-ent-general-win10-m365-gen2" # VS 2019 Enterprise + Win10 + M365 +"galleries/default/images/microsoftvisualstudio_visualstudio2019plustools_vs-2019-pro-general-win10-m365-gen2" # VS 2019 Professional + Win10 + M365 + +# Windows Base Images +"galleries/default/images/microsoftwindowsdesktop_windows-ent-cpc_win11-24h2-ent-cpc-m365" # Windows 11 Enterprise 24H2 + M365 +"galleries/default/images/microsoftwindowsdesktop_windows-ent-cpc_win11-24h2-ent-cpc" # Windows 11 Enterprise 24H2 +"galleries/default/images/microsoftwindowsdesktop_windows-ent-cpc_win11-23h2-ent-cpc-m365" # Windows 11 Enterprise 23H2 + M365 +"galleries/default/images/microsoftwindowsdesktop_windows-ent-cpc_win11-23h2-ent-cpc" # Windows 11 Enterprise 23H2 +"galleries/default/images/microsoftwindowsdesktop_windows-ent-cpc_win11-22h2-ent-cpc-m365" # Windows 11 Enterprise 22H2 + M365 +"galleries/default/images/microsoftwindowsdesktop_windows-ent-cpc_win11-22h2-ent-cpc" # Windows 11 Enterprise 22H2 +"galleries/default/images/microsoftwindowsdesktop_windows-ent-cpc_win11-22h2-ent-cpc-os" # Windows 11 Enterprise 22H2 + OS Optimizations +"galleries/default/images/microsoftwindowsdesktop_windows-ent-cpc_win10-22h2-ent-cpc-m365" # Windows 10 Enterprise 22H2 + M365 +"galleries/default/images/microsoftwindowsdesktop_windows-ent-cpc_win10-22h2-ent-cpc" # Windows 10 Enterprise 22H2 + +# Developer-Optimized Images +"galleries/default/images/microsoftvisualstudio_windowsplustools_base-win11-gen2" # Windows 11 + Developer Optimizations 24H2 +``` + +#### **List Available Built-in Images** +```bash +# List all available gallery images +az devcenter admin gallery list --dev-center-name "mydevcenter" --resource-group "myrg" + +# Get image details +az devcenter admin image list --dev-center-name "mydevcenter" --resource-group "myrg" --gallery-name "default" +``` + +#### **Custom Gallery Images** +```hcl +# Full resource ID format +image_reference_id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/myrg/providers/Microsoft.Compute/galleries/mygallery/images/myimage/versions/1.0.0" + +# Or use the object format +image_reference = { + id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/myrg/providers/Microsoft.Compute/galleries/mygallery/images/myimage" +} +``` + +### ๐Ÿ’ป **Finding Available SKUs** + +DevBox SKUs determine the virtual machine size and performance characteristics. + +#### **Common SKU Names** +```hcl +# General Purpose - Balanced CPU, Memory, and Storage +"general_i_8c32gb256ssd_v2" # 8 vCPU, 32 GB RAM, 256 GB SSD +"general_i_16c64gb512ssd_v2" # 16 vCPU, 64 GB RAM, 512 GB SSD +"general_i_32c128gb1024ssd_v2" # 32 vCPU, 128 GB RAM, 1024 GB SSD + +# Memory Optimized - Higher RAM for memory-intensive workloads +"general_i_8c64gb256ssd_v2" # 8 vCPU, 64 GB RAM, 256 GB SSD +"general_i_16c128gb512ssd_v2" # 16 vCPU, 128 GB RAM, 512 GB SSD + +# Compute Optimized - Higher CPU for compute-intensive workloads +"general_i_8c16gb256ssd_v2" # 8 vCPU, 16 GB RAM, 256 GB SSD +"general_i_16c32gb512ssd_v2" # 16 vCPU, 32 GB RAM, 512 GB SSD +``` + +#### **Complete SKU Reference Table** +| SKU Name | vCPUs | RAM (GB) | Storage (GB) | Use Case | Cost Tier | +|----------|-------|----------|--------------|----------|-----------| +| `general_i_4c16gb128ssd_v2` | 4 | 16 | 128 | Light development, testing | Low | +| `general_i_8c32gb256ssd_v2` | 8 | 32 | 256 | Standard development | Medium | +| `general_i_8c64gb256ssd_v2` | 8 | 64 | 256 | Memory-intensive apps | Medium-High | +| `general_i_16c64gb512ssd_v2` | 16 | 64 | 512 | Heavy development, IDEs | High | +| `general_i_16c128gb512ssd_v2`| 16 | 128 | 512 | Big data, large datasets | High | +| `general_i_32c128gb1024ssd_v2`| 32| 128 | 1024 | AI/ML, enterprise dev | Very High | + +#### **SKU Selection Guidelines** +- **๐Ÿ‘ฅ Team Size**: Larger teams may benefit from fewer, more powerful DevBoxes +- **๐Ÿ› ๏ธ Workload Type**: Match SKU to your development requirements +- **๐Ÿ’ฐ Budget**: Balance performance needs with cost constraints +- **โฑ๏ธ Usage Pattern**: Consider if DevBoxes will be used continuously or intermittently + +#### **List Available SKUs by Region** +```bash +# List DevBox SKUs available (subscription-wide, no location filter) +az devcenter admin sku list + +# List DevBox SKUs with output formatting +az devcenter admin sku list --output table + +# Filter DevBox SKUs for specific families +az devcenter admin sku list --query "[?contains(name, 'general_i')]" + +# Alternative: List VM SKUs by location for sizing reference +az vm list-skus --location "East US" --resource-type "virtualMachines" --query "[?contains(name, 'Standard_D')]" +``` + +#### **Advanced SKU Configuration** +```hcl +# Simple SKU (recommended for most use cases) +sku_name = "general_i_16c64gb512ssd_v2" + +# Advanced SKU with additional properties +sku = { + name = "general_i_16c64gb512ssd_v2" + tier = "Standard" # Free, Basic, Standard, Premium +} + +# Note: Additional fields like family, size, and capacity are optional +# and should only be used if specifically required by your DevCenter configuration +``` + +#### **SKU Configuration Guidelines** +- **Simple SKU**: Use `sku_name` for straightforward configurations (recommended) +- **Advanced SKU**: Use `sku` object when you need to specify additional properties like tier +- **Required Fields**: Only `name` is required in the SKU object +- **Optional Fields**: `tier`, `family`, `size`, and `capacity` can be omitted for most use cases + +### ๐Ÿ’พ **OS Storage Types** + +The `os_storage_type` property specifies the storage configuration for the OS disk. + +#### **Available Storage Types** +```hcl +# Standard SSD options +os_storage_type = "ssd_256gb" # 256 GB Standard SSD +os_storage_type = "ssd_512gb" # 512 GB Standard SSD +os_storage_type = "ssd_1024gb" # 1024 GB Standard SSD + +# Premium SSD options (if supported by SKU) +os_storage_type = "premium_256gb" # 256 GB Premium SSD +os_storage_type = "premium_512gb" # 512 GB Premium SSD +os_storage_type = "premium_1024gb" # 1024 GB Premium SSD +``` + +#### **Storage Type Guidelines** +- **Standard SSD**: Cost-effective, good performance for most development workloads +- **Premium SSD**: Higher performance, better for I/O intensive applications +- **Size Selection**: Consider your development tools, datasets, and build artifacts storage needs + +### ๐Ÿ” **Discovery Commands** + +#### **Azure CLI Commands for Discovery** +```bash +# List DevCenters in subscription +az devcenter admin devcenter list + +# List available DevBox definitions +az devcenter admin devbox-definition list --dev-center-name "mydevcenter" --resource-group "myrg" + +# Get DevBox definition details +az devcenter admin devbox-definition show --dev-center-name "mydevcenter" --resource-group "myrg" --name "mydevbox" + +# List available images in a gallery +az devcenter admin image list --dev-center-name "mydevcenter" --resource-group "myrg" --gallery-name "default" + +# Check image version details +az devcenter admin image-version list --dev-center-name "mydevcenter" --resource-group "myrg" --gallery-name "default" --image-name "microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win11-m365-gen2" + +# List DevBox SKUs with detailed output +az devcenter admin sku list --output table + +# Get specific SKU details with capabilities +az devcenter admin sku list --query "[?name=='general_i_16c64gb512ssd_v2']" + +# Check what image templates are available with structured output +az devcenter admin image list --dev-center-name "mydevcenter" --resource-group "myrg" --gallery-name "default" --query "[].{Name:name,Publisher:publisher,Offer:offer,Sku:sku}" + +# Validate if a specific image exists +az devcenter admin image show --dev-center-name "mydevcenter" --resource-group "myrg" --gallery-name "default" --name "microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win11-m365-gen2" + +# List VM SKUs by location (for reference/comparison) +az vm list-skus --location "East US" --resource-type "virtualMachines" --output table +``` + +#### **PowerShell Commands for Discovery** +```powershell +# Install the Az.DevCenter module if not already installed +# Install-Module -Name Az.DevCenter -Scope CurrentUser -Force + +# List available DevBox SKUs (note: this may not be DevCenter-specific) +Get-AzComputeResourceSku | Where-Object {$_.ResourceType -eq "virtualMachines"} | Format-Table Name, Locations, Restrictions + +# Get DevCenter information +Get-AzDevCenterAdminDevCenter -ResourceGroupName "myrg" + +# List DevBox definitions +Get-AzDevCenterAdminDevBoxDefinition -DevCenterName "mydevcenter" -ResourceGroupName "myrg" + +# Get available images in default gallery +Get-AzDevCenterAdminImage -DevCenterName "mydevcenter" -ResourceGroupName "myrg" -GalleryName "default" + +# Check specific image details +Get-AzDevCenterAdminImage -DevCenterName "mydevcenter" -ResourceGroupName "myrg" -GalleryName "default" -Name "microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win11-m365-gen2" + +# List available VM sizes for reference +Get-AzVMSize -Location "East US" | Where-Object {$_.Name -like "*Standard_D*"} | Format-Table Name, NumberOfCores, MemoryInMB, OSDiskSizeInMB + +# Get subscription context +Get-AzContext | Format-List Name, Account, Environment, Subscription +``` ## Simple Usage @@ -30,11 +287,20 @@ module "dev_box_definition" { use_slug = true } + location = "East US" dev_center_id = "/subscriptions/.../devcenters/mydevcenter" + dev_box_definition = { name = "win11-dev" - image_reference_id = "/subscriptions/.../galleries/mygallery/images/win11/versions/latest" + image_reference_id = "galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win11-m365-gen2" sku_name = "general_i_8c32gb256ssd_v2" + + hibernate_support = true + + tags = { + purpose = "development" + team = "engineering" + } } } ``` @@ -57,20 +323,20 @@ module "dev_box_definition" { dev_box_definition = { name = "ai-development-box" - + # Built-in Azure DevCenter image (recommended) image_reference_id = "galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win11-m365-gen2" - + # Or custom gallery image # image_reference = { # id = "galleries/mygallery/images/ai-dev-image" # } - sku_name = "general_i_32c128gb1024ssd_v2" - - hibernate_support = { - enabled = true - } - + + # Simple SKU configuration + sku_name = "general_i_32c128gb1024ssd_v2" + + hibernate_support = true + tags = { purpose = "ai-development" cost_center = "engineering" @@ -85,8 +351,183 @@ module "dev_box_definition" { } ``` +## Advanced SKU Object Usage + +```hcl +module "enterprise_dev_box_definition" { + source = "./modules/dev_center_dev_box_definition" + + global_settings = { + prefixes = ["enterprise"] + random_length = 3 + passthrough = false + use_slug = true + } + + location = "eastus" + dev_center_id = "/subscriptions/.../devcenters/mydevcenter" + + dev_box_definition = { + name = "enterprise-development-box" + + # Built-in Azure DevCenter image + image_reference_id = "galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-pro-general-win11-m365-gen2" + + # Advanced SKU object configuration + sku = { + name = "general_i_16c64gb512ssd_v2" + tier = "Standard" + } + + hibernate_support = true + + os_storage_type = "ssd_512gb" + + tags = { + purpose = "enterprise-development" + cost_center = "engineering" + sku_config = "advanced" + } + } + + tags = { + managed_by = "terraform" + module = "dev_center_dev_box_definition" + } +} +``` + For more examples, see the [DevBox Definition examples](../../../examples/dev_center_dev_box_definition/). +## Ready-to-Use Configuration Templates + +### ๐Ÿข **Enterprise .NET Development** +```hcl +module "enterprise_dotnet_devbox" { + source = "./modules/dev_center_dev_box_definition" + + global_settings = { + prefixes = ["corp"] + random_length = 3 + passthrough = false + use_slug = true + } + + location = "East US" + dev_center_id = "/subscriptions/your-subscription-id/resourceGroups/your-rg/providers/Microsoft.DevCenter/devcenters/your-devcenter" + + dev_box_definition = { + name = "enterprise-dotnet-dev" + image_reference_id = "galleries/default/images/microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win11-m365-gen2" + sku_name = "general_i_16c64gb512ssd_v2" + os_storage_type = "ssd_512gb" + + hibernate_support = true + + tags = { + purpose = "enterprise-development" + team = "engineering" + cost_center = "IT" + } + } +} +``` + +### ๐Ÿง **Windows Development Environment** +```hcl +module "windows_devbox" { + source = "./modules/dev_center_dev_box_definition" + + global_settings = { + prefixes = ["dev"] + random_length = 2 + passthrough = false + use_slug = true + } + + location = "West US 2" + dev_center_id = "/subscriptions/your-subscription-id/resourceGroups/your-rg/providers/Microsoft.DevCenter/devcenters/your-devcenter" + + dev_box_definition = { + name = "windows-dev-env" + image_reference_id = "galleries/default/images/microsoftwindowsdesktop_windows-ent-cpc_win11-24h2-ent-cpc-m365" + sku_name = "general_i_8c32gb256ssd_v2" + os_storage_type = "ssd_256gb" + + hibernate_support = true + + tags = { + os_type = "windows" + purpose = "development" + } + } +} +``` + +### ๐Ÿค– **AI/ML Development Workstation** +```hcl +module "ai_ml_devbox" { + source = "./modules/dev_center_dev_box_definition" + + global_settings = { + prefixes = ["ai"] + random_length = 4 + passthrough = false + use_slug = true + } + + location = "East US" + dev_center_id = "/subscriptions/your-subscription-id/resourceGroups/your-rg/providers/Microsoft.DevCenter/devcenters/your-devcenter" + + dev_box_definition = { + name = "ai-ml-workstation" + image_reference_id = "galleries/default/images/microsoftvisualstudio_windowsplustools_base-win11-gen2" + sku_name = "general_i_32c128gb1024ssd_v2" + os_storage_type = "premium_1024gb" + + hibernate_support = true # Save costs when not actively training models + + tags = { + workload_type = "ai-ml" + gpu_required = "false" + cost_center = "research" + } + } +} +``` + +### ๐Ÿงช **Testing and CI/CD Environment** +```hcl +module "testing_devbox" { + source = "./modules/dev_center_dev_box_definition" + + global_settings = { + prefixes = ["test"] + random_length = 2 + passthrough = false + use_slug = true + } + + location = "Central US" + dev_center_id = "/subscriptions/your-subscription-id/resourceGroups/your-rg/providers/Microsoft.DevCenter/devcenters/your-devcenter" + + dev_box_definition = { + name = "automated-testing" + image_reference_id = "galleries/default/images/microsoftwindowsdesktop_windows-ent-cpc_win11-24h2-ent-cpc" + sku_name = "general_i_4c16gb128ssd_v2" # Minimal resources for testing + os_storage_type = "ssd_128gb" + + hibernate_support = false # Testing environments need quick startup + + tags = { + purpose = "automated-testing" + environment = "ci-cd" + shutdown = "auto" + } + } +} +``` + ## Resources - Azure DevCenter DevBox Definition (`Microsoft.DevCenter/devcenters/devboxdefinitions`) @@ -102,14 +543,14 @@ This module implements the [Microsoft.DevCenter/devcenters/devboxdefinitions](ht |------|---------| | [terraform](#requirement\_terraform) | >= 1.9.0 | | [azapi](#requirement\_azapi) | ~> 2.4.0 | -| [azurecaf](#requirement\_azurecaf) | ~> 1.2.0 | +| [azurecaf](#requirement\_azurecaf) | ~> 1.2.29 | ## Providers | Name | Version | |------|---------| | [azapi](#provider\_azapi) | 2.4.0 | -| [azurecaf](#provider\_azurecaf) | 1.2.28 | +| [azurecaf](#provider\_azurecaf) | 1.2.29 | ## Modules @@ -121,28 +562,35 @@ No modules. |------|------| | [azapi_resource.dev_box_definition](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource) | resource | | [azurecaf_name.dev_box_definition](https://registry.terraform.io/providers/aztfmod/azurecaf/latest/docs/resources/name) | resource | +| [azapi_client_config.current](https://registry.terraform.io/providers/Azure/azapi/latest/docs/data-sources/client_config) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [dev\_box\_definition](#input\_dev\_box\_definition) | Configuration object for the DevBox Definition |
object({
name = string

# Image reference - supports both direct ID and object form
image_reference_id = optional(string)
image_reference = optional(object({
id = string
}))

# SKU configuration - storage is defined within the SKU name itself
sku_name = string

# Hibernate support
hibernate_support = optional(object({
enabled = optional(bool, false)
}))

# Tags
tags = optional(map(string), {})
})
| n/a | yes | +| [dev\_box\_definition](#input\_dev\_box\_definition) | Configuration object for the DevBox Definition |
object({
name = string

# Image reference - supports both direct ID and object form
image_reference_id = optional(string)
image_reference = optional(object({
id = string
}))

# SKU configuration - supports both simple name and full object
sku_name = optional(string)
sku = optional(object({
name = string # Required: The name of the SKU
capacity = optional(number) # Optional: Integer for scale out/in support
family = optional(string) # Optional: Hardware generation
size = optional(string) # Optional: Standalone SKU size code
tier = optional(string) # Optional: Free, Basic, Standard, Premium
}))

# OS Storage type for the Operating System disk
os_storage_type = optional(string)

# Hibernate support - simplified boolean (maps to "Enabled"/"Disabled" in API)
hibernate_support = optional(bool, false)

# Tags
tags = optional(map(string), {})
})
| n/a | yes | | [dev\_center\_id](#input\_dev\_center\_id) | The ID of the Dev Center where the DevBox Definition will be created | `string` | n/a | yes | | [global\_settings](#input\_global\_settings) | Global settings object for naming conventions and standard parameters |
object({
prefixes = list(string)
random_length = number
passthrough = bool
use_slug = bool
tags = optional(map(string), {})
})
| n/a | yes | +| [location](#input\_location) | The Azure region where the DevBox Definition will be created | `string` | n/a | yes | | [tags](#input\_tags) | Additional tags to apply to the DevBox Definition | `map(string)` | `{}` | no | ## Outputs | Name | Description | |------|-------------| +| [active\_image\_reference](#output\_active\_image\_reference) | Image reference information for the currently active image | | [dev\_center\_id](#output\_dev\_center\_id) | The ID of the Dev Center | | [hibernate\_support](#output\_hibernate\_support) | The hibernate support status | | [id](#output\_id) | The ID of the DevBox Definition | | [image\_reference](#output\_image\_reference) | The image reference configuration | +| [image\_validation\_error\_details](#output\_image\_validation\_error\_details) | Details for image validator error | +| [image\_validation\_status](#output\_image\_validation\_status) | Validation status of the configured image | | [name](#output\_name) | The name of the DevBox Definition | +| [os\_storage\_type](#output\_os\_storage\_type) | The storage type used for the Operating System disk | | [provisioning\_state](#output\_provisioning\_state) | The provisioning state of the DevBox Definition | | [sku](#output\_sku) | The SKU configuration | | [tags](#output\_tags) | The tags assigned to the DevBox Definition | +| [validation\_status](#output\_validation\_status) | Validation status for the Dev Box Definition | ## Validation Rules @@ -152,6 +600,144 @@ The module includes comprehensive validation for: - **DevBox Definition Name**: Must be 63 characters or less and follow Azure naming conventions - **Image Reference**: Either `image_reference_id` or `image_reference` object must be provided - **Dev Center ID**: Must be a valid Azure resource ID format +- **SKU Configuration**: When using SKU object, `name` field is required +- **SKU Tier**: Must be one of: `Free`, `Basic`, `Standard`, `Premium` +- **SKU Capacity**: Must be a positive integer when specified +- **OS Storage Type**: Must follow pattern `(ssd|premium)_(128|256|512|1024)gb` + +### Input Validation Examples + +#### โœ… **Valid Configurations:** +```hcl +# Valid OS storage types +os_storage_type = "ssd_256gb" +os_storage_type = "premium_512gb" +os_storage_type = "ssd_1024gb" + +# Valid SKU configurations +sku_name = "general_i_16c64gb512ssd_v2" + +sku = { + name = "general_i_16c64gb512ssd_v2" + tier = "Standard" + capacity = 1 +} +``` + +#### โŒ **Invalid Configurations:** +```hcl +# Invalid OS storage type +os_storage_type = "invalid_storage" # Error: Must match pattern + +# Invalid SKU tier +sku = { + name = "general_i_16c64gb512ssd_v2" + tier = "InvalidTier" # Error: Must be Free, Basic, Standard, or Premium +} + +# Invalid SKU capacity +sku = { + name = "general_i_16c64gb512ssd_v2" + capacity = -1 # Error: Must be positive integer +} +``` + +## Troubleshooting Guide + +### ๐Ÿ”ง **Common Issues and Solutions** + +#### **SKU Not Available Error** +``` +Error: The SKU 'general_i_16c64gb512ssd_v2' is not available in region 'West Europe' +``` + +**Solution**: Check SKU availability in your target region: +```bash +# Check which DevBox SKUs are available (subscription-wide) +az devcenter admin sku list --query "[].name" -o table + +# Find alternative DevBox SKUs with similar specs +az devcenter admin sku list --query "[?contains(name, '16c')]" + +# Check VM SKUs available in your region (for comparison) +az vm list-skus --location "West Europe" --resource-type "virtualMachines" --query "[?contains(name, 'Standard_D')]" +``` + +#### **Image Not Found Error** +``` +Error: The specified image 'galleries/default/images/nonexistent-image' could not be found +``` + +**Solution**: Verify image availability and get the correct name: +```bash +# List all available images in the default gallery +az devcenter admin image list --dev-center-name "mydevcenter" --resource-group "myrg" --gallery-name "default" --query "[].name" -o table + +# Search for specific image patterns +az devcenter admin image list --dev-center-name "mydevcenter" --resource-group "myrg" --gallery-name "default" --query "[?contains(name, 'visualstudio')]" +``` + +#### **Hibernate Support Compatibility** +Not all SKUs support hibernation. Check SKU capabilities: +```bash +# Check if a SKU supports hibernation +az devcenter admin sku list --query "[?name=='general_i_8c32gb256ssd_v2'].capabilities" + +# List all SKUs with hibernation support +az devcenter admin sku list --query "[?capabilities[?name=='HibernateSupport' && value=='true']].name" -o table +``` + +#### **Storage Type Validation** +Ensure the `os_storage_type` matches the SKU's storage configuration: +```hcl +# For SKUs with 256GB storage, use matching storage type +sku_name = "general_i_8c32gb256ssd_v2" +os_storage_type = "ssd_256gb" # โœ… Correct - matches SKU storage + +# This would be incorrect: +# os_storage_type = "ssd_512gb" # โŒ Wrong - doesn't match SKU +``` + +### ๐Ÿ” **Validation Commands** + +Before applying your configuration, validate your choices: + +```bash +# Validate DevCenter exists and is accessible +az devcenter admin devcenter show --name "mydevcenter" --resource-group "myrg" + +# Check if the image exists in the gallery +az devcenter admin image show --dev-center-name "mydevcenter" --resource-group "myrg" --gallery-name "default" --name "YOUR_IMAGE_NAME" + +# Verify DevBox SKU availability +az devcenter admin sku list --query "[?name=='YOUR_SKU_NAME']" + +# Alternative: Check VM SKU availability in target region +az vm list-skus --location "YOUR_REGION" --resource-type "virtualMachines" --query "[?name=='YOUR_VM_SIZE']" + +# Test terraform configuration +terraform plan -var-file="configuration.tfvars" +``` + +### ๐Ÿš€ **Performance Optimization Tips** + +#### **Choosing the Right SKU** +- **Development**: `general_i_8c32gb256ssd_v2` - Good for general development work +- **Heavy Development**: `general_i_16c64gb512ssd_v2` - Visual Studio, large projects +- **AI/ML Development**: `general_i_32c128gb1024ssd_v2` - Data science, model training +- **Testing/CI**: `general_i_4c16gb128ssd_v2` - Automated testing workloads + +#### **Storage Considerations** +- **SSD vs Premium**: Use Premium SSD for I/O intensive workloads +- **Size Planning**: Consider OS (40-60GB) + Tools (20-40GB) + Projects (remaining) +- **Cost vs Performance**: Balance storage performance with budget requirements + +#### **Regional Selection** +Choose regions based on: +- **Latency**: Closest to your users/developers +- **SKU Availability**: Some advanced SKUs may not be available in all regions +- **Cost**: Pricing may vary between regions +- **Compliance**: Data residency requirements ## Automatic Subscription ID Resolution diff --git a/modules/dev_center_dev_box_definition/module.tf b/modules/dev_center_dev_box_definition/module.tf index c0ef193..e10501b 100644 --- a/modules/dev_center_dev_box_definition/module.tf +++ b/modules/dev_center_dev_box_definition/module.tf @@ -3,7 +3,7 @@ terraform { required_providers { azurecaf = { source = "aztfmod/azurecaf" - version = "~> 1.2.0" + version = "~> 1.2.29" } azapi = { source = "Azure/azapi" @@ -48,11 +48,22 @@ locals { var.dev_box_definition.image_reference.id ) } : null + + # Build SKU object for advanced configuration - filter null values to avoid type issues + sku_object = var.dev_box_definition.sku != null ? merge( + { + name = var.dev_box_definition.sku.name + }, + var.dev_box_definition.sku.capacity != null ? { capacity = var.dev_box_definition.sku.capacity } : {}, + var.dev_box_definition.sku.family != null ? { family = var.dev_box_definition.sku.family } : {}, + var.dev_box_definition.sku.size != null ? { size = var.dev_box_definition.sku.size } : {}, + var.dev_box_definition.sku.tier != null ? { tier = var.dev_box_definition.sku.tier } : {} + ) : null } resource "azurecaf_name" "dev_box_definition" { name = var.dev_box_definition.name - resource_type = "general" + resource_type = "azurerm_dev_center_dev_box_definition" prefixes = var.global_settings.prefixes random_length = var.global_settings.random_length clean_input = true @@ -68,8 +79,8 @@ resource "azapi_resource" "dev_box_definition" { tags = local.tags response_export_values = ["properties.provisioningState", "properties.imageReference", "properties.sku"] - # Disable schema validation as the provider validation is overly strict for preview APIs - schema_validation_enabled = false + # Enable schema validation + schema_validation_enabled = true body = { properties = merge( # Image reference configuration @@ -79,16 +90,25 @@ resource "azapi_resource" "dev_box_definition" { imageReference = { id = local.processed_image_reference_id } - } : {}, # SKU configuration - { + } : {}, + + # SKU configuration - supports both simple name and full object + local.sku_object != null ? { + sku = local.sku_object + } : var.dev_box_definition.sku_name != null ? { sku = { name = var.dev_box_definition.sku_name } - }, + } : {}, + + # OS Storage Type configuration + try(var.dev_box_definition.os_storage_type, null) != null ? { + osStorageType = var.dev_box_definition.os_storage_type + } : {}, # Hibernate support configuration - try(var.dev_box_definition.hibernate_support, null) != null ? { - hibernateSupport = try(var.dev_box_definition.hibernate_support.enabled, false) ? "Enabled" : "Disabled" + var.dev_box_definition.hibernate_support != null ? { + hibernateSupport = var.dev_box_definition.hibernate_support ? "Enabled" : "Disabled" } : {} ) } diff --git a/modules/dev_center_dev_box_definition/output.tf b/modules/dev_center_dev_box_definition/output.tf index 3a5bc7a..c5349fd 100644 --- a/modules/dev_center_dev_box_definition/output.tf +++ b/modules/dev_center_dev_box_definition/output.tf @@ -35,6 +35,31 @@ output "hibernate_support" { value = try(azapi_resource.dev_box_definition.output.properties.hibernateSupport, null) } +output "os_storage_type" { + description = "The storage type used for the Operating System disk" + value = try(azapi_resource.dev_box_definition.output.properties.osStorageType, null) +} + +output "image_validation_status" { + description = "Validation status of the configured image" + value = try(azapi_resource.dev_box_definition.output.properties.imageValidationStatus, null) +} + +output "image_validation_error_details" { + description = "Details for image validator error" + value = try(azapi_resource.dev_box_definition.output.properties.imageValidationErrorDetails, null) +} + +output "validation_status" { + description = "Validation status for the Dev Box Definition" + value = try(azapi_resource.dev_box_definition.output.properties.validationStatus, null) +} + +output "active_image_reference" { + description = "Image reference information for the currently active image" + value = try(azapi_resource.dev_box_definition.output.properties.activeImageReference, null) +} + output "tags" { description = "The tags assigned to the DevBox Definition" value = azapi_resource.dev_box_definition.tags diff --git a/modules/dev_center_dev_box_definition/variables.tf b/modules/dev_center_dev_box_definition/variables.tf index 2091773..128233d 100644 --- a/modules/dev_center_dev_box_definition/variables.tf +++ b/modules/dev_center_dev_box_definition/variables.tf @@ -33,14 +33,24 @@ variable "dev_box_definition" { image_reference_id = optional(string) image_reference = optional(object({ id = string - })) # SKU configuration - storage is defined within the SKU name itself - sku_name = string + })) - # Hibernate support - hibernate_support = optional(object({ - enabled = optional(bool, false) + # SKU configuration - supports both simple name and full object + sku_name = optional(string) + sku = optional(object({ + name = string # Required: The name of the SKU + capacity = optional(number) # Optional: Integer for scale out/in support + family = optional(string) # Optional: Hardware generation + size = optional(string) # Optional: Standalone SKU size code + tier = optional(string) # Optional: Free, Basic, Standard, Premium })) + # OS Storage type for the Operating System disk + os_storage_type = optional(string) + + # Hibernate support - simplified boolean (maps to "Enabled"/"Disabled" in API) + hibernate_support = optional(bool, false) + # Tags tags = optional(map(string), {}) }) @@ -53,6 +63,39 @@ variable "dev_box_definition" { error_message = "Either image_reference_id or image_reference must be specified." } + validation { + condition = ( + var.dev_box_definition.sku_name != null || + var.dev_box_definition.sku != null + ) + error_message = "Either sku_name or sku must be specified." + } + + validation { + condition = ( + var.dev_box_definition.sku == null || + var.dev_box_definition.sku.name != null + ) + error_message = "When using sku object, the 'name' field is required." + } + + validation { + condition = ( + var.dev_box_definition.sku == null || + var.dev_box_definition.sku.tier == null || + contains(["Free", "Basic", "Standard", "Premium"], var.dev_box_definition.sku.tier) + ) + error_message = "SKU tier must be one of: Free, Basic, Standard, Premium." + } + + validation { + condition = ( + var.dev_box_definition.sku == null || + var.dev_box_definition.sku.capacity == null || + var.dev_box_definition.sku.capacity >= 1 + ) + error_message = "SKU capacity must be a positive integer when specified." + } validation { condition = length(var.dev_box_definition.name) <= 63 @@ -63,6 +106,14 @@ variable "dev_box_definition" { condition = can(regex("^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$", var.dev_box_definition.name)) error_message = "DevBox Definition name must start and end with alphanumeric characters and can contain hyphens." } + + validation { + condition = ( + var.dev_box_definition.os_storage_type == null || + can(regex("^(ssd|premium)_(128|256|512|1024)gb$", var.dev_box_definition.os_storage_type)) + ) + error_message = "OS storage type must follow the pattern: (ssd|premium)_(128|256|512|1024)gb (e.g., 'ssd_256gb', 'premium_512gb')." + } } variable "tags" { diff --git a/modules/dev_center_project/README.md b/modules/dev_center_project/README.md index 2f0f91d..4558f62 100644 --- a/modules/dev_center_project/README.md +++ b/modules/dev_center_project/README.md @@ -118,7 +118,7 @@ For more examples, see the [Dev Center Project examples](../../../examples/dev_c | Name | Version | |------|---------| | [azapi](#provider\_azapi) | 2.4.0 | -| [azurecaf](#provider\_azurecaf) | 1.2.28 | +| [azurecaf](#provider\_azurecaf) | 1.2.29 | ## Modules diff --git a/tests/run_tests.sh b/tests/run_tests.sh index caae0d8..ef547f9 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -61,7 +61,7 @@ echo -e "----------------\n" failed_tests=() # Run all unit tests -unit_test_dirs=("tests/unit/resource_group" "tests/unit/dev_center" "tests/unit/dev_center_environment_type" "tests/unit/dev_center_project" "tests/unit/dev_center_catalog") +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") for dir in "${unit_test_dirs[@]}"; do test_name=$(basename "$dir") diff --git a/tests/unit/dev_center_dev_box_definition/README.md b/tests/unit/dev_center_dev_box_definition/README.md new file mode 100644 index 0000000..9cf60c6 --- /dev/null +++ b/tests/unit/dev_center_dev_box_definition/README.md @@ -0,0 +1,82 @@ +# DevBox Definition Unit Tests + +This directory contains comprehensive unit tests for the Azure DevCenter DevBox Definition module. + +## Test Coverage + +The `devbox_definition_test.tftest.hcl` file includes the following test scenarios: + +### 1. `test_basic_devbox_definition_with_id` +- Tests basic DevBox definition creation using `image_reference_id` +- Verifies that a simple DevBox definition with mandatory fields is created successfully +- Uses basic SKU name and hibernate support enabled + +### 2. `test_devbox_definition_with_object` +- Tests DevBox definition creation using `image_reference` object format +- Verifies the alternative image reference configuration method +- Uses basic SKU name with hibernate support disabled + +### 3. `test_hibernate_support` +- Specifically tests hibernate support configuration +- Verifies that hibernate support can be enabled for DevBox definitions +- Tests the boolean to string conversion for the Azure API + +### 4. `test_storage_type` +- Tests OS storage type configuration +- Verifies that custom storage types can be specified +- Uses `os_storage_type` field with SSD configuration + +### 5. `test_advanced_sku` +- Tests advanced SKU configuration using the SKU object +- Verifies that complex SKU configurations with name and tier work properly +- Tests the structured SKU approach vs simple SKU name + +### 6. `test_naming_convention` +- Tests the azurecaf naming convention integration +- Verifies that different global settings affect resource naming +- Uses different prefixes and random length settings + +### 7. `test_multiple_definitions` +- Tests multiple DevBox definitions in a single configuration +- Verifies that different configurations can coexist +- Tests both image reference formats and SKU configurations simultaneously + +## Test Infrastructure + +The tests use: +- Mock providers for `azapi` and `azurecaf` +- Predefined resource groups and dev centers for testing +- All required root module variables to ensure proper isolation + +## Running Tests + +### Individual Test File +```bash +cd tests/unit/dev_center_dev_box_definition +terraform test devbox_definition_test.tftest.hcl -verbose +``` + +### All DevBox Definition Tests +```bash +# From root directory (change to test directory and back) +cd tests/unit/dev_center_dev_box_definition && terraform test devbox_definition_test.tftest.hcl -verbose && cd ../../.. +``` + +### All Unit Tests +```bash +./tests/run_tests.sh +``` + +## Test Results + +All tests should pass successfully: +- โœ… 7 tests passing +- โœ… 0 tests failing +- โœ… Complete coverage of module functionality + +## Notes + +- The tests use mock Azure subscription IDs and resource paths +- Each test runs in isolation with its own variable overrides +- Tests verify both the creation logic and the output structure +- Schema validation is enabled to ensure Azure API compatibility diff --git a/tests/unit/dev_center_dev_box_definition/devbox_definition_test.tftest.hcl b/tests/unit/dev_center_dev_box_definition/devbox_definition_test.tftest.hcl index 2136840..22bbac3 100644 --- a/tests/unit/dev_center_dev_box_definition/devbox_definition_test.tftest.hcl +++ b/tests/unit/dev_center_dev_box_definition/devbox_definition_test.tftest.hcl @@ -44,9 +44,7 @@ variables { } image_reference_id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Compute/galleries/testgallery/images/testimage/versions/latest" sku_name = "general_i_8c32gb256ssd_v2" - hibernate_support = { - enabled = true - } + hibernate_support = true tags = { environment = "test" module = "dev_center_dev_box_definition" @@ -65,10 +63,8 @@ variables { image_reference = { id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Compute/galleries/testgallery/images/testimage2/versions/1.0.0" } - sku_name = "general_i_16c64gb512ssd_v2" - hibernate_support = { - enabled = false - } + sku_name = "general_i_16c64gb512ssd_v2" + hibernate_support = false tags = { environment = "test" module = "dev_center_dev_box_definition" @@ -103,40 +99,127 @@ mock_provider "azurecaf" {} run "test_basic_devbox_definition_with_id" { command = plan - module { - source = "../../../" + providers = { + azapi = azapi + azurecaf = azurecaf } + variables { + // Override with only the first definition + dev_center_dev_box_definitions = { + definition1 = { + name = "test-definition-1" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + image_reference_id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Compute/galleries/testgallery/images/testimage/versions/latest" + sku_name = "general_i_8c32gb256ssd_v2" + hibernate_support = true + tags = { + environment = "test" + module = "dev_center_dev_box_definition" + } + } + } + } + + module { source = "../../../" } + assert { condition = module.dev_center_dev_box_definitions["definition1"] != null error_message = "DevBox Definition with image_reference_id should be created" } + + assert { + condition = length(keys(module.dev_center_dev_box_definitions)) == 1 + error_message = "Should only have one DevBox definition" + } } // Test for DevBox Definition creation with image_reference object run "test_devbox_definition_with_object" { command = plan - module { - source = "../../../" + providers = { + azapi = azapi + azurecaf = azurecaf + } + + variables { + // Override with only the second definition + dev_center_dev_box_definitions = { + definition2 = { + name = "test-definition-2" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + image_reference = { + id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Compute/galleries/testgallery/images/testimage2/versions/1.0.0" + } + sku_name = "general_i_16c64gb512ssd_v2" + hibernate_support = false + tags = { + environment = "test" + module = "dev_center_dev_box_definition" + test_case = "image_reference_object" + } + } + } } + module { source = "../../../" } + assert { condition = module.dev_center_dev_box_definitions["definition2"] != null error_message = "DevBox Definition with image_reference object should be created" } + + assert { + condition = length(keys(module.dev_center_dev_box_definitions)) == 1 + error_message = "Should only have one DevBox definition (definition2)" + } } // Test hibernate support configuration run "test_hibernate_support" { command = plan - module { - source = "../../../" + providers = { + azapi = azapi + azurecaf = azurecaf } + variables { + // Test with hibernate enabled + dev_center_dev_box_definitions = { + hibernate_enabled = { + name = "test-hibernate-enabled" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + image_reference_id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Compute/galleries/testgallery/images/testimage/versions/latest" + sku_name = "general_i_8c32gb256ssd_v2" + hibernate_support = true + tags = { + test_case = "hibernate_enabled" + } + } + } + } + + module { source = "../../../" } + assert { - condition = module.dev_center_dev_box_definitions["definition1"] != null + condition = module.dev_center_dev_box_definitions["hibernate_enabled"] != null error_message = "DevBox Definition with hibernate support should be planned for creation" } } @@ -145,12 +228,79 @@ run "test_hibernate_support" { run "test_storage_type" { command = plan - module { - source = "../../../" + providers = { + azapi = azapi + azurecaf = azurecaf + } + + variables { + // Test with OS storage type + dev_center_dev_box_definitions = { + storage_test = { + name = "test-storage-type" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + image_reference_id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Compute/galleries/testgallery/images/testimage/versions/latest" + sku_name = "general_i_16c64gb512ssd_v2" + os_storage_type = "ssd_1024gb" + hibernate_support = false + tags = { + test_case = "storage_type" + } + } + } } + + module { source = "../../../" } + assert { - condition = module.dev_center_dev_box_definitions["definition2"] != null - error_message = "DevBox Definition with SKU should be planned for creation" + condition = module.dev_center_dev_box_definitions["storage_test"] != null + error_message = "DevBox Definition with OS storage type should be planned for creation" + } +} + +// Test advanced SKU configuration +run "test_advanced_sku" { + command = plan + + providers = { + azapi = azapi + azurecaf = azurecaf + } + + variables { + // Test with advanced SKU object + dev_center_dev_box_definitions = { + advanced_sku = { + name = "test-advanced-sku" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + image_reference_id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Compute/galleries/testgallery/images/testimage/versions/latest" + sku = { + name = "general_i_32c128gb1024ssd_v2" + tier = "Standard" + } + hibernate_support = true + tags = { + test_case = "advanced_sku" + } + } + } + } + + module { source = "../../../" } + + assert { + condition = module.dev_center_dev_box_definitions["advanced_sku"] != null + error_message = "DevBox Definition with advanced SKU should be planned for creation" } } @@ -158,12 +308,106 @@ run "test_storage_type" { run "test_naming_convention" { command = plan - module { - source = "../../../" + providers = { + azapi = azapi + azurecaf = azurecaf + } + + variables { + // Test naming convention with different global settings + global_settings = { + prefixes = ["prod"] + random_length = 5 + passthrough = false + use_slug = true + } + + dev_center_dev_box_definitions = { + naming_test = { + name = "naming-convention-test" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + image_reference_id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Compute/galleries/testgallery/images/testimage/versions/latest" + sku_name = "general_i_8c32gb256ssd_v2" + hibernate_support = false + tags = { + test_case = "naming_convention" + } + } + } } + module { source = "../../../" } + assert { - condition = module.dev_center_dev_box_definitions["definition1"] != null + condition = module.dev_center_dev_box_definitions["naming_test"] != null error_message = "DevBox Definition should be planned for creation with proper naming" } } + +// Test validation - multiple definitions with different configurations +run "test_multiple_definitions" { + command = plan + + providers = { + azapi = azapi + azurecaf = azurecaf + } + + variables { + // Test with multiple definitions + dev_center_dev_box_definitions = { + def1 = { + name = "multi-test-1" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + image_reference_id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Compute/galleries/testgallery/images/testimage/versions/latest" + sku_name = "general_i_8c32gb256ssd_v2" + hibernate_support = true + } + def2 = { + name = "multi-test-2" + dev_center = { + key = "devcenter1" + } + resource_group = { + key = "rg1" + } + image_reference = { + id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Compute/galleries/testgallery/images/testimage2/versions/1.0.0" + } + sku = { + name = "general_i_16c64gb512ssd_v2" + tier = "Standard" + } + os_storage_type = "ssd_512gb" + hibernate_support = false + } + } + } + + module { source = "../../../" } + + assert { + condition = length(keys(module.dev_center_dev_box_definitions)) == 2 + error_message = "Should have exactly two DevBox definitions" + } + + assert { + condition = module.dev_center_dev_box_definitions["def1"] != null + error_message = "First DevBox definition should be created" + } + + assert { + condition = module.dev_center_dev_box_definitions["def2"] != null + error_message = "Second DevBox definition should be created" + } +} diff --git a/variables.tf b/variables.tf index 4ee69ad..e17a6ce 100644 --- a/variables.tf +++ b/variables.tf @@ -95,13 +95,21 @@ variable "dev_center_dev_box_definitions" { id = string })) - # SKU configuration - storage is defined within the SKU name itself - sku_name = string + # SKU configuration - supports both simple name and full object + sku_name = optional(string) + sku = optional(object({ + name = string + capacity = optional(number) + family = optional(string) + size = optional(string) + tier = optional(string) # Free, Basic, Standard, Premium + })) + + # OS Storage type for the Operating System disk + os_storage_type = optional(string) # Hibernate support - hibernate_support = optional(object({ - enabled = optional(bool, false) - })) + hibernate_support = optional(bool, false) # Tags tags = optional(map(string), {}) From f1cc961fd335adbf2da43b25e2b249400a6ff81b Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Tue, 17 Jun 2025 13:57:07 +0800 Subject: [PATCH 04/10] refactor: Remove tflint ignore comment for dev_center_dev_box_definitions variable --- variables.tf | 1 - 1 file changed, 1 deletion(-) diff --git a/variables.tf b/variables.tf index e17a6ce..b4e9ef4 100644 --- a/variables.tf +++ b/variables.tf @@ -77,7 +77,6 @@ variable "dev_center_galleries" { default = {} } -# tflint-ignore: terraform_unused_declarations variable "dev_center_dev_box_definitions" { description = "Dev Center Dev Box Definitions configuration objects" type = map(object({ From a452690c95ede58db14e83689848d100c4156f21 Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Tue, 17 Jun 2025 14:00:21 +0800 Subject: [PATCH 05/10] refactor: Clean up formatting in provider.tf by removing unnecessary blank lines --- provider.tf | 3 --- 1 file changed, 3 deletions(-) diff --git a/provider.tf b/provider.tf index 8e72d04..b4248fe 100644 --- a/provider.tf +++ b/provider.tf @@ -10,7 +10,4 @@ terraform { } } required_version = ">= 1.12.1" - -} - } From 63ae93938c0d759ee8cedf0afb3931577100da38 Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Tue, 17 Jun 2025 14:16:33 +0800 Subject: [PATCH 06/10] fix: Add terraform init command to plan, apply, and destroy tasks in tasks.json --- .vscode/tasks.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 329873c..8ac25d4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,7 +7,7 @@ "command": "/bin/bash", "args": [ "-c", - "export ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv) && terraform plan -var-file=${input:devCenterExample}" + "export ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv) && terraform init && terraform plan -var-file=${input:devCenterExample}" ], "options": { "cwd": "${workspaceFolder}" @@ -24,7 +24,7 @@ "command": "/bin/bash", "args": [ "-c", - "export ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv) && terraform apply -auto-approve -var-file=${input:devCenterExample}" + "export ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv) && terraform init && terraform apply -auto-approve -var-file=${input:devCenterExample}" ], "options": { "cwd": "${workspaceFolder}" @@ -37,7 +37,7 @@ "command": "/bin/bash", "args": [ "-c", - "export ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv) && terraform destroy -auto-approve -var-file=${input:devCenterExample}" + "export ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv) && terraform init && terraform destroy -auto-approve -var-file=${input:devCenterExample}" ], "options": { "cwd": "${workspaceFolder}" From ba1e1733f0a35aa298d057518d79b795103d31e2 Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Thu, 19 Jun 2025 15:12:00 +0800 Subject: [PATCH 07/10] feat: Add DevCenter Project Pool Schedule module - Introduced a new Terraform module for managing Azure DevCenter project pool schedules. - Implemented features including flexible schedule types (StopDevBox, StartDevBox), time zone support, and state management. - Added comprehensive variable validation for time formats, types, and states. - Included automatic merging of global and resource-specific tags. - Integrated with the AzAPI provider using the latest Azure API preview version. - Created README documentation with usage examples, requirements, and validation details. - Developed unit tests for the new module to ensure functionality and correctness. --- .github/copilot-instructions.md | 8 + .../instructions/devbox-tf.instructions.md | 311 ++++++++++++++++++ .gitignore | 3 + .vscode/mcp.json | 4 + CHANGES_SUMMARY.md | 31 ++ dev_center_project_pool_schedules.tf | 19 ++ dev_center_project_pools.tf | 13 + .../enhanced_case/configuration.tfvars | 185 +++++++++++ .../simple_case/configuration.tfvars | 87 +++++ .../enhanced_case/configuration.tfvars | 151 +++++++++ .../simple_case/configuration.tfvars | 95 ++++++ modules/dev_center_project_pool/README.md | 133 ++++++++ modules/dev_center_project_pool/module.tf | 86 +++++ modules/dev_center_project_pool/output.tf | 42 +++ modules/dev_center_project_pool/variables.tf | 66 ++++ .../README.md | 137 ++++++++ .../module.tf | 60 ++++ .../output.tf | 42 +++ .../variables.tf | 57 ++++ tests/run_tests.sh | 87 ++++- .../pool_test.tftest.hcl | 216 ++++++++++++ .../pool_test_simple.tftest.hcl | 216 ++++++++++++ .../schedule_test.tftest.hcl | 234 +++++++++++++ .../schedule_test_simple.tftest.hcl | 234 +++++++++++++ variables.tf | 73 +++- 25 files changed, 2567 insertions(+), 23 deletions(-) create mode 100644 .github/instructions/devbox-tf.instructions.md create mode 100644 dev_center_project_pool_schedules.tf create mode 100644 dev_center_project_pools.tf create mode 100644 examples/dev_center_project_pool/enhanced_case/configuration.tfvars create mode 100644 examples/dev_center_project_pool/simple_case/configuration.tfvars create mode 100644 examples/dev_center_project_pool_schedule/enhanced_case/configuration.tfvars create mode 100644 examples/dev_center_project_pool_schedule/simple_case/configuration.tfvars create mode 100644 modules/dev_center_project_pool/README.md create mode 100644 modules/dev_center_project_pool/module.tf create mode 100644 modules/dev_center_project_pool/output.tf create mode 100644 modules/dev_center_project_pool/variables.tf create mode 100644 modules/dev_center_project_pool_schedule/README.md create mode 100644 modules/dev_center_project_pool_schedule/module.tf create mode 100644 modules/dev_center_project_pool_schedule/output.tf create mode 100644 modules/dev_center_project_pool_schedule/variables.tf create mode 100644 tests/unit/dev_center_project_pool/pool_test.tftest.hcl create mode 100644 tests/unit/dev_center_project_pool/pool_test_simple.tftest.hcl create mode 100644 tests/unit/dev_center_project_pool_schedule/schedule_test.tftest.hcl create mode 100644 tests/unit/dev_center_project_pool_schedule/schedule_test_simple.tftest.hcl 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 44e16e6..c6bdfe4 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -20,6 +20,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..2c7dc09 100644 --- a/CHANGES_SUMMARY.md +++ b/CHANGES_SUMMARY.md @@ -4,6 +4,37 @@ 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) + +### 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..02af3af --- /dev/null +++ b/dev_center_project_pools.tf @@ -0,0 +1,13 @@ +# 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 = merge(each.value, { + dev_box_definition_name = module.dev_center_dev_box_definitions[each.value.dev_box_definition_name].name + }) + 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..d0ca734 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)) }) } @@ -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 = {} +} From c00f9a26b81249d0c9e522e301027d225aa547b3 Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Thu, 19 Jun 2025 15:18:05 +0800 Subject: [PATCH 08/10] fix: Resolve merge conflicts in PR #24 and update documentation --- CHANGES_SUMMARY.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES_SUMMARY.md b/CHANGES_SUMMARY.md index 2c7dc09..f1c2a4b 100644 --- a/CHANGES_SUMMARY.md +++ b/CHANGES_SUMMARY.md @@ -6,6 +6,18 @@ This document summarizes the updates made to the Azure DevCenter module to imple ## 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` From acd7ee46bdf6accaf40a42473b73bd9a50e1bee2 Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Tue, 24 Jun 2025 19:28:54 +0700 Subject: [PATCH 09/10] Update dev_center_project_pools.tf Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dev_center_project_pools.tf | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dev_center_project_pools.tf b/dev_center_project_pools.tf index 02af3af..1e65d3e 100644 --- a/dev_center_project_pools.tf +++ b/dev_center_project_pools.tf @@ -4,9 +4,11 @@ module "dev_center_project_pools" { for_each = try(var.dev_center_project_pools, {}) global_settings = var.global_settings - pool = merge(each.value, { + 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 From 58c2d39fc7db72a0d10e7d90242b4dd59af54f45 Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Tue, 24 Jun 2025 19:29:04 +0700 Subject: [PATCH 10/10] Update variables.tf Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- variables.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variables.tf b/variables.tf index d0ca734..02d22ef 100644 --- a/variables.tf +++ b/variables.tf @@ -6,7 +6,7 @@ variable "global_settings" { passthrough = optional(bool) use_slug = optional(bool) tags = optional(map(string)) - regions = optional(map(string)) + regions = optional(map(string)) # Ensure downstream modules accept this field }) }