Source: main (497baf0) Excluded: live tenant exports, generated artifacts, and dev-only tooling.
579 lines
24 KiB
PowerShell
Executable File
579 lines
24 KiB
PowerShell
Executable File
#requires -Version 5.1
|
|
<#
|
|
.SYNOPSIS
|
|
One-stop provisioning script for the ASTRAL change probe.
|
|
|
|
.DESCRIPTION
|
|
This script handles the entire probe deployment in one pass:
|
|
1. Creates (or updates) a dedicated Entra app registration with Graph permissions.
|
|
2. Grants admin consent.
|
|
3. Provisions Azure resources (Resource Group, Storage Account, Function App).
|
|
4. Configures Function App settings.
|
|
5. Optionally deploys the function code if the Azure Functions Core Tools (func) are installed.
|
|
|
|
Any parameter omitted on the command line is prompted for interactively.
|
|
|
|
.PARAMETER AppDisplayName
|
|
Display name for the Entra app registration. Default: "ASTRAL Change Probe".
|
|
|
|
.PARAMETER ResourceGroup
|
|
Azure resource group name. Default: "rg-astral-probe".
|
|
|
|
.PARAMETER Location
|
|
Azure region. Default: "westeurope".
|
|
|
|
.PARAMETER SubscriptionId
|
|
Azure subscription ID. If omitted, the current default subscription is used.
|
|
|
|
.PARAMETER AdoOrganization
|
|
Azure DevOps organization name (e.g. "contoso").
|
|
|
|
.PARAMETER AdoProject
|
|
Azure DevOps project name.
|
|
|
|
.PARAMETER AdoPipelineId
|
|
Azure DevOps pipeline ID (numeric).
|
|
|
|
.PARAMETER AdoToken
|
|
Azure DevOps Personal Access Token with Build (Read & Execute) scope.
|
|
|
|
.PARAMETER AdoBranch
|
|
Git branch the pipeline should run against. Default: "main".
|
|
|
|
.PARAMETER QuietWindowMinutes
|
|
Debouncer quiet window. Default: 15.
|
|
|
|
.PARAMETER CooldownMinutes
|
|
Debouncer cooldown. Default: 30.
|
|
|
|
.EXAMPLE
|
|
.\provision-change-probe.ps1
|
|
|
|
.EXAMPLE
|
|
.\provision-change-probe.ps1 -AdoOrganization "cqre" -AdoProject "ASTRAL" -AdoPipelineId "42"
|
|
#>
|
|
[CmdletBinding()]
|
|
param (
|
|
[string]$AppDisplayName = "ASTRAL Change Probe",
|
|
[string]$ResourceGroup = "rg-astral-probe",
|
|
[string]$Location = "westeurope",
|
|
[string]$SubscriptionId = "",
|
|
[string]$AdoOrganization = "",
|
|
[string]$AdoProject = "",
|
|
[string]$AdoPipelineId = "",
|
|
[string]$AdoToken = "",
|
|
[string]$AdoBranch = "main",
|
|
[int]$QuietWindowMinutes = 15,
|
|
[int]$CooldownMinutes = 30
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
function Get-OrPrompt {
|
|
param ([string]$Value, [string]$Prompt, [switch]$Sensitive)
|
|
if ($Value) { return $Value }
|
|
if ($Sensitive) {
|
|
return Read-Host -Prompt $Prompt -AsSecureString | ForEach-Object { [PSCredential]::New("x", $_).GetNetworkCredential().Password }
|
|
}
|
|
return Read-Host -Prompt $Prompt
|
|
}
|
|
|
|
function Test-Command {
|
|
param ([string]$Name)
|
|
return [bool](Get-Command $Name -ErrorAction SilentlyContinue)
|
|
}
|
|
|
|
function Invoke-AzCli {
|
|
param (
|
|
[string[]]$ArgumentList,
|
|
[switch]$NoRetry
|
|
)
|
|
# Clone the array so recursive calls don't double-append --subscription.
|
|
$argsCopy = @() + $ArgumentList
|
|
if ($SubscriptionId) {
|
|
$argsCopy += @("--subscription", $SubscriptionId)
|
|
}
|
|
# Suppress Python SyntaxWarnings that leak from the Azure CLI into stderr/stdout.
|
|
$env:PYTHONWARNINGS = "ignore"
|
|
$output = & az @argsCopy 2>&1
|
|
$env:PYTHONWARNINGS = ""
|
|
if ($LASTEXITCODE -ne 0) {
|
|
$outputStrings = @()
|
|
$hasSubNotFound = $false
|
|
foreach ($line in $output) {
|
|
$str = if ($line -is [string]) { $line } else { $line.ToString() }
|
|
$outputStrings += $str
|
|
if ($str -match "SubscriptionNotFound") { $hasSubNotFound = $true }
|
|
}
|
|
$outputString = $outputStrings -join "`n"
|
|
if ((-not $NoRetry) -and $hasSubNotFound) {
|
|
Write-Host "`nARM returned SubscriptionNotFound. Clearing token cache and re-authenticating..." -ForegroundColor Yellow
|
|
$subTenantId = Get-SubscriptionTenantId -SubId $SubscriptionId
|
|
$promptTenant = if ($subTenantId) { $subTenantId } else { $tenantId }
|
|
& az account clear | Out-Null
|
|
& az login --tenant $promptTenant | Out-Host
|
|
if ($LASTEXITCODE -ne 0) { throw "az login --tenant $promptTenant failed." }
|
|
# Explicitly set subscription and give token cache time to settle.
|
|
& az account set --subscription $SubscriptionId | Out-Null
|
|
Start-Sleep -Seconds 2
|
|
Invoke-AzCli -ArgumentList $ArgumentList -NoRetry
|
|
return
|
|
}
|
|
throw "az command failed: az $($argsCopy -join ' ')`n$outputString"
|
|
}
|
|
return $output
|
|
}
|
|
|
|
function Test-ModuleInstalled {
|
|
param ([string]$Name)
|
|
$mod = Get-Module -ListAvailable -Name $Name | Select-Object -First 1
|
|
if (-not $mod) {
|
|
Write-Host "Installing module: $Name" -ForegroundColor Cyan
|
|
Install-Module $Name -Scope CurrentUser -Force -AllowClobber
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Prerequisites
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Write-Host "=== ASTRAL Change Probe Provisioning ===" -ForegroundColor Green
|
|
|
|
if (-not (Test-Command "az")) {
|
|
throw "Azure CLI (az) is not installed or not in PATH. Install from https://aka.ms/installazurecli"
|
|
}
|
|
|
|
Write-Host "Checking Microsoft Graph modules..." -ForegroundColor Cyan
|
|
Test-ModuleInstalled "Microsoft.Graph.Applications"
|
|
Import-Module Microsoft.Graph.Applications
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Interactive prompts
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Write-Host "`n--- Azure DevOps Settings ---" -ForegroundColor Cyan
|
|
$AdoOrganization = Get-OrPrompt -Value $AdoOrganization -Prompt "Azure DevOps Organization (e.g. 'cqre')"
|
|
$AdoProject = Get-OrPrompt -Value $AdoProject -Prompt "Azure DevOps Project"
|
|
$AdoPipelineId = Get-OrPrompt -Value $AdoPipelineId -Prompt "Azure DevOps Pipeline ID (numeric)"
|
|
$AdoToken = Get-OrPrompt -Value $AdoToken -Prompt "Azure DevOps PAT (Build Read & Execute)" -Sensitive
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Graph authentication
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Write-Host "`nConnecting to Microsoft Graph..." -ForegroundColor Cyan
|
|
Connect-MgGraph -Scopes "Application.ReadWrite.All","AppRoleAssignment.ReadWrite.All","Directory.Read.All" -NoWelcome
|
|
|
|
$tenant = Get-MgOrganization | Select-Object -First 1
|
|
Write-Host "Tenant: $($tenant.DisplayName) ($($tenant.Id))" -ForegroundColor Green
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# App registration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
$requiredPermissions = @(
|
|
"AuditLog.Read.All",
|
|
"DeviceManagementApps.Read.All",
|
|
"DeviceManagementConfiguration.Read.All",
|
|
"DeviceManagementManagedDevices.Read.All",
|
|
"DeviceManagementScripts.Read.All",
|
|
"DeviceManagementServiceConfig.Read.All"
|
|
)
|
|
|
|
$graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
|
|
if (-not $graphSp) { throw "Microsoft Graph service principal not found." }
|
|
|
|
$appRoles = @()
|
|
foreach ($permName in $requiredPermissions) {
|
|
$appRole = $graphSp.AppRoles | Where-Object { $_.Value -eq $permName } | Select-Object -First 1
|
|
if (-not $appRole) {
|
|
Write-Warning "Permission '$permName' not found. Skipping."
|
|
continue
|
|
}
|
|
$appRoles += $appRole
|
|
}
|
|
|
|
$resourceAccess = @()
|
|
foreach ($ar in $appRoles) {
|
|
$resourceAccess += @{ id = $ar.Id; type = "Role" }
|
|
}
|
|
|
|
$requiredResourceAccess = @(
|
|
@{
|
|
resourceAppId = $graphSp.AppId
|
|
resourceAccess = $resourceAccess
|
|
}
|
|
)
|
|
|
|
$existingApp = Get-MgApplication -Filter "displayName eq '$AppDisplayName'" | Select-Object -First 1
|
|
if ($existingApp) {
|
|
Write-Host "Found existing app registration: $($existingApp.AppId)" -ForegroundColor Yellow
|
|
$app = $existingApp
|
|
Update-MgApplication -ApplicationId $app.Id -RequiredResourceAccess $requiredResourceAccess
|
|
Write-Host "Updated required resource access." -ForegroundColor Green
|
|
} else {
|
|
Write-Host "Creating app registration: $AppDisplayName" -ForegroundColor Cyan
|
|
$app = New-MgApplication -DisplayName $AppDisplayName -SignInAudience "AzureADMyOrg" -RequiredResourceAccess $requiredResourceAccess
|
|
Write-Host "Created app registration. AppId: $($app.AppId)" -ForegroundColor Green
|
|
}
|
|
|
|
$sp = Get-MgServicePrincipal -Filter "appId eq '$($app.AppId)'" | Select-Object -First 1
|
|
if (-not $sp) {
|
|
Write-Host "Creating service principal..." -ForegroundColor Cyan
|
|
$sp = New-MgServicePrincipal -AppId $app.AppId
|
|
}
|
|
|
|
Write-Host "Granting admin consent..." -ForegroundColor Cyan
|
|
foreach ($ar in $appRoles) {
|
|
$existingAssignment = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id | Where-Object { $_.AppRoleId -eq $ar.Id }
|
|
if (-not $existingAssignment) {
|
|
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id -PrincipalId $sp.Id -ResourceId $graphSp.Id -AppRoleId $ar.Id | Out-Null
|
|
}
|
|
}
|
|
Write-Host "Admin consent granted." -ForegroundColor Green
|
|
|
|
# Client secret
|
|
$secretDescription = "ChangeProbeSecret"
|
|
$appWithCreds = Get-MgApplication -ApplicationId $app.Id -Property "id,passwordCredentials"
|
|
$existingSecrets = $appWithCreds.PasswordCredentials | Where-Object { $_.DisplayName -eq $secretDescription }
|
|
foreach ($cred in $existingSecrets) {
|
|
Write-Host "Removing old client secret ($($cred.KeyId))..." -ForegroundColor Yellow
|
|
Remove-MgApplicationPassword -ApplicationId $app.Id -BodyParameter @{ "keyId" = $cred.KeyId }
|
|
}
|
|
|
|
Write-Host "Creating new client secret (valid 1 year)..." -ForegroundColor Cyan
|
|
$passwordCred = @{
|
|
displayName = $secretDescription
|
|
endDateTime = (Get-Date).AddYears(1).ToString("o")
|
|
}
|
|
$secret = Add-MgApplicationPassword -ApplicationId $app.Id -BodyParameter $passwordCred
|
|
$probeAppId = $app.AppId
|
|
$probeAppSecret = $secret.SecretText
|
|
$tenantId = $tenant.Id
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Azure authentication
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Write-Host "`n--- Azure Resources ---" -ForegroundColor Cyan
|
|
|
|
function Ensure-AzLogin {
|
|
param ([string]$TenantId)
|
|
try {
|
|
$null = Invoke-AzCli -ArgumentList @("account", "show", "--output", "none")
|
|
} catch {
|
|
if ($_ -match "az login") {
|
|
$answer = Read-Host -Prompt "You are not logged in to Azure CLI. Run 'az login' now? [Y/n]"
|
|
if ($answer -eq "" -or $answer -match "^[Yy]") {
|
|
if ($TenantId) {
|
|
& az login --tenant $TenantId | Out-Host
|
|
} else {
|
|
& az login | Out-Host
|
|
}
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "az login failed. Please run 'az login' manually and retry."
|
|
}
|
|
} else {
|
|
throw "Azure login required. Run 'az login' and retry."
|
|
}
|
|
} else {
|
|
throw
|
|
}
|
|
}
|
|
}
|
|
|
|
Ensure-AzLogin -TenantId $tenantId
|
|
|
|
function Select-Subscription {
|
|
param ([string]$CurrentId)
|
|
# Run az directly and filter out stderr warning objects so only stdout strings reach ConvertFrom-Json.
|
|
$lines = & az account list --output json 2>&1
|
|
$stringLines = $lines | Where-Object { $_ -is [string] }
|
|
if ($LASTEXITCODE -ne 0) {
|
|
$errorLines = $lines | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] } | ForEach-Object { $_.ToString() }
|
|
throw "az account list failed:`n$($errorLines -join "`n")"
|
|
}
|
|
$subs = ($stringLines -join "`n") | ConvertFrom-Json
|
|
if ($subs.Count -eq 0) {
|
|
throw "No Azure subscriptions found. Ensure your account has access to at least one subscription."
|
|
}
|
|
if ($subs.Count -eq 1) {
|
|
$sub = $subs[0]
|
|
Invoke-AzCli -ArgumentList @("account", "set", "--subscription", $sub.id)
|
|
return $sub
|
|
}
|
|
Write-Host "`nAvailable subscriptions:" -ForegroundColor Cyan
|
|
for ($i = 0; $i -lt $subs.Count; $i++) {
|
|
$marker = if ($subs[$i].id -eq $CurrentId) { " (*)" } else { "" }
|
|
Write-Host " [$i] $($subs[$i].name) ($($subs[$i].id))$marker"
|
|
}
|
|
$selection = Read-Host -Prompt "Select subscription by number"
|
|
if (-not [int]::TryParse($selection, [ref]$null)) {
|
|
throw "Invalid selection. Aborting."
|
|
}
|
|
$chosen = $subs[[int]$selection]
|
|
if (-not $chosen) {
|
|
throw "Invalid selection. Aborting."
|
|
}
|
|
Invoke-AzCli -ArgumentList @("account", "set", "--subscription", $chosen.id)
|
|
return $chosen
|
|
}
|
|
|
|
$azLines = & az account show --output json 2>&1
|
|
$azStringLines = $azLines | Where-Object { $_ -is [string] }
|
|
if ($LASTEXITCODE -ne 0) {
|
|
$azErrorLines = $azLines | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] } | ForEach-Object { $_.ToString() }
|
|
throw "az account show failed:`n$($azErrorLines -join "`n")"
|
|
}
|
|
$azAccount = ($azStringLines -join "`n") | ConvertFrom-Json
|
|
$currentSubId = $azAccount.id
|
|
|
|
function Get-SubscriptionTenantId {
|
|
param ([string]$SubId)
|
|
$lines = & az account list --output json 2>&1
|
|
$stringLines = $lines | Where-Object { $_ -is [string] }
|
|
$subs = ($stringLines -join "`n") | ConvertFrom-Json
|
|
$sub = $subs | Where-Object { $_.id -eq $SubId } | Select-Object -First 1
|
|
if ($sub) { return $sub.tenantId } else { return $null }
|
|
}
|
|
|
|
if ($SubscriptionId) {
|
|
Invoke-AzCli -ArgumentList @("account", "set", "--subscription", $SubscriptionId)
|
|
$subTenantId = Get-SubscriptionTenantId -SubId $SubscriptionId
|
|
$azTenantLines = & az account show --query tenantId --output tsv 2>&1 | Where-Object { $_ -is [string] }
|
|
$azTenantId = ($azTenantLines -join "").Trim()
|
|
if ($subTenantId -and $azTenantId -ne $subTenantId) {
|
|
Write-Host "`nSubscription '$SubscriptionId' belongs to tenant '$subTenantId' but current az context is '$azTenantId'." -ForegroundColor Yellow
|
|
Write-Host "Re-authenticating to the subscription's tenant..." -ForegroundColor Yellow
|
|
& az account clear | Out-Null
|
|
& az login --tenant $subTenantId | Out-Host
|
|
if ($LASTEXITCODE -ne 0) { throw "az login --tenant $subTenantId failed." }
|
|
Invoke-AzCli -ArgumentList @("account", "set", "--subscription", $SubscriptionId)
|
|
}
|
|
Write-Host "Using specified subscription: $SubscriptionId" -ForegroundColor Green
|
|
} else {
|
|
$chosenSub = Select-Subscription -CurrentId $currentSubId
|
|
$SubscriptionId = $chosenSub.id
|
|
$subTenantId = $chosenSub.tenantId
|
|
$azTenantLines = & az account show --query tenantId --output tsv 2>&1 | Where-Object { $_ -is [string] }
|
|
$azTenantId = ($azTenantLines -join "").Trim()
|
|
if ($subTenantId -and $azTenantId -ne $subTenantId) {
|
|
Write-Host "`nSubscription '$SubscriptionId' belongs to tenant '$subTenantId' but current az context is '$azTenantId'." -ForegroundColor Yellow
|
|
Write-Host "Re-authenticating to the subscription's tenant..." -ForegroundColor Yellow
|
|
& az account clear | Out-Null
|
|
& az login --tenant $subTenantId | Out-Host
|
|
if ($LASTEXITCODE -ne 0) { throw "az login --tenant $subTenantId failed." }
|
|
$chosenSub = Select-Subscription -CurrentId $SubscriptionId
|
|
$SubscriptionId = $chosenSub.id
|
|
}
|
|
Write-Host "Using subscription: $SubscriptionId" -ForegroundColor Green
|
|
}
|
|
|
|
# Validate the subscription is accessible for ARM operations (catches tenant mismatches).
|
|
try {
|
|
$null = Invoke-AzCli -ArgumentList @("group", "list", "--output", "none")
|
|
} catch {
|
|
if ($_ -match "SubscriptionNotFound") {
|
|
Write-Host "`nThe selected subscription is listed but ARM operations fail with 'SubscriptionNotFound'." -ForegroundColor Yellow
|
|
Write-Host "This usually means the subscription belongs to a different Entra tenant." -ForegroundColor Yellow
|
|
$subTenantId = Get-SubscriptionTenantId -SubId $SubscriptionId
|
|
$promptTenant = if ($subTenantId) { $subTenantId } else { $tenantId }
|
|
$answer = Read-Host -Prompt "Run 'az login --tenant $promptTenant' now and retry? [Y/n]"
|
|
if ($answer -eq "" -or $answer -match "^[Yy]") {
|
|
& az account clear | Out-Null
|
|
& az login --tenant $promptTenant | Out-Host
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "az login --tenant failed. Please run it manually and retry."
|
|
}
|
|
$chosenSub = Select-Subscription -CurrentId $SubscriptionId
|
|
$SubscriptionId = $chosenSub.id
|
|
Write-Host "Using subscription: $SubscriptionId" -ForegroundColor Green
|
|
# Validate again
|
|
$null = Invoke-AzCli -ArgumentList @("group", "list", "--output", "none")
|
|
} else {
|
|
throw "Subscription validation failed. Run 'az login --tenant $promptTenant' and retry."
|
|
}
|
|
} else {
|
|
throw
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Resource Group
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Write-Host "Ensuring resource group '$ResourceGroup'..." -ForegroundColor Cyan
|
|
Invoke-AzCli -ArgumentList @("group", "create", "--name", $ResourceGroup, "--location", $Location, "--output", "none")
|
|
|
|
# Quick diagnostic: confirm ARM can read back the RG in this subscription.
|
|
try {
|
|
$diag = Invoke-AzCli -ArgumentList @("group", "show", "--name", $ResourceGroup, "--query", "id", "--output", "tsv")
|
|
Write-Host "ARM context OK (RG id: $diag)" -ForegroundColor Green
|
|
} catch {
|
|
Write-Host "WARNING: ARM diagnostic failed: $_" -ForegroundColor Yellow
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Storage Account
|
|
# ---------------------------------------------------------------------------
|
|
|
|
$randomSuffix = [System.Guid]::NewGuid().ToString("n").Substring(0, 8)
|
|
$StorageName = "stastralprobe$randomSuffix"
|
|
$FunctionAppName = "func-astral-probe-$randomSuffix"
|
|
|
|
function Wait-ProviderRegistration {
|
|
param ([string]$Namespace)
|
|
$state = ""
|
|
$attempts = 0
|
|
while ($state -ne "Registered" -and $attempts -lt 30) {
|
|
$state = Invoke-AzCli -ArgumentList @("provider", "show", "--namespace", $Namespace, "--query", "registrationState", "--output", "tsv")
|
|
if ($state -eq "Registered") { break }
|
|
Start-Sleep -Seconds 10
|
|
$attempts++
|
|
}
|
|
if ($state -ne "Registered") {
|
|
throw "Timed out waiting for $Namespace provider to register."
|
|
}
|
|
}
|
|
|
|
Write-Host "Creating storage account '$StorageName'..." -ForegroundColor Cyan
|
|
|
|
# Ensure Microsoft.Storage provider is registered (required for new subscriptions).
|
|
$storageProv = Invoke-AzCli -ArgumentList @("provider", "show", "--namespace", "Microsoft.Storage", "--query", "registrationState", "--output", "tsv")
|
|
if ($storageProv -ne "Registered") {
|
|
Write-Host "Registering Microsoft.Storage provider..." -ForegroundColor Yellow
|
|
Invoke-AzCli -ArgumentList @("provider", "register", "--namespace", "Microsoft.Storage")
|
|
Wait-ProviderRegistration -Namespace "Microsoft.Storage"
|
|
Write-Host "Microsoft.Storage registered." -ForegroundColor Green
|
|
}
|
|
|
|
Invoke-AzCli -ArgumentList @(
|
|
"storage", "account", "create",
|
|
"--name", $StorageName,
|
|
"--resource-group", $ResourceGroup,
|
|
"--location", $Location,
|
|
"--sku", "Standard_LRS",
|
|
"--kind", "StorageV2",
|
|
"--output", "none"
|
|
)
|
|
|
|
$storageConnection = Invoke-AzCli -ArgumentList @(
|
|
"storage", "account", "show-connection-string",
|
|
"--name", $StorageName,
|
|
"--resource-group", $ResourceGroup,
|
|
"--query", "connectionString",
|
|
"--output", "tsv"
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Table and Queue
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Write-Host "Creating Table and Queue..." -ForegroundColor Cyan
|
|
Invoke-AzCli -ArgumentList @("storage", "table", "create", "--name", "ProbeState", "--connection-string", $storageConnection, "--output", "none")
|
|
Invoke-AzCli -ArgumentList @("storage", "queue", "create", "--name", "backup-trigger-queue", "--connection-string", $storageConnection, "--output", "none")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Function App
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Ensure Microsoft.Web provider is registered (required for Function Apps).
|
|
$webProv = Invoke-AzCli -ArgumentList @("provider", "show", "--namespace", "Microsoft.Web", "--query", "registrationState", "--output", "tsv")
|
|
if ($webProv -ne "Registered") {
|
|
Write-Host "Registering Microsoft.Web provider..." -ForegroundColor Yellow
|
|
Invoke-AzCli -ArgumentList @("provider", "register", "--namespace", "Microsoft.Web")
|
|
Wait-ProviderRegistration -Namespace "Microsoft.Web"
|
|
Write-Host "Microsoft.Web registered." -ForegroundColor Green
|
|
}
|
|
|
|
Write-Host "Creating Function App '$FunctionAppName'..." -ForegroundColor Cyan
|
|
Invoke-AzCli -ArgumentList @(
|
|
"functionapp", "create",
|
|
"--name", $FunctionAppName,
|
|
"--resource-group", $ResourceGroup,
|
|
"--storage-account", $StorageName,
|
|
"--consumption-plan-location", $Location,
|
|
"--os-type", "Linux",
|
|
"--runtime", "python",
|
|
"--runtime-version", "3.11",
|
|
"--functions-version", "4",
|
|
"--output", "none"
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# App Settings
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Write-Host "Configuring Function App settings..." -ForegroundColor Cyan
|
|
Invoke-AzCli -ArgumentList @(
|
|
"functionapp", "config", "appsettings", "set",
|
|
"--name", $FunctionAppName,
|
|
"--resource-group", $ResourceGroup,
|
|
"--settings",
|
|
"AzureWebJobsStorage=$storageConnection",
|
|
"FUNCTIONS_EXTENSION_VERSION=~4",
|
|
"FUNCTIONS_WORKER_RUNTIME=python",
|
|
"WEBSITE_RUN_FROM_PACKAGE=1",
|
|
"PROBE_APP_ID=$probeAppId",
|
|
"PROBE_APP_SECRET=$probeAppSecret",
|
|
"TENANT_ID=$tenantId",
|
|
"GRAPH_TOKEN=",
|
|
"ADO_ORGANIZATION=$AdoOrganization",
|
|
"ADO_PROJECT=$AdoProject",
|
|
"ADO_PIPELINE_ID=$AdoPipelineId",
|
|
"ADO_TOKEN=$AdoToken",
|
|
"ADO_BRANCH=$AdoBranch",
|
|
"PROBE_QUIET_WINDOW_MINUTES=$QuietWindowMinutes",
|
|
"PROBE_COOLDOWN_MINUTES=$CooldownMinutes",
|
|
"REPO_ROOT=/home/site/wwwroot",
|
|
"--output", "none"
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Optional: code deployment
|
|
# ---------------------------------------------------------------------------
|
|
|
|
$funcAvailable = Test-Command "func"
|
|
if ($funcAvailable) {
|
|
$repoRoot = Split-Path -Parent $PSScriptRoot
|
|
$probePath = Join-Path $repoRoot "infra" "change-probe"
|
|
if (Test-Path $probePath) {
|
|
$deployNow = Read-Host -Prompt "`nDeploy function code now? [Y/n]"
|
|
if ($deployNow -eq "" -or $deployNow -match "^[Yy]") {
|
|
Write-Host "Deploying function code..." -ForegroundColor Cyan
|
|
Push-Location $probePath
|
|
try {
|
|
& func azure functionapp publish $FunctionAppName
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Warning "Function deployment returned exit code $LASTEXITCODE. You can retry manually later."
|
|
}
|
|
} finally {
|
|
Pop-Location
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
Write-Host "`nAzure Functions Core Tools (func) not found. Skipping code deployment." -ForegroundColor Yellow
|
|
Write-Host "Install from https://github.com/Azure/azure-functions-core-tools#installing" -ForegroundColor Yellow
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Summary
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Write-Host "`n=== Provisioning Complete ===" -ForegroundColor Green
|
|
Write-Host "Subscription: $SubscriptionId"
|
|
Write-Host "Resource Group: $ResourceGroup"
|
|
Write-Host "Storage Account: $StorageName"
|
|
Write-Host "Function App: $FunctionAppName"
|
|
Write-Host "App Registration: $probeAppId"
|
|
Write-Host "`nNext steps:"
|
|
Write-Host " - Verify the timer trigger in the Azure Portal or with:"
|
|
Write-Host " az functionapp function show --name $FunctionAppName --resource-group $ResourceGroup --function-name probe_timer"
|
|
Write-Host " - To redeploy code later:"
|
|
Write-Host " cd infra/change-probe && func azure functionapp publish $FunctionAppName"
|