content/src/scripts/Invoke-PlatformBootstrap.ps1
|
# --------------------------------------------------------------------------- # Invoke-PlatformBootstrap.ps1 # Axeon Core Engine — Data-Driven Azure Landing Zone Orchestrator # # Reads platform-spec.json, validates it, authenticates to Azure, # and orchestrates modular Bicep deployments. # # Usage: # ./Invoke-PlatformBootstrap.ps1 -ConfigPath "./platform-spec.json" # ./Invoke-PlatformBootstrap.ps1 -ConfigPath "./platform-spec.json" -WhatIf # --------------------------------------------------------------------------- #Requires -Version 7.0 #Requires -Modules Az.Accounts, Az.Resources [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Leaf })] [string]$ConfigPath, [string]$BicepRoot, [string]$SchemaPath ) $ErrorActionPreference = "Stop" $InformationPreference = "Continue" # Dot-source the naming convention engine . (Join-Path $PSScriptRoot "Resolve-ResourceName.ps1") # ============================================================================ # HELPER FUNCTIONS # ============================================================================ function Write-Banner { param([string]$Title) $sep = "=" * 65 Write-Host "" Write-Host $sep -ForegroundColor DarkCyan Write-Host " $Title" -ForegroundColor Cyan Write-Host $sep -ForegroundColor DarkCyan Write-Host "" } function Write-StepHeader { param([int]$Step, [string]$Title) Write-Host "[$Step] $Title" -ForegroundColor Yellow Write-Host ("-" * 50) -ForegroundColor DarkGray } function Write-Success { param([string]$Message) Write-Host " [OK] $Message" -ForegroundColor Green } function Write-Detail { param([string]$Message) Write-Host " $Message" -ForegroundColor Gray } # ============================================================================ # STEP 1 — Load & Validate Configuration # ============================================================================ function Read-PlatformSpec { param([string]$Path) Write-StepHeader -Step 1 -Title "Loading platform specification" Write-Detail "Config: $Path" $raw = Get-Content $Path -Raw $spec = $raw | ConvertFrom-Json -AsHashtable # Required top-level fields $requiredFields = @('platformId', 'environment', 'location', 'naming') foreach ($field in $requiredFields) { if (-not $spec.ContainsKey($field) -or (-not ($spec[$field] -is [hashtable]) -and [string]::IsNullOrWhiteSpace($spec[$field]))) { throw "platform-spec.json is missing required field: '$field'" } } # Defaults if (-not $spec.ContainsKey('testMode')) { $spec['testMode'] = $false } # Validate naming configuration Write-Detail "Validating naming convention..." $namingValid = Test-NamingConfig -NamingConfig $spec.naming if (-not $namingValid) { throw "Naming configuration is invalid. See warnings above." } Write-Success "Naming convention validated" # Resolve all resource names and attach to spec $spec['_resolvedNames'] = Resolve-AllResourceNames -NamingConfig $spec.naming -Index 0 Write-Success "Configuration loaded" Write-Detail "Platform ID : $($spec.platformId)" Write-Detail "Environment : $($spec.environment)" Write-Detail "Location : $($spec.location)" Write-Detail "Test Mode : $($spec.testMode)" Write-Host "" # Show naming preview Show-NamingPreview -NamingConfig $spec.naming -Index 0 return $spec } function Test-PlatformSpec { param( [hashtable]$Spec, [string]$SchemaFile ) if (-not $SchemaFile -or -not (Test-Path $SchemaFile)) { Write-Detail "Schema validation skipped (no schema found)" return } Write-Detail "Validating against schema: $SchemaFile" # Basic structural validation — a full JSON Schema validator (e.g. # NJsonSchema) can be plugged in here. For now we validate key types. $schema = Get-Content $SchemaFile -Raw | ConvertFrom-Json -AsHashtable if ($schema.ContainsKey('required')) { foreach ($field in $schema.required) { if (-not $Spec.ContainsKey($field)) { throw "Schema validation failed: missing required field '$field'" } } } Write-Success "Schema validation passed" Write-Host "" } # ============================================================================ # STEP 2 — Azure Authentication # ============================================================================ function Connect-AzureContext { param([hashtable]$Spec) Write-StepHeader -Step 2 -Title "Authenticating to Azure" # Check if already connected $context = Get-AzContext -ErrorAction SilentlyContinue if ($context) { Write-Success "Already authenticated as: $($context.Account.Id)" Write-Detail "Subscription : $($context.Subscription.Name) ($($context.Subscription.Id))" Write-Host "" return $context } # Try Managed Identity / Workload Identity (CI environments) try { Write-Detail "Attempting Workload Identity / Managed Identity..." Connect-AzAccount -Identity -ErrorAction Stop | Out-Null $context = Get-AzContext Write-Success "Authenticated via Managed Identity" } catch { # Fall back to interactive login for local development Write-Detail "Managed Identity not available. Falling back to interactive login..." Connect-AzAccount -ErrorAction Stop | Out-Null $context = Get-AzContext Write-Success "Authenticated interactively as: $($context.Account.Id)" } Write-Detail "Subscription : $($context.Subscription.Name) ($($context.Subscription.Id))" Write-Host "" return $context } # ============================================================================ # STEP 3 — Resource Group Provisioning # ============================================================================ function Ensure-ResourceGroup { param( [hashtable]$Spec, [switch]$TestMode ) Write-StepHeader -Step 3 -Title "Ensuring resource group" $rgName = $Spec._resolvedNames.resourceGroup $location = $Spec.location Write-Detail "Resource Group : $rgName" Write-Detail "Location : $location" if ($TestMode) { Write-Detail "[Test Mode] Would create/ensure resource group '$rgName' in '$location'" Write-Host "" return $rgName } $existing = Get-AzResourceGroup -Name $rgName -ErrorAction SilentlyContinue if ($existing) { Write-Success "Resource group already exists" } else { if ($PSCmdlet.ShouldProcess($rgName, "Create resource group")) { New-AzResourceGroup -Name $rgName -Location $location -Force | Out-Null Write-Success "Resource group created" } } Write-Host "" return $rgName } # ============================================================================ # STEP 4 — Bicep Deployment # ============================================================================ function Invoke-BicepDeployment { param( [hashtable]$Spec, [string]$ResourceGroupName, [string]$BicepFile, [switch]$TestMode ) Write-StepHeader -Step 4 -Title "Deploying infrastructure" if (-not (Test-Path $BicepFile)) { Write-Detail "Bicep entry point not found at: $BicepFile" Write-Detail "Skipping Bicep deployment (no templates found)" Write-Host "" return } Write-Detail "Template : $BicepFile" Write-Detail "Target : $ResourceGroupName" $deploymentName = "axeon-$($Spec.platformId)-$(Get-Date -Format 'yyyyMMdd-HHmmss')" # Build Bicep parameter object from platform-spec + resolved names $resolvedNames = $Spec._resolvedNames $parameters = @{ platformId = $Spec.platformId environment = $Spec.environment location = $Spec.location vnetName = $resolvedNames.virtualNetwork } # Add networking parameters if present if ($Spec.ContainsKey('networking')) { $net = $Spec.networking if ($net.ContainsKey('addressPrefix')) { $parameters['addressPrefix'] = $net.addressPrefix } } # Convert to Bicep-compatible parameter hashtable $templateParams = @{} foreach ($key in $parameters.Keys) { $templateParams[$key] = @{ value = $parameters[$key] } } if ($TestMode) { # Validation-only mode (What-If) Write-Detail "[Test Mode] Running deployment validation (What-If)..." $result = Test-AzResourceGroupDeployment ` -ResourceGroupName $ResourceGroupName ` -TemplateFile $BicepFile ` -TemplateParameterObject $parameters ` -ErrorAction Stop if ($result) { Write-Host " [!] Validation issues:" -ForegroundColor DarkYellow foreach ($err in $result) { Write-Host " - $($err.Message)" -ForegroundColor DarkYellow } } else { Write-Success "Deployment validation passed — no errors detected" } # Also run What-If for a detailed change preview Write-Detail "" Write-Detail "What-If deployment preview:" $whatIf = Get-AzResourceGroupDeploymentWhatIfResult ` -ResourceGroupName $ResourceGroupName ` -TemplateFile $BicepFile ` -TemplateParameterObject $parameters ` -ErrorAction Stop foreach ($change in $whatIf.Changes) { $symbol = switch ($change.ChangeType) { 'Create' { '+' } 'Delete' { '-' } 'Modify' { '~' } 'NoChange'{ '=' } default { '?' } } Write-Detail " [$symbol] $($change.ChangeType): $($change.ResourceId)" } Write-Host "" return } # Full deployment if ($PSCmdlet.ShouldProcess($ResourceGroupName, "Deploy Bicep template")) { Write-Detail "Starting deployment: $deploymentName" $deployment = New-AzResourceGroupDeployment ` -Name $deploymentName ` -ResourceGroupName $ResourceGroupName ` -TemplateFile $BicepFile ` -TemplateParameterObject $parameters ` -ErrorAction Stop Write-Success "Deployment completed" Write-Detail "Provisioning State : $($deployment.ProvisioningState)" Write-Detail "Deployment Name : $($deployment.DeploymentName)" if ($deployment.Outputs) { Write-Host "" Write-Detail "Outputs:" foreach ($key in $deployment.Outputs.Keys) { Write-Detail " $key = $($deployment.Outputs[$key].Value)" } } } Write-Host "" } # ============================================================================ # STEP 5 — Summary # ============================================================================ function Write-DeploymentSummary { param( [hashtable]$Spec, [string]$ResourceGroupName, [switch]$TestMode ) Write-StepHeader -Step 5 -Title "Deployment summary" $mode = if ($TestMode) { "VALIDATION ONLY (Test Mode)" } else { "DEPLOYED" } Write-Host "" Write-Host " Platform ID : $($Spec.platformId)" -ForegroundColor White Write-Host " Environment : $($Spec.environment)" -ForegroundColor White Write-Host " Location : $($Spec.location)" -ForegroundColor White Write-Host " Resource Group : $ResourceGroupName" -ForegroundColor White Write-Host " Status : $mode" -ForegroundColor $(if ($TestMode) { 'Yellow' } else { 'Green' }) Write-Host "" if ($TestMode) { Write-Host " To deploy for real, set testMode to false in platform-spec.json" -ForegroundColor DarkGray } } # ============================================================================ # MAIN EXECUTION # ============================================================================ $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() Write-Banner "Axeon Platform Bootstrap Engine" # Resolve paths $scriptDir = $PSScriptRoot if (-not $BicepRoot) { $BicepRoot = Join-Path $scriptDir ".." "bicep" } if (-not $SchemaPath) { $SchemaPath = Join-Path $scriptDir ".." "schemas" "platform-spec.schema.json" } $bicepEntry = Join-Path $BicepRoot "main.bicep" try { # Step 1 — Load & validate config $spec = Read-PlatformSpec -Path $ConfigPath Test-PlatformSpec -Spec $spec -SchemaFile $SchemaPath $isTestMode = [bool]$spec.testMode # Step 2 — Authenticate Connect-AzureContext -Spec $spec # Step 3 — Ensure resource group $rgName = Ensure-ResourceGroup -Spec $spec -TestMode:$isTestMode # Step 4 — Deploy Bicep Invoke-BicepDeployment ` -Spec $spec ` -ResourceGroupName $rgName ` -BicepFile $bicepEntry ` -TestMode:$isTestMode # Step 5 — Summary Write-DeploymentSummary -Spec $spec -ResourceGroupName $rgName -TestMode:$isTestMode $stopwatch.Stop() Write-Host " Completed in $([math]::Round($stopwatch.Elapsed.TotalSeconds, 1))s" -ForegroundColor DarkGray Write-Host "" } catch { $stopwatch.Stop() Write-Host "" Write-Host " BOOTSTRAP FAILED" -ForegroundColor Red Write-Host " $($_.Exception.Message)" -ForegroundColor Red Write-Host "" throw } |