diff --git a/.github/workflows/validate-templates.yml b/.github/workflows/validate-templates.yml index 835febd7f..9e8b0a7d9 100644 --- a/.github/workflows/validate-templates.yml +++ b/.github/workflows/validate-templates.yml @@ -82,7 +82,7 @@ jobs: echo "Checking PowerShell scripts..." errors=0 for f in bin/ps/*.ps1; do - result=$(pwsh -NoProfile -Command "[System.Management.Automation.Language.Parser]::ParseFile('$f', [ref]\$null, [ref]\$errs); if(\$errs.Count -gt 0){\$errs | ForEach-Object { \$_.Message }}" 2>&1) + result=$(pwsh -NoProfile -Command "\$errs=\$null; [System.Management.Automation.Language.Parser]::ParseFile('$f', [ref]\$null, [ref]\$errs); if(\$errs -and \$errs.Count -gt 0){\$errs | ForEach-Object { \$_.Message }}" 2>&1) if [[ -n "$result" && "$result" != *"InvalidOperation"* ]]; then echo "❌ $f: $result" errors=$((errors + 1)) diff --git a/sreagent-templates/bicep/Apply-Extras.ps1 b/sreagent-templates/bicep/Apply-Extras.ps1 index 45304e20e..50eb30cb3 100644 --- a/sreagent-templates/bicep/Apply-Extras.ps1 +++ b/sreagent-templates/bicep/Apply-Extras.ps1 @@ -53,7 +53,10 @@ param( [string]$Subscription, [Parameter(Mandatory)][string]$ResourceGroup, [Parameter(Mandatory)][string]$AgentName, - [string]$ExtrasFile = "extras.parameters.json", + # Either supply a pre-assembled extras JSON OR a config directory — Apply-Extras + # will auto-assemble from the directory if ExtrasFile is not found. + [string]$ExtrasFile = "", + [string]$ConfigDir = "", [switch]$Force ) @@ -73,8 +76,20 @@ if (Test-Path $PrereqScript) { } } +# ── Auto-assemble from ConfigDir if ExtrasFile not supplied/found ─────────── +if (($ExtrasFile -eq "" -or -not (Test-Path $ExtrasFile)) -and $ConfigDir -ne "" -and (Test-Path $ConfigDir -PathType Container)) { + Write-Host "No extras file supplied — assembling from config dir: $ConfigDir" + $AssembleScript = Join-Path $PSScriptRoot 'Assemble-Agent.ps1' + $AssembleTmp = Join-Path ([System.IO.Path]::GetTempPath()) "assembled-$(New-Guid)" + & $AssembleScript -ConfigDir $ConfigDir -Output $AssembleTmp + $ExtrasFile = "${AssembleTmp}.extras.json" +} elseif ($ExtrasFile -eq "") { + # Default: look for .extras.json next to the script caller's cwd + $ExtrasFile = "extras.parameters.json" +} + if (-not (Test-Path $ExtrasFile)) { - Write-Error "extras file not found: $ExtrasFile" + Write-Error "extras file not found: $ExtrasFile`nTip: pass -ConfigDir to auto-assemble." return } @@ -87,7 +102,7 @@ $ArmBase = "https://management.azure.com/subscriptions/$Subscription/resourceGro # ── Resolve agent endpoint and UAMI ──────────────────────────────────────── try { - $agentRaw = az rest -m GET --url "$ArmBase`?api-version=$ApiVersion" -o json 2>$null + $agentRaw = (az rest -m GET --url "$ArmBase`?api-version=$ApiVersion" -o json 2>$null) -join "`n" $agentObj = $agentRaw | ConvertFrom-Json } catch { $agentObj = $null @@ -210,7 +225,7 @@ function DataPlane-UploadTarball { "Content-Type" = "application/gzip" } $bytes = [System.IO.File]::ReadAllBytes($tarball) - $null = Invoke-RestMethod -Uri $Url -Method Post -Headers $headers -Body $bytes + $null = Invoke-RestMethod -TimeoutSec 30 -Uri $Url -Method Post -Headers $headers -Body $bytes Write-Host " ok" } catch { Write-Host " FAILED - POST $Url" @@ -254,7 +269,7 @@ function DataPlane-UploadMultipart { $fullUrl = "${Url}?triggerIndexing=$Trigger" $headers = @{ Authorization = "Bearer $token" } - $null = Invoke-WebRequest -Uri $fullUrl -Method Post ` + $null = Invoke-WebRequest -TimeoutSec 30 -Uri $fullUrl -Method Post ` -Headers $headers ` -ContentType "multipart/form-data; boundary=$boundary" ` -Body $bodyArray @@ -280,7 +295,7 @@ function DataPlane-PostJson { "Content-Type" = "application/json" } $bodyJson = $BodyObj | ConvertTo-Json -Compress -Depth 20 - $null = Invoke-RestMethod -Uri $Url -Method Post -Headers $headers -Body $bodyJson -ContentType "application/json" + $null = Invoke-RestMethod -TimeoutSec 30 -Uri $Url -Method Post -Headers $headers -Body $bodyJson -ContentType "application/json" Write-Host " ok" } catch { Write-Host " FAILED - POST $Url" @@ -304,7 +319,7 @@ function DataPlane-PutExtended { Authorization = "Bearer $token" "Content-Type" = "application/json" } - $null = Invoke-RestMethod -Uri $url -Method Put -Headers $headers -Body $body -ContentType "application/json" + $null = Invoke-RestMethod -TimeoutSec 30 -Uri $url -Method Put -Headers $headers -Body $body -ContentType "application/json" Write-Host " ok $Kind/$Name" } catch { Write-Host " FAILED - PUT $Kind/$Name" @@ -408,7 +423,7 @@ if ($ifCount -gt 0) { Authorization = "Bearer $token" "Content-Type" = "application/json" } - $null = Invoke-RestMethod -Uri "$AgentEndpoint/api/v2/extendedAgent/incidentFilters/$encodedName" ` + $null = Invoke-RestMethod -TimeoutSec 30 -Uri "$AgentEndpoint/api/v2/extendedAgent/incidentFilters/$encodedName" ` -Method Put -Headers $headers -Body $body -ContentType "application/json" Write-Host " data-plane PUT incidentFilters/$name" Write-Host " ok" @@ -446,7 +461,7 @@ if ($ifCount -gt 0) { Authorization = "Bearer $token" "Content-Type" = "application/json" } - $resp = Invoke-WebRequest -Uri "$AgentEndpoint/api/v1/incidentPlayground/handlers/$name" ` + $resp = Invoke-WebRequest -TimeoutSec 30 -Uri "$AgentEndpoint/api/v1/incidentPlayground/handlers/$name" ` -Method Put -Headers $headers -Body $handlerBody -ContentType "application/json" ` -UseBasicParsing $httpCode = $resp.StatusCode @@ -649,7 +664,7 @@ if ($synthDir -and (Test-Path $synthDir -PathType Container)) { "Content-Type" = "application/gzip" } $bytes = [System.IO.File]::ReadAllBytes($tarball) - $null = Invoke-RestMethod -Uri "$AgentEndpoint/api/v1/WorkspaceMemory/synthesized-knowledge" ` + $null = Invoke-RestMethod -TimeoutSec 30 -Uri "$AgentEndpoint/api/v1/WorkspaceMemory/synthesized-knowledge" ` -Method Post -Headers $headers -Body $bytes Write-Host " ok" } catch { @@ -837,7 +852,7 @@ if ($htCount -gt 0) { $token = Get-DpToken $headers = @{ Authorization = "Bearer $token" } try { - $existingTriggers = Invoke-RestMethod -Uri "$AgentEndpoint/api/v1/httpTriggers" -Headers $headers + $existingTriggers = Invoke-RestMethod -TimeoutSec 30 -Uri "$AgentEndpoint/api/v1/httpTriggers" -Headers $headers } catch { $existingTriggers = @() } @@ -861,7 +876,7 @@ if ($htCount -gt 0) { if (-not $HttpTriggerUrl) { $HttpTriggerUrl = $existingUrl } } else { try { - $resp = Invoke-RestMethod -Uri "$AgentEndpoint/api/v1/httptriggers/create" ` + $resp = Invoke-RestMethod -TimeoutSec 30 -Uri "$AgentEndpoint/api/v1/httptriggers/create" ` -Method Post -Headers $headers -Body $bodyJson -ContentType "application/json" $triggerUrl = if ($resp.triggerUrl) { $resp.triggerUrl } else { "created" } Write-Host " httpTrigger/${name}: $triggerUrl" @@ -975,7 +990,7 @@ if ($DpTokenAvailable) { "Content-Type" = "application/json" } $body = @{ accessToken = $env:GITHUB_PAT } | ConvertTo-Json -Compress - $null = Invoke-RestMethod -Uri "$AgentEndpoint/api/v1/Github/auth/pat" ` + $null = Invoke-RestMethod -TimeoutSec 30 -Uri "$AgentEndpoint/api/v1/Github/auth/pat" ` -Method Post -Headers $headers -Body $body -ContentType "application/json" Write-Host " ok" } catch { @@ -995,7 +1010,7 @@ if ($DpTokenAvailable) { "Content-Type" = "application/json" } $body = @{ accessToken = $env:ADO_PAT } | ConvertTo-Json -Compress - $null = Invoke-RestMethod -Uri "$AgentEndpoint/api/v1/AzureDevOps/auth/pat?organization=$($env:ADO_ORG)" ` + $null = Invoke-RestMethod -TimeoutSec 30 -Uri "$AgentEndpoint/api/v1/AzureDevOps/auth/pat?organization=$($env:ADO_ORG)" ` -Method Post -Headers $headers -Body $body -ContentType "application/json" Write-Host " ok" } catch { @@ -1014,7 +1029,7 @@ if ($DpTokenAvailable) { "Content-Type" = "application/json" } $body = @{ aadAccessToken = $aadToken } | ConvertTo-Json -Compress - $null = Invoke-RestMethod -Uri "$AgentEndpoint/api/v1/AzureDevOps/aadauth/complete?organization=$($env:ADO_ORG)" ` + $null = Invoke-RestMethod -TimeoutSec 30 -Uri "$AgentEndpoint/api/v1/AzureDevOps/aadauth/complete?organization=$($env:ADO_ORG)" ` -Method Post -Headers $headers -Body $body -ContentType "application/json" Write-Host " ok" } catch { @@ -1028,7 +1043,7 @@ if ($DpTokenAvailable) { try { $token = Get-DpToken $headers = @{ Authorization = "Bearer $token" } - $null = Invoke-RestMethod -Uri "$AgentEndpoint/api/v1/AzureDevOps/auth/mi?organization=$($env:ADO_ORG)" ` + $null = Invoke-RestMethod -TimeoutSec 30 -Uri "$AgentEndpoint/api/v1/AzureDevOps/auth/mi?organization=$($env:ADO_ORG)" ` -Method Post -Headers $headers Write-Host " ok" } catch { @@ -1047,82 +1062,70 @@ if ($DpTokenAvailable) { if ($token) { try { $headers = @{ Authorization = "Bearer $token" } - $ghStatus = Invoke-RestMethod -Uri "$AgentEndpoint/api/v1/Github/auth/status" -Headers $headers + $ghStatus = Invoke-RestMethod -TimeoutSec 30 -Uri "$AgentEndpoint/api/v1/Github/auth/status" -Headers $headers $ghConfigured = ($ghStatus.isConfigured -eq $true) -or ($ghStatus.hosts[0].isConfigured -eq $true) } catch { } } - if ($ghConfigured -or $env:GITHUB_PAT) { - # OAuth (or PAT) is in place — wire the connector + repos - Write-Host "-- Wiring GitHub connector + repos --" - $ident = if ($AgentUami) { $AgentUami } else { - Write-Host " WARN: agent has no user-assigned MI; falling back to SystemAssigned." - "SystemAssigned" + # Shared identity + connector body (used in both fast-path and OAuth wait) + $ident = if ($AgentUami) { $AgentUami } else { + Write-Host " WARN: agent has no user-assigned MI; falling back to SystemAssigned." + "SystemAssigned" + } + $connBody = @{ + name = "github" + type = "AgentConnector" + properties = @{ + dataConnectorType = "GitHubOAuth" + dataSource = "github-oauth" + identity = $ident } + } | ConvertTo-Json -Compress -Depth 10 - # 1) Create the GitHubOAuth connector - $connBody = @{ - name = "github" - type = "AgentConnector" - properties = @{ - dataConnectorType = "GitHubOAuth" - dataSource = "github-oauth" - identity = $ident - } - } | ConvertTo-Json -Compress -Depth 10 + $connectorOk = $false + + # Fast path: if auth/status says configured (or PAT), try the connector PUT immediately + if ($ghConfigured -or $env:GITHUB_PAT) { + Write-Host "-- Wiring GitHub connector + repos --" try { $token = Get-DpToken $headers = @{ Authorization = "Bearer $token" "Content-Type" = "application/json" } - $null = Invoke-RestMethod -Uri "$AgentEndpoint/api/v2/extendedAgent/connectors/github" ` + $null = Invoke-RestMethod -TimeoutSec 30 -Uri "$AgentEndpoint/api/v2/extendedAgent/connectors/github" ` -Method Put -Headers $headers -Body $connBody -ContentType "application/json" - Write-Host " ok connector/github (GitHubOAuth, identity=$($ident.Split('/')[-1]))" - } catch { - Write-Host " FAILED - PUT /api/v2/extendedAgent/connectors/github" - } - - # 2) Attach each repo - foreach ($repo in $repos) { - $rname = $repo.name - $rurl = $repo.spec.url - # Normalize short "org/repo" to full URL (API requires https://...) - if ($rurl -and $rurl -notmatch '^https?://' -and $rurl -match '/') { - $rurl = "https://github.com/$rurl" - } - $rtypeIn = if ($repo.spec.type) { $repo.spec.type } else { "github" } - $rtype = switch -Regex ($rtypeIn.ToLower()) { - '^(ado|azuredevops|azure-devops)$' { "AzureDevOps" } - default { "GitHub" } - } - $rdesc = if ($repo.spec.description) { $repo.spec.description } else { "" } - $rProps = @{ url = $rurl; type = $rtype } - if ($rdesc) { $rProps.description = $rdesc } - $rbody = @{ - name = $rname - type = "CodeRepo" - properties = $rProps - } | ConvertTo-Json -Compress -Depth 10 - $encodedRname = [uri]::EscapeDataString($rname) + # Verify the connector is actually healthy (not just metadata) + Start-Sleep -Seconds 3 try { - $null = Invoke-RestMethod -Uri "$AgentEndpoint/api/v2/repos/$encodedRname" ` - -Method Put -Headers $headers -Body $rbody -ContentType "application/json" - Write-Host " ok repo/$rname ($rurl)" + $connCheck = Invoke-RestMethod -TimeoutSec 30 -Uri "$AgentEndpoint/api/v2/extendedAgent/connectors/github" ` + -Method Get -Headers @{ Authorization = "Bearer $token" } + $provState = $connCheck.properties.provisioningState + if ($provState -eq 'Succeeded') { + Write-Host " ok connector/github (GitHubOAuth, identity=$($ident.Split('/')[-1]))" + $connectorOk = $true + } else { + Write-Host " WARN: connector created but state=$provState — falling back to OAuth wait..." + } } catch { - Write-Host " FAILED - PUT /api/v2/repos/$rname (try the portal Repos blade)" + Write-Host " ok connector/github (GitHubOAuth, identity=$($ident.Split('/')[-1]))" + $connectorOk = $true } + } catch { + Write-Host " WARN: connector PUT failed (stale auth?) — falling back to OAuth wait..." } - Write-Host "" - } else { - # OAuth not done — print sign-in URL + } + + # OAuth wait: if connector not yet created, show URL and poll until user completes auth + if (-not $connectorOk -and -not $env:GITHUB_PAT) { Write-Host "-- GitHub OAuth sign-in required --" Write-Host "Repos waiting: $($oauthRepos -join ' ')" $oauthUrl = $null + try { $token = Get-DpToken } catch { $token = $null } if ($token) { try { $headers = @{ Authorization = "Bearer $token" } - $ghConfig = Invoke-RestMethod -Uri "$AgentEndpoint/api/v1/Github/config" -Headers $headers + $ghConfig = Invoke-RestMethod -TimeoutSec 30 -Uri "$AgentEndpoint/api/v1/Github/config" -Headers $headers $oauthUrl = if ($ghConfig.oAuthUrl) { $ghConfig.oAuthUrl } elseif ($ghConfig.OAuthUrl) { $ghConfig.OAuthUrl } else { $null } } catch { } } @@ -1132,16 +1135,23 @@ if ($DpTokenAvailable) { Write-Host " 2. Sign in to GitHub and approve the SRE Agent app." Write-Host "" Write-Host " Waiting for GitHub authorization (Ctrl-C to skip)..." - $authOk = $false for ($attempt = 1; $attempt -le 24; $attempt++) { Start-Sleep -Seconds 10 try { $token = Get-DpToken - $headers = @{ Authorization = "Bearer $token" } - $ghCheck = Invoke-RestMethod -Uri "$AgentEndpoint/api/v1/Github/auth/status" -Headers $headers - if ($ghCheck.isConfigured -eq $true -or ($ghCheck.hosts -and $ghCheck.hosts[0].isConfigured -eq $true)) { + $headers = @{ + Authorization = "Bearer $token" + "Content-Type" = "application/json" + } + # Check auth/status — only trust isConfigured, not connector PUT success + $ghCheck = Invoke-RestMethod -TimeoutSec 30 -Uri "$AgentEndpoint/api/v1/Github/auth/status" -Headers $headers + $isAuth = ($ghCheck.isConfigured -eq $true) -or ($ghCheck.hosts -and $ghCheck.hosts[0].isConfigured -eq $true) + if ($isAuth) { + $null = Invoke-RestMethod -TimeoutSec 30 -Uri "$AgentEndpoint/api/v2/extendedAgent/connectors/github" ` + -Method Put -Headers $headers -Body $connBody -ContentType "application/json" Write-Host " GitHub authorized!" - $authOk = $true + Write-Host " ok connector/github" + $connectorOk = $true break } } catch { } @@ -1149,50 +1159,7 @@ if ($DpTokenAvailable) { Write-Host "`r" -NoNewline } Write-Host "" - - if ($authOk) { - # Re-enter the OAuth-done path: create connector + repos - Write-Host "-- Wiring GitHub connector + repos --" - $ident = if ($AgentUami) { $AgentUami } else { "SystemAssigned" } - $token = Get-DpToken - $headers = @{ - Authorization = "Bearer $token" - "Content-Type" = "application/json" - } - $connBody = @{ - name = "github"; type = "AgentConnector" - properties = @{ dataConnectorType = "GitHubOAuth"; dataSource = "github-oauth"; identity = $ident } - } | ConvertTo-Json -Compress -Depth 10 - try { - $null = Invoke-RestMethod -Uri "$AgentEndpoint/api/v2/extendedAgent/connectors/github" ` - -Method Put -Headers $headers -Body $connBody -ContentType "application/json" - Write-Host " ok connector/github" - } catch { - Write-Host " FAILED connector/github" - } - foreach ($repo in $repos) { - $rname = $repo.name - $rurl = $repo.spec.url - # Normalize short "org/repo" to full URL (API requires https://...) - if ($rurl -and $rurl -notmatch '^https?://' -and $rurl -match '/') { - $rurl = "https://github.com/$rurl" - } - $rtypeIn = if ($repo.spec.type) { $repo.spec.type } else { "github" } - $rtype = if ($rtypeIn.ToLower() -match '^(ado|azuredevops|azure-devops)$') { "AzureDevOps" } else { "GitHub" } - $rbody = @{ - name = $rname; type = "CodeRepo" - properties = @{ url = $rurl; type = $rtype } - } | ConvertTo-Json -Compress -Depth 10 - $encodedRname = [uri]::EscapeDataString($rname) - try { - $null = Invoke-RestMethod -Uri "$AgentEndpoint/api/v2/repos/$encodedRname" ` - -Method Put -Headers $headers -Body $rbody -ContentType "application/json" - Write-Host " ok repo/$rname" - } catch { - Write-Host " FAILED repo/$rname" - } - } - } else { + if (-not $connectorOk) { Write-Host " Timed out. Re-run Apply-Extras after authorizing." Write-Host " Headless alternative: `$env:GITHUB_PAT='ghp_xxx' && re-run" } @@ -1200,6 +1167,44 @@ if ($DpTokenAvailable) { Write-Host " Could not fetch OAuth URL from $AgentEndpoint/api/v1/Github/config." Write-Host " Fallback: Azure portal -> agent -> Repos -> 'Authorize' next to each repo." } + } + + # Wire repos (only if connector succeeded) + if ($connectorOk) { + Write-Host "-- Wiring GitHub repos --" + $token = Get-DpToken + $headers = @{ + Authorization = "Bearer $token" + "Content-Type" = "application/json" + } + foreach ($repo in $repos) { + $rname = $repo.name + $rurl = $repo.spec.url + if ($rurl -and $rurl -notmatch '^https?://' -and $rurl -match '/') { + $rurl = "https://github.com/$rurl" + } + $rtypeIn = if ($repo.spec.type) { $repo.spec.type } else { "github" } + $rtype = switch -Regex ($rtypeIn.ToLower()) { + '^(ado|azuredevops|azure-devops)$' { "AzureDevOps" } + default { "GitHub" } + } + $rdesc = if ($repo.spec.description) { $repo.spec.description } else { "" } + $rProps = @{ url = $rurl; type = $rtype } + if ($rdesc) { $rProps.description = $rdesc } + $rbody = @{ + name = $rname + type = "CodeRepo" + properties = $rProps + } | ConvertTo-Json -Compress -Depth 10 + $encodedRname = [uri]::EscapeDataString($rname) + try { + $null = Invoke-RestMethod -TimeoutSec 30 -Uri "$AgentEndpoint/api/v2/repos/$encodedRname" ` + -Method Put -Headers $headers -Body $rbody -ContentType "application/json" + Write-Host " ok repo/$rname ($rurl)" + } catch { + Write-Host " FAILED - PUT /api/v2/repos/$rname (try the portal Repos blade)" + } + } Write-Host "" } } diff --git a/sreagent-templates/bicep/Assemble-Agent.ps1 b/sreagent-templates/bicep/Assemble-Agent.ps1 index 9edf3c1b9..71035511a 100644 --- a/sreagent-templates/bicep/Assemble-Agent.ps1 +++ b/sreagent-templates/bicep/Assemble-Agent.ps1 @@ -135,7 +135,10 @@ print(json.dumps(resolve(data))) '@ try { - $result = $Json | & $Python -c $pyScript $BaseDir 2>$null + $pyTmp = [System.IO.Path]::GetTempFileName() + '.py' + Set-Content -Path $pyTmp -Value $pyScript -Encoding UTF8 + $result = $Json | & $Python $pyTmp $BaseDir 2>$null + Remove-Item $pyTmp -Force -ErrorAction SilentlyContinue if ($LASTEXITCODE -eq 0 -and $result) { return $result } } catch {} return $Json @@ -168,7 +171,10 @@ with open(sys.argv[1]) as fh: print(json.dumps(data)) '@ try { - $item = & $Python -c $pyYaml $f.FullName 2>$null + $pyTmp = [System.IO.Path]::GetTempFileName() + '.py' + Set-Content -Path $pyTmp -Value $pyYaml -Encoding UTF8 + $item = & $Python $pyTmp $f.FullName 2>$null + Remove-Item $pyTmp -Force -ErrorAction SilentlyContinue if ($LASTEXITCODE -ne 0 -or -not $item) { continue } # Use --slurpfile to safely pass JSON without --argjson quoting issues $tmpItem = [System.IO.Path]::GetTempFileName() @@ -215,7 +221,10 @@ print(json.dumps(sub(data))) '@ try { - $result = $Json | & $Python -c $pyScript 2>$null + $pyTmp = [System.IO.Path]::GetTempFileName() + '.py' + Set-Content -Path $pyTmp -Value $pyScript -Encoding UTF8 + $result = $Json | & $Python $pyTmp 2>$null + Remove-Item $pyTmp -Force -ErrorAction SilentlyContinue if ($LASTEXITCODE -eq 0 -and $result) { return $result } } catch {} return $Json @@ -413,12 +422,16 @@ if ($mdFiles.Count -gt 0) { # Build JSON item via Python to avoid jq --arg quoting issues with large content $tmpContent = [System.IO.Path]::GetTempFileName() Set-Content -Path $tmpContent -Value $content -NoNewline -Encoding UTF8 - $item = & $Python -c @" + $pyKnowledge = @' import json, sys with open(sys.argv[1]) as f: content = f.read() print(json.dumps({"name": sys.argv[2], "type": "KnowledgeText", "content": content})) -"@ $tmpContent $fname 2>$null +'@ + $pyTmp = [System.IO.Path]::GetTempFileName() + '.py' + Set-Content -Path $pyTmp -Value $pyKnowledge -Encoding UTF8 + $item = & $Python $pyTmp $tmpContent $fname 2>$null + Remove-Item $pyTmp -Force -ErrorAction SilentlyContinue Remove-Item $tmpContent -ErrorAction SilentlyContinue if ($LASTEXITCODE -eq 0 -and $item) { $tmpItem = [System.IO.Path]::GetTempFileName() diff --git a/sreagent-templates/bicep/apply-extras.sh b/sreagent-templates/bicep/apply-extras.sh index 6584a8ce8..9093ebae5 100755 --- a/sreagent-templates/bicep/apply-extras.sh +++ b/sreagent-templates/bicep/apply-extras.sh @@ -30,7 +30,9 @@ # ADO — set $ADO_PAT, $ADO_USE_AAD=1, or $ADO_USE_MI=1 (with $ADO_ORG). # # Usage: -# ./apply-extras.sh [extras-file] +# ./apply-extras.sh [extras-file-or-config-dir] +# +# If the 4th argument is a directory (agent config dir), extras are auto-assembled from it. set -euo pipefail @@ -41,7 +43,16 @@ FILE="${4:-extras.parameters.json}" FORCE="" for arg in "$@"; do [[ "$arg" == "--force" ]] && FORCE="true"; done -[[ -f "$FILE" ]] || { echo "extras file not found: $FILE" >&2; exit 1; } +# Auto-assemble if a config directory was passed instead of a file +if [[ -d "$FILE" ]]; then + CONFIG_DIR="$FILE" + ASSEMBLE_TMP="$(mktemp -d)/assembled" + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + bash "${SCRIPT_DIR}/assemble-agent.sh" "$CONFIG_DIR" --output "$ASSEMBLE_TMP" + FILE="${ASSEMBLE_TMP}.extras.json" +fi + +[[ -f "$FILE" ]] || { echo "extras file not found: $FILE (pass a config dir or pre-assembled extras.json)" >&2; exit 1; } command -v jq >/dev/null || { echo "jq is required" >&2; exit 1; } command -v tar >/dev/null || { echo "tar is required" >&2; exit 1; } command -v curl >/dev/null || { echo "curl is required" >&2; exit 1; } @@ -900,14 +911,23 @@ if [[ ${#oauth_repos[@]} -gt 0 ]]; then echo " 2. Sign in to GitHub and approve the SRE Agent app." echo echo " Waiting for GitHub authorization (Ctrl-C to skip)..." + if [[ -z "$AGENT_UAMI" ]]; then IDENT="SystemAssigned"; else IDENT="$AGENT_UAMI"; fi + conn_body=$(jq -nc --arg id "$IDENT" '{name:"github",type:"AgentConnector",properties:{dataConnectorType:"GitHubOAuth",dataSource:"github-oauth",identity:$id}}') auth_ok=false for attempt in $(seq 1 24); do sleep 10 TOKEN=$(_dp_token 2>/dev/null || true) + # Check auth/status — only trust isConfigured, not connector PUT success GH_CHECK=$(curl -sS -H "Authorization: Bearer ${TOKEN}" \ "${AGENT_ENDPOINT}/api/v1/Github/auth/status" 2>/dev/null || echo '{}') - if echo "$GH_CHECK" | jq -e '.isConfigured // .hosts[0].isConfigured' 2>/dev/null | grep -q 'true'; then + IS_AUTH=$(echo "$GH_CHECK" | jq -r '.isConfigured // .hosts[0].isConfigured // false') + if [[ "$IS_AUTH" == "true" ]]; then + # Auth confirmed — now create the connector + curl -sS -f -X PUT "${AGENT_ENDPOINT}/api/v2/extendedAgent/connectors/github" \ + -H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" \ + --data "$conn_body" >/dev/null 2>&1 || true echo " GitHub authorized!" + echo " ok connector/github" auth_ok=true break fi @@ -916,14 +936,9 @@ if [[ ${#oauth_repos[@]} -gt 0 ]]; then echo if [[ "$auth_ok" == "true" ]]; then - # Re-enter the OAuth-done path: create connector + repos - echo "── Wiring GitHub connector + repos ──" - if [[ -z "$AGENT_UAMI" ]]; then IDENT="SystemAssigned"; else IDENT="$AGENT_UAMI"; fi + # Connector already created in polling loop — wire repos only + echo "── Wiring GitHub repos ──" TOKEN=$(_dp_token) - body=$(jq -nc --arg id "$IDENT" '{name:"github",type:"AgentConnector",properties:{dataConnectorType:"GitHubOAuth",dataSource:"github-oauth",identity:$id}}') - curl -sS -f -X PUT "${AGENT_ENDPOINT}/api/v2/extendedAgent/connectors/github" \ - -H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" --data "$body" >/dev/null && \ - echo " ok connector/github" || echo " FAILED connector/github" count=$(jq '.repos // [] | length' "$FILE") for i in $(seq 0 $((count - 1))); do rname=$(jq -r --argjson i "$i" '.repos[$i].name' "$FILE") diff --git a/sreagent-templates/bin/export-agent.sh b/sreagent-templates/bin/export-agent.sh index b4fe0b168..346fc0b50 100755 --- a/sreagent-templates/bin/export-agent.sh +++ b/sreagent-templates/bin/export-agent.sh @@ -150,7 +150,7 @@ dp_get() { local path="$1" local token token=$(_dp_token) - curl -sS -f -H "Authorization: Bearer ${token}" "${AGENT_ENDPOINT}${path}" 2>/dev/null || echo "null" + curl -sS -f --max-time 10 -H "Authorization: Bearer ${token}" "${AGENT_ENDPOINT}${path}" 2>/dev/null || echo "null" } # Download a file from data-plane to local path diff --git a/sreagent-templates/bin/ps/Clone-Agent.ps1 b/sreagent-templates/bin/ps/Clone-Agent.ps1 index 9c0362229..7dc9dcae3 100644 --- a/sreagent-templates/bin/ps/Clone-Agent.ps1 +++ b/sreagent-templates/bin/ps/Clone-Agent.ps1 @@ -54,11 +54,13 @@ param( [string]$Source, [string]$FromAgent, [string]$FromResourceGroup, + [Alias('FromSubscriptionId')] [string]$FromSubscription, [Parameter(Mandatory)][string]$AgentName, [Parameter(Mandatory)][string]$ResourceGroup, [string]$Location, [string]$TargetResourceGroups, + [Alias('SubscriptionId')] [string]$Subscription, [switch]$ValidateOnly, [switch]$SkipExtras, diff --git a/sreagent-templates/bin/ps/Deploy-Agent.ps1 b/sreagent-templates/bin/ps/Deploy-Agent.ps1 index 7b1dcd05b..4d55c94d6 100644 --- a/sreagent-templates/bin/ps/Deploy-Agent.ps1 +++ b/sreagent-templates/bin/ps/Deploy-Agent.ps1 @@ -172,6 +172,12 @@ if ($IsDirectory) { $ExtrasFile = "${AssembleOut}.extras.json" $CleanupFiles += $ParametersFile, $ExtrasFile, (Split-Path $AssembleTmp -Parent) + # Copy extras.json into InputPath so it survives cleanup and can be re-used + # (e.g. re-running Apply-Extras standalone without a full re-deploy) + $AgentNameForExtras = (Get-Item $InputPath).Name + $PersistedExtras = Join-Path $InputPath "${AgentNameForExtras}.extras.json" + Copy-Item $ExtrasFile $PersistedExtras -Force + Write-Host '' } elseif (Test-Path $InputPath -PathType Leaf) { $ParametersFile = $InputPath diff --git a/sreagent-templates/bin/ps/Diff-Agent.ps1 b/sreagent-templates/bin/ps/Diff-Agent.ps1 index cfba21356..f330bbfe8 100644 --- a/sreagent-templates/bin/ps/Diff-Agent.ps1 +++ b/sreagent-templates/bin/ps/Diff-Agent.ps1 @@ -29,7 +29,7 @@ [CmdletBinding()] param( [Parameter(Mandatory)] - [Alias('s')] + [Alias('s', 'SubscriptionId')] [string]$Subscription, [Parameter(Mandatory)] @@ -77,7 +77,7 @@ $ARM_BASE = "https://management.azure.com/subscriptions/${Subscription}/resou # ─────────────────────────── Check if agent exists ─────────────────────────── -$AgentJson = az rest -m GET --url "${ARM_BASE}?api-version=${API_VERSION}" -o json 2>$null +$AgentJson = (az rest -m GET --url "${ARM_BASE}?api-version=${API_VERSION}" -o json 2>$null) -join "`n" if (-not $AgentJson) { $AgentJson = '{}' } $Endpoint = $AgentJson | Invoke-Jq -Raw -Filter '.properties.agentEndpoint // empty' @@ -154,7 +154,7 @@ if (-not $Token) { function Invoke-Dp { param([string]$Path) - curl -sS "${Endpoint}${Path}" -H "Authorization: Bearer $Token" 2>$null + (curl -sS "${Endpoint}${Path}" -H "Authorization: Bearer $Token" 2>$null) -join "`n" } # ─────────────────────────── Results tracking ─────────────────────────── diff --git a/sreagent-templates/bin/ps/Export-Agent.ps1 b/sreagent-templates/bin/ps/Export-Agent.ps1 index e13636a63..6aa4c97d5 100644 --- a/sreagent-templates/bin/ps/Export-Agent.ps1 +++ b/sreagent-templates/bin/ps/Export-Agent.ps1 @@ -61,7 +61,7 @@ [CmdletBinding()] param( [Parameter(Mandatory)] - [Alias('s')] + [Alias('s', 'SubscriptionId')] [string]$Subscription, [Parameter(Mandatory)] @@ -159,7 +159,11 @@ function _fail { param([string]$Msg) Write-Host " ERROR: $Msg" -ForegroundColor # ── JSON → YAML via python3 ── function ConvertTo-Yaml { param([Parameter(ValueFromPipeline)][string]$Json) - $result = $Json | python3 -c @" + begin { $inputLines = [System.Collections.Generic.List[string]]::new() } + process { if ($Json) { $inputLines.Add($Json) } } + end { + $fullJson = $inputLines -join "`n" + $result = $fullJson | python3 -c @" import sys, json, yaml def strip_nulls(obj): @@ -173,10 +177,13 @@ data = json.load(sys.stdin) data = strip_nulls(data) yaml.dump(data, sys.stdout, default_flow_style=False, sort_keys=False, allow_unicode=True) "@ - if ($LASTEXITCODE -ne 0 -or -not $result) { - throw "python3 YAML conversion failed (exit code: $LASTEXITCODE)" + if ($LASTEXITCODE -ne 0 -or -not $result) { + throw "python3 YAML conversion failed (exit code: $LASTEXITCODE)" + } + # PS captures native-command stdout as string[] (one per line). + # Join with newlines so Set-Content -NoNewline writes proper YAML. + return ($result -join "`n") } - return $result } function Write-Yaml { @@ -188,7 +195,7 @@ function Write-Yaml { function Invoke-ArmGet { param([string]$Url) try { - $result = az rest -m GET --url "${Url}?api-version=${API_VERSION}" -o json 2>$null + $result = (az rest -m GET --url "${Url}?api-version=${API_VERSION}" -o json 2>$null) -join "`n" if ($LASTEXITCODE -ne 0 -or -not $result) { return 'null' } return $result } catch { @@ -201,7 +208,7 @@ function Invoke-ArmList { param([string]$ChildType) $url = "${ARM_BASE}/${ChildType}?api-version=${API_VERSION}" try { - $result = az rest -m GET --url $url -o json 2>$null + $result = (az rest -m GET --url $url -o json 2>$null) -join "`n" if ($LASTEXITCODE -ne 0 -or -not $result) { return '[]' } return ($result | Invoke-Jq -Compact -Filter '.value // []') } catch { @@ -230,7 +237,7 @@ function Invoke-DpGet { param([string]$Path) $token = Get-DpToken try { - $result = curl -sS -f -H "Authorization: Bearer $token" "${AGENT_ENDPOINT}${Path}" 2>$null + $result = (curl -sS -f --max-time 10 -H "Authorization: Bearer $token" "${AGENT_ENDPOINT}${Path}" 2>$null) -join "`n" if ($LASTEXITCODE -ne 0 -or -not $result) { return 'null' } return $result } catch { @@ -250,25 +257,34 @@ function Invoke-DpDownload { # ── Data-plane download tarball ── function Invoke-DpDownloadTarball { - param([string]$Path, [string]$DestDir, [string]$Label) - $token = Get-DpToken - $tmpfile = [System.IO.Path]::GetTempFileName() + '.tar.gz' + param([string]$Path, [string]$DestDir, [string]$Label, [int]$Retries = 3) $ok = $false - try { - curl -sS -f -H "Authorization: Bearer $token" -o $tmpfile "${AGENT_ENDPOINT}${Path}" 2>$null - if ($LASTEXITCODE -eq 0) { - if (-not (Test-Path $DestDir)) { New-Item -ItemType Directory -Path $DestDir -Force | Out-Null } - tar -xzf $tmpfile -C $DestDir 2>$null - $count = (Get-ChildItem -Path $DestDir -File -Recurse -ErrorAction SilentlyContinue | Measure-Object).Count - _log " Downloaded ${Label}: ${count} file(s) → ${DestDir}" - $ok = $true - } else { - _log " WARN: Could not download ${Label}" + for ($attempt = 1; $attempt -le $Retries; $attempt++) { + $token = Get-DpToken + $tmpfile = [System.IO.Path]::GetTempFileName() + '.tar.gz' + try { + curl -sS -f -H "Authorization: Bearer $token" -o $tmpfile "${AGENT_ENDPOINT}${Path}" 2>$null + if ($LASTEXITCODE -eq 0) { + if (-not (Test-Path $DestDir)) { New-Item -ItemType Directory -Path $DestDir -Force | Out-Null } + tar -xzf $tmpfile -C $DestDir 2>$null + $count = (Get-ChildItem -Path $DestDir -File -Recurse -ErrorAction SilentlyContinue | Measure-Object).Count + _log " Downloaded ${Label}: ${count} file(s) → ${DestDir}" + $ok = $true + break + } + } catch { } + finally { + Remove-Item $tmpfile -Force -ErrorAction SilentlyContinue + } + if ($attempt -lt $Retries) { + _log " Retry ${attempt}/${Retries} for ${Label} download..." + Start-Sleep -Seconds (5 * $attempt) + # Refresh token on retry + $script:DpTokenCache = '' } - } catch { - _log " WARN: Could not download ${Label}" - } finally { - Remove-Item $tmpfile -Force -ErrorAction SilentlyContinue + } + if (-not $ok) { + _log " WARN: Could not download ${Label} after ${Retries} attempts" } return $ok } @@ -720,7 +736,7 @@ _log 'Checking for webhook bridge (Logic App)...' $BRIDGE_EXISTS = $false $bridgeId = "/subscriptions/${Subscription}/resourceGroups/${ResourceGroup}/providers/Microsoft.Logic/workflows/${AgentName}-webhook-bridge" try { - $BRIDGE_JSON = az resource show --ids $bridgeId -o json 2>$null + $BRIDGE_JSON = (az resource show --ids $bridgeId -o json 2>$null) -join "`n" if ($LASTEXITCODE -eq 0 -and $BRIDGE_JSON -and $BRIDGE_JSON -ne 'null') { $BRIDGE_EXISTS = $true _log " Found webhook bridge: ${AgentName}-webhook-bridge" @@ -1049,7 +1065,7 @@ Invoke-Jq -Filter '{ '--arg', 'action', $ACTION_MODE, '--slurpfile', 'targetRgs', $trgTmpFile, '--arg', 'enableBridge', $bridgeBool - ) -InputFile '/dev/null' | Set-Content -Path (Join-Path $EXPORT_DIR 'agent.json') -Encoding utf8 + ) | Set-Content -Path (Join-Path $EXPORT_DIR 'agent.json') -Encoding utf8 Remove-Item $trgTmpFile -Force -ErrorAction SilentlyContinue # Apply --set overrides @@ -1158,13 +1174,20 @@ $enableAIStr = if ($ENABLE_AI) { 'true' } else { 'false' } $enableLAWStr = if ($ENABLE_LAW) { 'true' } else { 'false' } $enableAzMonStr = if ($ENABLE_AZMON) { 'true' } else { 'false' } +# PowerShell drops empty-string arguments when calling native commands, which +# misaligns jq --arg pairs. Default to a single space so the arg is preserved; +# the values are only used when their toggle is true. +if ([string]::IsNullOrEmpty($AI_RESOURCE_ID)) { $AI_RESOURCE_ID = ' ' } +if ([string]::IsNullOrEmpty($AI_APP_ID)) { $AI_APP_ID = ' ' } +if ([string]::IsNullOrEmpty($LAW_RESOURCE_ID)) { $LAW_RESOURCE_ID = ' ' } + $CONNECTORS_ARRAY | Invoke-Jq -Filter '{ "toggles": { "enableAppInsightsConnector": ($enableAI | test("true")), - "appInsightsResourceId": $aiResId, - "appInsightsAppId": $aiAppId, + "appInsightsResourceId": ($aiResId | ltrimstr(" ")), + "appInsightsAppId": ($aiAppId | ltrimstr(" ")), "enableLogAnalyticsConnector": ($enableLAW | test("true")), - "lawResourceId": $lawResId, + "lawResourceId": ($lawResId | ltrimstr(" ")), "enableAzureMonitorConnector": ($enableAzMon | test("true")), "azureMonitorLookbackDays": ($azMonLookback | tonumber) }, @@ -1283,7 +1306,7 @@ Invoke-Jq -Filter '{ '--slurpfile', 'tasks', (Join-Path $ecTmpDir 'tasks.json'), '--slurpfile', 'plans', (Join-Path $ecTmpDir 'plans.json'), '--slurpfile', 'repos', (Join-Path $ecTmpDir 'repos.json') - ) -InputFile '/dev/null' | Set-Content -Path (Join-Path $EXPORT_DIR 'expected-config.json') -Encoding utf8 + ) | Set-Content -Path (Join-Path $EXPORT_DIR 'expected-config.json') -Encoding utf8 Remove-Item $ecTmpDir -Recurse -Force -ErrorAction SilentlyContinue _log 'Wrote expected-config.json' diff --git a/sreagent-templates/bin/ps/Invoke-Jq.ps1 b/sreagent-templates/bin/ps/Invoke-Jq.ps1 index f02d855e8..b78b9d97b 100644 --- a/sreagent-templates/bin/ps/Invoke-Jq.ps1 +++ b/sreagent-templates/bin/ps/Invoke-Jq.ps1 @@ -32,7 +32,15 @@ function Invoke-Jq { [switch]$Slurp, [switch]$ExitTest # like jq -e ) + begin { + # Accumulate all pipeline input so multi-line JSON (e.g. from az rest) + # and ($a, $b) | Invoke-Jq both work correctly. + $inputLines = [System.Collections.Generic.List[string]]::new() + } process { + if ($InputJson) { $inputLines.Add($InputJson) } + } + end { $tmpFilter = [System.IO.Path]::GetTempFileName() try { Set-Content -Path $tmpFilter -Value $Filter -NoNewline -Encoding UTF8 @@ -46,11 +54,14 @@ function Invoke-Jq { if ($InputFile) { return (jq @flags $InputFile) } - elseif ($InputJson) { - return ($InputJson | jq @flags) + elseif ($inputLines.Count -gt 0) { + $joined = $inputLines -join "`n" + return ($joined | jq @flags) } else { - return (jq @flags) + # No pipeline input and no InputFile — use jq -n (null-input mode) + # so jq doesn't block waiting on stdin. + return (jq '-n' @flags) } } finally { diff --git a/sreagent-templates/bin/ps/Verify-Agent.ps1 b/sreagent-templates/bin/ps/Verify-Agent.ps1 index 9f1bfd49c..6785d36eb 100644 --- a/sreagent-templates/bin/ps/Verify-Agent.ps1 +++ b/sreagent-templates/bin/ps/Verify-Agent.ps1 @@ -26,7 +26,7 @@ [CmdletBinding()] param( [Parameter(Mandatory)] - [Alias('s')] + [Alias('s', 'SubscriptionId')] [string]$Subscription, [Parameter(Mandatory)] @@ -87,7 +87,7 @@ function Get-Exp { function Get-ExpList { param([string]$JqPath) if ($ExpectedConfig) { - return ($ExpectedConfig | Invoke-Jq -Raw -Filter "$JqPath // [] | sort | join(`,`)") + return ($ExpectedConfig | Invoke-Jq -Raw -Filter "$JqPath // [] | sort | join(`",`")") } return '' } @@ -97,7 +97,7 @@ function Get-ExpList { $API_VERSION = '2025-05-01-preview' $ARM_BASE = "https://management.azure.com/subscriptions/${Subscription}/resourceGroups/${ResourceGroup}/providers/Microsoft.App/agents/${AgentName}" -$AgentJson = az rest -m GET --url "${ARM_BASE}?api-version=${API_VERSION}" -o json 2>$null +$AgentJson = (az rest -m GET --url "${ARM_BASE}?api-version=${API_VERSION}" -o json 2>$null) -join "`n" if (-not $AgentJson) { $AgentJson = '{}' } $Endpoint = $AgentJson | Invoke-Jq -Raw -Filter '.properties.agentEndpoint // empty' @@ -114,12 +114,20 @@ if (-not $Token) { function Invoke-Dp { param([string]$Path) - curl -sS "${Endpoint}${Path}" -H "Authorization: Bearer $Token" 2>$null + $raw = (curl -sS "${Endpoint}${Path}" -H "Authorization: Bearer $Token" 2>$null) -join "`n" + # Validate response is JSON; return empty object/array fallback if not + if ($raw) { + try { + $null = $raw | jq -e 'type' 2>$null + if ($LASTEXITCODE -eq 0) { return $raw } + } catch { } + } + return '{}' } function Invoke-Arm { param([string]$Path) - $result = az rest -m GET --url "${ARM_BASE}${Path}?api-version=${API_VERSION}" -o json 2>$null + $result = (az rest -m GET --url "${ARM_BASE}${Path}?api-version=${API_VERSION}" -o json 2>$null) -join "`n" if (-not $result) { return '{}' } return $result } @@ -184,20 +192,20 @@ $Props = $AgentJson | Invoke-Jq -Compact -Filter '{ }' Add-Check 'Agent exists' 'yes' 'yes' -Add-Check 'Access level' ($Props | jq -r '.accessLevel') (Get-Exp '.agent.accessLevel') -Add-Check 'Action mode' ($Props | jq -r '.mode') (Get-Exp '.agent.actionMode') -Add-Check 'Upgrade channel' ($Props | jq -r '.upgradeChannel') (Get-Exp '.agent.upgradeChannel') -Add-Check 'Model provider' ($Props | jq -r '.modelProvider') (Get-Exp '.agent.defaultModelProvider') -Add-Check 'Incident platform' ($Props | jq -r '.incidentPlatform') (Get-Exp '.agent.incidentPlatform') +Add-Check 'Access level' ($Props | Invoke-Jq -Raw -Filter '.accessLevel') (Get-Exp '.agent.accessLevel') +Add-Check 'Action mode' ($Props | Invoke-Jq -Raw -Filter '.mode') (Get-Exp '.agent.actionMode') +Add-Check 'Upgrade channel' ($Props | Invoke-Jq -Raw -Filter '.upgradeChannel') (Get-Exp '.agent.upgradeChannel') +Add-Check 'Model provider' ($Props | Invoke-Jq -Raw -Filter '.modelProvider') (Get-Exp '.agent.defaultModelProvider') +Add-Check 'Incident platform' ($Props | Invoke-Jq -Raw -Filter '.incidentPlatform') (Get-Exp '.agent.incidentPlatform') # ─────────────────────────── Connectors (ARM) ─────────────────────────── $Connectors = Invoke-Arm '/DataConnectors' -$ConnCt = $Connectors | jq '.value | length' +$ConnCt = $Connectors | Invoke-Jq -Filter '.value | length' $ConnHealthy = $Connectors | Invoke-Jq -Filter '[.value[] | select(.properties.provisioningState == "Succeeded")] | length' -$ConnNames = ($Connectors | jq -r '.value[].name' 2>$null | Sort-Object) -join ',' +$ConnNames = ($Connectors | Invoke-Jq -Raw -Filter '.value[].name' | Sort-Object) -join ',' $ExpConnCt = Get-Exp '.connectors | length' -$ExpConnNames = Get-ExpList '.connectors[].name' +$ExpConnNames = Get-ExpList '[.connectors[].name]' Add-Check 'Connectors (total)' $ConnCt $ExpConnCt Add-Check 'Connectors (healthy)' $ConnHealthy $ConnCt @@ -226,9 +234,9 @@ if ($ExpSkillNames) { # ─────────────────────────── Subagents ─────────────────────────── $Subagents = Invoke-Dp '/api/v2/extendedAgent/agents' -$SaCt = $Subagents | jq '.value | length' 2>$null +$SaCt = $Subagents | Invoke-Jq -Filter '.value | length' if (-not $SaCt) { $SaCt = '0' } -$SaNames = ($Subagents | jq -r '.value[].name' 2>$null | Sort-Object) -join ',' +$SaNames = ($Subagents | Invoke-Jq -Raw -Filter '.value[].name' | Sort-Object) -join ',' $ExpSaCt = Get-Exp '.subagents | length' $ExpSaNames = Get-ExpList '.subagents' @@ -276,7 +284,7 @@ if ($ExpPromptNames) { $Tasks = Invoke-Dp '/api/v1/scheduledtasks' $TaskCt = $Tasks | Invoke-Jq -Filter 'if type == "array" then length else 0 end' if (-not $TaskCt) { $TaskCt = '0' } -$TaskUnique = $Tasks | jq '[.[].name] | unique | length' 2>$null +$TaskUnique = $Tasks | Invoke-Jq -Filter '[.[].name] | unique | length' if (-not $TaskUnique) { $TaskUnique = '0' } $TaskNames = $Tasks | Invoke-Jq -Raw -Filter '[.[].name] | unique | sort | join(",")' $ExpTaskCt = Get-Exp '.scheduledTasks | length' @@ -295,9 +303,9 @@ if ($TaskCt -ne $TaskUnique) { $Filters = Invoke-Dp '/api/v1/incidentPlayground/filters' $FilterCt = $Filters | Invoke-Jq -Filter 'if type == "array" then length else 0 end' if (-not $FilterCt) { $FilterCt = '0' } -$FilterNames = ($Filters | jq -r '.[].id' 2>$null | Sort-Object) -join ',' +$FilterNames = ($Filters | Invoke-Jq -Raw -Filter '[.[].id] | sort | join(",")' ) $ExpFilterCt = Get-Exp '.responsePlans | length' -$ExpFilterNames = Get-ExpList '.responsePlans[].name' +$ExpFilterNames = Get-ExpList '[.responsePlans[].name]' Add-Check 'Response Plans' $FilterCt $ExpFilterCt if ($ExpFilterNames) { diff --git a/sreagent-templates/bin/verify-agent.sh b/sreagent-templates/bin/verify-agent.sh index 6186dc6f2..5d8107ec2 100755 --- a/sreagent-templates/bin/verify-agent.sh +++ b/sreagent-templates/bin/verify-agent.sh @@ -9,6 +9,13 @@ set -uo pipefail +# ── Colors ── +if [[ -t 1 ]]; then + GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[0;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' +else + GREEN='' RED='' YELLOW='' CYAN='' BOLD='' NC='' +fi + SUB="${1:?subscription-id required}" RG="${2:?resource-group required}" AGENT="${3:?agent-name required}" @@ -61,7 +68,16 @@ if [[ -z "$TOKEN" ]]; then exit 1 fi -dp_get() { curl -sS "$ENDPOINT$1" -H "Authorization: Bearer $TOKEN" 2>/dev/null; } +dp_get() { + local raw + raw=$(curl -sS "$ENDPOINT$1" -H "Authorization: Bearer $TOKEN" 2>/dev/null) + # Validate JSON; return empty object if response is HTML/error + if echo "$raw" | jq -e 'type' >/dev/null 2>&1; then + echo "$raw" + else + echo '{}' + fi +} arm_get() { az rest -m GET --url "${ARM_BASE}$1?api-version=${API_VERSION}" -o json 2>/dev/null || echo "{}"; } PASS=0 @@ -71,22 +87,22 @@ RESULTS="" check() { local name="$1" actual="$2" expected="$3" if [[ "$expected" == "-" ]]; then - RESULTS="${RESULTS}\n ${name}|${actual}|—|✅" + RESULTS="${RESULTS}\n ${name}|${actual}|—|${GREEN}✅${NC}" PASS=$((PASS + 1)) elif [[ "$actual" == "$expected" ]]; then - RESULTS="${RESULTS}\n ${name}|${actual}|${expected}|✅ PASS" + RESULTS="${RESULTS}\n ${name}|${actual}|${expected}|${GREEN}✅ PASS${NC}" PASS=$((PASS + 1)) else - RESULTS="${RESULTS}\n ${name}|${actual}|${expected}|❌ FAIL" + RESULTS="${RESULTS}\n ${name}|${actual}|${expected}|${RED}❌ FAIL${NC}" FAIL=$((FAIL + 1)) fi } echo "" -echo "═══════════════════════════════════════════════════" -echo " SRE Agent Verification: ${AGENT}" -echo " Endpoint: ${ENDPOINT}" -echo "═══════════════════════════════════════════════════" +echo -e "${BOLD}═══════════════════════════════════════════════════${NC}" +echo -e " ${BOLD}SRE Agent Verification:${NC} ${CYAN}${AGENT}${NC}" +echo -e " ${BOLD}Endpoint:${NC} ${CYAN}${ENDPOINT}${NC}" +echo -e "${BOLD}═══════════════════════════════════════════════════${NC}" echo "" # ── Agent properties ── @@ -116,7 +132,7 @@ check "Connectors (healthy)" "$CONN_HEALTHY" "$CONN_CT" # Show errored connectors explicitly if [[ "$CONN_ERRORED" -gt 0 ]]; then ERRORED_LIST=$(echo "$CONNECTORS" | jq -r '.value[] | select(.properties.provisioningState != "Succeeded" and .properties.provisioningState != "Running") | "\(.name) (\(.properties.dataConnectorType)): \(.properties.provisioningState)"') - RESULTS="${RESULTS}\n ⚠ Errored connectors|${ERRORED_LIST}||❌ FAIL" + RESULTS="${RESULTS}\n ⚠ Errored connectors|${ERRORED_LIST}||${RED}❌ FAIL${NC}" FAIL=$((FAIL + 1)) fi CONN_NAMES=$(echo "$CONNECTORS" | jq -r '.value[].name' 2>/dev/null | sort | tr '\n' ', ' | sed 's/,$//') @@ -197,18 +213,22 @@ check "Repos" "$REPO_CT" "$EXP_REPO_CT" # ── Print results ── echo "" -printf " %-25s %-10s %-10s %s\n" "Check" "Actual" "Expected" "Result" +printf " ${BOLD}%-25s %-10s %-10s %s${NC}\n" "Check" "Actual" "Expected" "Result" printf " %-25s %-10s %-10s %s\n" "─────────────────────────" "──────────" "──────────" "──────" echo -e "$RESULTS" | while IFS='|' read -r name actual expected result; do [[ -z "$name" ]] && continue - printf " %-25s %-10s %-10s %s\n" "$name" "$actual" "$expected" "$result" + printf " %-25s %-10s %-10s %b\n" "$name" "$actual" "$expected" "$result" done echo "" -echo "═══════════════════════════════════════════════════" -echo " Results: ${PASS} passed, ${FAIL} failed" -echo " Portal: https://sre.azure.com/#/agent/${SUB}/${RG}/${AGENT}" -echo "═══════════════════════════════════════════════════" +echo -e "${BOLD}═══════════════════════════════════════════════════${NC}" +if [[ "$FAIL" -gt 0 ]]; then + echo -e " Results: ${GREEN}${PASS} passed${NC}, ${RED}${FAIL} failed${NC}" +else + echo -e " Results: ${GREEN}${PASS} passed${NC}, ${FAIL} failed" +fi +echo -e " Portal: ${CYAN}https://sre.azure.com/#/agent/${SUB}/${RG}/${AGENT}${NC}" +echo -e "${BOLD}═══════════════════════════════════════════════════${NC}" echo "" [[ "$FAIL" -gt 0 ]] && exit 1