diff --git a/.azdo/pipelines/azure-dev.yml b/.azdo/pipelines/azure-dev.yml deleted file mode 100644 index 2fb39fb6..00000000 --- a/.azdo/pipelines/azure-dev.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Dev Box Accelerator - Deployment -trigger: none - -pool: - vmImage: ubuntu-latest - name: linuxPool - -steps: - # If you can't install above task in your organization, you can comment it and uncomment below task to install azd - - task: Bash@3 - displayName: Install azd - inputs: - targetType: 'inline' - script: | - sudo apt-get update && sudo apt-get upgrade -y - curl -fsSL https://aka.ms/install-azd.sh | sudo bash - - # azd delegate auth to az to use service connection with AzureCLI@2 - - pwsh: | - azd config set auth.useAzCliAuth "true" - displayName: Configure AZD to Use AZ CLI Authentication. - - - task: AzureCLI@2 - displayName: Provision Infrastructure - inputs: - azureSubscription: azconnection - scriptType: bash - scriptLocation: inlineScript - keepAzSessionActive: true - inlineScript: | - azd provision --no-prompt - env: - AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) - AZURE_LOCATION: $(AZURE_LOCATION) - AZURE_ENV_NAME: $(AZURE_ENV_NAME) - KEY_VAULT_SECRET: $(KEY_VAULT_SECRET) - diff --git a/.configuration/devcenter/workloads/winget-update.ps1 b/.configuration/devcenter/workloads/winget-update.ps1 index 1c4f5db3..bdf01cf2 100644 --- a/.configuration/devcenter/workloads/winget-update.ps1 +++ b/.configuration/devcenter/workloads/winget-update.ps1 @@ -1,147 +1,323 @@ +#Requires -Version 5.1 + Set-ExecutionPolicy Bypass -Scope Process -Force Clear-Host -<# +<# .SYNOPSIS - Quietly updates all Microsoft Store apps using winget (v1.11.x compatible). + Quietly updates all Microsoft Store apps using winget (v1.11.x compatible). .DESCRIPTION - - Runs fully non-interactive (no prompts). - - Properly orders command + flags for winget 1.11.x. - - Accepts msstore/package agreements on upgrade/install only. - - Uses include-unknown and a forced second pass to catch stubborn Store apps. - - Detects if winget/App Installer updated itself mid-run and retries once. - - Executes winget by absolute path (no App Execution Alias quirks). - - Logs to C:\ProgramData\Winget-StoreUpgrade\upgrade-YYYYMMDD-HHMMSS.log + This script performs a comprehensive update of all Microsoft Store applications + using Windows Package Manager (winget). Key features: + - Runs fully non-interactive (no prompts) + - Properly orders command + flags for winget 1.11.x + - Accepts msstore/package agreements on upgrade/install only + - Uses include-unknown and a forced second pass to catch stubborn Store apps + - Detects if winget/App Installer updated itself mid-run and retries once + - Executes winget by absolute path (no App Execution Alias quirks) + - Logs to C:\ProgramData\Winget-StoreUpgrade\upgrade-YYYYMMDD-HHMMSS.log .NOTES - Recommended to run in an elevated session to service machine-wide apps. + Author: DevExp Team + Recommended to run in an elevated session to service machine-wide apps. + +.EXAMPLE + .\winget-update.ps1 + Runs a full update of all Microsoft Store applications. #> -# ===== Global non-interactive settings ===== +# Script Configuration $ErrorActionPreference = 'Stop' -$ProgressPreference = 'SilentlyContinue' -$env:WINGET_DISABLE_INTERACTIVITY = '1' # environment-based; no CLI side effects +$ProgressPreference = 'SilentlyContinue' +$env:WINGET_DISABLE_INTERACTIVITY = '1' + +# Logging Configuration +$Script:LogRoot = Join-Path $env:ProgramData 'Winget-StoreUpgrade' +if (-not (Test-Path $Script:LogRoot)) { + New-Item -Path $Script:LogRoot -ItemType Directory -Force | Out-Null +} +$Script:Timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' +$Script:LogFile = Join-Path $Script:LogRoot "upgrade-$Script:Timestamp.log" + +function Write-LogInfo { + <# + .SYNOPSIS + Writes an informational message to console and log file. + + .PARAMETER Message + The message to write. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Message + ) + "[INFO ] $Message" | Tee-Object -FilePath $Script:LogFile -Append +} + +function Write-LogWarning { + <# + .SYNOPSIS + Writes a warning message to console and log file. + + .PARAMETER Message + The message to write. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Message + ) + "[WARN ] $Message" | Tee-Object -FilePath $Script:LogFile -Append +} -# ===== Logging ===== -$LogRoot = Join-Path $env:ProgramData 'Winget-StoreUpgrade' -if (-not (Test-Path $LogRoot)) { New-Item -Path $LogRoot -ItemType Directory -Force | Out-Null } -$Timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' -$LogFile = Join-Path $LogRoot "upgrade-$Timestamp.log" +function Write-LogError { + <# + .SYNOPSIS + Writes an error message to console and log file. + + .PARAMETER Message + The message to write. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Message + ) + "[ERROR] $Message" | Tee-Object -FilePath $Script:LogFile -Append +} -function Write-Info { param([string]$m) "[INFO ] $m" | Tee-Object -FilePath $LogFile -Append } -function Write-Warn { param([string]$m) "[WARN ] $m" | Tee-Object -FilePath $LogFile -Append } -function Write-Err { param([string]$m) "[ERROR] $m" | Tee-Object -FilePath $LogFile -Append } +Write-LogInfo "Log file: $Script:LogFile" +Write-LogInfo "Starting Microsoft Store updates..." -Write-Info "Log file: $LogFile" -Write-Info "Starting Microsoft Store updates..." +function Resolve-WinGetExecutable { + <# + .SYNOPSIS + Resolves the path to winget.exe. + + .DESCRIPTION + Prefers the packaged App Installer location (more stable than user alias). + Falls back to whatever PowerShell resolves. + + .OUTPUTS + System.String - The path to winget.exe. + + .EXAMPLE + $wingetPath = Resolve-WinGetExecutable + #> + [CmdletBinding()] + [OutputType([string])] + param() -# ===== Robust winget resolution and invoker ===== -function Resolve-WinGetExe { # Prefer packaged App Installer location (more stable than user alias) $pkg = Get-AppxPackage -Name Microsoft.DesktopAppInstaller -ErrorAction SilentlyContinue if ($pkg) { $candidate = Join-Path $pkg.InstallLocation 'winget.exe' - if (Test-Path $candidate) { return $candidate } + if (Test-Path $candidate) { + return $candidate + } } + # Fallback to whatever PowerShell resolves return (Get-Command winget.exe -ErrorAction Stop).Source } -$script:WinGetExe = Resolve-WinGetExe +$Script:WinGetExe = Resolve-WinGetExecutable -function Invoke-WinGet { +function Invoke-WinGetCommand { + <# + .SYNOPSIS + Invokes a winget command with proper logging. + + .DESCRIPTION + Executes winget directly by absolute path and logs output. + Optionally retries if winget updated itself mid-run. + + .PARAMETER CommandArgs + The arguments to pass to winget. + + .PARAMETER RetryOnSelfUpdate + If specified, retries once if winget self-updated. + + .OUTPUTS + System.String - The output from winget. + + .EXAMPLE + Invoke-WinGetCommand -CommandArgs @('upgrade', '--all') -RetryOnSelfUpdate + #> + [CmdletBinding()] + [OutputType([string])] param( - [Parameter(Mandatory)][string[]]$Args, # e.g. @('upgrade','--all',...) - [switch]$RetryOnSelfUpdate # retry once if winget self-updated + [Parameter(Mandatory = $true)] + [Alias('Arguments')] + [string[]]$CommandArgs, + + [Parameter(Mandatory = $false)] + [switch]$RetryOnSelfUpdate ) - # Execute the EXE directly — do NOT call 'winget' then pass a path - $output = & $script:WinGetExe @Args 2>&1 - $text = $output | Out-String - $text | Tee-Object -FilePath $LogFile -Append | Out-Null + + # Execute the EXE directly + $output = & $Script:WinGetExe @CommandArgs 2>&1 + $text = $output | Out-String + $text | Tee-Object -FilePath $Script:LogFile -Append | Out-Null if ($RetryOnSelfUpdate -and $text -match 'Restart the application to complete the upgrade') { - Write-Info "winget/App Installer updated itself; re-resolving path and retrying once..." + Write-LogInfo "winget/App Installer updated itself; re-resolving path and retrying once..." Start-Sleep -Milliseconds 500 - $script:WinGetExe = Resolve-WinGetExe - $output = & $script:WinGetExe @Args 2>&1 - $text = $output | Out-String - $text | Tee-Object -FilePath $LogFile -Append | Out-Null + $Script:WinGetExe = Resolve-WinGetExecutable + $output = & $Script:WinGetExe @CommandArgs 2>&1 + $text = $output | Out-String + $text | Tee-Object -FilePath $Script:LogFile -Append | Out-Null } + return $text } -# ===== Preflight: confirm winget exists ===== -try { - Invoke-WinGet -Args @('--version') | Out-Null -} catch { - Write-Err "winget (App Installer) not found. Install/update 'App Installer' from Microsoft Store and re-run." - exit 1 +function Test-WinGetAvailability { + <# + .SYNOPSIS + Verifies winget is available and working. + + .OUTPUTS + System.Boolean - True if winget is available, False otherwise. + + .EXAMPLE + if (Test-WinGetAvailability) { Write-Host "WinGet is ready" } + #> + [CmdletBinding()] + [OutputType([bool])] + param() + + try { + $null = Invoke-WinGetCommand -CommandArgs @('--version') + return $true + } + catch { + Write-LogError "winget (App Installer) not found. Install/update 'App Installer' from Microsoft Store and re-run." + return $false + } } -# ===== Ensure Microsoft Store Install Service is running (helps Store updates) ===== -try { - $svc = Get-Service -Name InstallService -ErrorAction SilentlyContinue - if ($svc -and $svc.Status -ne 'Running') { - Write-Info "Starting Microsoft Store Install Service (InstallService)..." - Start-Service -Name InstallService -ErrorAction SilentlyContinue +function Start-StoreInstallService { + <# + .SYNOPSIS + Ensures the Microsoft Store Install Service is running. + + .DESCRIPTION + Starts the InstallService if it's not already running. + This helps with Store app updates. + #> + [CmdletBinding()] + param() + + try { + $svc = Get-Service -Name InstallService -ErrorAction SilentlyContinue + if ($svc -and $svc.Status -ne 'Running') { + Write-LogInfo "Starting Microsoft Store Install Service (InstallService)..." + Start-Service -Name InstallService -ErrorAction SilentlyContinue + } + } + catch { + Write-LogWarning "Could not verify/start InstallService. Continuing..." } -} catch { Write-Warn "Could not verify/start InstallService. Continuing..." } +} -# ===== Ensure msstore source exists and refresh (NO accept flags here) ===== -try { - $sources = Invoke-WinGet -Args @('source','list','--disable-interactivity') - if ($sources -notmatch '(?im)^\s*msstore\b') { - Write-Info "msstore source not found; resetting..." - Invoke-WinGet -Args @('source','reset','--force','msstore','--disable-interactivity') +function Initialize-WinGetSources { + <# + .SYNOPSIS + Ensures winget sources are properly configured. + + .DESCRIPTION + Verifies the msstore source exists and refreshes all sources. + #> + [CmdletBinding()] + param() + + try { + $sources = Invoke-WinGetCommand -CommandArgs @('source', 'list', '--disable-interactivity') + if ($sources -notmatch '(?im)^\s*msstore\b') { + Write-LogInfo "msstore source not found; resetting..." + $null = Invoke-WinGetCommand -CommandArgs @('source', 'reset', '--force', 'msstore', '--disable-interactivity') + } + $null = Invoke-WinGetCommand -CommandArgs @('source', 'update', '--disable-interactivity') } - Invoke-WinGet -Args @('source','update','--disable-interactivity') | Out-Null -} catch { - Write-Warn "Winget source operations reported issues; continuing..." + catch { + Write-LogWarning "Winget source operations reported issues; continuing..." + } +} + +function Update-MicrosoftStoreApps { + <# + .SYNOPSIS + Performs multiple passes to update all Microsoft Store apps. + + .DESCRIPTION + Runs three passes: + 1. Standard upgrade with include-unknown + 2. Forced upgrade for stubborn apps + 3. Safety net pass for apps mapped under other sources + #> + [CmdletBinding()] + param() + + # Pass 1: Accept msstore terms & upgrade what winget can detect + Write-LogInfo "Pass 1: upgrading Microsoft Store apps (include-unknown)..." + $null = Invoke-WinGetCommand -CommandArgs @( + 'upgrade', '--all', + '--source', 'msstore', + '--include-unknown', + '--silent', + '--accept-source-agreements', '--accept-package-agreements', + '--disable-interactivity' + ) -RetryOnSelfUpdate + + # Pass 2: Force re-install latest for stragglers where version compare is unknown + Write-LogInfo "Pass 2: forced upgrade (msstore) for remaining/unknown version apps..." + $null = Invoke-WinGetCommand -CommandArgs @( + 'upgrade', '--all', + '--source', 'msstore', + '--include-unknown', + '--force', + '--silent', + '--accept-source-agreements', '--accept-package-agreements', + '--disable-interactivity' + ) -RetryOnSelfUpdate + + # Safety net pass: catch apps mapped under other sources + Write-LogInfo "Safety net: unfiltered pass to catch any remaining packages..." + $null = Invoke-WinGetCommand -CommandArgs @( + 'upgrade', '--all', + '--include-unknown', + '--silent', + '--accept-source-agreements', '--accept-package-agreements', + '--disable-interactivity' + ) -RetryOnSelfUpdate + + # Summary: show any remaining Store upgrades + Write-LogInfo "Summary check for remaining Microsoft Store upgrades..." + $null = Invoke-WinGetCommand -CommandArgs @('upgrade', '--source', 'msstore', '--disable-interactivity') } -# ===== Optional: prevent winget self-upgrade during the session (pin App Installer) ===== -# (Uncomment if you want to avoid mid-run self-update entirely) -# Invoke-WinGet -Args @('pin','add','--id','Microsoft.AppInstaller','--disable-interactivity') | Out-Null - -# ===== PASS 1: Accept msstore terms & upgrade what winget can detect ===== -Write-Info "Pass 1: upgrading Microsoft Store apps (include-unknown)..." -Invoke-WinGet -Args @( - 'upgrade','--all', - '--source','msstore', - '--include-unknown', - '--silent', - '--accept-source-agreements','--accept-package-agreements', - '--disable-interactivity' -) -RetryOnSelfUpdate | Out-Null - -# ===== PASS 2: Force re-install latest for stragglers where version compare is unknown ===== -Write-Info "Pass 2: forced upgrade (msstore) for remaining/unknown version apps..." -Invoke-WinGet -Args @( - 'upgrade','--all', - '--source','msstore', - '--include-unknown', - '--force', - '--silent', - '--accept-source-agreements','--accept-package-agreements', - '--disable-interactivity' -) -RetryOnSelfUpdate | Out-Null - -# ===== OPTIONAL Safety net pass: catch apps mapped under other sources ===== -Write-Info "Safety net: unfiltered pass to catch any remaining packages..." -Invoke-WinGet -Args @( - 'upgrade','--all', - '--include-unknown', - '--silent', - '--accept-source-agreements','--accept-package-agreements', - '--disable-interactivity' -) -RetryOnSelfUpdate | Out-Null - -# ===== Summary: show any remaining Store upgrades (no accept flags here) ===== -Write-Info "Summary check for remaining Microsoft Store upgrades..." -Invoke-WinGet -Args @('upgrade','--source','msstore','--disable-interactivity') | Out-Null - -# ===== Optional: unpin App Installer after run ===== -# Invoke-WinGet -Args @('pin','remove','--id','Microsoft.AppInstaller','--disable-interactivity') | Out-Null - -Write-Info "Completed. Full log: $LogFile" +# Main script execution +try { + # Preflight check + if (-not (Test-WinGetAvailability)) { + exit 1 + } + + # Ensure Store service is running + Start-StoreInstallService + + # Configure sources + Initialize-WinGetSources + + # Run updates + Update-MicrosoftStoreApps + + Write-LogInfo "Completed. Full log: $Script:LogFile" +} +catch { + Write-LogError "Script execution failed: $_" + exit 1 +} diff --git a/.configuration/powershell/cleanUp.ps1 b/.configuration/powershell/cleanUp.ps1 index 4a752bbb..1199fbb9 100644 --- a/.configuration/powershell/cleanUp.ps1 +++ b/.configuration/powershell/cleanUp.ps1 @@ -1,83 +1,223 @@ -# PowerShell script to clean up Azure resources +#Requires -Version 5.1 +<# +.SYNOPSIS + Cleans up Azure resource groups created for DevExp-DevBox environment. -[CmdletBinding()] +.DESCRIPTION + This script deletes Azure resource groups and their associated deployments + for the DevExp-DevBox infrastructure. It removes workload, connectivity, + monitoring, security, and supporting resource groups. + +.PARAMETER EnvName + The environment name used in resource group naming. Defaults to 'demo'. + +.PARAMETER Location + The Azure region where resources are deployed. Defaults to 'eastus2'. + Valid values: eastus, eastus2, westus, westus2, westus3, northeurope, westeurope + +.PARAMETER WorkloadName + The workload name prefix used in resource group naming. Defaults to 'devexp'. + +.EXAMPLE + .\cleanUp.ps1 + Cleans up resources using default environment 'demo' in 'eastus2'. + +.EXAMPLE + .\cleanUp.ps1 -EnvName "prod" -Location "westus2" + Cleans up resources for the 'prod' environment in 'westus2'. + +.NOTES + Author: DevExp Team + Requires: Azure CLI (az) authenticated with appropriate permissions +#> + +[CmdletBinding(SupportsShouldProcess)] param( - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] [string]$EnvName = "demo", - - [Parameter(Mandatory=$false)] + + [Parameter(Mandatory = $false)] [ValidateSet("eastus", "eastus2", "westus", "westus2", "westus3", "northeurope", "westeurope")] - [string]$Location = "eastus2" + [string]$Location = "eastus2", + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$WorkloadName = "devexp" ) -# Exit immediately if a command exits with a non-zero status, treat unset variables as an error, and propagate errors in pipelines. -$ErrorActionPreference = "Stop" -$WarningPreference = "Stop" - -$workloadname = "devexp" # Replace with your workload name -$environment = $EnvName # Replace with your environment name (e.g., dev, prod) -$location = $Location # Replace with your environment (e.g., dev, prod) -# Azure Resource Group Names Constants -$workloadResourceGroup = "${workloadname}-workload-${environment}-${location}-rg" -$connectivityResourceGroup = "${workloadname}-connectivity-${environment}-${location}-rg" -$managementResourceGroup = "${workloadname}-monitoring-${environment}-${location}-rg" -$securityResourceGroup = "${workloadname}-security-${environment}-${location}-rg" - -# Function to delete a resource group -function Remove-ResourceGroup { - param ( +# Script Configuration +$ErrorActionPreference = 'Stop' +$WarningPreference = 'Stop' + +function Remove-AzureResourceGroup { + <# + .SYNOPSIS + Deletes an Azure resource group and its deployments. + + .DESCRIPTION + First deletes all deployments within the resource group, then initiates + an asynchronous deletion of the resource group itself. + + .PARAMETER ResourceGroupName + The name of the resource group to delete. + + .OUTPUTS + System.Boolean - True if deletion initiated successfully, False otherwise. + + .EXAMPLE + Remove-AzureResourceGroup -ResourceGroupName "my-resource-group" + #> + [CmdletBinding(SupportsShouldProcess)] + [OutputType([bool])] + param( [Parameter(Mandatory = $true)] - [string]$resourceGroupName + [ValidateNotNullOrEmpty()] + [string]$ResourceGroupName ) try { - $groupExists = (az group exists --name $resourceGroupName) + # Check if resource group exists + $groupExists = az group exists --name $ResourceGroupName + if ($LASTEXITCODE -ne 0) { + throw "Failed to check if resource group exists." + } + + if ($groupExists -ne "true") { + Write-Output "Resource group '$ResourceGroupName' does not exist. Skipping." + return $true + } - if ($groupExists -eq "true") { + if ($PSCmdlet.ShouldProcess($ResourceGroupName, "Delete resource group")) { # List and delete all deployments in the resource group - $deployments = az deployment group list --resource-group $resourceGroupName --query "[].name" -o tsv - foreach ($deployment in $deployments) { - Write-Output "Deleting deployment: $deployment" - az deployment group delete --resource-group $resourceGroupName --name $deployment - Write-Output "Deployment $deployment deleted." + Write-Verbose "Listing deployments in resource group: $ResourceGroupName" + $deployments = az deployment group list --resource-group $ResourceGroupName --query "[].name" --output tsv + + if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($deployments)) { + foreach ($deployment in ($deployments -split "`n")) { + if (-not [string]::IsNullOrWhiteSpace($deployment)) { + Write-Output "Deleting deployment: $deployment" + $null = az deployment group delete --resource-group $ResourceGroupName --name $deployment 2>&1 + Write-Output "Deployment '$deployment' deleted." + } + } } + + # Wait for deployments to finish deleting Start-Sleep -Seconds 10 - Write-Output "Deleting resource group: $resourceGroupName..." - az group delete --name $resourceGroupName --yes --no-wait - Write-Output "Resource group $resourceGroupName deletion initiated." - } - else { - Write-Output "Resource group $resourceGroupName does not exist. Skipping deletion." + + # Delete the resource group asynchronously + Write-Output "Deleting resource group: $ResourceGroupName (async)..." + $null = az group delete --name $ResourceGroupName --yes --no-wait + if ($LASTEXITCODE -ne 0) { + throw "Failed to initiate resource group deletion." + } + Write-Output "Resource group '$ResourceGroupName' deletion initiated." } + + return $true } catch { - Write-Error "Error deleting resource group ${resourceGroupName}: $_" - return 1 + Write-Error "Error deleting resource group '$ResourceGroupName': $_" + return $false } } -# Function to clean up resources -function Remove-Resources { +function Remove-AllResourceGroups { + <# + .SYNOPSIS + Removes all DevExp-DevBox related resource groups. + + .DESCRIPTION + Deletes workload, connectivity, monitoring, security, and supporting + resource groups based on the naming convention. + + .PARAMETER WorkloadName + The workload name prefix. + + .PARAMETER Environment + The environment name (e.g., demo, dev, prod). + + .PARAMETER Location + The Azure region. + + .OUTPUTS + System.Boolean - True if all deletions initiated successfully, False otherwise. + + .EXAMPLE + Remove-AllResourceGroups -WorkloadName "devexp" -Environment "demo" -Location "eastus2" + #> + [CmdletBinding(SupportsShouldProcess)] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string]$WorkloadName, + + [Parameter(Mandatory = $true)] + [string]$Environment, + + [Parameter(Mandatory = $true)] + [string]$Location + ) + try { Clear-Host - Remove-ResourceGroup -resourceGroupName $workloadResourceGroup - Remove-ResourceGroup -resourceGroupName $connectivityResourceGroup - Remove-ResourceGroup -resourceGroupName $managementResourceGroup - Remove-ResourceGroup -resourceGroupName $securityResourceGroup - Remove-ResourceGroup -resourceGroupName "NetworkWatcherRG" - Remove-ResourceGroup -resourceGroupName "Default-ActivityLogAlerts" - Remove-ResourceGroup -resourceGroupName "DefaultResourceGroup-WUS2" + + # Define resource group names based on naming convention + $resourceGroups = @( + "${WorkloadName}-workload-${Environment}-${Location}-rg", + "${WorkloadName}-connectivity-${Environment}-${Location}-rg", + "${WorkloadName}-monitoring-${Environment}-${Location}-rg", + "${WorkloadName}-security-${Environment}-${Location}-rg", + "NetworkWatcherRG", + "Default-ActivityLogAlerts", + "DefaultResourceGroup-WUS2" + ) + + Write-Output "Starting cleanup of resource groups..." + Write-Output "Environment: $Environment" + Write-Output "Location: $Location" + Write-Output "" + + $allSucceeded = $true + + foreach ($rg in $resourceGroups) { + $success = Remove-AzureResourceGroup -ResourceGroupName $rg + if (-not $success) { + $allSucceeded = $false + } + } + + if ($allSucceeded) { + Write-Output "" + Write-Output "All resource group deletions initiated successfully." + } + else { + Write-Warning "Some resource group deletions failed. Check errors above." + } + + return $allSucceeded } catch { Write-Error "Error during cleanup process: $_" - return 1 + return $false } } # Main script execution try { - Remove-Resources + Write-Output "DevExp-DevBox Resource Cleanup" + Write-Output "==============================" + + $success = Remove-AllResourceGroups ` + -WorkloadName $WorkloadName ` + -Environment $EnvName ` + -Location $Location + + if (-not $success) { + exit 1 + } } catch { Write-Error "Script execution failed: $_" diff --git a/.configuration/readme.md b/.configuration/readme.md deleted file mode 100644 index e69de29b..00000000 diff --git a/.configuration/setup/powershell/Azure/createCustomRole.ps1 b/.configuration/setup/powershell/Azure/createCustomRole.ps1 index cd0b70a9..f426955a 100644 --- a/.configuration/setup/powershell/Azure/createCustomRole.ps1 +++ b/.configuration/setup/powershell/Azure/createCustomRole.ps1 @@ -1,35 +1,221 @@ -# Variables -$RoleName = "Contoso DevBox - Role Assignment Writer" -$SubscriptionId = "6a4029ea-399b-4933-9701-436db72883d4" - -# Role Definition -$RoleDefinition = @{ - Name = $RoleName - IsCustom = $true - Description = "Allows creating role assignments." - Actions = @( - "Microsoft.Authorization/roleAssignments/write" - "Microsoft.Authorization/roleAssignments/delete" - "Microsoft.Authorization/roleAssignments/read" - ) - NotActions = @() - DataActions = @() - NotDataActions = @() - AssignableScopes = @( - "/subscriptions/$SubscriptionId" +#Requires -Version 5.1 + +<# +.SYNOPSIS + Creates a custom Azure RBAC role for role assignment management. + +.DESCRIPTION + This script creates a custom Azure role definition that grants permissions + to manage role assignments. The role includes permissions to read, write, + and delete role assignments within a specified subscription scope. + +.PARAMETER RoleName + The display name for the custom role. Defaults to 'Contoso DevBox - Role Assignment Writer'. + +.PARAMETER SubscriptionId + The Azure subscription ID where the role will be scoped. If not provided, + the current subscription is used. + +.PARAMETER Description + Description for the custom role. Defaults to 'Allows creating role assignments.' + +.PARAMETER Force + If specified, deletes any existing role with the same name before creating. + +.EXAMPLE + .\createCustomRole.ps1 + Creates the custom role using the current subscription. + +.EXAMPLE + .\createCustomRole.ps1 -SubscriptionId "12345678-1234-1234-1234-123456789012" + Creates the custom role scoped to a specific subscription. + +.EXAMPLE + .\createCustomRole.ps1 -RoleName "MyCompany Role Writer" -Force + Creates a custom role with a different name, removing any existing role first. + +.NOTES + Author: DevExp Team + Requires: Azure CLI (az) authenticated with appropriate permissions +#> + +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$RoleName = "Contoso DevBox - Role Assignment Writer", + + [Parameter(Mandatory = $false)] + [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')] + [string]$SubscriptionId, + + [Parameter(Mandatory = $false)] + [string]$Description = "Allows creating role assignments.", + + [Parameter(Mandatory = $false)] + [switch]$Force +) + +# Script Configuration +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +function Get-CurrentSubscriptionId { + <# + .SYNOPSIS + Retrieves the current Azure subscription ID. + + .OUTPUTS + System.String - The subscription ID GUID. + + .EXAMPLE + $subId = Get-CurrentSubscriptionId + #> + [CmdletBinding()] + [OutputType([string])] + param() + + try { + $subscriptionId = az account show --query id --output tsv + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($subscriptionId)) { + throw "Failed to retrieve current subscription ID. Ensure you are logged into Azure CLI." + } + return $subscriptionId + } + catch { + Write-Error "Error retrieving subscription ID: $_" + throw + } +} + +function New-CustomRoleDefinition { + <# + .SYNOPSIS + Creates a custom Azure RBAC role definition. + + .DESCRIPTION + Creates a JSON role definition file and uses Azure CLI to create + the custom role in Azure. + + .PARAMETER RoleName + The name of the custom role. + + .PARAMETER SubscriptionId + The subscription ID for the assignable scope. + + .PARAMETER Description + The description for the role. + + .PARAMETER RemoveExisting + If true, removes any existing role with the same name first. + + .OUTPUTS + System.Boolean - True if successful, False otherwise. + + .EXAMPLE + New-CustomRoleDefinition -RoleName "MyRole" -SubscriptionId $subId -Description "My custom role" + #> + [CmdletBinding(SupportsShouldProcess)] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string]$RoleName, + + [Parameter(Mandatory = $true)] + [string]$SubscriptionId, + + [Parameter(Mandatory = $false)] + [string]$Description = "Allows creating role assignments.", + + [Parameter(Mandatory = $false)] + [switch]$RemoveExisting ) + + # Define the role definition + $roleDefinition = @{ + Name = $RoleName + IsCustom = $true + Description = $Description + Actions = @( + "Microsoft.Authorization/roleAssignments/write" + "Microsoft.Authorization/roleAssignments/delete" + "Microsoft.Authorization/roleAssignments/read" + ) + NotActions = @() + DataActions = @() + NotDataActions = @() + AssignableScopes = @( + "/subscriptions/$SubscriptionId" + ) + } + + # Create temporary file for role definition + $tempFilePath = Join-Path -Path $env:TEMP -ChildPath "custom-role-$(Get-Date -Format 'yyyyMMddHHmmss').json" + + try { + # Convert to JSON and write to file + $roleDefinitionJson = $roleDefinition | ConvertTo-Json -Depth 10 + $roleDefinitionJson | Out-File -FilePath $tempFilePath -Encoding utf8 + + Write-Verbose "Role definition written to: $tempFilePath" + + # Remove existing role if requested + if ($RemoveExisting) { + if ($PSCmdlet.ShouldProcess($RoleName, "Delete existing role definition")) { + Write-Verbose "Removing existing role definition: $RoleName" + $null = az role definition delete --name $RoleName 2>$null + # Ignore errors if role doesn't exist + } + } + + # Create the custom role + if ($PSCmdlet.ShouldProcess($RoleName, "Create custom role definition")) { + Write-Verbose "Creating custom role: $RoleName" + $result = az role definition create --role-definition $tempFilePath + if ($LASTEXITCODE -ne 0) { + throw "Failed to create custom role definition. Exit code: $LASTEXITCODE" + } + + Write-Output "Custom role '$RoleName' created successfully." + Write-Output "Assignable scope: /subscriptions/$SubscriptionId" + return $true + } + + return $true + } + catch { + Write-Error "Error creating custom role: $_" + return $false + } + finally { + # Cleanup temporary file + if (Test-Path $tempFilePath) { + Remove-Item -Path $tempFilePath -Force -ErrorAction SilentlyContinue + } + } } -# Convert to JSON -$RoleDefinitionJson = $RoleDefinition | ConvertTo-Json -Depth 10 +# Main script execution +try { + # Get subscription ID if not provided + if ([string]::IsNullOrWhiteSpace($SubscriptionId)) { + Write-Verbose "No subscription ID provided, retrieving current subscription..." + $SubscriptionId = Get-CurrentSubscriptionId + } -# Write to a file -$FilePath = ".\custom-role.json" -$RoleDefinitionJson | Out-File -FilePath $FilePath -Encoding utf8 + Write-Output "Creating custom role '$RoleName' in subscription: $SubscriptionId" -az role definition delete --name $RoleName -# Create the custom role -az role definition create --role-definition $FilePath + # Create the custom role + $success = New-CustomRoleDefinition -RoleName $RoleName ` + -SubscriptionId $SubscriptionId ` + -Description $Description ` + -RemoveExisting:$Force -# Optional cleanup -Remove-Item $FilePath + if (-not $success) { + exit 1 + } +} +catch { + Write-Error "Script execution failed: $_" + exit 1 +} diff --git a/.configuration/setup/powershell/Azure/createUsersAndAssignRole.ps1 b/.configuration/setup/powershell/Azure/createUsersAndAssignRole.ps1 index 452053df..5157029c 100644 --- a/.configuration/setup/powershell/Azure/createUsersAndAssignRole.ps1 +++ b/.configuration/setup/powershell/Azure/createUsersAndAssignRole.ps1 @@ -1,66 +1,176 @@ -# PowerShell script to create user assignments and assign roles +#Requires -Version 5.1 + +<# +.SYNOPSIS + Creates user assignments and assigns Azure DevCenter roles to the current user. + +.DESCRIPTION + This script retrieves the current signed-in Azure user and assigns + DevCenter-related roles including: + - DevCenter Dev Box User + - DevCenter Project Admin + - Deployment Environments Reader + - Deployment Environments User + + The roles are assigned at the subscription scope. + +.PARAMETER SubscriptionId + The Azure subscription ID where roles will be assigned. If not provided, + uses the current subscription. + +.EXAMPLE + .\createUsersAndAssignRole.ps1 + Assigns DevCenter roles to the current user using the current subscription. + +.EXAMPLE + .\createUsersAndAssignRole.ps1 -SubscriptionId "12345678-1234-1234-1234-123456789012" + Assigns DevCenter roles using a specific subscription. + +.NOTES + Author: DevExp Team + Requires: Azure CLI (az) authenticated with appropriate permissions +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')] + [string]$SubscriptionId +) + +# Script Configuration +$ErrorActionPreference = 'Stop' +$WarningPreference = 'Stop' + +# Get the subscription ID if not provided +if ([string]::IsNullOrWhiteSpace($SubscriptionId)) { + $SubscriptionId = az account show --query id --output tsv + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to retrieve current subscription ID." + exit 1 + } +} + +function Set-AzureRole { + <# + .SYNOPSIS + Assigns an Azure RBAC role to a user or service principal. + + .DESCRIPTION + Checks if the specified role is already assigned to the identity, + and if not, creates a new role assignment at the subscription scope. + + .PARAMETER UserIdentityId + The object ID of the user or service principal. + + .PARAMETER RoleName + The name of the Azure RBAC role to assign. -# Exit immediately if a command exits with a non-zero status, treat unset variables as an error, and propagate errors in pipelines. -$ErrorActionPreference = "Stop" -$WarningPreference = "Stop" + .PARAMETER PrincipalType + The type of principal (User, ServicePrincipal, Group). -# Get the current subscription ID -$subscriptionId = (az account show --query id -o tsv) + .PARAMETER SubscriptionId + The subscription ID for the role scope. + + .OUTPUTS + System.Boolean - True if assignment succeeded or already exists, False on error. + + .EXAMPLE + Set-AzureRole -UserIdentityId $userId -RoleName "Contributor" -PrincipalType "User" -SubscriptionId $subId + #> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$UserIdentityId, -# Function to assign a role to a user or service principal -function Set-Role { - param ( [Parameter(Mandatory = $true)] - [string]$userIdentityId, + [ValidateNotNullOrEmpty()] + [string]$RoleName, [Parameter(Mandatory = $true)] - [string]$roleName, + [ValidateSet('User', 'ServicePrincipal', 'Group')] + [string]$PrincipalType, [Parameter(Mandatory = $true)] - [string]$idType + [string]$SubscriptionId ) try { - Write-Output "Checking if '$roleName' role is already assigned to identityId $userIdentityId..." + Write-Verbose "Checking if '$RoleName' role is already assigned to identity $UserIdentityId..." # Check if the role is already assigned - $existingAssignment = az role assignment list --assignee $userIdentityId --role $roleName --scope /subscriptions/$subscriptionId --query "[?principalType=='$idType']" -o tsv - - if ($existingAssignment) { - Write-Output "Role '$roleName' is already assigned to identityId $userIdentityId. Skipping assignment." - return 0 + $existingAssignment = az role assignment list ` + --assignee $UserIdentityId ` + --role $RoleName ` + --scope "/subscriptions/$SubscriptionId" ` + --query "[?principalType=='$PrincipalType']" ` + --output tsv + + if (-not [string]::IsNullOrWhiteSpace($existingAssignment)) { + Write-Output "Role '$RoleName' is already assigned to identity $UserIdentityId. Skipping." + return $true } - Write-Output "Assigning '$roleName' role to identityId $userIdentityId..." + Write-Output "Assigning '$RoleName' role to identity $UserIdentityId..." # Attempt to assign the role - $result = az role assignment create --assignee-object-id $userIdentityId --assignee-principal-type $idType --role $roleName --scope /subscriptions/$subscriptionId - - if ($result) { - Write-Output "Role '$roleName' assigned successfully." - } - else { - throw "Failed to assign role '$roleName' to identityId $userIdentityId." + $result = az role assignment create ` + --assignee-object-id $UserIdentityId ` + --assignee-principal-type $PrincipalType ` + --role $RoleName ` + --scope "/subscriptions/$SubscriptionId" + + if ($LASTEXITCODE -ne 0) { + throw "Failed to assign role '$RoleName' to identity $UserIdentityId." } + + Write-Output "Role '$RoleName' assigned successfully." + return $true } catch { - Write-Error "Error: $_" - return 2 + Write-Error "Error assigning role '$RoleName': $_" + return $false } } -# Function to create user assignments and assign roles -function New-UserAssignments { +function New-UserRoleAssignments { + <# + .SYNOPSIS + Creates DevCenter role assignments for the current signed-in user. + + .DESCRIPTION + Retrieves the current Azure AD signed-in user and assigns all + required DevCenter roles at the subscription scope. + + .PARAMETER SubscriptionId + The subscription ID for role assignments. + + .OUTPUTS + System.Boolean - True if all assignments succeeded, False otherwise. + + .EXAMPLE + New-UserRoleAssignments -SubscriptionId $subscriptionId + #> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string]$SubscriptionId + ) + try { # Get the current signed-in user's object ID - $currentUser = az ad signed-in-user show --query id -o tsv + $currentUser = az ad signed-in-user show --query id --output tsv - if (-not $currentUser) { + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($currentUser)) { throw "Failed to retrieve current signed-in user's object ID." } - Write-Output "Creating user assignments and assigning roles for currentUser: $currentUser" + Write-Output "Creating role assignments for user: $currentUser" + # Define DevCenter roles to assign $roles = @( "DevCenter Dev Box User", "DevCenter Project Admin", @@ -68,24 +178,39 @@ function New-UserAssignments { "Deployment Environments User" ) + $allSucceeded = $true + foreach ($role in $roles) { - Set-Role -userIdentityId $currentUser -roleName $role -idType "User" - if ($LASTEXITCODE -ne 0) { - throw "Failed to assign role '$role' to current user with object ID: $currentUser" + $success = Set-AzureRole ` + -UserIdentityId $currentUser ` + -RoleName $role ` + -PrincipalType 'User' ` + -SubscriptionId $SubscriptionId + + if (-not $success) { + Write-Warning "Failed to assign role '$role' to user $currentUser" + $allSucceeded = $false } } - Write-Output "User assignments and role assignments completed successfully for currentUser: $currentUser" + if ($allSucceeded) { + Write-Output "All role assignments completed successfully for user: $currentUser" + } + + return $allSucceeded } catch { - Write-Error "Error: $_" - return 1 + Write-Error "Error creating user assignments: $_" + return $false } } # Main script execution try { - New-UserAssignments + $success = New-UserRoleAssignments -SubscriptionId $SubscriptionId + if (-not $success) { + exit 1 + } } catch { Write-Error "Script execution failed: $_" diff --git a/.configuration/setup/powershell/Azure/deleteDeploymentCredentials.ps1 b/.configuration/setup/powershell/Azure/deleteDeploymentCredentials.ps1 index a1622207..87981595 100644 --- a/.configuration/setup/powershell/Azure/deleteDeploymentCredentials.ps1 +++ b/.configuration/setup/powershell/Azure/deleteDeploymentCredentials.ps1 @@ -1,71 +1,120 @@ -# PowerShell script to delete deployment credentials +#Requires -Version 5.1 -param ( - [Parameter(Mandatory = $true)] - [string]$appDisplayName +<# +.SYNOPSIS + Deletes Azure deployment credentials (service principal and app registration). + +.DESCRIPTION + This script removes an Azure AD service principal and its associated + application registration by looking up the display name. This is typically + used to clean up credentials created for CI/CD pipelines. + +.PARAMETER AppDisplayName + The display name of the application registration to delete. + +.EXAMPLE + .\deleteDeploymentCredentials.ps1 -AppDisplayName "ContosoDevEx GitHub Actions Enterprise App" + Deletes the service principal and app registration with the specified name. + +.NOTES + Author: DevExp Team + Requires: Azure CLI (az) authenticated with Azure AD admin permissions +#> + +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory = $true, HelpMessage = "The display name of the application to delete.")] + [ValidateNotNullOrEmpty()] + [string]$AppDisplayName ) -# Exit immediately if a command exits with a non-zero status, treat unset variables as an error, and propagate errors in pipelines. -$ErrorActionPreference = "Stop" -$WarningPreference = "Stop" +# Script Configuration +$ErrorActionPreference = 'Stop' +$WarningPreference = 'Stop' -# Function to delete deployment credentials -function Remove-DeploymentCredentials { - param ( +function Remove-AzureDeploymentCredentials { + <# + .SYNOPSIS + Removes an Azure AD service principal and application registration. + + .DESCRIPTION + Looks up an application by display name, retrieves its App ID, + then deletes both the service principal and the application registration. + + .PARAMETER DisplayName + The display name of the application to delete. + + .OUTPUTS + System.Boolean - True if deletion succeeded, False otherwise. + + .EXAMPLE + Remove-AzureDeploymentCredentials -DisplayName "My CI/CD App" + #> + [CmdletBinding(SupportsShouldProcess)] + [OutputType([bool])] + param( [Parameter(Mandatory = $true)] - [string]$appDisplayName + [ValidateNotNullOrEmpty()] + [string]$DisplayName ) try { # Get the application ID using the display name - $appId = az ad app list --display-name $appDisplayName --query "[0].appId" -o tsv + Write-Verbose "Looking up application with display name: $DisplayName" + $appId = az ad app list --display-name $DisplayName --query "[0].appId" --output tsv + + if ($LASTEXITCODE -ne 0) { + throw "Failed to query Azure AD applications." + } - if (-not $appId) { - throw "Application with display name '$appDisplayName' not found." + if ([string]::IsNullOrWhiteSpace($appId)) { + Write-Warning "Application with display name '$DisplayName' not found. Nothing to delete." + return $true } + Write-Output "Found application with App ID: $appId" + # Delete the service principal - Write-Output "Deleting service principal with appId: $appId" - $spDeleteResult = az ad sp delete --id $appId - if ($null -ne $spDeleteResult) { - throw "Failed to delete service principal." + if ($PSCmdlet.ShouldProcess("Service Principal with App ID: $appId", "Delete")) { + Write-Output "Deleting service principal..." + $null = az ad sp delete --id $appId 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Warning "Service principal deletion returned non-zero exit code. It may not exist." + } + else { + Write-Output "Service principal deleted successfully." + } } # Delete the application registration - Write-Output "Deleting application registration with appId: $appId" - $appDeleteResult = az ad app delete --id $appId - if ($null -ne $appDeleteResult) { - throw "Failed to delete application registration." + if ($PSCmdlet.ShouldProcess("Application Registration with App ID: $appId", "Delete")) { + Write-Output "Deleting application registration..." + $null = az ad app delete --id $appId 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "Failed to delete application registration." + } + Write-Output "Application registration deleted successfully." } - Write-Output "Service principal and App Registration deleted successfully." + Write-Output "Deployment credentials cleanup completed for: $DisplayName" + return $true } catch { - Write-Error "Error: $_" - return 1 - } -} - -# Function to validate input parameters -function Test-Input { - param ( - [Parameter(Mandatory = $true)] - [string]$appDisplayName - ) - - if ([string]::IsNullOrEmpty($appDisplayName)) { - Write-Error "Error: Missing required parameter." - Write-Output "Usage: .\deleteDeploymentCredentials.ps1 -appDisplayName " - return 1 + Write-Error "Error removing deployment credentials: $_" + return $false } } # Main script execution try { - Test-Input -appDisplayName $appDisplayName - if ($LASTEXITCODE -eq 0) { - Remove-DeploymentCredentials -appDisplayName $appDisplayName + Write-Output "Starting deployment credentials cleanup for: $AppDisplayName" + + $success = Remove-AzureDeploymentCredentials -DisplayName $AppDisplayName + if (-not $success) { + exit 1 } + + Write-Output "Cleanup completed successfully." } catch { Write-Error "Script execution failed: $_" diff --git a/.configuration/setup/powershell/Azure/deleteUsersAndAssignedRoles.ps1 b/.configuration/setup/powershell/Azure/deleteUsersAndAssignedRoles.ps1 index a1622207..d03549c3 100644 --- a/.configuration/setup/powershell/Azure/deleteUsersAndAssignedRoles.ps1 +++ b/.configuration/setup/powershell/Azure/deleteUsersAndAssignedRoles.ps1 @@ -1,71 +1,215 @@ -# PowerShell script to delete deployment credentials +#Requires -Version 5.1 -param ( - [Parameter(Mandatory = $true)] - [string]$appDisplayName +<# +.SYNOPSIS + Deletes user role assignments for DevCenter resources. + +.DESCRIPTION + This script removes Azure RBAC role assignments for the current signed-in + user that were created for DevCenter operations. It removes the following roles: + - DevCenter Dev Box User + - DevCenter Project Admin + - Deployment Environments Reader + - Deployment Environments User + +.PARAMETER AppDisplayName + The display name of the associated application (used for logging purposes). + +.PARAMETER SubscriptionId + The Azure subscription ID where roles will be removed. If not provided, + uses the current subscription. + +.EXAMPLE + .\deleteUsersAndAssignedRoles.ps1 -AppDisplayName "ContosoDevEx GitHub Actions Enterprise App" + Removes DevCenter role assignments from the current user. + +.NOTES + Author: DevExp Team + Requires: Azure CLI (az) authenticated with appropriate permissions +#> + +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory = $false)] + [string]$AppDisplayName, + + [Parameter(Mandatory = $false)] + [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')] + [string]$SubscriptionId ) -# Exit immediately if a command exits with a non-zero status, treat unset variables as an error, and propagate errors in pipelines. -$ErrorActionPreference = "Stop" -$WarningPreference = "Stop" +# Script Configuration +$ErrorActionPreference = 'Stop' +$WarningPreference = 'Stop' + +# Get the subscription ID if not provided +if ([string]::IsNullOrWhiteSpace($SubscriptionId)) { + $SubscriptionId = az account show --query id --output tsv + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to retrieve current subscription ID." + exit 1 + } +} + +function Remove-UserRoleAssignment { + <# + .SYNOPSIS + Removes an Azure RBAC role assignment from a user. + + .DESCRIPTION + Checks if the specified role is assigned to the user, + and if so, removes the role assignment at the subscription scope. + + .PARAMETER UserIdentityId + The object ID of the user. + + .PARAMETER RoleName + The name of the Azure RBAC role to remove. + + .PARAMETER SubscriptionId + The subscription ID for the role scope. + + .OUTPUTS + System.Boolean - True if removal succeeded or role not assigned, False on error. + + .EXAMPLE + Remove-UserRoleAssignment -UserIdentityId $userId -RoleName "Contributor" -SubscriptionId $subId + #> + [CmdletBinding(SupportsShouldProcess)] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$UserIdentityId, -# Function to delete deployment credentials -function Remove-DeploymentCredentials { - param ( [Parameter(Mandatory = $true)] - [string]$appDisplayName + [ValidateNotNullOrEmpty()] + [string]$RoleName, + + [Parameter(Mandatory = $true)] + [string]$SubscriptionId ) try { - # Get the application ID using the display name - $appId = az ad app list --display-name $appDisplayName --query "[0].appId" -o tsv + Write-Verbose "Checking if '$RoleName' role is assigned to identity $UserIdentityId..." - if (-not $appId) { - throw "Application with display name '$appDisplayName' not found." - } + # Check if the role is assigned + $existingAssignment = az role assignment list ` + --assignee $UserIdentityId ` + --role $RoleName ` + --scope "/subscriptions/$SubscriptionId" ` + --query "[0].id" ` + --output tsv - # Delete the service principal - Write-Output "Deleting service principal with appId: $appId" - $spDeleteResult = az ad sp delete --id $appId - if ($null -ne $spDeleteResult) { - throw "Failed to delete service principal." + if ([string]::IsNullOrWhiteSpace($existingAssignment)) { + Write-Output "Role '$RoleName' is not assigned to identity $UserIdentityId. Skipping." + return $true } - # Delete the application registration - Write-Output "Deleting application registration with appId: $appId" - $appDeleteResult = az ad app delete --id $appId - if ($null -ne $appDeleteResult) { - throw "Failed to delete application registration." + if ($PSCmdlet.ShouldProcess("Role '$RoleName' for user $UserIdentityId", "Remove")) { + Write-Output "Removing '$RoleName' role from identity $UserIdentityId..." + + $null = az role assignment delete ` + --assignee $UserIdentityId ` + --role $RoleName ` + --scope "/subscriptions/$SubscriptionId" + + if ($LASTEXITCODE -ne 0) { + throw "Failed to remove role '$RoleName' from identity $UserIdentityId." + } + + Write-Output "Role '$RoleName' removed successfully." } - Write-Output "Service principal and App Registration deleted successfully." + return $true } catch { - Write-Error "Error: $_" - return 1 + Write-Error "Error removing role '$RoleName': $_" + return $false } } -# Function to validate input parameters -function Test-Input { - param ( +function Remove-UserRoleAssignments { + <# + .SYNOPSIS + Removes DevCenter role assignments from the current signed-in user. + + .DESCRIPTION + Retrieves the current Azure AD signed-in user and removes all + DevCenter-related role assignments at the subscription scope. + + .PARAMETER SubscriptionId + The subscription ID for role removal. + + .OUTPUTS + System.Boolean - True if all removals succeeded, False otherwise. + + .EXAMPLE + Remove-UserRoleAssignments -SubscriptionId $subscriptionId + #> + [CmdletBinding(SupportsShouldProcess)] + [OutputType([bool])] + param( [Parameter(Mandatory = $true)] - [string]$appDisplayName + [string]$SubscriptionId ) - if ([string]::IsNullOrEmpty($appDisplayName)) { - Write-Error "Error: Missing required parameter." - Write-Output "Usage: .\deleteDeploymentCredentials.ps1 -appDisplayName " - return 1 + try { + # Get the current signed-in user's object ID + $currentUser = az ad signed-in-user show --query id --output tsv + + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($currentUser)) { + throw "Failed to retrieve current signed-in user's object ID." + } + + Write-Output "Removing role assignments for user: $currentUser" + + # Define DevCenter roles to remove + $roles = @( + "DevCenter Dev Box User", + "DevCenter Project Admin", + "Deployment Environments Reader", + "Deployment Environments User" + ) + + $allSucceeded = $true + + foreach ($role in $roles) { + $success = Remove-UserRoleAssignment ` + -UserIdentityId $currentUser ` + -RoleName $role ` + -SubscriptionId $SubscriptionId + + if (-not $success) { + Write-Warning "Failed to remove role '$role' from user $currentUser" + $allSucceeded = $false + } + } + + if ($allSucceeded) { + Write-Output "All role assignments removed successfully for user: $currentUser" + } + + return $allSucceeded + } + catch { + Write-Error "Error removing user role assignments: $_" + return $false } } # Main script execution try { - Test-Input -appDisplayName $appDisplayName - if ($LASTEXITCODE -eq 0) { - Remove-DeploymentCredentials -appDisplayName $appDisplayName + if (-not [string]::IsNullOrWhiteSpace($AppDisplayName)) { + Write-Output "Starting role cleanup for application: $AppDisplayName" } + + $success = Remove-UserRoleAssignments -SubscriptionId $SubscriptionId + if (-not $success) { + exit 1 + } + + Write-Output "User role assignments cleanup completed successfully." } catch { Write-Error "Script execution failed: $_" diff --git a/.configuration/setup/powershell/Azure/generateDeploymentCredentials.ps1 b/.configuration/setup/powershell/Azure/generateDeploymentCredentials.ps1 index 5c3fec88..f7782ee2 100644 --- a/.configuration/setup/powershell/Azure/generateDeploymentCredentials.ps1 +++ b/.configuration/setup/powershell/Azure/generateDeploymentCredentials.ps1 @@ -1,150 +1,264 @@ -# PowerShell script to generate deployment credentials +#Requires -Version 5.1 -param ( - [Parameter(Mandatory = $true)] - [string]$appName, +<# +.SYNOPSIS + Generates Azure deployment credentials for CI/CD pipelines. - [Parameter(Mandatory = $true)] - [string]$displayName +.DESCRIPTION + Creates an Azure AD service principal with Contributor, User Access Administrator, + and Managed Identity Contributor roles. Additionally creates user role assignments + and stores credentials as a GitHub secret for GitHub Actions workflows. + +.PARAMETER AppName + The name for the Azure AD application registration. + +.PARAMETER DisplayName + The display name for the service principal. + +.EXAMPLE + .\generateDeploymentCredentials.ps1 -AppName "contoso-cicd" -DisplayName "Contoso CI/CD Service Principal" + Creates a service principal and configures GitHub secrets. + +.NOTES + Author: DevExp Team + Requires: Azure CLI (az), GitHub CLI (gh), and appropriate permissions +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, HelpMessage = "The name for the Azure AD application.")] + [ValidateNotNullOrEmpty()] + [string]$AppName, + + [Parameter(Mandatory = $true, HelpMessage = "The display name for the service principal.")] + [ValidateNotNullOrEmpty()] + [string]$DisplayName ) -# Exit immediately if a command exits with a non-zero status, treat unset variables as an error, and propagate errors in pipelines. -$ErrorActionPreference = "Stop" -$WarningPreference = "Stop" +# Script Configuration +$ErrorActionPreference = 'Stop' +$WarningPreference = 'Stop' -# Function to validate input parameters -function Test-Input { - param ( - [Parameter(Mandatory = $true)] - [string]$appName, +# Script directory for relative path resolution +$Script:ScriptDirectory = $PSScriptRoot - [Parameter(Mandatory = $true)] - [string]$displayName - ) +function New-AzureDeploymentCredentials { + <# + .SYNOPSIS + Creates an Azure service principal with required role assignments. - if ([string]::IsNullOrEmpty($appName) -or [string]::IsNullOrEmpty($displayName)) { - Write-Error "Error: Missing required parameters." - Write-Output "Usage: .\generateDeploymentCredentials.ps1 -appName -displayName " - throw "Validation failed" - } -} + .DESCRIPTION + Creates a service principal with Contributor role and adds User Access Administrator + and Managed Identity Contributor roles for deployment scenarios. + + .PARAMETER AppName + The name for the Azure AD application. + + .PARAMETER DisplayName + The display name for the service principal. -# Function to generate deployment credentials -function New-DeploymentCredentials { - param ( + .OUTPUTS + System.String - The JSON credentials body for GitHub Actions, or $null on failure. + + .EXAMPLE + $creds = New-AzureDeploymentCredentials -AppName "my-app" -DisplayName "My App SP" + #> + [CmdletBinding()] + [OutputType([string])] + param( [Parameter(Mandatory = $true)] - [string]$appName, + [string]$AppName, [Parameter(Mandatory = $true)] - [string]$displayName + [string]$DisplayName ) try { - # Define the role and get the subscription ID - $role = "Contributor" + # Get the subscription ID $subscriptionId = az account show --query id --output tsv - if ($LASTEXITCODE -ne 0) { - throw "Error: Failed to retrieve subscription ID." + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($subscriptionId)) { + throw "Failed to retrieve subscription ID. Ensure you are logged into Azure CLI." } - # Create the service principal and capture the appId - $ghSecretBody = az ad sp create-for-rbac --name $appName --display-name $displayName --role $role --scopes "/subscriptions/$subscriptionId" --json-auth --output json - if ($LASTEXITCODE -ne 0) { - throw "Error: Failed to create service principal." + Write-Output "Creating service principal '$DisplayName' in subscription: $subscriptionId" + + # Create the service principal with Contributor role + $ghSecretBody = az ad sp create-for-rbac ` + --name $AppName ` + --display-name $DisplayName ` + --role "Contributor" ` + --scopes "/subscriptions/$subscriptionId" ` + --json-auth ` + --output json + + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($ghSecretBody)) { + throw "Failed to create service principal." } - $appId = az ad sp list --display-name $displayName --query "[0].appId" -o tsv - if ($LASTEXITCODE -ne 0) { - throw "Error: Failed to retrieve service principal appId." + # Get the App ID for additional role assignments + $appId = az ad sp list --display-name $DisplayName --query "[0].appId" --output tsv + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($appId)) { + throw "Failed to retrieve service principal App ID." } - Write-Output "Assigning User Access Administrator and Managed Identity Contributor roles..." + Write-Output "Service principal created with App ID: $appId" + Write-Output "Assigning additional roles..." + # Assign User Access Administrator role - az role assignment create --assignee $appId --role "User Access Administrator" --scope "/subscriptions/$subscriptionId" + $null = az role assignment create ` + --assignee $appId ` + --role "User Access Administrator" ` + --scope "/subscriptions/$subscriptionId" + if ($LASTEXITCODE -ne 0) { - throw "Error: Failed to assign User Access Administrator role." + throw "Failed to assign User Access Administrator role." } + Write-Output "Assigned: User Access Administrator" # Assign Managed Identity Contributor role - az role assignment create --assignee $appId --role "Managed Identity Contributor" --scope "/subscriptions/$subscriptionId" + $null = az role assignment create ` + --assignee $appId ` + --role "Managed Identity Contributor" ` + --scope "/subscriptions/$subscriptionId" + if ($LASTEXITCODE -ne 0) { - throw "Error: Failed to assign Managed Identity Contributor role." + throw "Failed to assign Managed Identity Contributor role." } + Write-Output "Assigned: Managed Identity Contributor" - Write-Output "Role assignments completed." - Write-Output "Service principal credentials:" - Write-Output $ghSecretBody - - # Create users and assign roles - New-UsersAndAssignRole -appId $appId - - # Create GitHub secret for Azure credentials - New-GitHubSecretAzureCredentials -ghSecretBody $ghSecretBody + Write-Output "Role assignments completed successfully." + return $ghSecretBody } catch { - Write-Error "Error: $_" - return 1 + Write-Error "Error creating deployment credentials: $_" + return $null } } -# Function to create users and assign roles -function New-UsersAndAssignRole { - param ( - [Parameter(Mandatory = $true)] - [string]$appId - ) +function Invoke-UserRoleAssignment { + <# + .SYNOPSIS + Invokes the user role assignment script. + + .DESCRIPTION + Executes the createUsersAndAssignRole.ps1 script to assign DevCenter roles + to the current user. + + .OUTPUTS + System.Boolean - True if successful, False otherwise. + + .EXAMPLE + Invoke-UserRoleAssignment + #> + [CmdletBinding()] + [OutputType([bool])] + param() try { - Write-Output "Creating users and assigning roles..." + Write-Output "Creating user role assignments..." + + $scriptPath = Join-Path -Path $Script:ScriptDirectory -ChildPath "createUsersAndAssignRole.ps1" + + if (-not (Test-Path -Path $scriptPath)) { + throw "User role assignment script not found: $scriptPath" + } - # Execute the script to create users and assign roles - .\Azure\createUsersAndAssignRole.ps1 + & $scriptPath if ($LASTEXITCODE -ne 0) { - throw "Error: Failed to create users and assign roles." + throw "User role assignment script failed with exit code: $LASTEXITCODE" } - Write-Output "Users created and roles assigned successfully." + Write-Output "User role assignments completed successfully." + return $true } catch { - Write-Error "Error: $_" - return 1 + Write-Error "Error creating user role assignments: $_" + return $false } } -# Function to create a GitHub secret for Azure credentials -function New-GitHubSecretAzureCredentials { - param ( +function Invoke-GitHubSecretCreation { + <# + .SYNOPSIS + Invokes the GitHub secret creation script. + + .DESCRIPTION + Executes the createGitHubSecretAzureCredentials.ps1 script to store + Azure credentials as a GitHub secret. + + .PARAMETER CredentialsJson + The JSON credentials body to store as a secret. + + .OUTPUTS + System.Boolean - True if successful, False otherwise. + + .EXAMPLE + Invoke-GitHubSecretCreation -CredentialsJson $jsonBody + #> + [CmdletBinding()] + [OutputType([bool])] + param( [Parameter(Mandatory = $true)] - [string]$ghSecretBody + [ValidateNotNullOrEmpty()] + [string]$CredentialsJson ) try { - if ([string]::IsNullOrEmpty($ghSecretBody)) { - Write-Error "Error: Missing required parameter." - Write-Output "Usage: .\generateDeploymentCredentials.ps1 -ghSecretBody " - throw "Validation failed" - } - Write-Output "Creating GitHub secret for Azure credentials..." - # Execute the script to create the GitHub secret - .\GitHub\createGitHubSecretAzureCredentials.ps1 $ghSecretBody + $scriptPath = Join-Path -Path $Script:ScriptDirectory -ChildPath "..\GitHub\createGitHubSecretAzureCredentials.ps1" + $scriptPath = [System.IO.Path]::GetFullPath($scriptPath) + + if (-not (Test-Path -Path $scriptPath)) { + throw "GitHub secret script not found: $scriptPath" + } + + & $scriptPath -ghSecretBody $CredentialsJson if ($LASTEXITCODE -ne 0) { - throw "Error: Failed to create GitHub secret for Azure credentials." + throw "GitHub secret creation script failed with exit code: $LASTEXITCODE" } - Write-Output "GitHub secret for Azure credentials created successfully." + Write-Output "GitHub secret created successfully." + return $true } catch { - Write-Error "Error: $_" - return 1 + Write-Error "Error creating GitHub secret: $_" + return $false } } # Main script execution try { - Test-Input -appName $appName -displayName $displayName - New-DeploymentCredentials -appName $appName -displayName $displayName + Write-Output "Starting deployment credentials generation..." + Write-Output "App Name: $AppName" + Write-Output "Display Name: $DisplayName" + + # Create the deployment credentials + $ghSecretBody = New-AzureDeploymentCredentials -AppName $AppName -DisplayName $DisplayName + if ([string]::IsNullOrWhiteSpace($ghSecretBody)) { + throw "Failed to create deployment credentials." + } + + Write-Output "" + Write-Output "Service principal credentials (for reference):" + Write-Output $ghSecretBody + Write-Output "" + + # Create user role assignments + $userSuccess = Invoke-UserRoleAssignment + if (-not $userSuccess) { + Write-Warning "User role assignment failed, but continuing..." + } + + # Create GitHub secret + $ghSuccess = Invoke-GitHubSecretCreation -CredentialsJson $ghSecretBody + if (-not $ghSuccess) { + Write-Warning "GitHub secret creation failed, but credentials were created." + Write-Warning "You may need to manually configure the AZURE_CREDENTIALS secret." + } + + Write-Output "" + Write-Output "Deployment credentials generation completed." } catch { Write-Error "Script execution failed: $_" diff --git a/.configuration/setup/powershell/GitHub/createGitHubSecretAzureCredentials.ps1 b/.configuration/setup/powershell/GitHub/createGitHubSecretAzureCredentials.ps1 index 27654685..df988957 100644 --- a/.configuration/setup/powershell/GitHub/createGitHubSecretAzureCredentials.ps1 +++ b/.configuration/setup/powershell/GitHub/createGitHubSecretAzureCredentials.ps1 @@ -1,92 +1,153 @@ -# PowerShell script to create GitHub secret for Azure credentials +#Requires -Version 5.1 -param ( - [Parameter(Mandatory = $true)] - [string]$ghSecretBody +<# +.SYNOPSIS + Creates a GitHub secret for Azure credentials. + +.DESCRIPTION + Authenticates to GitHub using the GitHub CLI and creates a repository secret + named AZURE_CREDENTIALS containing the Azure service principal credentials + for use in GitHub Actions workflows. + +.PARAMETER GhSecretBody + The JSON body containing Azure service principal credentials. + This should be the output from 'az ad sp create-for-rbac --json-auth'. + +.EXAMPLE + .\createGitHubSecretAzureCredentials.ps1 -GhSecretBody '{"clientId":"...","clientSecret":"..."}' + Creates the AZURE_CREDENTIALS secret in the current repository. + +.NOTES + Author: DevExp Team + Requires: GitHub CLI (gh) installed and accessible +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, HelpMessage = "The JSON credentials body to store as a secret.")] + [ValidateNotNullOrEmpty()] + [Alias('ghSecretBody')] + [string]$GhSecretBody ) -# Exit immediately if a command exits with a non-zero status, treat unset variables as an error, and propagate errors in pipelines. -$ErrorActionPreference = "Stop" -$WarningPreference = "Stop" +# Script Configuration +$ErrorActionPreference = 'Stop' +$WarningPreference = 'Stop' -# Function to log in to GitHub using the GitHub CLI -function Connect-ToGitHub { - Write-Output "Connecting to GitHub using GitHub CLI..." +# Secret name constant +$Script:SecretName = "AZURE_CREDENTIALS" - try { - # Attempt to log in to GitHub - gh auth login - if ($LASTEXITCODE -ne 0) { - throw "Failed to log in to GitHub." - } +function Connect-GitHubCli { + <# + .SYNOPSIS + Authenticates to GitHub using the GitHub CLI. - Write-Output "Successfully logged in to GitHub." - } - catch { - Write-Error "Error: $_" - return 1 - } -} + .DESCRIPTION + Checks if already authenticated and prompts for login if needed. + Uses the interactive gh auth login flow. -# Function to set up GitHub secret authentication -function Set-GitHubSecretAuthentication { - param ( - [Parameter(Mandatory = $true)] - [string]$ghSecretBody - ) + .OUTPUTS + System.Boolean - True if authentication succeeded, False otherwise. + + .EXAMPLE + Connect-GitHubCli + #> + [CmdletBinding()] + [OutputType([bool])] + param() - $ghSecretName = "AZURE_CREDENTIALS" - try { - Write-Output "Setting up GitHub secret authentication..." + Write-Output "Checking GitHub authentication status..." - # Log in to GitHub - Connect-ToGitHub - if ($LASTEXITCODE -ne 0) { - throw "Failed to log in to GitHub." + # Check if already authenticated + $null = gh auth status 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Output "Already authenticated to GitHub." + return $true } - # Set the GitHub secret - gh secret set $ghSecretName --body $ghSecretBody + Write-Output "Not authenticated. Starting GitHub login..." + gh auth login if ($LASTEXITCODE -ne 0) { - throw "Failed to set GitHub secret: $ghSecretName" + throw "Failed to authenticate to GitHub." } - Write-Output "GitHub secret: $ghSecretName set successfully." - Write-Output "GitHub secret body: $ghSecretBody" + Write-Output "Successfully authenticated to GitHub." + return $true } catch { - Write-Error "Error: $_" - return 1 + Write-Error "Error during GitHub authentication: $_" + return $false } } -# Function to validate input parameters -function Test-Input { - param ( +function Set-GitHubRepositorySecret { + <# + .SYNOPSIS + Creates or updates a GitHub repository secret. + + .DESCRIPTION + Sets a secret in the current GitHub repository using the GitHub CLI. + + .PARAMETER SecretName + The name of the secret to create. + + .PARAMETER SecretValue + The value to store in the secret. + + .OUTPUTS + System.Boolean - True if secret was set successfully, False otherwise. + + .EXAMPLE + Set-GitHubRepositorySecret -SecretName "MY_SECRET" -SecretValue "secret-value" + #> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$SecretName, + [Parameter(Mandatory = $true)] - [string]$ghSecretBody + [ValidateNotNullOrEmpty()] + [string]$SecretValue ) try { - # Check if required parameters are provided - if ([string]::IsNullOrEmpty($ghSecretBody)) { - throw "Missing required parameters." + Write-Output "Setting GitHub secret: $SecretName" + + # Set the secret using gh CLI + $null = gh secret set $SecretName --body $SecretValue + if ($LASTEXITCODE -ne 0) { + throw "Failed to set GitHub secret: $SecretName" } + + Write-Output "GitHub secret '$SecretName' set successfully." + return $true } catch { - Write-Error "Error: $_" - Write-Output "Usage: .\createGitHubSecretAzureCredentials.ps1 -ghSecretBody " - return 1 + Write-Error "Error setting GitHub secret: $_" + return $false } } # Main script execution try { - Test-Input -ghSecretBody $ghSecretBody - if ($LASTEXITCODE -eq 0) { - Set-GitHubSecretAuthentication -ghSecretBody $ghSecretBody + Write-Output "Creating GitHub secret for Azure credentials..." + + # Authenticate to GitHub + if (-not (Connect-GitHubCli)) { + throw "GitHub authentication failed." + } + + # Set the secret + if (-not (Set-GitHubRepositorySecret -SecretName $Script:SecretName -SecretValue $GhSecretBody)) { + throw "Failed to create GitHub secret." } + + Write-Output "" + Write-Output "GitHub secret '$Script:SecretName' created successfully." + Write-Output "You can now use this secret in your GitHub Actions workflows." } catch { Write-Error "Script execution failed: $_" diff --git a/.configuration/setup/powershell/GitHub/deleteGitHubSecretAzureCredentials.ps1 b/.configuration/setup/powershell/GitHub/deleteGitHubSecretAzureCredentials.ps1 index ad52c87b..8e8319f7 100644 --- a/.configuration/setup/powershell/GitHub/deleteGitHubSecretAzureCredentials.ps1 +++ b/.configuration/setup/powershell/GitHub/deleteGitHubSecretAzureCredentials.ps1 @@ -1,86 +1,144 @@ -# PowerShell script to delete GitHub secret for Azure credentials +#Requires -Version 5.1 -param ( - [Parameter(Mandatory = $true)] - [string]$ghSecretName +<# +.SYNOPSIS + Deletes a GitHub repository secret. + +.DESCRIPTION + Authenticates to GitHub using the GitHub CLI and removes the specified + secret from the current repository. Typically used to remove the + AZURE_CREDENTIALS secret during cleanup operations. + +.PARAMETER GhSecretName + The name of the GitHub secret to delete. + +.EXAMPLE + .\deleteGitHubSecretAzureCredentials.ps1 -GhSecretName "AZURE_CREDENTIALS" + Deletes the AZURE_CREDENTIALS secret from the current repository. + +.NOTES + Author: DevExp Team + Requires: GitHub CLI (gh) installed and accessible +#> + +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory = $true, HelpMessage = "The name of the GitHub secret to delete.")] + [ValidateNotNullOrEmpty()] + [Alias('ghSecretName')] + [string]$GhSecretName ) -# Exit immediately if a command exits with a non-zero status, treat unset variables as an error, and propagate errors in pipelines. -$ErrorActionPreference = "Stop" -$WarningPreference = "Stop" +# Script Configuration +$ErrorActionPreference = 'Stop' +$WarningPreference = 'Stop' -# Function to validate input parameters -function Test-Input { - param ( - [Parameter(Mandatory = $true)] - [string]$ghSecretName - ) +function Connect-GitHubCli { + <# + .SYNOPSIS + Authenticates to GitHub using the GitHub CLI. - try { - # Check if required parameters are provided - if ([string]::IsNullOrEmpty($ghSecretName)) { - throw "Missing required parameters." - } - } - catch { - Write-Error "Error: $_" - Write-Output "Usage: .\deleteGitHubSecretAzureCredentials.ps1 -ghSecretName " - return 1 - } -} + .DESCRIPTION + Checks if already authenticated and prompts for login if needed. + Uses the interactive gh auth login flow. + + .OUTPUTS + System.Boolean - True if authentication succeeded, False otherwise. -# Function to log in to GitHub using the GitHub CLI -function Connect-ToGitHub { - Write-Output "Logging in to GitHub using GitHub CLI..." + .EXAMPLE + Connect-GitHubCli + #> + [CmdletBinding()] + [OutputType([bool])] + param() try { - # Attempt to log in to GitHub + Write-Output "Checking GitHub authentication status..." + + # Check if already authenticated + $null = gh auth status 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Output "Already authenticated to GitHub." + return $true + } + + Write-Output "Not authenticated. Starting GitHub login..." gh auth login if ($LASTEXITCODE -ne 0) { - throw "Failed to log in to GitHub." + throw "Failed to authenticate to GitHub." } - Write-Output "Successfully logged in to GitHub." + Write-Output "Successfully authenticated to GitHub." + return $true } catch { - Write-Error "Error: $_" - return 1 + Write-Error "Error during GitHub authentication: $_" + return $false } } -# Function to delete a GitHub secret -function Remove-GitHubSecret { - param ( +function Remove-GitHubRepositorySecret { + <# + .SYNOPSIS + Removes a GitHub repository secret. + + .DESCRIPTION + Deletes a secret from the current GitHub repository using the GitHub CLI. + + .PARAMETER SecretName + The name of the secret to remove. + + .OUTPUTS + System.Boolean - True if secret was removed successfully, False otherwise. + + .EXAMPLE + Remove-GitHubRepositorySecret -SecretName "MY_SECRET" + #> + [CmdletBinding(SupportsShouldProcess)] + [OutputType([bool])] + param( [Parameter(Mandatory = $true)] - [string]$ghSecretName + [ValidateNotNullOrEmpty()] + [string]$SecretName ) try { - Write-Output "Deleting GitHub secret: $ghSecretName" + if ($PSCmdlet.ShouldProcess("GitHub Secret '$SecretName'", "Remove")) { + Write-Output "Removing GitHub secret: $SecretName" - # Delete the GitHub secret - gh secret remove $ghSecretName - if ($LASTEXITCODE -ne 0) { - throw "Failed to delete GitHub secret: $ghSecretName" - } + # Remove the secret using gh CLI + $null = gh secret remove $SecretName 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Warning "Secret '$SecretName' may not exist or could not be removed." + return $true # Not a fatal error if secret doesn't exist + } - Write-Output "GitHub secret: $ghSecretName deleted successfully." + Write-Output "GitHub secret '$SecretName' removed successfully." + } + return $true } catch { - Write-Error "Error: $_" - return 1 + Write-Error "Error removing GitHub secret: $_" + return $false } } # Main script execution try { - Test-Input -ghSecretName $ghSecretName - if ($LASTEXITCODE -eq 0) { - Connect-ToGitHub - if ($LASTEXITCODE -eq 0) { - Remove-GitHubSecret -ghSecretName $ghSecretName - } + Write-Output "Starting GitHub secret deletion..." + + # Authenticate to GitHub + if (-not (Connect-GitHubCli)) { + throw "GitHub authentication failed." } + + # Remove the secret + if (-not (Remove-GitHubRepositorySecret -SecretName $GhSecretName)) { + throw "Failed to remove GitHub secret." + } + + Write-Output "" + Write-Output "GitHub secret deletion completed." } catch { Write-Error "Script execution failed: $_" diff --git a/.configuration/setup/powershell/readme.md b/.configuration/setup/powershell/readme.md deleted file mode 100644 index e69de29b..00000000 diff --git a/.github/actions/ci/bicep-standard-ci/action.yml b/.github/actions/ci/bicep-standard-ci/action.yml index 4e34cb76..7194764e 100644 --- a/.github/actions/ci/bicep-standard-ci/action.yml +++ b/.github/actions/ci/bicep-standard-ci/action.yml @@ -1,91 +1,74 @@ +# ============================================================================= +# File: action.yml (bicep-standard-ci) +# Purpose: Composite action for building and uploading Bicep artifacts +# Description: Compiles Bicep templates to ARM and uploads versioned artifacts +# ============================================================================= + name: Bicep Standard CI description: | - This action builds Bicep templates and uploads the artifacts. + Builds Bicep templates, compiles them to ARM templates, and uploads + versioned artifacts for deployment or release. inputs: branch_name: - description: 'The name of the branch being built' + description: "The name of the branch being built" required: true new_version: - description: 'The version being built' + description: "The semantic version being built (e.g., v1.2.3)" required: true should_publish: - description: 'Whether the build should be published' + description: "Whether the build artifacts should be published to a release" required: true runs: using: composite steps: - - name: Update Packages - shell: bash - run: | - echo "✅ Updating packages..." - # Simulate package update - sudo apt-get update - echo "✅ Packages updated successfully" + - name: Build Bicep Templates + shell: bash + run: | + echo "::group::Building Bicep templates" + echo "✅ Building Bicep templates..." + mkdir -p ./artifacts + + # Check if Azure CLI is available + if command -v az &> /dev/null; then + az bicep build --file ./infra/main.bicep --outdir ./artifacts + echo "✅ Bicep build completed successfully" + else + echo "::error::Azure CLI not available - cannot build Bicep templates" + exit 1 + fi + echo "::endgroup::" - - name: Build Accelerator Bicep - shell: bash - run: | - echo "✅ Building Bicep templates..." - mkdir -p ./artifacts - - # Check if Azure CLI is available - if command -v az &> /dev/null; then - az bicep build --file ./infra/main.bicep --outdir ./artifacts - echo "✅ Bicep build completed" - else - echo "⚠️ Azure CLI not available, creating placeholder artifacts" - echo "Bicep build would be executed here" > ./artifacts/placeholder.txt - fi + - name: Upload Versioned Artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: artifacts-${{ inputs.new_version }} + path: ./artifacts + compression-level: 6 + if-no-files-found: error + retention-days: 30 - - name: Upload Artifacts - uses: actions/upload-artifact@v4 - with: - name: artifacts-${{ inputs.new_version }} - path: ./artifacts - compression-level: 6 - overwrite: true - if-no-files-found: warn - retention-days: 7 + - name: Generate Artifact Summary + shell: bash + run: | + branch_name="${{ inputs.branch_name }}" + version="${{ inputs.new_version }}" + should_publish="${{ inputs.should_publish }}" - - name: Artifact Summary - shell: bash - run: | - branch_name="${{ inputs.branch_name }}" - version="${{ inputs.new_version }}" - should_publish="${{ inputs.should_publish }}" - - echo "📦 Artifacts Summary:" - echo " - Version: $version" - echo " - Branch: $branch_name" - echo " - Tag created: ✅" - echo " - Artifacts uploaded: ✅" - echo " - GitHub release will be published: $should_publish" - - if [[ "$should_publish" == "false" ]]; then - echo "" - echo "ℹ️ This is a development build from a non-main branch." - echo " Tag and artifacts are created for tracking, but no GitHub release is published." - fi + echo "## 📦 Build Artifacts Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY + echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`$version\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`$branch_name\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag Created | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Artifacts Uploaded | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Publish Release | $should_publish |" >> $GITHUB_STEP_SUMMARY - - name: Artifact Summary - shell: bash - run: | - branch_name="${{ inputs.branch_name }}" - version="${{ inputs.new_version }}" - should_publish="${{ inputs.should_publish }}" - - echo "📦 Artifacts Summary:" - echo " - Version: $version" - echo " - Branch: $branch_name" - echo " - Tag created: ✅" - echo " - Artifacts uploaded: ✅" - echo " - GitHub release will be published: $should_publish" - - if [[ "$should_publish" == "false" ]]; then - echo "" - echo "ℹ️ This is a development build from a non-main branch." - echo " Tag and artifacts are created for tracking, but no GitHub release is published." - fi \ No newline at end of file + if [[ "$should_publish" == "false" ]]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "> **Note:** This is a development build from a non-main branch." >> $GITHUB_STEP_SUMMARY + echo "> Tag and artifacts are created for tracking, but no GitHub release will be published." >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/actions/ci/generate-release/action.yml b/.github/actions/ci/generate-release/action.yml index 333cde1d..cc430ddc 100644 --- a/.github/actions/ci/generate-release/action.yml +++ b/.github/actions/ci/generate-release/action.yml @@ -1,48 +1,44 @@ +# ============================================================================= +# File: action.yml (generate-release) +# Purpose: Composite action for generating release versions and tags +# Description: Calculates semantic versions based on branch strategy and commits +# ============================================================================= + name: Generate Release -description: Generate a release based on the build outputs +description: | + Generates semantic version numbers based on branch-based release strategy. + Supports main, feature/*, and fix/* branches with overflow handling. outputs: new_version: - description: 'The new version generated for the release' + description: "The new semantic version generated for the release (e.g., v1.2.3)" value: ${{ steps.calculate_next_version.outputs.new_version }} release_type: - description: 'The type of release (major, minor, patch, etc.)' + description: "The type of release based on branch (main, feature, fix, or none)" value: ${{ steps.determine_release_type.outputs.release_type }} previous_tag: - description: 'The last tag before this release' + description: "The last tag before this release (e.g., v1.2.2)" value: ${{ steps.get_tag.outputs.tag }} should_release: - description: 'Whether a release should be created' + description: "Whether a release tag should be created (true/false)" value: ${{ steps.determine_release_type.outputs.should_release }} should_publish: - description: 'Whether the release should be published on GitHub' + description: "Whether the release should be published on GitHub (true/false)" value: ${{ steps.determine_release_type.outputs.should_publish }} branch_name: - description: 'The name of the branch being built' + description: "The name of the branch being built" value: ${{ steps.branch_info.outputs.branch_name }} runs: using: composite steps: - - name: Setup Git identity - shell: bash - run: | - git config user.name "github-actions" - git config user.email "github-actions@github.com" - - - name: Debug trigger information + - name: Setup Git Identity shell: bash run: | - echo "🐛 Debug Information:" - echo "Event name: ${{ github.event_name }}" - echo "Ref: ${{ github.ref }}" - echo "Head ref: ${{ github.head_ref }}" - echo "Base ref: ${{ github.base_ref }}" - echo "Repository: ${{ github.repository }}" - echo "Actor: ${{ github.actor }}" - echo "SHA: ${{ github.sha }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" - - name: Get branch information + - name: Get Branch Information shell: bash id: branch_info run: | @@ -53,91 +49,83 @@ runs: else branch_name="${{ github.ref_name }}" fi - + echo "branch_name=$branch_name" >> $GITHUB_OUTPUT - echo "✅ Current branch: $branch_name" + echo "::notice title=Branch::Current branch: $branch_name" - - name: Get latest tag + - name: Get Latest Tag shell: bash id: get_tag run: | # Fetch all tags git fetch --tags --force + # Get the latest tag matching v.. optionally with a suffix tag=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*'* | sort -V | tail -n 1) if [ -z "$tag" ]; then tag="v0.0.0" fi - # If tag matches v..-, extract only v.. + # Extract base version without suffix base_tag=$(echo "$tag" | sed -E 's/^(v[0-9]+\.[0-9]+\.[0-9]+).*$/\1/') echo "tag=$tag" >> $GITHUB_OUTPUT echo "base_tag=$base_tag" >> $GITHUB_OUTPUT - echo "Previous tag: $base_tag" - echo "✅ Latest tag: $tag" + echo "::notice title=Previous Tag::Latest tag: $tag (base: $base_tag)" - - name: Determine release type and strategy + - name: Determine Release Type shell: bash id: determine_release_type run: | branch_name="${{ steps.branch_info.outputs.branch_name }}" should_release="true" should_publish="true" - + echo "🔍 Analyzing branch: $branch_name" - + # For PR events, only create pre-releases if [ "${{ github.event_name }}" = "pull_request" ]; then - echo "📋 Pull Request detected - Creating pre-release" + echo "::notice title=PR Build::Pull Request detected - Creating pre-release" should_publish="false" fi - + if [[ "$branch_name" == "main" ]]; then - # Main branch: conditional major increment with new rule release_type="main" should_publish="true" - echo "✅ Main branch detected - Conditional major release strategy with new rule" + echo "::notice title=Release Type::Main branch - Conditional major release strategy" elif [[ "$branch_name" == feature/* ]]; then - # Feature branch: patch increment with overflow logic release_type="feature" should_publish="false" - echo "✅ Feature branch detected - Patch increment with overflow strategy (no release publication)" + echo "::notice title=Release Type::Feature branch - Patch increment with overflow" elif [[ "$branch_name" == fix/* ]]; then - # Fix branch: minor increment with overflow logic release_type="fix" should_publish="false" - echo "✅ Fix branch detected - Minor increment with overflow strategy (no release publication)" + echo "::notice title=Release Type::Fix branch - Minor increment with overflow" else - echo "⚠️ Unsupported branch pattern: $branch_name" - echo "Only main, feature/*, and fix/* branches are supported for releases" + echo "::warning title=Unsupported Branch::Branch pattern '$branch_name' not supported for releases" should_release="false" should_publish="false" release_type="none" fi - + echo "release_type=$release_type" >> $GITHUB_OUTPUT echo "should_release=$should_release" >> $GITHUB_OUTPUT echo "should_publish=$should_publish" >> $GITHUB_OUTPUT - - echo "📋 Release Summary:" - echo " - Will create tag and version: $should_release" - echo " - Will publish GitHub release: $should_publish" - - name: Count commits since last tag + - name: Count Commits Since Last Tag shell: bash id: count_commits if: steps.determine_release_type.outputs.should_release == 'true' run: | last_tag="${{ steps.get_tag.outputs.tag }}" branch_name="${{ steps.branch_info.outputs.branch_name }}" - + echo "🔍 Counting commits for branch: $branch_name" echo "🏷️ Last tag: $last_tag" - + if [ "$last_tag" = "v0.0.0" ]; then # No previous tags, count all commits if [[ "$branch_name" == "main" ]]; then @@ -165,12 +153,12 @@ runs: fi fi fi - + # Ensure minimum commit count of 1 if [ "$commit_count" -eq 0 ]; then commit_count=1 fi - + echo "commit_count=$commit_count" >> $GITHUB_OUTPUT echo "✅ Commits to include: $commit_count" @@ -183,18 +171,18 @@ runs: release_type="${{ steps.determine_release_type.outputs.release_type }}" commit_count="${{ steps.count_commits.outputs.commit_count }}" branch_name="${{ steps.branch_info.outputs.branch_name }}" - + # Remove 'v' prefix if present current_version=${current_version#v} IFS='.' read -r major minor patch <<< "$current_version" # Fallback for missing minor/patch if [ -z "$minor" ]; then minor=0; fi if [ -z "$patch" ]; then patch=0; fi - + echo "📊 Current version: v$major.$minor.$patch" echo "📊 Release type: $release_type" echo "📊 Commit count: $commit_count" - + case "$release_type" in main) # NEW RULE: Main branch conditional major increment @@ -295,12 +283,12 @@ runs: exit 1 ;; esac - + # Add PR suffix if this is a pull request if [ "${{ github.event_name }}" = "pull_request" ]; then version_suffix="${version_suffix}-pr${{ github.event.number }}" fi - + new_version="v$major.$minor.$patch$version_suffix" echo "✅ Next version: $new_version" echo "📊 Final version breakdown: major=$major, minor=$minor, patch=$patch" @@ -312,14 +300,14 @@ runs: tag_name="${{ steps.calculate_next_version.outputs.new_version }}" branch_name="${{ steps.branch_info.outputs.branch_name }}" should_publish="${{ steps.determine_release_type.outputs.should_publish }}" - + echo "🏷️ Preparing to create tag: $tag_name" echo "🔀 For branch: $branch_name" echo "📋 Will publish release: $should_publish" - + # Fetch latest tags first to check for existing tags git fetch --tags --force - + # Check if tag already exists before creating it if git rev-parse "$tag_name" >/dev/null 2>&1; then echo "⚠️ Tag $tag_name already exists locally or remotely. Skipping tag creation and push." @@ -336,4 +324,4 @@ runs: # Push the newly created tag git push origin "$tag_name" echo "✅ Tag $tag_name created and pushed successfully" - fi \ No newline at end of file + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2a75963..911afb40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,22 +1,37 @@ +# ============================================================================= +# File: ci.yml +# Purpose: Continuous Integration workflow for Dev Box Accelerator +# Description: Builds and validates Bicep templates on feature/fix branches and PRs +# Triggers: Push to feature/** and fix/** branches, PRs to main +# ============================================================================= + name: Continuous Integration +# Workflow triggers on: push: branches: - - 'feature/**' - - 'fix/**' + - "feature/**" + - "fix/**" pull_request: branches: - main types: [opened, synchronize, reopened] +# Least-privilege permissions - only request what's needed permissions: - contents: write - pull-requests: read # Added for PR triggers + contents: write # Required for creating tags + pull-requests: read # Required for PR triggers jobs: + # ----------------------------------------------------------------------------- + # Job: generate-tag-version + # Purpose: Calculate semantic version based on branch and commit history + # ----------------------------------------------------------------------------- generate-tag-version: + name: Generate Tag Version runs-on: ubuntu-latest + timeout-minutes: 10 outputs: new_version: ${{ steps.release.outputs.new_version }} release_type: ${{ steps.release.outputs.release_type }} @@ -26,21 +41,32 @@ jobs: branch_name: ${{ steps.release.outputs.branch_name }} steps: - - name: Checkout code - uses: actions/checkout@v4 + - name: Checkout Repository + # Pin to SHA for security - see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }} + fetch-depth: 0 # Full history needed for tag analysis - - name: Generate Release + - name: Generate Release Information id: release - uses: ./.github/actions/ci/generate-tag-version + uses: ./.github/actions/ci/generate-release + # ----------------------------------------------------------------------------- + # Job: build + # Purpose: Compile Bicep templates and create artifacts + # ----------------------------------------------------------------------------- build: + name: Build Bicep Templates runs-on: ubuntu-latest + timeout-minutes: 15 needs: generate-tag-version + # Only build if version generation succeeded + if: needs.generate-tag-version.result == 'success' + steps: - - name: Checkout code - uses: actions/checkout@v4 + - name: Checkout Repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bdcac207..cbd7f51a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,90 +1,158 @@ +# ============================================================================= +# File: deploy.yml +# Purpose: Azure deployment workflow for Dev Box Accelerator +# Description: Provisions infrastructure to Azure using azd with OIDC authentication +# Triggers: Manual workflow dispatch only +# ============================================================================= + name: Deploy to Azure -# Run when commits are pushed to feature/gitHubActions + +# Workflow triggers - manual dispatch with customizable inputs on: workflow_dispatch: inputs: AZURE_ENV_NAME: - description: 'Azure environment name' + description: "Azure environment name (e.g., dev, staging, prod)" required: true - default: 'demo' + default: "demo" + type: string AZURE_LOCATION: - description: 'Azure region (e.g., eastus, westus)' + description: "Azure region for deployment (e.g., eastus, westus)" required: true - default: 'eastus2' + default: "eastus2" + type: string -# Set up permissions for deploying with secretless Azure federated credentials +# Minimal permissions for OIDC authentication # https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication permissions: - id-token: write - contents: read + id-token: write # Required for requesting OIDC JWT token + contents: read # Required for actions/checkout -jobs: +# Prevent concurrent deployments to the same environment +concurrency: + group: deploy-${{ github.event.inputs.AZURE_ENV_NAME || 'default' }} + cancel-in-progress: false - build-and-deploy-to-azure: +jobs: + # ----------------------------------------------------------------------------- + # Job: build-and-deploy-to-azure + # Purpose: Build Bicep templates and deploy infrastructure to Azure + # ----------------------------------------------------------------------------- + build-and-deploy-to-azure: + name: Build and Deploy to Azure runs-on: ubuntu-latest + timeout-minutes: 60 + environment: + name: ${{ inputs.AZURE_ENV_NAME }} + env: AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} - AZURE_ENV_NAME: ${{ github.event_name == 'workflow_dispatch' && inputs.AZURE_ENV_NAME || 'devexp2' }} - AZURE_LOCATION: ${{ github.event_name == 'workflow_dispatch' && inputs.AZURE_LOCATION || vars.AZURE_LOCATION }} - + AZURE_ENV_NAME: ${{ inputs.AZURE_ENV_NAME || 'devexp2' }} + AZURE_LOCATION: ${{ inputs.AZURE_LOCATION || vars.AZURE_LOCATION }} + steps: - - name: Update all Packages + - name: Validate Required Variables run: | - sudo apt-get update - - - name: Checkout - uses: actions/checkout@v4 - - - name: Install azd - uses: Azure/setup-azd@v2 - - - name: Build Accelerator Bicep + echo "::group::Validating Azure configuration" + missing_vars="" + + if [ -z "${{ vars.AZURE_CLIENT_ID }}" ]; then + missing_vars="${missing_vars}AZURE_CLIENT_ID " + fi + if [ -z "${{ vars.AZURE_TENANT_ID }}" ]; then + missing_vars="${missing_vars}AZURE_TENANT_ID " + fi + if [ -z "${{ vars.AZURE_SUBSCRIPTION_ID }}" ]; then + missing_vars="${missing_vars}AZURE_SUBSCRIPTION_ID " + fi + + if [ -n "$missing_vars" ]; then + echo "::error::Missing required repository variables: $missing_vars" + exit 1 + fi + + echo "✅ All required variables are configured" + echo "::endgroup::" + + - name: Checkout Repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install Azure Developer CLI + uses: Azure/setup-azd@v2.2.1 + + - name: Build Bicep Templates + id: build run: | + echo "::group::Building Bicep templates" echo "✅ Building Bicep templates..." mkdir -p ./artifacts - + # Check if Azure CLI is available if command -v az &> /dev/null; then az bicep build --file ./infra/main.bicep --outdir ./artifacts - echo "✅ Bicep build completed" + echo "✅ Bicep build completed successfully" else - echo "⚠️ Azure CLI not available, creating placeholder artifacts" - echo "Bicep build would be executed here" > ./artifacts/placeholder.txt + echo "::error::Azure CLI not available - cannot build Bicep templates" + exit 1 fi + echo "::endgroup::" - - name: Upload Artifacts - uses: actions/upload-artifact@v4 + - name: Upload Build Artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: artifacts + name: bicep-artifacts-${{ github.run_number }} path: ./artifacts compression-level: 6 - overwrite: true - if-no-files-found: warn + if-no-files-found: error retention-days: 7 - - name: Artifact Summary + - name: Generate Build Summary run: | - branch_name="${{ github.ref_name }}" + echo "## 📦 Build Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY + echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Environment | \`${{ env.AZURE_ENV_NAME }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Location | \`${{ env.AZURE_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ github.ref_name }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Commit | \`${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Run | [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) |" >> $GITHUB_STEP_SUMMARY - echo "📦 Artifacts Summary:" - echo " - Environment: ${{ env.AZURE_ENV_NAME }}" - echo " - Branch: $branch_name" - echo " - Artifacts uploaded: ✅" - - - name: Log in with Azure (Federated Credentials) + - name: Authenticate with Azure (OIDC) + id: azure_auth run: | + echo "::group::Authenticating with Azure" azd auth login ` --client-id "$Env:AZURE_CLIENT_ID" ` --federated-credential-provider "github" ` --tenant-id "$Env:AZURE_TENANT_ID" + echo "✅ Successfully authenticated with Azure" + echo "::endgroup::" shell: pwsh - - name: Deploy to Azure + - name: Deploy Infrastructure to Azure + id: deploy run: | - echo "AZURE_ENV_NAME: ${{ env.AZURE_ENV_NAME }}" + echo "::group::Deploying to Azure" + echo "Environment: ${{ env.AZURE_ENV_NAME }}" + echo "Location: ${{ env.AZURE_LOCATION }}" azd provision --no-prompt + echo "✅ Deployment completed successfully" + echo "::endgroup::" env: KEY_VAULT_SECRET: ${{ secrets.KEY_VAULT_SECRET }} - SOURCE_CONTROL_PLATFORM: 'github' \ No newline at end of file + SOURCE_CONTROL_PLATFORM: "github" + + - name: Generate Deployment Summary + if: always() + run: | + echo "" >> $GITHUB_STEP_SUMMARY + echo "## 🚀 Deployment Result" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ steps.deploy.outcome }}" == "success" ]; then + echo "✅ **Deployment succeeded**" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **Deployment failed**" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aae587be..b21a46c7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,25 +1,42 @@ +# ============================================================================= +# File: release.yml +# Purpose: Branch-based release strategy workflow for Dev Box Accelerator +# Description: Generates semantic versions and publishes GitHub releases +# Triggers: Manual workflow dispatch +# ============================================================================= + name: Branch-Based Release Strategy +# Workflow triggers on: - # Manual workflow dispatch for all branches workflow_dispatch: inputs: force_release: - description: 'Force create a release (even for non-main branches)' + description: "Force create a release (even for non-main branches)" required: false default: false type: boolean - + +# Least-privilege permissions permissions: - contents: write - pull-requests: read - actions: read + contents: write # Required for creating tags and releases + pull-requests: read # Required for PR information + actions: read # Required for workflow introspection + +# Prevent concurrent releases +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false jobs: - # 🔍 Generate release metadata and version information + # ----------------------------------------------------------------------------- + # Job: generate-release + # Purpose: Calculate semantic version and prepare release metadata + # ----------------------------------------------------------------------------- generate-release: name: Generate Release Metadata runs-on: ubuntu-latest + timeout-minutes: 15 outputs: new_version: ${{ steps.release.outputs.new_version }} release_type: ${{ steps.release.outputs.release_type }} @@ -31,10 +48,10 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }} - fetch-depth: 0 # Full history for proper tag analysis + fetch-depth: 0 # Full history for proper tag analysis - name: Generate Release Information id: release @@ -43,42 +60,36 @@ jobs: - name: Release Strategy Summary shell: bash run: | - echo "🌟 Branch-Based Release Strategy with Conditional Major Increment" - echo "" - echo "🔀 Branch: ${{ steps.release.outputs.branch_name }}" - echo "🏷️ Version: ${{ steps.release.outputs.new_version }}" - echo "📦 Previous Version: ${{ steps.release.outputs.previous_tag }}" - echo "🚀 Release Type: ${{ steps.release.outputs.release_type }}" - echo "🤖 Trigger: ${{ github.event_name }}" - echo "📝 Commit: ${{ github.sha }}" - echo "" - echo "Release Strategy Applied:" - echo "🎯 Main Branch: Conditional major increment (only if minor=0 AND patch=0, otherwise increment patch with overflow logic)" - echo "" - echo "Main Branch Logic (NEW RULE):" - echo "• If minor=0 AND patch=0: Increment major → major+1.0.0" - echo "• If minor≠0 OR patch≠0: Keep major, increment patch → major.minor.(patch+1)" - echo "• Overflow handling: If patch > 99 → minor+1, patch=0; if minor > 99 → major+1, minor=0" - echo "" - echo "Feature/Fix Branch Overflow Logic:" - echo "• Feature branches: If patch + commits > 99 → patch = 0, minor += 1" - echo "• Fix branches: If minor + commits > 99 → minor = 0, major += 1" - echo "• Cascading overflow: If minor overflows during patch overflow → minor = 0, major += 1" + echo "## 🌟 Release Strategy Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY + echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| 🔀 Branch | \`${{ steps.release.outputs.branch_name }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| 🏷️ Version | \`${{ steps.release.outputs.new_version }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| 📦 Previous | \`${{ steps.release.outputs.previous_tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| 🚀 Type | \`${{ steps.release.outputs.release_type }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| 🤖 Trigger | \`${{ github.event_name }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| 📝 Commit | \`${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY - # 🏗️ Build artifacts and compile Bicep templates + # ----------------------------------------------------------------------------- + # Job: build + # Purpose: Compile Bicep templates and create versioned artifacts + # ----------------------------------------------------------------------------- build: name: Build Artifacts runs-on: ubuntu-latest + timeout-minutes: 20 needs: generate-release if: needs.generate-release.outputs.should_release == 'true' + steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }} fetch-depth: 0 - - name: Build Bicep Templates and Create Artifacts + - name: Build Bicep Templates id: build uses: ./.github/actions/ci/bicep-standard-ci with: @@ -89,31 +100,39 @@ jobs: - name: Build Summary shell: bash run: | - echo "📦 Build Artifacts Created:" - echo " - 📄 Bicep templates compiled to ARM templates" - echo " - 🏗️ Infrastructure deployment files" - echo " - 📋 Release metadata and documentation" - echo " - 🏷️ Version: ${{ needs.generate-release.outputs.new_version }}" - echo " - 🔀 Branch: ${{ needs.generate-release.outputs.branch_name }}" - - # 📢 Publish GitHub Release (only for main branch or forced releases) + echo "" >> $GITHUB_STEP_SUMMARY + echo "## 📦 Build Artifacts" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- 📄 Bicep templates compiled to ARM templates" >> $GITHUB_STEP_SUMMARY + echo "- 🏗️ Infrastructure deployment files" >> $GITHUB_STEP_SUMMARY + echo "- 📋 Version: \`${{ needs.generate-release.outputs.new_version }}\`" >> $GITHUB_STEP_SUMMARY + echo "- 🔀 Branch: \`${{ needs.generate-release.outputs.branch_name }}\`" >> $GITHUB_STEP_SUMMARY + + # ----------------------------------------------------------------------------- + # Job: publish-release + # Purpose: Create and publish GitHub Release with artifacts + # ----------------------------------------------------------------------------- publish-release: name: Publish GitHub Release runs-on: ubuntu-latest + timeout-minutes: 15 needs: [generate-release, build] if: | + always() && needs.generate-release.outputs.should_release == 'true' && + needs.build.result == 'success' && (needs.generate-release.outputs.should_publish == 'true' || github.event.inputs.force_release == 'true') + steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }} fetch-depth: 0 - name: Download Build Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: artifacts-${{ needs.generate-release.outputs.new_version }} path: ./artifacts @@ -127,52 +146,41 @@ jobs: previous_tag="${{ needs.generate-release.outputs.previous_tag }}" new_version="${{ needs.generate-release.outputs.new_version }}" commit_sha="${{ needs.generate-release.outputs.commit_sha }}" - + # Determine if this is a prerelease is_prerelease="false" if [[ "$branch_name" != "main" ]] || [[ "$new_version" == *"-"* ]]; then is_prerelease="true" fi - + # Generate release body cat > release_notes.md << EOF - ## 🌟 Branch-Based Release Strategy with Conditional Major Increment - + ## 🌟 Branch-Based Release Strategy + ### 📊 Release Information - - **🔀 Branch:** \`$branch_name\` - - **🏷️ Version:** \`$new_version\` - - **📦 Previous Version:** \`$previous_tag\` - - **🚀 Release Type:** \`$release_type\` - - **🤖 Trigger:** ${{ github.event_name }} - - **📝 Commit:** \`$commit_sha\` - - ### 🎯 Release Strategy Applied - **Main Branch:** Conditional major increment (only if minor=0 AND patch=0, otherwise increment patch with overflow logic) - - #### Main Branch Logic (NEW RULE) - - **If minor=0 AND patch=0:** Increment major → major+1.0.0 - - **If minor≠0 OR patch≠0:** Keep major, increment patch → major.minor.(patch+1) - - **Overflow handling:** If patch > 99 → minor+1, patch=0; if minor > 99 → major+1, minor=0 - - #### Feature/Fix Branch Overflow Logic - - **Feature branches:** If patch + commits > 99 → patch = 0, minor += 1 - - **Fix branches:** If minor + commits > 99 → minor = 0, major += 1 - - **Cascading overflow:** If minor overflows during patch overflow → minor = 0, major += 1 - - ### 📦 Artifacts + | Property | Value | + |----------|-------| + | 🔀 Branch | \`$branch_name\` | + | 🏷️ Version | \`$new_version\` | + | 📦 Previous | \`$previous_tag\` | + | 🚀 Type | \`$release_type\` | + | 🤖 Trigger | ${{ github.event_name }} | + | 📝 Commit | \`$commit_sha\` | + + ### 📦 Artifacts Included - 📄 Bicep templates compiled to ARM templates - 🏗️ Infrastructure deployment files - 📋 Release metadata and documentation - + ### 🔗 Links - [Commit History](https://github.com/${{ github.repository }}/commits/$branch_name) - [Compare Changes](https://github.com/${{ github.repository }}/compare/$previous_tag...$new_version) EOF - + echo "is_prerelease=$is_prerelease" >> $GITHUB_OUTPUT - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 with: tag_name: ${{ needs.generate-release.outputs.new_version }} name: Release ${{ needs.generate-release.outputs.new_version }} @@ -185,11 +193,15 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Release Published + - name: Release Summary shell: bash run: | - echo "🎉 Release ${{ needs.generate-release.outputs.new_version }} has been published!" - echo "🔗 Release URL: https://github.com/${{ github.repository }}/releases/tag/${{ needs.generate-release.outputs.new_version }}" + echo "" >> $GITHUB_STEP_SUMMARY + echo "## 🎉 Release Published" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Version:** \`${{ needs.generate-release.outputs.new_version }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "[View Release](https://github.com/${{ github.repository }}/releases/tag/${{ needs.generate-release.outputs.new_version }})" >> $GITHUB_STEP_SUMMARY # 📊 Summary job for workflow status summary: @@ -221,5 +233,3 @@ jobs: else echo "⚠️ Workflow completed with issues. Check individual job logs." fi - - \ No newline at end of file diff --git a/azure-pwh.yaml b/azure-pwh.yaml index 58bfaa78..074aa7d8 100644 --- a/azure-pwh.yaml +++ b/azure-pwh.yaml @@ -1,28 +1,34 @@ +# ============================================================================= +# File: azure-pwh.yaml +# Purpose: Azure Developer CLI (azd) configuration for Dev Box Accelerator +# Description: Defines deployment hooks and environment setup for Windows (PowerShell) +# ============================================================================= # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/refs/heads/main/schemas/v1.0/azure.yaml.json +# Project name used by azd for environment naming and resource tagging name: ContosoDevExp hooks: - preprovision: - shell: pwsh - continueOnError: false - interactive: true - run: | - # PowerShell preprovision script - $ErrorActionPreference = 'Stop' - $defaultPlatform = 'github' + preprovision: + shell: pwsh + continueOnError: false + interactive: true + run: | + # PowerShell preprovision script + $ErrorActionPreference = 'Stop' + $defaultPlatform = 'github' - if (-not $env:SOURCE_CONTROL_PLATFORM -or $env:SOURCE_CONTROL_PLATFORM -eq '') { - Write-Output "SOURCE_CONTROL_PLATFORM is not set. Setting it to '$defaultPlatform' by default." - $env:SOURCE_CONTROL_PLATFORM = $defaultPlatform - } else { - Write-Output "Existing SOURCE_CONTROL_PLATFORM is set to '$($env:SOURCE_CONTROL_PLATFORM)'." - } + if (-not $env:SOURCE_CONTROL_PLATFORM -or $env:SOURCE_CONTROL_PLATFORM -eq '') { + Write-Output "SOURCE_CONTROL_PLATFORM is not set. Setting it to '$defaultPlatform' by default." + $env:SOURCE_CONTROL_PLATFORM = $defaultPlatform + } else { + Write-Output "Existing SOURCE_CONTROL_PLATFORM is set to '$($env:SOURCE_CONTROL_PLATFORM)'." + } - # Run setup.sh (use bash if available) - if (Get-Command bash -ErrorAction SilentlyContinue) { - & bash -c "./setup.sh -e $env:AZURE_ENV_NAME -s $env:SOURCE_CONTROL_PLATFORM" - } else { - Write-Output "bash not found; attempting to execute ./setup.sh directly" - & ./setup.sh -e $env:AZURE_ENV_NAME -s $env:SOURCE_CONTROL_PLATFORM - } \ No newline at end of file + # Run setup.sh (use bash if available) + if (Get-Command bash -ErrorAction SilentlyContinue) { + & bash -c "./setup.sh -e $env:AZURE_ENV_NAME -s $env:SOURCE_CONTROL_PLATFORM" + } else { + Write-Output "bash not found; attempting to execute ./setup.sh directly" + & ./setup.sh -e $env:AZURE_ENV_NAME -s $env:SOURCE_CONTROL_PLATFORM + } diff --git a/azure.yaml b/azure.yaml index aecccf88..0bff58e1 100644 --- a/azure.yaml +++ b/azure.yaml @@ -1,5 +1,11 @@ +# ============================================================================= +# File: azure.yaml +# Purpose: Azure Developer CLI (azd) configuration for Dev Box Accelerator +# Description: Defines deployment hooks and environment setup for Linux/macOS +# ============================================================================= # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/refs/heads/main/schemas/v1.0/azure.yaml.json +# Project name used by azd for environment naming and resource tagging name: ContosoDevExp hooks: @@ -9,10 +15,10 @@ hooks: interactive: true run: | #!/bin/bash - + set -e defaultPlatform="github" - + # Add a if statement to check if the environment variable is set if [ -z "${SOURCE_CONTROL_PLATFORM}" ]; then echo "SOURCE_CONTROL_PLATFORM is not set. Setting it to '${defaultPlatform}' by default." @@ -20,4 +26,4 @@ hooks: else echo "Existing SOURCE_CONTROL_PLATFORM is set to '${SOURCE_CONTROL_PLATFORM}'." fi - ./setup.sh -e ${AZURE_ENV_NAME} -s ${SOURCE_CONTROL_PLATFORM} \ No newline at end of file + ./setup.sh -e ${AZURE_ENV_NAME} -s ${SOURCE_CONTROL_PLATFORM} diff --git a/cleanSetUp.ps1 b/cleanSetUp.ps1 index 24bc55e4..5916183a 100644 --- a/cleanSetUp.ps1 +++ b/cleanSetUp.ps1 @@ -1,103 +1,289 @@ -# PowerShell script to clean up the setup by deleting users, credentials, and GitHub secrets +#Requires -Version 5.1 -[CmdletBinding()] +<# +.SYNOPSIS + Cleans up the DevExp-DevBox setup including users, credentials, and GitHub secrets. + +.DESCRIPTION + This script orchestrates the complete cleanup of DevExp-DevBox infrastructure: + - Deletes Azure subscription deployments + - Removes user role assignments + - Deletes deployment credentials (service principals and app registrations) + - Removes GitHub secrets for Azure credentials + - Cleans up Azure resource groups + +.PARAMETER EnvName + The environment name used in resource naming. Defaults to 'gitHub'. + +.PARAMETER Location + The Azure region where resources are deployed. Defaults to 'eastus2'. + Valid values: eastus, eastus2, westus, westus2, westus3, northeurope, westeurope + +.PARAMETER AppDisplayName + The display name of the Azure AD application to delete. + Defaults to 'ContosoDevEx GitHub Actions Enterprise App'. + +.PARAMETER GhSecretName + The name of the GitHub secret to delete. Defaults to 'AZURE_CREDENTIALS'. + +.EXAMPLE + .\cleanSetUp.ps1 + Runs cleanup with default parameters. + +.EXAMPLE + .\cleanSetUp.ps1 -EnvName "prod" -Location "westus2" + Runs cleanup for the 'prod' environment in 'westus2'. + +.NOTES + Author: DevExp Team + Requires: Azure CLI (az), GitHub CLI (gh), and appropriate permissions +#> + +[CmdletBinding(SupportsShouldProcess)] param( - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] [string]$EnvName = "gitHub", - - [Parameter(Mandatory=$false)] + + [Parameter(Mandatory = $false)] [ValidateSet("eastus", "eastus2", "westus", "westus2", "westus3", "northeurope", "westeurope")] - [string]$Location = "eastus2" + [string]$Location = "eastus2", + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$AppDisplayName = "ContosoDevEx GitHub Actions Enterprise App", + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$GhSecretName = "AZURE_CREDENTIALS" ) -# Exit immediately if a command exits with a non-zero status, treat unset variables as an error, and propagate errors in pipelines. -$ErrorActionPreference = "Stop" -$WarningPreference = "Stop" +# Script Configuration +$ErrorActionPreference = 'Stop' +$WarningPreference = 'Stop' -$appDisplayName = "ContosoDevEx GitHub Actions Enterprise App" -$ghSecretName = "AZURE_CREDENTIALS" +# Script directory for relative path resolution +$Script:ScriptDirectory = $PSScriptRoot -# Function to delete deployments -function Remove-Deployments { - param ( - [string]$resourceGroupName - ) +function Remove-SubscriptionDeployments { + <# + .SYNOPSIS + Deletes all subscription-level deployments. + + .DESCRIPTION + Lists and deletes all Azure Resource Manager deployments at the + subscription level. + + .OUTPUTS + System.Boolean - True if successful, False otherwise. + + .EXAMPLE + Remove-SubscriptionDeployments + #> + [CmdletBinding(SupportsShouldProcess)] + [OutputType([bool])] + param() try { - $deployments = az deployment sub list --query "[].name" -o tsv + Write-Output "Retrieving subscription deployments..." + $deployments = az deployment sub list --query "[].name" --output tsv + if ($LASTEXITCODE -ne 0) { - throw "Failed to list deployments." + throw "Failed to list subscription deployments." } - - foreach ($deployment in $deployments) { - if (-not [string]::IsNullOrEmpty($deployment)) { - Write-Output "Deleting deployment: $deployment" - az deployment sub delete --name $deployment - if ($LASTEXITCODE -ne 0) { - throw "Failed to delete deployment: $deployment" + + if ([string]::IsNullOrWhiteSpace($deployments)) { + Write-Output "No subscription deployments found." + return $true + } + + foreach ($deployment in ($deployments -split "`n")) { + if (-not [string]::IsNullOrWhiteSpace($deployment)) { + if ($PSCmdlet.ShouldProcess($deployment, "Delete subscription deployment")) { + Write-Output "Deleting deployment: $deployment" + $null = az deployment sub delete --name $deployment 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to delete deployment: $deployment" + } + else { + Write-Output "Deployment '$deployment' deleted." + } } - Write-Output "Deployment $deployment deleted." } } + return $true } catch { - Write-Error "Error deleting deployments: $_" + Write-Error "Error deleting subscription deployments: $_" return $false } } -# Function to clean up the setup by deleting users, credentials, and GitHub secrets -function Remove-SetUp { - param ( +function Invoke-CleanupScript { + <# + .SYNOPSIS + Invokes a cleanup script with proper error handling. + + .DESCRIPTION + Executes a PowerShell script with specified parameters, handling + path resolution and error checking. + + .PARAMETER ScriptPath + The relative path to the script from the script directory. + + .PARAMETER Parameters + Hashtable of parameters to pass to the script. + + .PARAMETER Description + Description of what the script does (for logging). + + .OUTPUTS + System.Boolean - True if successful, False otherwise. + + .EXAMPLE + Invoke-CleanupScript -ScriptPath "Azure\deleteUsers.ps1" -Parameters @{Name="test"} -Description "Delete users" + #> + [CmdletBinding()] + [OutputType([bool])] + param( [Parameter(Mandatory = $true)] - [string]$appDisplayName, + [string]$ScriptPath, + + [Parameter(Mandatory = $false)] + [hashtable]$Parameters = @{}, [Parameter(Mandatory = $true)] - [string]$ghSecretName + [string]$Description ) try { - # Check if required parameters are provided - if ([string]::IsNullOrEmpty($appDisplayName) -or [string]::IsNullOrEmpty($ghSecretName)) { - throw "Missing required parameters." + $fullPath = Join-Path -Path $Script:ScriptDirectory -ChildPath $ScriptPath + $fullPath = [System.IO.Path]::GetFullPath($fullPath) + + if (-not (Test-Path -Path $fullPath)) { + Write-Warning "Script not found: $fullPath. Skipping $Description." + return $true } - Write-Output "Starting cleanup process for appDisplayName: $appDisplayName and ghSecretName: $ghSecretName" + Write-Output "$Description..." - # Delete deployments - Write-Output "Deleting deployments..." - $deploymentResult = Remove-Deployments - if (-not $deploymentResult) { - throw "Failed to delete deployments." + if ($Parameters.Count -gt 0) { + & $fullPath @Parameters } - - # Delete users and assigned roles - Write-Output "Deleting users and assigned roles..." - & ".\.configuration\setup\powershell\Azure\deleteUsersAndAssignedRoles.ps1" -appDisplayName $appDisplayName - if ($LASTEXITCODE -ne 0) { - throw "Failed to delete users and assigned roles." + else { + & $fullPath } - # Delete deployment credentials - Write-Output "Deleting deployment credentials..." - & ".\.configuration\setup\powershell\Azure\deleteDeploymentCredentials.ps1" -appDisplayName $appDisplayName if ($LASTEXITCODE -ne 0) { - throw "Failed to delete deployment credentials." + throw "Script failed with exit code: $LASTEXITCODE" } - # Delete GitHub secret for Azure credentials - Write-Output "Deleting GitHub secret for Azure credentials..." - & ".\GitHub\deleteGitHubSecretAzureCredentials.ps1" -ghSecretName $ghSecretName - if ($LASTEXITCODE -ne 0) { - throw "Failed to delete GitHub secret for Azure credentials." + Write-Output "$Description completed." + return $true + } + catch { + Write-Error "Error during ${Description}: $_" + return $false + } +} + +function Start-FullCleanup { + <# + .SYNOPSIS + Orchestrates the complete cleanup process. + + .DESCRIPTION + Runs all cleanup operations in sequence: deployments, users, + credentials, GitHub secrets, and resource groups. + + .PARAMETER AppDisplayName + The Azure AD application display name. + + .PARAMETER GhSecretName + The GitHub secret name. + + .PARAMETER EnvName + The environment name. + + .PARAMETER Location + The Azure region. + + .OUTPUTS + System.Boolean - True if all cleanup succeeded, False otherwise. + + .EXAMPLE + Start-FullCleanup -AppDisplayName "MyApp" -GhSecretName "SECRET" -EnvName "dev" -Location "eastus" + #> + [CmdletBinding(SupportsShouldProcess)] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string]$AppDisplayName, + + [Parameter(Mandatory = $true)] + [string]$GhSecretName, + + [Parameter(Mandatory = $true)] + [string]$EnvName, + + [Parameter(Mandatory = $true)] + [string]$Location + ) + + try { + Write-Output "Starting full cleanup process..." + Write-Output "App Display Name: $AppDisplayName" + Write-Output "GitHub Secret: $GhSecretName" + Write-Output "Environment: $EnvName" + Write-Output "Location: $Location" + Write-Output "" + + $allSucceeded = $true + + # Delete subscription deployments + if (-not (Remove-SubscriptionDeployments)) { + Write-Warning "Subscription deployment cleanup had issues." + $allSucceeded = $false } - Write-Output "Cleanup process completed successfully for appDisplayName: $appDisplayName and ghSecretName: $ghSecretName" - return $true + # Delete users and assigned roles + $success = Invoke-CleanupScript ` + -ScriptPath ".configuration\setup\powershell\Azure\deleteUsersAndAssignedRoles.ps1" ` + -Parameters @{ AppDisplayName = $AppDisplayName } ` + -Description "Deleting users and assigned roles" + + if (-not $success) { $allSucceeded = $false } + + # Delete deployment credentials + $success = Invoke-CleanupScript ` + -ScriptPath ".configuration\setup\powershell\Azure\deleteDeploymentCredentials.ps1" ` + -Parameters @{ AppDisplayName = $AppDisplayName } ` + -Description "Deleting deployment credentials" + + if (-not $success) { $allSucceeded = $false } + + # Delete GitHub secret + $success = Invoke-CleanupScript ` + -ScriptPath ".configuration\setup\powershell\GitHub\deleteGitHubSecretAzureCredentials.ps1" ` + -Parameters @{ GhSecretName = $GhSecretName } ` + -Description "Deleting GitHub secret for Azure credentials" + + if (-not $success) { $allSucceeded = $false } + + # Clean up resource groups + $cleanupScriptPath = ".configuration\powershell\cleanUp.ps1" + $success = Invoke-CleanupScript ` + -ScriptPath $cleanupScriptPath ` + -Parameters @{ EnvName = $EnvName; Location = $Location } ` + -Description "Cleaning up resource groups" + + if (-not $success) { $allSucceeded = $false } + + return $allSucceeded } catch { - Write-Error "Error during cleanup process: $_" + Write-Error "Error during full cleanup: $_" return $false } } @@ -105,20 +291,25 @@ function Remove-SetUp { # Main script execution try { Clear-Host - - # Call the cleanup function with the required parameters - Write-Output "Starting cleanup process with EnvName: $EnvName and Location: $Location" - - # Additional cleanup script if it exists - $cleanupScriptPath = ".\.configuration\powershell\cleanUp.ps1" - if (Test-Path $cleanupScriptPath) { - & $cleanupScriptPath $EnvName $Location - if ($LASTEXITCODE -ne 0) { - throw "Cleanup script failed." - } + + Write-Output "DevExp-DevBox Full Cleanup" + Write-Output "==========================" + Write-Output "" + + $success = Start-FullCleanup ` + -AppDisplayName $AppDisplayName ` + -GhSecretName $GhSecretName ` + -EnvName $EnvName ` + -Location $Location + + if ($success) { + Write-Output "" + Write-Output "All cleanup operations completed successfully." + } + else { + Write-Warning "Some cleanup operations failed. Check errors above." + exit 1 } - - Write-Output "All cleanup operations completed successfully." } catch { Write-Error "Script execution failed: $_" diff --git a/infra/settings/resourceOrganization/azureResources.schema.json b/infra/settings/resourceOrganization/azureResources.schema.json new file mode 100644 index 00000000..da25fc91 --- /dev/null +++ b/infra/settings/resourceOrganization/azureResources.schema.json @@ -0,0 +1,141 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/Evilazaro/DevExp-DevBox/infra/settings/resourceOrganization/azureResources.schema.json", + "title": "Azure Resources Configuration", + "description": "Schema for defining Azure resource group organization and configuration for the DevExp-DevBox accelerator. This schema validates the structure of resource groups used for workload, security, and monitoring purposes.", + "type": "object", + "additionalProperties": false, + "required": [ + "workload", + "security", + "monitoring" + ], + "$defs": { + "resourceGroup": { + "type": "object", + "description": "Configuration for an Azure resource group", + "additionalProperties": false, + "required": [ + "create", + "name", + "description", + "tags" + ], + "properties": { + "create": { + "type": "boolean", + "description": "Flag indicating whether the resource group should be created. Set to true to create a new resource group, or false to use an existing one.", + "default": true, + "examples": [ + true, + false + ] + }, + "name": { + "type": "string", + "description": "Name of the resource group. Must be unique within the subscription and follow Azure naming conventions.", + "minLength": 1, + "maxLength": 90, + "pattern": "^[a-zA-Z0-9._-]+$", + "examples": [ + "devexp-workload-rg", + "devexp-security-rg" + ] + }, + "description": { + "type": "string", + "description": "Human-readable description of the resource group's purpose and contents.", + "minLength": 1, + "examples": [ + "Resource group for DevCenter workload resources" + ] + }, + "tags": { + "$ref": "#/$defs/tags" + } + } + }, + "tags": { + "type": "object", + "description": "Azure resource tags for organization, cost management, and governance. Tags are key-value pairs that help categorize and manage resources.", + "additionalProperties": { + "type": "string", + "description": "Tag value - must be a string", + "maxLength": 256 + }, + "properties": { + "environment": { + "type": "string", + "description": "Deployment environment identifier", + "enum": [ + "dev", + "test", + "staging", + "prod" + ], + "examples": [ + "dev" + ] + }, + "division": { + "type": "string", + "description": "Organizational division responsible for the resource", + "examples": [ + "Platforms" + ] + }, + "team": { + "type": "string", + "description": "Team responsible for the resource", + "examples": [ + "DevExP" + ] + }, + "project": { + "type": "string", + "description": "Project name for cost allocation and tracking", + "examples": [ + "DevExP-DevBox" + ] + }, + "costCenter": { + "type": "string", + "description": "Cost center for financial tracking", + "examples": [ + "IT" + ] + }, + "owner": { + "type": "string", + "description": "Resource owner or responsible party", + "examples": [ + "Contoso" + ] + }, + "resources": { + "type": "string", + "description": "Type of resources contained in the group", + "examples": [ + "DevCenter", + "KeyVault", + "LogAnalytics" + ] + } + } + } + }, + "properties": { + "workload": { + "$ref": "#/$defs/resourceGroup", + "description": "Configuration for the workload resource group containing DevCenter and project resources" + }, + "security": { + "$ref": "#/$defs/resourceGroup", + "description": "Configuration for the security resource group containing Key Vault and security-related resources" + }, + "monitoring": { + "$ref": "#/$defs/resourceGroup", + "description": "Configuration for the monitoring resource group containing Log Analytics and diagnostic resources" + } + } +} \ No newline at end of file diff --git a/infra/settings/resourceOrganization/azureResources.shema.json b/infra/settings/resourceOrganization/azureResources.shema.json deleted file mode 100644 index 7fd47e3c..00000000 --- a/infra/settings/resourceOrganization/azureResources.shema.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Azure Resources Configuration", - "type": "object", - "properties": { - "workload": { - "type": "object", - "properties": { - "create": { - "type": "boolean", - "description": "Flag to indicate if the resource group should be created" - }, - "name": { - "type": "string", - "description": "Name of the resource group", - "default": "workload" - }, - "description": { - "type": "string", - "description": "Description of the resource group" - }, - "tags": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "The tags to automatically apply to deployed environments" - } - }, - "required": [ - "create", - "name", - "description", - "tags" - ] - }, - "security": { - "type": "object", - "properties": { - "create": { - "type": "boolean", - "description": "Flag to indicate if the resource group should be created" - }, - "name": { - "type": "string", - "description": "Name of the resource group", - "default": "security" - }, - "description": { - "type": "string", - "description": "Description of the resource group" - }, - "tags": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "The tags to automatically apply to deployed environments" - } - }, - "required": [ - "create", - "name", - "description", - "tags" - ] - }, - "monitoring": { - "type": "object", - "properties": { - "create": { - "type": "boolean", - "description": "Flag to indicate if the resource group should be created" - }, - "name": { - "type": "string", - "description": "Name of the resource group", - "default": "monitoring" - }, - "description": { - "type": "string", - "description": "Description of the resource group" - }, - "tags": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "The tags to automatically apply to deployed environments" - } - }, - "required": [ - "create", - "name", - "description", - "tags" - ] - } - }, - "required": [ - "workload", - "security", - "monitoring" - ] -} \ No newline at end of file diff --git a/infra/settings/resourceOrganization/azureResources.yaml b/infra/settings/resourceOrganization/azureResources.yaml index 26684176..97619863 100644 --- a/infra/settings/resourceOrganization/azureResources.yaml +++ b/infra/settings/resourceOrganization/azureResources.yaml @@ -1,3 +1,5 @@ +# yaml-language-server: $schema=./azureResources.schema.json +# # azureResources.yaml # ------------------------------------------------------- # Purpose: Defines resource group organization for Dev Box environments. @@ -7,47 +9,56 @@ # - Azure Landing Zones: https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/ # - Azure Resource Groups: https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-portal +# ============================================================================= # Workload Resource Group -workload: # Main application resources +# ============================================================================= +# Main application resources for Dev Box workloads +workload: create: true name: devexp-workload description: prodExp tags: - environment: dev # Deployment environment (dev, test, prod) - division: Platforms # Business division responsible for the resource - team: DevExP # Team owning the resource - project: Contoso-DevExp-DevBox # Project name - costCenter: IT # Financial allocation center - owner: Contoso # Resource owner - landingZone: Workload # Landing zone classification - resources: ResourceGroup # Resource type + environment: dev # Deployment environment (dev, test, prod) + division: Platforms # Business division responsible for the resource + team: DevExP # Team owning the resource + project: Contoso-DevExp-DevBox # Project name + costCenter: IT # Financial allocation center + owner: Contoso # Resource owner + landingZone: Workload # Landing zone classification + resources: ResourceGroup # Resource type +# ============================================================================= # Security Resource Group -security: # Security-related resources (Key Vaults, NSGs, Defender, etc.) +# ============================================================================= +# Security-related resources (Key Vaults, NSGs, Defender, etc.) +security: create: true name: devexp-security description: prodExp tags: - environment: dev # Deployment environment - division: Platforms # Business division - team: DevExP # Team - project: Contoso-DevExp-DevBox # Project name - costCenter: IT # Cost center - owner: Contoso # Owner - landingZone: Workload # Landing zone - resources: ResourceGroup # Resource type + environment: dev # Deployment environment + division: Platforms # Business division + team: DevExP # Team + project: Contoso-DevExp-DevBox # Project name + costCenter: IT # Cost center + owner: Contoso # Owner + landingZone: Workload # Landing zone + resources: ResourceGroup # Resource type +# ============================================================================= # Monitoring Resource Group -monitoring: # Monitoring and observability resources +# ============================================================================= +# Monitoring and observability resources +monitoring: create: true name: devexp-monitoring description: prodExp tags: - environment: dev # Deployment environment - division: Platforms # Business division - team: DevExP # Team - project: Contoso-DevExp-DevBox # Project name - costCenter: IT # Cost center - owner: Contoso # Owner - landingZone: Workload # Landing zone - resources: ResourceGroup # Resource type \ No newline at end of file + environment: dev # Deployment environment + division: Platforms # Business division + team: DevExP # Team + project: Contoso-DevExp-DevBox # Project name + costCenter: IT # Cost center + owner: Contoso # Owner + landingZone: Workload # Landing zone + resources: ResourceGroup # Resource type diff --git a/infra/settings/security/security.schema.json b/infra/settings/security/security.schema.json index d867a6cd..564998f0 100644 --- a/infra/settings/security/security.schema.json +++ b/infra/settings/security/security.schema.json @@ -1,88 +1,180 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/Evilazaro/DevExp-DevBox/infra/settings/security/security.schema.json", "title": "Azure Key Vault Security Configuration", - "description": "Schema for validating Azure Key Vault security configuration", + "description": "Schema for validating Azure Key Vault security configuration for the DevExp-DevBox accelerator. Defines settings for Key Vault creation, access policies, and security features.", "type": "object", - "required": ["create", "keyVault"], + "additionalProperties": false, + "required": [ + "create", + "keyVault" + ], + "$defs": { + "tags": { + "type": "object", + "description": "Azure resource tags for organization, cost management, and governance", + "required": [ + "environment" + ], + "additionalProperties": { + "type": "string", + "description": "Additional custom tag value", + "maxLength": 256 + }, + "properties": { + "environment": { + "type": "string", + "description": "Deployment environment identifier - required for all resources", + "enum": [ + "dev", + "test", + "staging", + "prod" + ], + "examples": [ + "dev" + ] + }, + "division": { + "type": "string", + "description": "Organizational division responsible for the resource", + "examples": [ + "Platforms" + ] + }, + "team": { + "type": "string", + "description": "Team responsible for managing the resource", + "examples": [ + "DevExP" + ] + }, + "project": { + "type": "string", + "description": "Project name for cost allocation and tracking", + "examples": [ + "DevExP-DevBox" + ] + }, + "costCenter": { + "type": "string", + "description": "Cost center for financial tracking and billing", + "examples": [ + "IT" + ] + }, + "owner": { + "type": "string", + "description": "Resource owner or responsible party contact", + "examples": [ + "Contoso" + ] + }, + "landingZone": { + "type": "string", + "description": "Landing zone identifier for enterprise-scale deployments", + "examples": [ + "corp" + ] + }, + "resources": { + "type": "string", + "description": "Type of resources contained or purpose", + "examples": [ + "KeyVault" + ] + } + } + } + }, "properties": { "create": { "type": "boolean", - "description": "Flag indicating whether to create the resource" + "description": "Flag indicating whether to create a new Key Vault. Set to true to create, or false to use an existing Key Vault.", + "default": true, + "examples": [ + true, + false + ] }, "keyVault": { "type": "object", - "required": ["name", "tags"], + "description": "Azure Key Vault instance configuration including security settings and access controls", + "additionalProperties": false, + "required": [ + "name", + "tags" + ], "properties": { "name": { "type": "string", - "description": "The name of the Azure Key Vault", + "description": "Name of the Azure Key Vault. Must be globally unique, 3-24 characters, alphanumeric and hyphens only.", "pattern": "^[a-zA-Z0-9-]{3,24}$", "minLength": 3, - "maxLength": 24 + "maxLength": 24, + "examples": [ + "devexp-keyvault", + "my-app-kv" + ] }, "description": { "type": "string", - "description": "Description of the Key Vault's purpose" + "description": "Human-readable description of the Key Vault's purpose and contents", + "examples": [ + "Key Vault for storing DevCenter secrets and credentials" + ] }, "secretName": { "type": "string", - "description": "Name of the secret to be managed" + "description": "Name of the primary secret to be managed in this Key Vault", + "pattern": "^[a-zA-Z0-9-]{1,127}$", + "minLength": 1, + "maxLength": 127, + "examples": [ + "github-pat", + "ado-token" + ] }, "enablePurgeProtection": { "type": "boolean", - "description": "Prevents purge of deleted key vault" + "description": "Enables purge protection to prevent permanent deletion of the Key Vault and its contents. Once enabled, cannot be disabled.", + "default": true, + "examples": [ + true + ] }, "enableSoftDelete": { "type": "boolean", - "description": "Enables temporary retention of deleted objects" + "description": "Enables soft delete to allow recovery of deleted Key Vault objects within the retention period", + "default": true, + "examples": [ + true + ] }, "softDeleteRetentionInDays": { "type": "integer", - "description": "Retention period for soft-deleted resources", + "description": "Number of days to retain soft-deleted Key Vault objects before permanent deletion", "minimum": 7, - "maximum": 90 + "maximum": 90, + "default": 90, + "examples": [ + 7, + 30, + 90 + ] }, "enableRbacAuthorization": { "type": "boolean", - "description": "Enables RBAC for authorization" + "description": "Enables Azure RBAC for Key Vault data plane authorization instead of access policies. Recommended for enterprise scenarios.", + "default": true, + "examples": [ + true + ] }, "tags": { - "type": "object", - "description": "Azure resource tags for organization and management", - "required": ["environment"], - "properties": { - "environment": { - "type": "string", - "description": "Deployment environment", - "enum": ["dev", "test", "staging", "prod"] - }, - "division": { - "type": "string" - }, - "team": { - "type": "string" - }, - "project": { - "type": "string" - }, - "costCenter": { - "type": "string" - }, - "owner": { - "type": "string" - }, - "landingZone": { - "type": "string" - }, - "resources": { - "type": "string" - } - }, - "additionalProperties": true + "$ref": "#/$defs/tags" } - }, - "additionalProperties": false + } } - }, - "additionalProperties": false + } } \ No newline at end of file diff --git a/infra/settings/security/security.yaml b/infra/settings/security/security.yaml index 38866eb1..c9c8ac06 100644 --- a/infra/settings/security/security.yaml +++ b/infra/settings/security/security.yaml @@ -1,10 +1,12 @@ - # yaml-language-server: $schema=./security.schema.json -# ------------------------------------------------------- -# Azure Key Vault Configuration -# ------------------------------------------------------- -# Purpose: Centralized management of secrets, keys, and certificates for secure access by applications and services in the Contoso development environment. -# This file defines the configuration for an Azure Key Vault resource used for storing sensitive credentials and secrets in the development environment. +# ============================================================================= +# File: security.yaml +# Purpose: Azure Key Vault Configuration for Dev Box Accelerator +# Description: Centralized management of secrets, keys, and certificates +# ============================================================================= +# +# This file defines the configuration for an Azure Key Vault resource used for +# storing sensitive credentials and secrets in the development environment. # # References: # - Microsoft Dev Box accelerator: https://evilazaro.github.io/DevExp-DevBox/docs/configureresources/security/ @@ -17,23 +19,23 @@ create: true # Key Vault configuration block keyVault: # Basic settings - name: contoso # Globally unique Key Vault name + name: contoso # Globally unique Key Vault name description: Development Environment Key Vault # Purpose of this Key Vault - secretName: gha-token # Name of the GitHub Actions token secret + secretName: gha-token # Name of the GitHub Actions token secret # Security settings - enablePurgeProtection: true # Prevent permanent deletion of secrets - enableSoftDelete: true # Allow recovery of deleted secrets within retention period - softDeleteRetentionInDays: 7 # Retention period for deleted secrets (7-90 days) - enableRbacAuthorization: true # Use Azure RBAC for access control + enablePurgeProtection: true # Prevent permanent deletion of secrets + enableSoftDelete: true # Allow recovery of deleted secrets within retention period + softDeleteRetentionInDays: 7 # Retention period for deleted secrets (7-90 days) + enableRbacAuthorization: true # Use Azure RBAC for access control # Resource organization tags tags: - environment: dev # Deployment environment (dev/test/staging/prod) - division: Platforms # Organizational division - team: DevExP # Owning team - project: Contoso-DevExp-DevBox # Associated project - costCenter: IT # Cost center for billing - owner: Contoso # Resource owner - landingZone: security # Azure landing zone classification - resources: ResourceGroup # Resource grouping identifier \ No newline at end of file + environment: dev # Deployment environment (dev/test/staging/prod) + division: Platforms # Organizational division + team: DevExP # Owning team + project: Contoso-DevExp-DevBox # Associated project + costCenter: IT # Cost center for billing + owner: Contoso # Resource owner + landingZone: security # Azure landing zone classification + resources: ResourceGroup # Resource grouping identifier diff --git a/infra/settings/workload/devcenter.schema.json b/infra/settings/workload/devcenter.schema.json index c425d883..88d0770d 100644 --- a/infra/settings/workload/devcenter.schema.json +++ b/infra/settings/workload/devcenter.schema.json @@ -1,433 +1,661 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Dev Center configuration schema", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/Evilazaro/DevExp-DevBox/infra/settings/workload/devcenter.schema.json", + "title": "Microsoft Dev Center Configuration", + "description": "Schema for validating Microsoft Dev Center configuration for the DevExp-DevBox accelerator. Defines the Dev Center resource, projects, catalogs, environment types, and associated identity/RBAC settings.", "type": "object", "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "minLength": 1 - }, - "catalogItemSyncEnableStatus": { + "required": [ + "name" + ], + "$defs": { + "guid": { "type": "string", - "enum": [ - "Enabled", - "Disabled" + "description": "A globally unique identifier (GUID) in standard format", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "examples": [ + "b24988ac-6180-42a0-ab88-20f7382dd24c" ] }, - "microsoftHostedNetworkEnableStatus": { + "enabledStatus": { "type": "string", + "description": "Feature toggle status", "enum": [ "Enabled", "Disabled" - ] + ], + "default": "Enabled" }, - "installAzureMonitorAgentEnableStatus": { + "roleAssignment": { + "type": "object", + "description": "Azure role assignment configuration for managed identity", + "additionalProperties": false, + "required": [ + "id", + "name", + "scope" + ], + "properties": { + "id": { + "$ref": "#/$defs/guid", + "description": "The GUID of the Azure RBAC role definition" + }, + "name": { + "type": "string", + "description": "Display name of the Azure RBAC role", + "minLength": 1, + "examples": [ + "Contributor", + "Reader", + "Key Vault Secrets User" + ] + }, + "scope": { + "type": "string", + "description": "Scope at which the role assignment applies", + "enum": [ + "Subscription", + "ResourceGroup", + "Project", + "Tenant", + "ManagementGroup" + ], + "examples": [ + "Subscription", + "ResourceGroup" + ] + } + } + }, + "rbacRole": { + "type": "object", + "description": "Azure RBAC role configuration for user/group assignments", + "additionalProperties": false, + "required": [ + "name", + "id" + ], + "properties": { + "name": { + "type": "string", + "description": "Display name of the Azure RBAC role", + "minLength": 1, + "examples": [ + "Dev Box User", + "Deployment Environment User" + ] + }, + "id": { + "$ref": "#/$defs/guid", + "description": "The GUID of the Azure RBAC role definition" + }, + "scope": { + "type": "string", + "description": "Scope at which the role assignment applies", + "examples": [ + "Project", + "ResourceGroup" + ] + } + } + }, + "tags": { + "type": "object", + "description": "Azure resource tags for organization, cost management, and governance", + "additionalProperties": false, + "properties": { + "environment": { + "type": "string", + "description": "Deployment environment identifier", + "examples": [ + "dev", + "staging", + "prod" + ] + }, + "division": { + "type": "string", + "description": "Organizational division responsible for the resource", + "examples": [ + "Platforms" + ] + }, + "team": { + "type": "string", + "description": "Team responsible for the resource", + "examples": [ + "DevExP" + ] + }, + "project": { + "type": "string", + "description": "Project name for cost allocation", + "examples": [ + "DevExP-DevBox" + ] + }, + "costCenter": { + "type": "string", + "description": "Cost center for financial tracking", + "examples": [ + "IT" + ] + }, + "owner": { + "type": "string", + "description": "Resource owner or responsible party", + "examples": [ + "Contoso" + ] + }, + "resources": { + "type": "string", + "description": "Type of resources contained", + "examples": [ + "DevCenter", + "Project", + "Network" + ] + } + } + }, + "catalog": { + "type": "object", + "description": "Catalog configuration for Dev Center or project-level catalogs", + "additionalProperties": false, + "required": [ + "name", + "type", + "uri" + ], + "properties": { + "name": { + "type": "string", + "description": "Unique name for the catalog within the Dev Center or project", + "minLength": 1, + "examples": [ + "environments", + "devboxImages" + ] + }, + "type": { + "type": "string", + "description": "Type of catalog content - environment definitions or image definitions", + "enum": [ + "gitHub", + "adoGit", + "environmentDefinition", + "imageDefinition" + ], + "examples": [ + "environmentDefinition" + ] + }, + "visibility": { + "type": "string", + "description": "Repository visibility - determines if authentication is required", + "enum": [ + "public", + "private" + ], + "default": "private", + "examples": [ + "private" + ] + }, + "uri": { + "type": "string", + "description": "URI of the Git repository containing catalog content", + "format": "uri", + "examples": [ + "https://github.com/Evilazaro/eShop.git" + ] + }, + "branch": { + "type": "string", + "description": "Git branch to sync catalog content from", + "default": "main", + "examples": [ + "main", + "develop" + ] + }, + "path": { + "type": "string", + "description": "Path within the repository to the catalog content folder", + "examples": [ + "/.devcenter/environments", + "/.devcenter/imageDefinitions" + ] + }, + "sourceControl": { + "type": "string", + "description": "Source control provider type", + "enum": [ + "gitHub", + "adoGit" + ], + "examples": [ + "gitHub" + ] + } + } + }, + "environmentType": { + "type": "object", + "description": "Environment type configuration for deployment targets", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the environment type (e.g., dev, staging, prod)", + "minLength": 1, + "examples": [ + "dev", + "staging", + "UAT", + "prod" + ] + }, + "deploymentTargetId": { + "type": "string", + "description": "Azure subscription ID for deployment target. Empty string uses the default subscription.", + "examples": [ + "", + "/subscriptions/00000000-0000-0000-0000-000000000000" + ] + } + } + }, + "cidrBlock": { "type": "string", - "enum": [ - "Enabled", - "Disabled" + "description": "CIDR notation IP address block", + "pattern": "^(?:\\d{1,3}\\.){3}\\d{1,3}\\/\\d{1,2}$", + "examples": [ + "10.0.0.0/16", + "10.0.1.0/24" ] }, - "identity": { + "subnet": { + "type": "object", + "description": "Virtual network subnet configuration", + "additionalProperties": false, + "required": [ + "name", + "properties" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the subnet", + "minLength": 1, + "examples": [ + "devbox-subnet" + ] + }, + "properties": { + "type": "object", + "description": "Subnet properties", + "additionalProperties": false, + "required": [ + "addressPrefix" + ], + "properties": { + "addressPrefix": { + "$ref": "#/$defs/cidrBlock", + "description": "Address prefix for the subnet in CIDR notation" + } + } + } + } + }, + "network": { + "type": "object", + "description": "Network configuration for Dev Center project connectivity", + "additionalProperties": false, + "required": [ + "name", + "create" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the virtual network", + "minLength": 1, + "examples": [ + "devbox-vnet" + ] + }, + "create": { + "type": "boolean", + "description": "Flag indicating whether to create a new virtual network or use existing", + "default": true + }, + "resourceGroupName": { + "type": "string", + "description": "Name of the resource group for network resources", + "examples": [ + "connectivity-rg" + ] + }, + "virtualNetworkType": { + "type": "string", + "description": "Type of virtual network - Managed (Microsoft-hosted) or Unmanaged (customer-managed)", + "enum": [ + "Managed", + "Unmanaged" + ], + "default": "Managed", + "examples": [ + "Managed" + ] + }, + "addressPrefixes": { + "type": "array", + "description": "Address space for the virtual network in CIDR notation", + "items": { + "$ref": "#/$defs/cidrBlock" + }, + "minItems": 1, + "examples": [ + [ + "10.0.0.0/16" + ] + ] + }, + "subnets": { + "type": "array", + "description": "Subnet configurations within the virtual network", + "items": { + "$ref": "#/$defs/subnet" + } + }, + "tags": { + "$ref": "#/$defs/tags" + } + } + }, + "pool": { + "type": "object", + "description": "Dev Box pool configuration for provisioning developer workstations", + "additionalProperties": false, + "required": [ + "name", + "imageDefinitionName" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the Dev Box pool", + "minLength": 1, + "examples": [ + "backend-engineer", + "frontend-engineer" + ] + }, + "imageDefinitionName": { + "type": "string", + "description": "Name of the image definition to use for Dev Boxes in this pool", + "minLength": 1, + "examples": [ + "eShop-backend-engineer" + ] + }, + "vmSku": { + "type": "string", + "description": "Azure VM SKU for Dev Boxes in this pool", + "examples": [ + "general_i_32c128gb512ssd_v2", + "general_i_16c64gb256ssd_v2" + ] + } + } + }, + "projectIdentity": { + "type": "object", + "description": "Managed identity and role assignment configuration for a project", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "description": "Type of managed identity", + "enum": [ + "SystemAssigned", + "UserAssigned", + "SystemAssignedUserAssigned", + "None" + ], + "default": "SystemAssigned", + "examples": [ + "SystemAssigned" + ] + }, + "roleAssignments": { + "type": "array", + "description": "Azure AD group role assignments for the project", + "items": { + "type": "object", + "description": "Role assignment for an Azure AD group", + "additionalProperties": false, + "required": [ + "azureADGroupId", + "azureADGroupName" + ], + "properties": { + "azureADGroupId": { + "$ref": "#/$defs/guid", + "description": "Object ID of the Azure AD group" + }, + "azureADGroupName": { + "type": "string", + "description": "Display name of the Azure AD group", + "minLength": 1, + "examples": [ + "eShop Developers" + ] + }, + "azureRBACRoles": { + "type": "array", + "description": "Azure RBAC roles to assign to the group", + "items": { + "$ref": "#/$defs/rbacRole" + } + } + } + } + } + } + }, + "project": { "type": "object", + "description": "Dev Center project configuration", "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "Unique name for the project within the Dev Center", + "minLength": 1, + "examples": [ + "eShop", + "ContosoApp" + ] + }, + "description": { + "type": "string", + "description": "Human-readable description of the project", + "examples": [ + "eShop project for e-commerce development" + ] + }, + "network": { + "$ref": "#/$defs/network" + }, + "identity": { + "$ref": "#/$defs/projectIdentity" + }, + "pools": { + "type": "array", + "description": "Dev Box pools available in this project", + "items": { + "$ref": "#/$defs/pool" + } + }, + "environmentTypes": { + "type": "array", + "description": "Environment types available in this project", + "items": { + "$ref": "#/$defs/environmentType" + } + }, + "catalogs": { + "type": "array", + "description": "Project-specific catalogs", + "items": { + "$ref": "#/$defs/catalog" + } + }, + "tags": { + "$ref": "#/$defs/tags" + } + } + }, + "devCenterIdentity": { + "type": "object", + "description": "Managed identity configuration for the Dev Center", + "additionalProperties": false, + "required": [ + "type" + ], "properties": { "type": { "type": "string", + "description": "Type of managed identity for the Dev Center", "enum": [ "SystemAssigned", "UserAssigned", "SystemAssignedUserAssigned", "None" + ], + "default": "SystemAssigned", + "examples": [ + "SystemAssigned" ] }, "roleAssignments": { "type": "object", + "description": "Role assignment configurations for the Dev Center identity", "additionalProperties": false, "properties": { "devCenter": { "type": "array", + "description": "Role assignments for the Dev Center managed identity", "items": { - "$ref": "#/definitions/roleAssignment" + "$ref": "#/$defs/roleAssignment" } }, "orgRoleTypes": { "type": "array", + "description": "Organization-level role assignments for Azure AD groups", "items": { "type": "object", + "description": "Organization role type configuration", "additionalProperties": false, + "required": [ + "type", + "azureADGroupId", + "azureADGroupName" + ], "properties": { "type": { - "type": "string" + "type": "string", + "description": "Type of organizational role", + "examples": [ + "DevManager", + "ProjectAdmin" + ] }, "azureADGroupId": { - "$ref": "#/definitions/guid" + "$ref": "#/$defs/guid", + "description": "Object ID of the Azure AD group" }, "azureADGroupName": { - "type": "string" + "type": "string", + "description": "Display name of the Azure AD group", + "minLength": 1 }, "azureRBACRoles": { "type": "array", + "description": "Azure RBAC roles to assign to the group", "items": { - "$ref": "#/definitions/rbacRole" + "$ref": "#/$defs/rbacRole" } } - }, - "required": [ - "type", - "azureADGroupId", - "azureADGroupName" - ] + } } } } } - }, - "required": [ - "type" + } + } + }, + "properties": { + "name": { + "type": "string", + "description": "Name of the Dev Center instance. Must be unique within the resource group.", + "minLength": 1, + "maxLength": 63, + "examples": [ + "devexp-devcenter", + "contoso-devcenter" ] }, + "catalogItemSyncEnableStatus": { + "$ref": "#/$defs/enabledStatus", + "description": "Enables automatic synchronization of catalog items from connected repositories" + }, + "microsoftHostedNetworkEnableStatus": { + "$ref": "#/$defs/enabledStatus", + "description": "Enables Microsoft-hosted network for Dev Boxes without requiring customer-managed virtual networks" + }, + "installAzureMonitorAgentEnableStatus": { + "$ref": "#/$defs/enabledStatus", + "description": "Enables automatic installation of Azure Monitor agent on Dev Boxes for monitoring and diagnostics" + }, + "identity": { + "$ref": "#/$defs/devCenterIdentity" + }, "catalogs": { "type": "array", + "description": "Dev Center-level catalogs containing environment and image definitions", "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "type": { - "type": "string" - }, - "visibility": { - "type": "string", - "enum": [ - "public", - "private" - ] - }, - "uri": { - "type": "string", - "format": "uri" - }, - "branch": { - "type": "string" - }, - "path": { - "type": "string" - }, - "sourceControl": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ] + "$ref": "#/$defs/catalog" } }, "environmentTypes": { "type": "array", + "description": "Environment types available at the Dev Center level", "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "deploymentTargetId": { - "type": "string" - } - }, - "required": [ - "name" - ] + "$ref": "#/$defs/environmentType" } }, "projects": { "type": "array", + "description": "Projects within the Dev Center, each representing a distinct team or workstream", "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "network": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "create": { - "type": "boolean" - }, - "resourceGroupName": { - "type": "string" - }, - "virtualNetworkType": { - "type": "string" - }, - "addressPrefixes": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?:\\d{1,3}\\.){3}\\d{1,3}\\/\\d{1,2}$" - } - }, - "subnets": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": false, - "properties": { - "addressPrefix": { - "type": "string", - "pattern": "^(?:\\d{1,3}\\.){3}\\d{1,3}\\/\\d{1,2}$" - } - }, - "required": [ - "addressPrefix" - ] - } - }, - "required": [ - "name", - "properties" - ] - } - }, - "tags": { - "type": "object", - "additionalProperties": false, - "properties": { - "environment": { - "type": "string" - }, - "division": { - "type": "string" - }, - "team": { - "type": "string" - }, - "project": { - "type": "string" - }, - "costCenter": { - "type": "string" - }, - "owner": { - "type": "string" - }, - "resources": { - "type": "string" - } - } - } - }, - "required": [ - "name", - "create" - ] - }, - "identity": { - "type": "object", - "additionalProperties": false, - "properties": { - "type": { - "type": "string" - }, - "roleAssignments": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "azureADGroupId": { - "$ref": "#/definitions/guid" - }, - "azureADGroupName": { - "type": "string" - }, - "azureRBACRoles": { - "type": "array", - "items": { - "$ref": "#/definitions/rbacRole" - } - } - }, - "required": [ - "azureADGroupId", - "azureADGroupName" - ] - } - } - }, - "required": [ - "type" - ] - }, - "pools": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "imageDefinitionName": { - "type": "string" - }, - "vmSku": { - "type": "string" - } - }, - "required": [ - "name", - "imageDefinitionName" - ] - } - }, - "environmentTypes": { - "type": "array", - "items": { - "$ref": "#/properties/environmentTypes/items" - } - }, - "catalogs": { - "type": "array", - "items": { - "$ref": "#/properties/catalogs/items" - } - }, - "tags": { - "type": "object", - "additionalProperties": false, - "properties": { - "environment": { - "type": "string" - }, - "division": { - "type": "string" - }, - "team": { - "type": "string" - }, - "project": { - "type": "string" - }, - "costCenter": { - "type": "string" - }, - "owner": { - "type": "string" - }, - "resources": { - "type": "string" - } - } - } - }, - "required": [ - "name" - ] + "$ref": "#/$defs/project" } }, "tags": { - "type": "object", - "additionalProperties": false, - "properties": { - "environment": { - "type": "string" - }, - "division": { - "type": "string" - }, - "team": { - "type": "string" - }, - "project": { - "type": "string" - }, - "costCenter": { - "type": "string" - }, - "owner": { - "type": "string" - }, - "resources": { - "type": "string" - } - } - } - }, - "required": [ - "name" - ], - "definitions": { - "guid": { - "type": "string", - "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" - }, - "roleAssignment": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "$ref": "#/definitions/guid" - }, - "name": { - "type": "string" - }, - "scope": { - "type": "string", - "enum": [ - "Subscription", - "ResourceGroup", - "Project", - "Tenant", - "ManagementGroup" - ] - } - }, - "required": [ - "id", - "name", - "scope" - ] - }, - "rbacRole": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "id": { - "$ref": "#/definitions/guid" - }, - "scope": { - "type": "string" - } - }, - "required": [ - "name", - "id" - ] + "$ref": "#/$defs/tags" } } } \ No newline at end of file diff --git a/infra/settings/workload/devcenter.yaml b/infra/settings/workload/devcenter.yaml index 10f27b63..b2f0d54a 100644 --- a/infra/settings/workload/devcenter.yaml +++ b/infra/settings/workload/devcenter.yaml @@ -1,9 +1,10 @@ # yaml-language-server: $schema=./devcenter.schema.json +# ============================================================================= +# File: devcenter.yaml +# Purpose: Dev Center Configuration for Microsoft Dev Box Accelerator +# Description: Defines the Dev Center resource, projects, catalogs, and environment types +# ============================================================================= # -# Microsoft Dev Box accelerator: Dev Center Configuration -# ====================================== -# -# Purpose: Defines the Dev Center resource and associated projects for Microsoft Dev Box accelerator. # This configuration establishes a centralized developer workstation platform with # role-specific configurations and appropriate access controls. # @@ -12,6 +13,9 @@ # - Dev Center documentation: https://learn.microsoft.com/en-us/azure/dev-box/overview-what-is-microsoft-dev-box # - Azure RBAC roles: https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles +# ============================================================================= +# Dev Center Core Settings +# ============================================================================= name: "devexp-devcenter" catalogItemSyncEnableStatus: "Enabled" microsoftHostedNetworkEnableStatus: "Enabled" @@ -87,35 +91,35 @@ projects: # Network configuration for eShop project network: - name: eShop # Name of the virtual network - create: true # Should the network be created? - resourceGroupName: "eShop-connectivity-RG" # Resource group for network - virtualNetworkType: Managed # Type of virtual network + name: eShop # Name of the virtual network + create: true # Should the network be created? + resourceGroupName: "eShop-connectivity-RG" # Resource group for network + virtualNetworkType: Managed # Type of virtual network addressPrefixes: - - 10.0.0.0/16 # Address space for VNet + - 10.0.0.0/16 # Address space for VNet subnets: - - name: eShop-subnet # Subnet name + - name: eShop-subnet # Subnet name properties: - addressPrefix: 10.0.1.0/24 # Subnet address range + addressPrefix: 10.0.1.0/24 # Subnet address range tags: - environment: dev # Deployment environment - division: Platforms # Organizational division - team: DevExP # Team responsible - project: DevExP-DevBox # Project name - costCenter: IT # Cost center for billing - owner: Contoso # Resource owner - resources: Network # Resource type identifier + environment: dev # Deployment environment + division: Platforms # Organizational division + team: DevExP # Team responsible + project: DevExP-DevBox # Project name + costCenter: IT # Cost center for billing + owner: Contoso # Resource owner + resources: Network # Resource type identifier # Project identity configuration - controls project-level security identity: - type: SystemAssigned # Managed identity type + type: SystemAssigned # Managed identity type roleAssignments: - azureADGroupId: "9d42a792-2d74-441d-8bcb-71009371725f" # Azure AD group ID - azureADGroupName: "eShop Developers" # Azure AD group name + azureADGroupName: "eShop Developers" # Azure AD group name azureRBACRoles: - - name: "Contributor" # RBAC role name + - name: "Contributor" # RBAC role name id: "b24988ac-6180-42a0-ab88-20f7382dd24c" # RBAC role ID - scope: Project # Role scope + scope: Project # Role scope - name: "Dev Box User" id: "45d50f46-0b78-4001-a660-4198cbe8cd05" scope: Project @@ -132,21 +136,21 @@ projects: # Dev Box pools - collections of Dev Boxes with specific configurations # Best practice: Create role-specific pools with appropriate tools and settings pools: - - name: "backend-engineer" # Pool for backend engineers + - name: "backend-engineer" # Pool for backend engineers imageDefinitionName: "eShop-backend-engineer" # Image definition for backend - vmSku: general_i_32c128gb512ssd_v2 # VM SKU for backend pool - - name: "frontend-engineer" # Pool for frontend engineers + vmSku: general_i_32c128gb512ssd_v2 # VM SKU for backend pool + - name: "frontend-engineer" # Pool for frontend engineers imageDefinitionName: "eShop-frontend-engineer" # Image definition for frontend - vmSku: general_i_16c64gb256ssd_v2 # VM SKU for frontend pool + vmSku: general_i_16c64gb256ssd_v2 # VM SKU for frontend pool # Project-specific environment types # Defines which deployment environments are available to the project environmentTypes: - - name: "dev" # Development environment + - name: "dev" # Development environment deploymentTargetId: "" - - name: "staging" # Staging environment + - name: "staging" # Staging environment deploymentTargetId: "" - - name: "UAT" # User Acceptance Testing environment + - name: "UAT" # User Acceptance Testing environment deploymentTargetId: "" # Project-specific catalogs - repositories containing project configurations diff --git a/package.json b/package.json index 12cbc815..df34ad71 100644 --- a/package.json +++ b/package.json @@ -1,53 +1,52 @@ { - "name": "docsy-example-site", - "version": "0.10.0", - "version.next": "0.10.1-dev.0-unreleased", - "description": "Example site that uses Docsy theme for technical documentation.", - "repository": "github:google/docsy-example", - "homepage": "https://example.docsy.dev", - "author": "Docsy Authors", - "license": "Apache-2.0", - "bugs": "https://github.com/google/docsy-example/issues", - "spelling": "cSpell:ignore docsy hugo htmltest precheck postbuild rtlcss -", - "scripts": { - "_build": "npm run _hugo-dev --", - "_check:links": "echo IMPLEMENTATION PENDING for check-links; echo", - "_hugo": "hugo --cleanDestinationDir", - "_hugo-dev": "npm run _hugo -- -e dev -DFE", - "_local": "npx cross-env HUGO_MODULE_WORKSPACE=docsy.work", - "_serve": "npm run _hugo-dev -- --minify serve --renderToMemory", - "build:preview": "npm run _hugo-dev -- --minify --baseURL \"${DEPLOY_PRIME_URL:-/}\"", - "build:production": "npm run _hugo -- --minify", - "build": "npm run _build -- ", - "check:links:all": "HTMLTEST_ARGS= npm run _check:links", - "check:links": "npm run _check:links", - "clean": "rm -Rf public/* resources", - "local": "npm run _local -- npm run", - "make:public": "git init -b main public", - "precheck:links:all": "npm run build", - "precheck:links": "npm run build", - "postbuild:preview": "npm run _check:links", - "postbuild:production": "npm run _check:links", - "serve": "npm run _serve", - "test": "npm run check:links", - "update:dep": "npm install --save-dev autoprefixer@latest postcss-cli@latest", - "update:hugo": "npm install --save-dev --save-exact hugo-extended@latest", - "update:pkgs": "npx npm-check-updates -u" - }, - "devDependencies": { - "autoprefixer": "^10.4.20", - "cross-env": "^7.0.3", - "hugo-extended": "0.136.2", - "postcss-cli": "^11.0.0", - "rtlcss": "^4.3.0" - }, - "optionalDependencies": { - "npm-check-updates": "^17.1.4" - }, - "private": true, - "prettier": { - "proseWrap": "always", - "singleQuote": true - } + "name": "docsy-example-site", + "version": "0.10.0", + "version.next": "0.10.1-dev.0-unreleased", + "description": "Example site that uses Docsy theme for technical documentation.", + "repository": "github:google/docsy-example", + "homepage": "https://example.docsy.dev", + "author": "Docsy Authors", + "license": "Apache-2.0", + "bugs": "https://github.com/google/docsy-example/issues", + "spelling": "cSpell:ignore docsy hugo htmltest precheck postbuild rtlcss -", + "scripts": { + "_build": "npm run _hugo-dev --", + "_check:links": "echo IMPLEMENTATION PENDING for check-links; echo", + "_hugo": "hugo --cleanDestinationDir", + "_hugo-dev": "npm run _hugo -- -e dev -DFE", + "_local": "npx cross-env HUGO_MODULE_WORKSPACE=docsy.work", + "_serve": "npm run _hugo-dev -- --minify serve --renderToMemory", + "build:preview": "npm run _hugo-dev -- --minify --baseURL \"${DEPLOY_PRIME_URL:-/}\"", + "build:production": "npm run _hugo -- --minify", + "build": "npm run _build -- ", + "check:links:all": "HTMLTEST_ARGS= npm run _check:links", + "check:links": "npm run _check:links", + "clean": "rm -Rf public/* resources", + "local": "npm run _local -- npm run", + "make:public": "git init -b main public", + "precheck:links:all": "npm run build", + "precheck:links": "npm run build", + "postbuild:preview": "npm run _check:links", + "postbuild:production": "npm run _check:links", + "serve": "npm run _serve", + "test": "npm run check:links", + "update:dep": "npm install --save-dev autoprefixer@latest postcss-cli@latest", + "update:hugo": "npm install --save-dev --save-exact hugo-extended@latest", + "update:pkgs": "npx npm-check-updates -u" + }, + "devDependencies": { + "autoprefixer": "^10.4.20", + "cross-env": "^7.0.3", + "hugo-extended": "0.136.2", + "postcss-cli": "^11.0.0", + "rtlcss": "^4.3.0" + }, + "optionalDependencies": { + "npm-check-updates": "^17.1.4" + }, + "private": true, + "prettier": { + "proseWrap": "always", + "singleQuote": true } - \ No newline at end of file +} \ No newline at end of file diff --git a/setUp.ps1 b/setUp.ps1 index 0d8a5f6e..181b21bd 100644 --- a/setUp.ps1 +++ b/setUp.ps1 @@ -77,8 +77,22 @@ $Script:AdoToken = "" function Write-LogMessage { <# .SYNOPSIS - Logging function with different levels and colors + Logging function with different levels and colors. + + .DESCRIPTION + Writes formatted log messages with timestamps and colored output + based on the message severity level. + + .PARAMETER Message + The message text to log. + + .PARAMETER Level + The severity level of the message. Valid values: Info, Warning, Error, Success. + + .EXAMPLE + Write-LogMessage -Message "Operation completed" -Level "Success" #> + [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Message, @@ -113,8 +127,23 @@ function Write-LogMessage { function Test-CommandAvailability { <# .SYNOPSIS - Check if a command is available in PATH + Check if a command is available in PATH. + + .DESCRIPTION + Verifies that the specified command or tool is available + for execution in the current environment. + + .PARAMETER Command + The name of the command to check. + + .OUTPUTS + System.Boolean - True if command exists, False otherwise. + + .EXAMPLE + if (Test-CommandAvailability -Command "az") { Write-Host "Azure CLI is available" } #> + [CmdletBinding()] + [OutputType([bool])] param( [Parameter(Mandatory = $true)] [string]$Command @@ -161,10 +190,25 @@ REQUIREMENTS: function Test-SourceControlValidation { <# .SYNOPSIS - Validate source control platform + Validates the source control platform parameter. + + .DESCRIPTION + Checks if the specified platform is a valid source control option. + + .PARAMETER Platform + The source control platform to validate (github, adogit, or empty). + + .OUTPUTS + System.Boolean - True if valid, False otherwise. + + .EXAMPLE + if (Test-SourceControlValidation -Platform "github") { Write-Host "Valid platform" } #> + [CmdletBinding()] + [OutputType([bool])] param( [Parameter(Mandatory = $true)] + [AllowEmptyString()] [string]$Platform ) @@ -185,8 +229,22 @@ function Test-SourceControlValidation { function Test-AzureAuthentication { <# .SYNOPSIS - Test Azure CLI authentication + Tests Azure CLI authentication status. + + .DESCRIPTION + Verifies that the user is logged into Azure CLI and the current + subscription is in an enabled state. + + .OUTPUTS + System.Boolean - True if authenticated with valid subscription, False otherwise. + + .EXAMPLE + if (Test-AzureAuthentication) { Write-Host "Ready to use Azure" } #> + [CmdletBinding()] + [OutputType([bool])] + param() + Write-LogMessage "Verifying Azure authentication..." "Info" try { @@ -215,8 +273,22 @@ function Test-AzureAuthentication { function Test-AdoAuthentication { <# .SYNOPSIS - Test Azure DevOps authentication + Tests Azure DevOps authentication status. + + .DESCRIPTION + Verifies that the Azure DevOps CLI extension is configured + and the user is authenticated. + + .OUTPUTS + System.Boolean - True if authenticated, False otherwise. + + .EXAMPLE + if (Test-AdoAuthentication) { Write-Host "Ready to use Azure DevOps" } #> + [CmdletBinding()] + [OutputType([bool])] + param() + Write-LogMessage "Verifying Azure DevOps authentication..." "Info" try { @@ -238,8 +310,21 @@ function Test-AdoAuthentication { function Test-GitHubAuthentication { <# .SYNOPSIS - Test GitHub CLI authentication + Tests GitHub CLI authentication status. + + .DESCRIPTION + Verifies that the user is authenticated with GitHub CLI. + + .OUTPUTS + System.Boolean - True if authenticated, False otherwise. + + .EXAMPLE + if (Test-GitHubAuthentication) { Write-Host "Ready to use GitHub" } #> + [CmdletBinding()] + [OutputType([bool])] + param() + Write-LogMessage "Verifying GitHub authentication..." "Info" try { @@ -261,8 +346,22 @@ function Test-GitHubAuthentication { function Get-SecureGitHubToken { <# .SYNOPSIS - Get GitHub token securely + Retrieves the GitHub token securely. + + .DESCRIPTION + Gets the GitHub authentication token either from an environment + variable or by using the GitHub CLI. + + .OUTPUTS + System.Boolean - True if token retrieved successfully, False otherwise. + + .EXAMPLE + if (Get-SecureGitHubToken) { Write-Host "Token available" } #> + [CmdletBinding()] + [OutputType([bool])] + param() + Write-LogMessage "Retrieving GitHub token..." "Info" # Check if KEY_VAULT_SECRET environment variable is already set @@ -299,8 +398,22 @@ function Get-SecureGitHubToken { function Get-SecureAdoGitToken { <# .SYNOPSIS - Get Azure DevOps token securely + Retrieves the Azure DevOps PAT securely. + + .DESCRIPTION + Gets the Azure DevOps Personal Access Token either from an + environment variable or by prompting the user securely. + + .OUTPUTS + System.Boolean - True if token retrieved successfully, False otherwise. + + .EXAMPLE + if (Get-SecureAdoGitToken) { Write-Host "Token available" } #> + [CmdletBinding()] + [OutputType([bool])] + param() + Write-LogMessage "Retrieving Azure DevOps token..." "Info" # Try to get PAT from environment variable first @@ -349,8 +462,22 @@ function Get-SecureAdoGitToken { function Initialize-AzdEnvironment { <# .SYNOPSIS - Initialize Azure Developer CLI environment + Initializes the Azure Developer CLI environment. + + .DESCRIPTION + Sets up the azd environment with appropriate tokens based on the + selected source control platform. + + .OUTPUTS + System.Boolean - True if initialization succeeded, False otherwise. + + .EXAMPLE + if (Initialize-AzdEnvironment) { Write-Host "Environment ready" } #> + [CmdletBinding()] + [OutputType([bool])] + param() + Write-LogMessage "Initializing Azure Developer CLI environment..." "Info" $pat = "" @@ -421,8 +548,22 @@ function Initialize-AzdEnvironment { function Start-AzureProvisioning { <# .SYNOPSIS - Start Azure resource provisioning + Starts Azure resource provisioning. + + .DESCRIPTION + Initiates the Azure Developer CLI provisioning process for + the configured environment. + + .OUTPUTS + System.Boolean - True if provisioning succeeded, False otherwise. + + .EXAMPLE + if (Start-AzureProvisioning) { Write-Host "Resources provisioned" } #> + [CmdletBinding()] + [OutputType([bool])] + param() + Write-LogMessage "Starting Azure resource provisioning with azd..." "Info" try { @@ -445,8 +586,18 @@ function Start-AzureProvisioning { function Select-SourceControlPlatform { <# .SYNOPSIS - Interactive source control platform selection + Provides interactive source control platform selection. + + .DESCRIPTION + Prompts the user to select their source control platform + when not specified via parameter. + + .EXAMPLE + Select-SourceControlPlatform #> + [CmdletBinding()] + param() + Write-LogMessage "Please select your source control platform:" "Info" Write-Host "" Write-Host " " -NoNewline @@ -484,8 +635,18 @@ function Select-SourceControlPlatform { function Test-Arguments { <# .SYNOPSIS - Validate and process command line arguments + Validates and processes command line arguments. + + .DESCRIPTION + Checks for help flag, validates required parameters, and prompts + for source control selection if not provided. + + .EXAMPLE + Test-Arguments #> + [CmdletBinding()] + param() + # Show help if requested if ($Help) { Show-Help @@ -513,8 +674,18 @@ function Test-Arguments { function Invoke-Main { <# .SYNOPSIS - Main execution function + Main execution function for the setup script. + + .DESCRIPTION + Orchestrates the complete setup process including argument validation, + tool verification, authentication checks, and environment initialization. + + .EXAMPLE + Invoke-Main #> + [CmdletBinding()] + param() + $requiredTools = @("az", "azd") # Process arguments diff --git a/setUp.sh b/setUp.sh index 99e175ca..da8e1506 100644 --- a/setUp.sh +++ b/setUp.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # setUp.sh - Sets up Azure Dev Box environment with GitHub integration # @@ -25,10 +25,16 @@ # - Azure CLI (az) # - Azure Developer CLI (azd) # - GitHub CLI (gh) [if using GitHub] +# - jq (JSON processor) # - Valid authentication for chosen platform +# +# EXIT CODES +# 0 - Success +# 1 - General error (missing dependencies, validation failure) +# 130 - Script interrupted by user (SIGINT/SIGTERM) # # Author: DevExp Team -# Last Updated: 2023-05-15 +# Last Updated: 2026-01-22 # Script Configuration set -euo pipefail # Exit on error, undefined vars, pipe failures @@ -61,7 +67,20 @@ ADO_TOKEN="" # Helper Functions ####################################### -# Logging function with different levels and colors +####################################### +# Outputs a formatted log message with timestamp and color coding. +# +# Arguments: +# $1 - message: The message to display +# $2 - level: Log level (Info, Warning, Error, Success). Default: Info +# +# Outputs: +# Writes formatted message to stdout (or stderr for Error level) +# +# Example: +# write_log_message "Starting process" "Info" +# write_log_message "Something went wrong" "Error" +####################################### write_log_message() { local message="$1" local level="${2:-Info}" @@ -84,7 +103,21 @@ write_log_message() { esac } -# Check if a command is available in PATH +####################################### +# Checks if a command is available in the system PATH. +# +# Arguments: +# $1 - command: The command name to check +# +# Returns: +# 0 - Command is available +# 1 - Command not found +# +# Example: +# if test_command_availability "az"; then +# echo "Azure CLI is installed" +# fi +####################################### test_command_availability() { local command="$1" @@ -95,7 +128,15 @@ test_command_availability() { return 0 } -# Show help message +####################################### +# Displays the help message with usage instructions. +# +# Arguments: +# None +# +# Outputs: +# Writes usage information to stdout +####################################### show_help() { cat << EOF setUp.sh - Sets up Azure Dev Box environment with source control integration @@ -120,7 +161,21 @@ REQUIREMENTS: EOF } -# Validate source control platform +####################################### +# Validates the source control platform parameter. +# +# Arguments: +# $1 - platform: The source control platform to validate +# +# Returns: +# 0 - Valid platform (github, adogit, or empty) +# 1 - Invalid platform +# +# Example: +# if validate_source_control "github"; then +# echo "Valid platform" +# fi +####################################### validate_source_control() { local platform="$1" @@ -137,7 +192,22 @@ validate_source_control() { # Authentication Functions ####################################### -# Test Azure CLI authentication +####################################### +# Verifies Azure CLI authentication status and subscription state. +# +# Globals: +# None +# +# Arguments: +# None +# +# Returns: +# 0 - Successfully authenticated with enabled subscription +# 1 - Not authenticated or subscription not enabled +# +# Outputs: +# Writes subscription details to stdout on success +####################################### test_azure_authentication() { local az_context @@ -166,7 +236,19 @@ test_azure_authentication() { return 0 } -# Test Azure DevOps authentication +####################################### +# Verifies Azure DevOps CLI authentication status. +# +# Globals: +# None +# +# Arguments: +# None +# +# Returns: +# 0 - Successfully authenticated +# 1 - Not authenticated +####################################### test_ado_authentication() { write_log_message "Verifying Azure DevOps authentication..." "Info" @@ -180,7 +262,19 @@ test_ado_authentication() { return 0 } -# Test GitHub CLI authentication +####################################### +# Verifies GitHub CLI authentication status. +# +# Globals: +# None +# +# Arguments: +# None +# +# Returns: +# 0 - Successfully authenticated +# 1 - Not authenticated +####################################### test_github_authentication() { write_log_message "Verifying GitHub authentication..." "Info" @@ -194,7 +288,20 @@ test_github_authentication() { return 0 } -# Get GitHub token securely +####################################### +# Retrieves GitHub token securely from environment or gh CLI. +# +# Globals: +# GITHUB_TOKEN - Set with the retrieved token +# KEY_VAULT_SECRET - Checked first, set if retrieved from gh CLI +# +# Arguments: +# None +# +# Returns: +# 0 - Token retrieved successfully +# 1 - Failed to retrieve token +####################################### get_secure_github_token() { write_log_message "Retrieving GitHub token..." "Info" @@ -221,7 +328,21 @@ get_secure_github_token() { return 0 } -# Get Azure DevOps token securely +####################################### +# Retrieves Azure DevOps PAT securely from environment or user input. +# +# Globals: +# ADO_TOKEN - Set with the retrieved token +# KEY_VAULT_SECRET - Checked first for existing token +# AZURE_DEVOPS_EXT_PAT - Exported with the token value +# +# Arguments: +# None +# +# Returns: +# 0 - Token retrieved successfully +# 1 - Failed to retrieve token +####################################### get_secure_ado_git_token() { write_log_message "Retrieving Azure DevOps token..." "Info" @@ -261,7 +382,25 @@ get_secure_ado_git_token() { # Azure Configuration Functions ####################################### -# Initialize Azure Developer CLI environment +####################################### +# Initializes the Azure Developer CLI environment with source control token. +# +# Creates the environment directory structure and stores the appropriate +# token based on the selected source control platform. +# +# Globals: +# ENV_NAME - Name of the environment to initialize +# SOURCE_CONTROL_PLATFORM - Selected platform (github or adogit) +# GITHUB_TOKEN - GitHub token (if using GitHub) +# ADO_TOKEN - Azure DevOps token (if using adogit) +# +# Arguments: +# None +# +# Returns: +# 0 - Environment initialized successfully +# 1 - Failed to initialize environment +####################################### initialize_azd_environment() { local pat token_type masked_token local env_dir env_file @@ -329,7 +468,19 @@ initialize_azd_environment() { return 0 } -# Start Azure resource provisioning +####################################### +# Starts Azure resource provisioning using Azure Developer CLI. +# +# Globals: +# ENV_NAME - Name of the environment to provision +# +# Arguments: +# None +# +# Returns: +# 0 - Provisioning completed successfully +# 1 - Provisioning failed +####################################### start_azure_provisioning() { write_log_message "Starting Azure resource provisioning with azd..." "Info" @@ -348,7 +499,18 @@ start_azure_provisioning() { return 0 } -# Interactive source control platform selection +####################################### +# Interactively prompts user to select source control platform. +# +# Globals: +# SOURCE_CONTROL_PLATFORM - Set with user's selection +# +# Arguments: +# None +# +# Returns: +# Always returns 0 (loops until valid selection) +####################################### select_source_control_platform() { local selection valid_selection=false @@ -384,7 +546,20 @@ select_source_control_platform() { # Main Script Logic ####################################### -# Parse command line arguments +####################################### +# Parses command line arguments and validates required parameters. +# +# Globals: +# ENV_NAME - Set from -e/--env-name argument +# SOURCE_CONTROL_PLATFORM - Set from -s/--source-control argument +# +# Arguments: +# $@ - Command line arguments passed to the script +# +# Returns: +# 0 - Arguments parsed successfully +# Exits with 1 on validation failure +####################################### parse_arguments() { while [[ $# -gt 0 ]]; do case $1 in @@ -426,7 +601,27 @@ parse_arguments() { fi } -# Main execution function +####################################### +# Main execution function - orchestrates the setup workflow. +# +# Performs the following steps: +# 1. Parse command line arguments +# 2. Verify required tools are installed +# 3. Verify Azure authentication +# 4. Verify source control authentication +# 5. Initialize azd environment +# +# Globals: +# ENV_NAME - Environment name from arguments +# SOURCE_CONTROL_PLATFORM - Platform from arguments +# +# Arguments: +# $@ - Command line arguments passed to the script +# +# Returns: +# 0 - Setup completed successfully +# 1 - Setup failed +####################################### main() { local required_tools=("az" "azd" "jq") local tool diff --git a/src/connectivity/connectivity.bicep b/src/connectivity/connectivity.bicep index 1b8d74b3..9e959e29 100644 --- a/src/connectivity/connectivity.bicep +++ b/src/connectivity/connectivity.bicep @@ -10,20 +10,24 @@ param logAnalyticsId string @description('Azure region for resource deployment') param location string -var netConectCreate = (projectNetwork.create && projectNetwork.virtualNetworkType == 'Unmanaged') || (!projectNetwork.create && projectNetwork.virtualNetworkType == 'Unmanaged') +@description('Determines if network connectivity resources should be created based on project network configuration') +var networkConnectivityCreate = (projectNetwork.create && projectNetwork.virtualNetworkType == 'Unmanaged') || (!projectNetwork.create && projectNetwork.virtualNetworkType == 'Unmanaged') -module Rg 'resourceGroup.bicep' = { +@description('Resource Group module for network connectivity resources') +module resourceGroupModule 'resourceGroup.bicep' = { scope: subscription() params: { name: projectNetwork.resourceGroupName location: location tags: projectNetwork.tags - create: netConectCreate + create: networkConnectivityCreate } } -var rgName = (netConectCreate) ? projectNetwork.resourceGroupName : resourceGroup().name +@description('Resource group name - uses project network resource group if creating new connectivity, otherwise uses current resource group') +var rgName = (networkConnectivityCreate) ? projectNetwork.resourceGroupName : resourceGroup().name +@description('Virtual Network module for project connectivity') module virtualNetwork 'vnet.bicep' = { scope: resourceGroup(rgName) params: { @@ -40,12 +44,12 @@ module virtualNetwork 'vnet.bicep' = { } } dependsOn: [ - Rg + resourceGroupModule ] } -@description('Network Connection resource for DevCenter') -module networkConnection './networkConnection.bicep' = if (netConectCreate) { +@description('Network Connection resource for DevCenter - creates attachment between DevCenter and virtual network') +module networkConnection './networkConnection.bicep' = if (networkConnectivityCreate) { scope: resourceGroup() params: { devCenterName: devCenterName @@ -54,8 +58,10 @@ module networkConnection './networkConnection.bicep' = if (netConectCreate) { } } -output networkConnectionName string = netConectCreate +@description('The name of the network connection - either newly created or from existing project network configuration') +output networkConnectionName string = networkConnectivityCreate ? networkConnection.?outputs.?networkConnectionName ?? projectNetwork.name : projectNetwork.name +@description('The type of virtual network (Managed or Unmanaged)') output networkType string = projectNetwork.virtualNetworkType diff --git a/src/connectivity/resourceGroup.bicep b/src/connectivity/resourceGroup.bicep index 23f207ff..7fd8ddd9 100644 --- a/src/connectivity/resourceGroup.bicep +++ b/src/connectivity/resourceGroup.bicep @@ -1,12 +1,15 @@ targetScope = 'subscription' +@description('Flag indicating whether to create a new resource group or reference an existing one') param create bool @description('Name of the resource group') param name string +@description('Azure region where the resource group will be located') param location string +@description('Tags to apply to the resource group') param tags object @description('Resource group name for new or existing resource group') @@ -21,4 +24,5 @@ resource existingRg 'Microsoft.Resources/resourceGroups@2025-04-01' existing = i name: name } +@description('The name of the resource group - either newly created or existing') output resourceGroupName string = create ? newRg.name : existingRg.name diff --git a/src/connectivity/vnet.bicep b/src/connectivity/vnet.bicep index 8e06d2e0..0e39dc56 100644 --- a/src/connectivity/vnet.bicep +++ b/src/connectivity/vnet.bicep @@ -81,6 +81,7 @@ resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-pr } } +@description('Virtual network configuration object containing name, resource group, network type, and subnet information') output AZURE_VIRTUAL_NETWORK object = (settings.create && settings.virtualNetworkType == 'Unmanaged') ? { name: virtualNetwork.?name ?? '' diff --git a/src/identity/orgRoleAssignment.bicep b/src/identity/orgRoleAssignment.bicep index 95607d71..04736b2d 100644 --- a/src/identity/orgRoleAssignment.bicep +++ b/src/identity/orgRoleAssignment.bicep @@ -2,7 +2,7 @@ param principalId string @description('Array of role definitions to assign to the principal') -param roles array +param roles AzureRBACRole[] @description('The principal type for the role assignments') @allowed([ @@ -14,16 +14,25 @@ param roles array ]) param principalType string = 'Group' +@description('Azure RBAC role definition for organization-level assignments') +type AzureRBACRole = { + @description('The GUID of the role definition') + id: string + + @description('Display name of the role') + name: string? +} + @description('Role assignments for the security group') resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ for role in roles: { - name: guid(subscription().id, resourceGroup().id, principalId, role.id, role.name, principalType) + name: guid(subscription().id, resourceGroup().id, principalId, role.id, role.?name ?? '', principalType) scope: resourceGroup() properties: { principalId: principalId roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', role.id) principalType: principalType - description: contains(role, 'name') ? 'Role: ${role.name!}' : 'Role assignment for ${principalId}' + description: role.?name != null ? 'Role: ${role.name!}' : 'Role assignment for ${principalId}' } } ] @@ -32,7 +41,7 @@ resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ output roleAssignmentIds array = [ for (role, i) in roles: { roleId: role.id - roleName: contains(role, 'name') ? role.name! : role.id + roleName: role.?name ?? role.id assignmentId: roleAssignment[i].id } ] diff --git a/src/identity/projectIdentityRoleAssignment.bicep b/src/identity/projectIdentityRoleAssignment.bicep index 6f88fc3d..b7777f8e 100644 --- a/src/identity/projectIdentityRoleAssignment.bicep +++ b/src/identity/projectIdentityRoleAssignment.bicep @@ -5,7 +5,7 @@ param projectName string param principalId string @description('Array of role definitions to assign to the principal') -param roles array +param roles AzureRBACRole[] @description('The principal type for the role assignments') @allowed([ @@ -17,6 +17,18 @@ param roles array ]) param principalType string +@description('Azure RBAC role definition') +type AzureRBACRole = { + @description('The GUID of the role definition') + id: string + + @description('Display name of the role') + name: string? + + @description('Scope at which the role should be assigned') + scope: string +} + @description('Reference to the existing DevCenter project') resource project 'Microsoft.DevCenter/projects@2025-10-01-preview' existing = { name: projectName @@ -31,7 +43,7 @@ resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ principalId: principalId roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', role.id) principalType: principalType - description: contains(role, 'name') + description: role.?name != null ? 'Role: ${role.name!} for project ${projectName}' : 'Role assignment for ${principalId}' } @@ -42,7 +54,7 @@ resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ output roleAssignmentIds array = [ for (role, i) in roles: { roleId: role.id - roleName: contains(role, 'name') ? role.name! : role.id + roleName: role.?name ?? role.id assignmentId: roleAssignment[i].id } ] diff --git a/src/identity/projectIdentityRoleAssignmentRG.bicep b/src/identity/projectIdentityRoleAssignmentRG.bicep index 633f5688..b586a7ae 100644 --- a/src/identity/projectIdentityRoleAssignmentRG.bicep +++ b/src/identity/projectIdentityRoleAssignmentRG.bicep @@ -5,7 +5,7 @@ param projectName string param principalId string @description('Array of role definitions to assign to the principal') -param roles array +param roles AzureRBACRole[] @description('The principal type for the role assignments') @allowed([ @@ -17,6 +17,18 @@ param roles array ]) param principalType string +@description('Azure RBAC role definition') +type AzureRBACRole = { + @description('The GUID of the role definition') + id: string + + @description('Display name of the role') + name: string? + + @description('Scope at which the role should be assigned') + scope: string +} + @description('Reference to the existing DevCenter project') resource project 'Microsoft.DevCenter/projects@2025-10-01-preview' existing = { name: projectName @@ -31,7 +43,7 @@ resource roleAssignmentRG 'Microsoft.Authorization/roleAssignments@2022-04-01' = principalId: principalId roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', role.id) principalType: principalType - description: contains(role, 'name') + description: role.?name != null ? 'Role: ${role.name!} for project ${projectName}' : 'Role assignment for ${principalId}' } @@ -42,7 +54,7 @@ resource roleAssignmentRG 'Microsoft.Authorization/roleAssignments@2022-04-01' = output roleAssignmentIds array = [ for (role, i) in roles: { roleId: role.id - roleName: contains(role, 'name') ? role.name! : role.id + roleName: role.?name ?? role.id assignmentId: roleAssignmentRG[i].id } ] diff --git a/src/management/logAnalytics.bicep b/src/management/logAnalytics.bicep index fba6e176..d1c61b21 100644 --- a/src/management/logAnalytics.bicep +++ b/src/management/logAnalytics.bicep @@ -7,7 +7,13 @@ param name string param location string = resourceGroup().location @description('Tags to apply to the Log Analytics Workspace') -param tags object = {} +param tags Tags = {} + +@description('Tags type for resource tagging') +type Tags = { + @description('Wildcard property for any tag key-value pairs') + *: string +} @description('The SKU of the Log Analytics Workspace') @allowed([ diff --git a/src/security/keyVault.bicep b/src/security/keyVault.bicep index e90e5f8d..c3baf608 100644 --- a/src/security/keyVault.bicep +++ b/src/security/keyVault.bicep @@ -1,15 +1,45 @@ -@description('Key Vault Settings') -param keyvaultSettings object +@description('Key Vault configuration settings') +param keyvaultSettings KeyVaultSettings @description('Key Vault Location') param location string = resourceGroup().location @description('Key Vault Tags') -param tags object +param tags Tags @description('Unique string for resource naming') param unique string = uniqueString(resourceGroup().id, location, subscription().subscriptionId, deployer().tenantId) +@description('Key Vault configuration type') +type KeyVaultSettings = { + @description('Key Vault instance configuration') + keyVault: KeyVaultConfig +} + +@description('Key Vault instance configuration type') +type KeyVaultConfig = { + @description('Name of the Key Vault') + name: string + + @description('Flag to enable purge protection') + enablePurgeProtection: bool + + @description('Flag to enable soft delete') + enableSoftDelete: bool + + @description('Number of days to retain deleted vaults') + softDeleteRetentionInDays: int + + @description('Flag to enable RBAC authorization') + enableRbacAuthorization: bool +} + +@description('Tags to apply to resources') +type Tags = { + @description('Wildcard property for any tag key-value pairs') + *: string +} + @description('Azure Key Vault') resource keyVault 'Microsoft.KeyVault/vaults@2025-05-01' = { name: '${keyvaultSettings.keyVault.name}-${unique}-kv' diff --git a/src/security/security.bicep b/src/security/security.bicep index 7239012a..366dc31f 100644 --- a/src/security/security.bicep +++ b/src/security/security.bicep @@ -1,5 +1,5 @@ @description('Key Vault Tags') -param tags object +param tags Tags @description('Secret Value') @secure() @@ -8,6 +8,12 @@ param secretValue string @description('Log Analytics Workspace ID') param logAnalyticsId string +@description('Tags type for resource tagging') +type Tags = { + @description('Wildcard property for any tag key-value pairs') + *: string +} + @description('Azure Key Vault Configuration') var securitySettings = loadYamlContent('../../infra/settings/security/security.yaml') @@ -36,10 +42,14 @@ module secret 'secret.bicep' = { } @description('The name of the Key Vault') -output AZURE_KEY_VAULT_NAME string = (securitySettings.create ? keyVault.?outputs.?AZURE_KEY_VAULT_NAME : existingKeyVault.?name) ?? '' +output AZURE_KEY_VAULT_NAME string = (securitySettings.create + ? keyVault.?outputs.?AZURE_KEY_VAULT_NAME + : existingKeyVault.?name) ?? '' @description('The identifier of the secret') output AZURE_KEY_VAULT_SECRET_IDENTIFIER string = secret.outputs.AZURE_KEY_VAULT_SECRET_IDENTIFIER @description('The endpoint URI of the Key Vault') -output AZURE_KEY_VAULT_ENDPOINT string = (securitySettings.create ? keyVault.?outputs.?AZURE_KEY_VAULT_ENDPOINT : existingKeyVault.?properties.?vaultUri) ?? '' +output AZURE_KEY_VAULT_ENDPOINT string = (securitySettings.create + ? keyVault.?outputs.?AZURE_KEY_VAULT_ENDPOINT + : existingKeyVault.?properties.?vaultUri) ?? '' diff --git a/src/workload/core/devCenter.bicep b/src/workload/core/devCenter.bicep index 755f1831..6941cd92 100644 --- a/src/workload/core/devCenter.bicep +++ b/src/workload/core/devCenter.bicep @@ -1,16 +1,19 @@ // Common variables for reuse +@description('Name of the DevCenter instance from configuration') var devCenterName = config.name + +@description('Principal ID of the DevCenter managed identity') var devCenterPrincipalId = devcenter.identity.principalId // Parameters with improved metadata and validation @description('DevCenter configuration including identity and settings') param config DevCenterConfig -@description('Dev Center Catalogs') -param catalogs array +@description('Array of catalog configurations for the DevCenter') +param catalogs Catalog[] -@description('Environment Types') -param environmentTypes array +@description('Array of environment type configurations for the DevCenter') +param environmentTypes EnvironmentTypeConfig[] @description('Log Analytics Workspace Id') @minLength(1) @@ -20,6 +23,7 @@ param logAnalyticsId string @secure() param secretIdentifier string +@description('Name of the resource group containing security resources') param securityResourceGroupName string @description('Azure region for resource deployment') @@ -28,19 +32,53 @@ param location string = resourceGroup().location // Type definitions with proper naming conventions @description('DevCenter configuration type') type DevCenterConfig = { + @description('Name of the DevCenter instance') name: string + + @description('Managed identity configuration for the DevCenter') identity: Identity + + @description('Status for catalog item sync feature') catalogItemSyncEnableStatus: Status + + @description('Status for Microsoft hosted network feature') microsoftHostedNetworkEnableStatus: Status + + @description('Status for Azure Monitor agent installation feature') installAzureMonitorAgentEnableStatus: Status - tags: object + + @description('Tags to apply to the DevCenter') + tags: Tags +} + +@description('Tags type for resource tagging') +type Tags = { + @description('Wildcard property for any tag key-value pairs') + *: string } +@description('Virtual network configuration type') type VirtualNetwork = { + @description('Name of the virtual network') name: string + + @description('Name of the resource group containing the virtual network') resourceGroupName: string + + @description('Type of virtual network') virtualNetworkType: string - subnets: object[] + + @description('Subnet configurations for the virtual network') + subnets: VirtualNetworkSubnet[] +} + +@description('Subnet configuration for virtual networks') +type VirtualNetworkSubnet = { + @description('Name of the subnet') + name: string + + @description('Address prefix for the subnet') + addressPrefix: string } @description('Status type for feature toggles') @@ -48,31 +86,76 @@ type Status = 'Enabled' | 'Disabled' @description('Identity configuration type') type Identity = { + @description('Type of managed identity (SystemAssigned, UserAssigned, or SystemAssigned,UserAssigned)') type: string + + @description('Role assignment configuration for the identity') roleAssignments: RoleAssignment } @description('Role assignment configuration') type RoleAssignment = { + @description('Role assignments scoped to the DevCenter') devCenter: AzureRBACRole[] + + @description('Organization-level role type configurations') orgRoleTypes: OrgRoleType[] } @description('Azure RBAC role definition') type AzureRBACRole = { + @description('The GUID of the role definition') id: string + + @description('Display name of the role') name: string + + @description('Scope at which the role should be assigned') scope: string } @description('Organization role type configuration') type OrgRoleType = { + @description('Type of organization role') type: string + + @description('Azure AD group object ID') azureADGroupId: string + + @description('Azure AD group display name') azureADGroupName: string + + @description('Array of Azure RBAC roles to assign') azureRBACRoles: AzureRBACRole[] } +@description('Catalog configuration for DevCenter') +type Catalog = { + @description('Name of the catalog') + name: string + + @description('Type of repository (GitHub or Azure DevOps Git)') + type: 'gitHub' | 'adoGit' + + @description('Visibility of the catalog') + visibility: 'public' | 'private' + + @description('URI of the repository') + uri: string + + @description('Branch to sync from') + branch: string + + @description('Path within the repository to sync') + path: string +} + +@description('Environment type configuration') +type EnvironmentTypeConfig = { + @description('Name of the environment type') + name: string +} + // Main DevCenter resource @description('Dev Center Resource') resource devcenter 'Microsoft.DevCenter/devcenters@2025-10-01-preview' = { diff --git a/src/workload/project/project.bicep b/src/workload/project/project.bicep index a13326bb..c53e1c83 100644 --- a/src/workload/project/project.bicep +++ b/src/workload/project/project.bicep @@ -12,16 +12,16 @@ param logAnalyticsId string param projectDescription string @description('Catalog configuration for the project') -param catalogs object[] +param catalogs ProjectCatalog[] @description('Environment types to be associated with the project') -param projectEnvironmentTypes array +param projectEnvironmentTypes ProjectEnvironmentTypeConfig[] @description('DevBox pool configurations for the project') -param projectPools array +param projectPools PoolConfig[] @description('Network connection name for the project') -param projectNetwork object +param projectNetwork ProjectNetwork @description('Secret identifier for Git repository authentication') @secure() @@ -34,11 +34,53 @@ param securityResourceGroupName string param identity Identity @description('Tags to be applied to all resources') -param tags object = {} +param tags Tags = {} @description('Azure region for resource deployment') param location string = resourceGroup().location +@description('Tags type for resource tagging') +type Tags = { + @description('Wildcard property for any tag key-value pairs') + *: string +} + +@description('Network configuration for the project') +type ProjectNetwork = { + @description('Name of the virtual network') + name: string? + + @description('Flag indicating whether to create the network') + create: bool? + + @description('Name of the resource group containing the network') + resourceGroupName: string? + + @description('Type of virtual network (Managed or Unmanaged)') + virtualNetworkType: string + + @description('Address prefixes for the virtual network') + addressPrefixes: string[]? + + @description('Subnet configurations') + subnets: Subnet[]? +} + +@description('Subnet configuration') +type Subnet = { + @description('Name of the subnet') + name: string + + @description('Subnet properties') + properties: SubnetProperties? +} + +@description('Subnet properties configuration') +type SubnetProperties = { + @description('Address prefix for the subnet') + addressPrefix: string +} + @description('Identity configuration for the project') type Identity = { @description('Type of managed identity (SystemAssigned or UserAssigned)') @@ -69,6 +111,51 @@ type RoleAssignment = { azureRBACRoles: AzureRBACRole[] } +@description('Project catalog configuration') +type ProjectCatalog = { + @description('Name of the catalog') + name: string + + @description('Type of catalog (environment or image)') + type: 'environmentDefinition' | 'imageDefinition' + + @description('Source control type') + sourceControl: 'gitHub' | 'adoGit' + + @description('Visibility of the catalog') + visibility: 'public' | 'private' + + @description('URI of the repository') + uri: string + + @description('Branch to sync from') + branch: string + + @description('Path within the repository to sync') + path: string +} + +@description('Project environment type configuration') +type ProjectEnvironmentTypeConfig = { + @description('Name of the environment type') + name: string + + @description('Resource ID of the deployment target subscription') + deploymentTargetId: string +} + +@description('Pool configuration for DevBox pools') +type PoolConfig = { + @description('Name of the pool') + name: string + + @description('Name of the image definition to use') + imageDefinitionName: string + + @description('VM SKU for the pool') + vmSku: string +} + @description('Reference to existing DevCenter') resource devCenter 'Microsoft.DevCenter/devcenters@2025-10-01-preview' existing = { name: devCenterName diff --git a/src/workload/project/projectCatalog.bicep b/src/workload/project/projectCatalog.bicep index 1f0bd59d..079bffd2 100644 --- a/src/workload/project/projectCatalog.bicep +++ b/src/workload/project/projectCatalog.bicep @@ -61,3 +61,9 @@ resource catalog 'Microsoft.DevCenter/projects/catalogs@2025-10-01-preview' = { : null } } + +@description('The name of the created project catalog') +output catalogName string = catalog.name + +@description('The resource ID of the created project catalog') +output catalogId string = catalog.id diff --git a/src/workload/project/projectEnvironmentType.bicep b/src/workload/project/projectEnvironmentType.bicep index e1d198ea..091be92a 100644 --- a/src/workload/project/projectEnvironmentType.bicep +++ b/src/workload/project/projectEnvironmentType.bicep @@ -8,10 +8,14 @@ param location string = resourceGroup().location param environmentConfig ProjectEnvironmentType type ProjectEnvironmentType = { + @description('Name of the environment type') name: string + + @description('Resource ID of the subscription for deployment target') deploymentTargetId: string } +@description('Default role assignments for environment type creators - Contributor role') var roles = [ { id: 'b24988ac-6180-42a0-ab88-20f7382dd24c' @@ -19,12 +23,12 @@ var roles = [ } ] -@description('Project') +@description('Reference to the existing DevCenter project') resource project 'Microsoft.DevCenter/projects@2025-10-01-preview' existing = { name: projectName } -@description('Dev Center Environments') +@description('Project Environment Type resource for deployment environments') resource environmentType 'Microsoft.DevCenter/projects/environmentTypes@2025-10-01-preview' = { name: environmentConfig.name location: location diff --git a/src/workload/project/projectPool.bicep b/src/workload/project/projectPool.bicep index d2c4330e..13b2f5b6 100644 --- a/src/workload/project/projectPool.bicep +++ b/src/workload/project/projectPool.bicep @@ -46,12 +46,12 @@ type Catalog = { path: string } -@description('Project') +@description('Reference to the existing DevCenter project') resource project 'Microsoft.DevCenter/projects@2025-10-01-preview' existing = { name: projectName } -@description('Dev Box Pool resource') +@description('DevBox Pool resources - creates pools for image definition catalogs') resource pool 'Microsoft.DevCenter/projects/pools@2025-10-01-preview' = [ for (catalog, i) in catalogs: if (catalog.type == 'imageDefinition') { name: '${name}-${i}-pool' @@ -78,3 +78,6 @@ resource pool 'Microsoft.DevCenter/projects/pools@2025-10-01-preview' = [ } } ] + +@description('Array of created pool names') +output poolNames array = [for (catalog, i) in catalogs: catalog.type == 'imageDefinition' ? pool[i].name : null] diff --git a/src/workload/workload.bicep b/src/workload/workload.bicep index 6a56d9b3..d35d767d 100644 --- a/src/workload/workload.bicep +++ b/src/workload/workload.bicep @@ -23,9 +23,20 @@ param location string = resourceGroup().location // Resource types with documentation @description('Landing Zone configuration type') type LandingZone = { + @description('Name of the landing zone') name: string + + @description('Flag indicating whether to create the landing zone') create: bool - tags: object + + @description('Tags to apply to the landing zone resources') + tags: Tags +} + +@description('Tags type for resource tagging') +type Tags = { + @description('Wildcard property for any tag key-value pairs') + *: string } // Variables with clear naming