param( [Parameter(Mandatory = $true)] [string]$KubeContext, [Parameter(Mandatory = $true)] [string]$Namespace, [Parameter(Mandatory = $true)] [string]$Release, [Parameter(Mandatory = $true)] [string]$GatewayBaseUrl, [Parameter(Mandatory = $true)] [string]$DatabaseUrl, [Parameter(Mandatory = $true)] [string]$ImageTag, [Parameter(Mandatory = $true)] [string]$AmiHost, [Parameter(Mandatory = $true)] [string]$AmiUser, [Parameter(Mandatory = $true)] [string]$AmiSecret, [Parameter(Mandatory = $true)] [string]$SftpHost, [Parameter(Mandatory = $true)] [string]$SftpUser, [Parameter(Mandatory = $true)] [string]$SftpPassword, [Parameter(Mandatory = $true)] [string]$QueueId, [Parameter(Mandatory = $true)] [string]$AppTokenSecret, [string]$ScaleValuesPath = "deployment/helm/values.scale500.yaml", [string]$StrictValuesPath = "deployment/helm/values.track9-strict.yaml", [string]$EvidenceOutDir = "", [switch]$Execute, [switch]$ForceUpgrade, [switch]$SkipChecks ) $ErrorActionPreference = "Stop" $root = Split-Path -Parent $PSScriptRoot Set-Location $root function Run-Checked { param( [Parameter(Mandatory = $true)] [string]$Label, [Parameter(Mandatory = $true)] [string[]]$Args ) Write-Host "" Write-Host "==> $Label" Write-Host " $($Args -join ' ')" $command = $Args[0] $arguments = @() if ($Args.Length -gt 1) { $arguments = $Args[1..($Args.Length - 1)] } & $command @arguments if ($LASTEXITCODE -ne 0) { throw "$Label failed with exit code $LASTEXITCODE" } } function Ensure-File { param([string]$PathValue) if (-not (Test-Path $PathValue)) { throw "Required file not found: $PathValue" } } Ensure-File -PathValue $ScaleValuesPath Ensure-File -PathValue $StrictValuesPath $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $artifactsDir = Join-Path $root ".artifacts/track9_2/$timestamp" New-Item -ItemType Directory -Path $artifactsDir -Force | Out-Null $helmTemplateOut = Join-Path $artifactsDir "helm_template.yaml" $cutoverValuesFile = Join-Path $artifactsDir "cutover.override.yaml" $podsSnapshotOut = Join-Path $artifactsDir "pods_before.txt" $historyBeforeOut = Join-Path $artifactsDir "helm_history_before.txt" $historyAfterOut = Join-Path $artifactsDir "helm_history_after.txt" $cutoverSummaryOut = Join-Path $artifactsDir "cutover-summary.txt" function Escape-YamlSingleQuoted { param([string]$Value) return ($Value -replace "'", "''") } $queueMapJson = '{"voice_lab":"' + $QueueId + '"}' $overrideYaml = @" namespace: '$(Escape-YamlSingleQuoted $Namespace)' image: tag: '$(Escape-YamlSingleQuoted $ImageTag)' auth: appTokenSecret: '$(Escape-YamlSingleQuoted $AppTokenSecret)' asteriskBridge: enabled: "1" amiHost: '$(Escape-YamlSingleQuoted $AmiHost)' amiUsername: '$(Escape-YamlSingleQuoted $AmiUser)' amiSecret: '$(Escape-YamlSingleQuoted $AmiSecret)' queueMapJson: '$(Escape-YamlSingleQuoted $queueMapJson)' sftpHost: '$(Escape-YamlSingleQuoted $SftpHost)' sftpUsername: '$(Escape-YamlSingleQuoted $SftpUser)' sftpPassword: '$(Escape-YamlSingleQuoted $SftpPassword)' "@ $overrideYaml | Out-File -FilePath $cutoverValuesFile -Encoding utf8 Run-Checked -Label "Switch kube context" -Args @("kubectl", "config", "use-context", $KubeContext) Run-Checked -Label "Helm lint" -Args @("helm", "lint", "deployment/helm") Write-Host "" Write-Host "==> Helm history (before)" Write-Host " helm -n $Namespace history $Release" & helm -n $Namespace history $Release | Out-File -FilePath $historyBeforeOut -Encoding utf8 if ($LASTEXITCODE -ne 0) { "release not found (this can be valid for first install)" | Out-File -FilePath $historyBeforeOut -Encoding utf8 Write-Warning "Release '$Release' not found in namespace '$Namespace'. Continuing with first-install flow." } Write-Host "" Write-Host "==> Pods snapshot (before)" Write-Host " kubectl -n $Namespace get pods -o wide" & kubectl -n $Namespace get pods -o wide | Out-File -FilePath $podsSnapshotOut -Encoding utf8 if ($LASTEXITCODE -ne 0) { "namespace missing or no pods yet (pre-install state)" | Out-File -FilePath $podsSnapshotOut -Encoding utf8 Write-Warning "Could not fetch pods for namespace '$Namespace'. Continuing." } Write-Host "" Write-Host "==> Helm template render" Write-Host " helm template $Release deployment/helm -n $Namespace -f $ScaleValuesPath -f $StrictValuesPath -f $cutoverValuesFile" & helm template $Release deployment/helm -n $Namespace ` -f $ScaleValuesPath ` -f $StrictValuesPath ` -f $cutoverValuesFile | Out-File -FilePath $helmTemplateOut -Encoding utf8 if ($LASTEXITCODE -ne 0) { throw "Helm template render failed with exit code $LASTEXITCODE" } $requiredStrictKeys = @( "ALLOW_LEGACY_HEADER_AUTH" "ASTERISK_BRIDGE_AUTH_MODE" "ASTERISK_BRIDGE_AUTH_FALLBACK_LEGACY" "VOICE_ADAPTER_TRUSTED_SERVICE_SUBJECTS" "RECORDING_IMPORT_TRUSTED_SERVICE_SUBJECTS" "RECORDING_IMPORT_ALLOW_ADMIN" ) foreach ($key in $requiredStrictKeys) { if (-not (Select-String -Path $helmTemplateOut -Pattern $key -SimpleMatch)) { throw "Rendered manifest does not contain expected strict key: $key" } } if (-not $Execute) { @( "Track 9.2 cutover dry-run completed." "Artifacts: $artifactsDir" "Rendered manifest: $helmTemplateOut" "To execute deploy, rerun with -Execute" ) | Out-File -FilePath $cutoverSummaryOut -Encoding utf8 Write-Host "" Write-Host "Dry-run completed." Write-Host "Artifacts: $artifactsDir" Write-Host "Run again with -Execute to apply Helm upgrade." exit 0 } $helmUpgradeArgs = @( "helm", "upgrade", "--install", $Release, "deployment/helm", "-n", $Namespace, "-f", $ScaleValuesPath, "-f", $StrictValuesPath, "-f", $cutoverValuesFile ) if ($ForceUpgrade) { $helmUpgradeArgs += "--force" } Run-Checked -Label "Helm upgrade/install" -Args $helmUpgradeArgs Run-Checked -Label "Rollout status: api-gateway" -Args @("kubectl", "-n", $Namespace, "rollout", "status", "deploy/api-gateway") Run-Checked -Label "Rollout status: asterisk-bridge-service" -Args @("kubectl", "-n", $Namespace, "rollout", "status", "deploy/asterisk-bridge-service") Run-Checked -Label "Rollout status: voice-adapter-service" -Args @("kubectl", "-n", $Namespace, "rollout", "status", "deploy/voice-adapter-service") Run-Checked -Label "Rollout status: recording-service" -Args @("kubectl", "-n", $Namespace, "rollout", "status", "deploy/recording-service") Run-Checked -Label "Helm history (after)" -Args @("helm", "-n", $Namespace, "history", $Release) & helm -n $Namespace history $Release | Out-File -FilePath $historyAfterOut -Encoding utf8 if (-not $SkipChecks) { Run-Checked -Label "Track9 preflight strict" -Args @( "python", "scripts/track9_preflight.py", "--base-url", $GatewayBaseUrl, "--check-sftp", "--require-strict-service-auth" ) Run-Checked -Label "Asterisk lab smoke" -Args @( "python", "scripts/asterisk_lab_smoke.py", "--base-url", $GatewayBaseUrl, "--database-url", $DatabaseUrl, "--require-recording" ) Run-Checked -Label "Track9 check" -Args @( "python", "scripts/track9_check.py", "--base-url", $GatewayBaseUrl, "--database-url", $DatabaseUrl, "--require-recording" ) } $evidenceArgs = @( "python", "scripts/track9_collect_evidence.py", "--base-url", $GatewayBaseUrl, "--database-url", $DatabaseUrl, "--run-checks", "--require-recording" ) if ($EvidenceOutDir.Trim()) { $evidenceArgs += @("--out-dir", $EvidenceOutDir) } Run-Checked -Label "Collect Track9 evidence" -Args $evidenceArgs $historyLines = Get-Content $historyAfterOut $deployedLine = $historyLines | Select-String -Pattern "deployed" | Select-Object -Last 1 $rollbackHint = "" if ($deployedLine) { $parts = ($deployedLine.ToString() -split "\s+", [System.StringSplitOptions]::RemoveEmptyEntries) if ($parts.Length -ge 1) { $currentRevision = $parts[0] $previousRevision = [int]$currentRevision - 1 if ($previousRevision -ge 1) { $rollbackHint = "helm -n $Namespace rollback $Release $previousRevision" } } } @( "Track 9.2 cutover execution completed." "Artifacts: $artifactsDir" "Gateway: $GatewayBaseUrl" "Database: $DatabaseUrl" if ($rollbackHint) { "Rollback command: $rollbackHint" } else { "Rollback command: check helm history manually." } ) | Out-File -FilePath $cutoverSummaryOut -Encoding utf8 Write-Host "" Write-Host "Track 9.2 cutover completed." Write-Host "Artifacts: $artifactsDir" if ($rollbackHint) { Write-Host "Rollback hint: $rollbackHint" }