feat(devops): local GitLab secret bootstrap + integration registration scripts

Adds PowerShell helpers to seed the local Stella Ops stack with a working
GitLab + integrations configuration:
- bootstrap-local-gitlab-secrets.ps1 provisions GitLab's JWT signing secret
  and admin PAT into Vault/Authority.
- register-local-integrations.ps1 POSTs the canonical integration records
  (GitLab, Jenkins, Harbor, Gitea, Nexus, etc.) against the Integrations
  service for first-run local environments.

Docs: INSTALL_GUIDE.md + integrations/LOCAL_SERVICES.md document the new
helpers. devops/compose README and router-gateway-local.json get the
corresponding route wiring. Two new sprint files track the follow-on work
(SPRINT_20260413_002, SPRINT_20260413_003).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-13 21:59:13 +03:00
parent 71dd1efc34
commit a19987979d
8 changed files with 934 additions and 2 deletions

View File

@@ -0,0 +1,351 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Bootstraps local GitLab credentials into the dev Vault for local integrations.
.DESCRIPTION
Reuses the current `secret/gitlab` credentials when they still pass the local
GitLab API probe (and optional registry token exchange probe). Otherwise, the
script signs in to the local GitLab CE instance as the configured root user,
revokes any earlier bootstrap personal access tokens with the same name,
creates a replacement token, verifies it, and stores:
- `access-token`
- `registry-basic`
under the configured Vault KV v2 path.
.PARAMETER GitLabUrl
Base URL of the local GitLab CE web service.
.PARAMETER GitLabRegistryUrl
Base URL of the local GitLab container registry endpoint.
.PARAMETER GitLabUsername
Local GitLab admin username used for the bootstrap flow.
.PARAMETER GitLabPassword
Local GitLab admin password used for the bootstrap flow.
.PARAMETER VaultUrl
Base URL of the local Vault dev server.
.PARAMETER VaultToken
Vault token used to read and write the local secret path.
.PARAMETER VaultSecretPath
KV v2 path in `<mount>/<path>` form. Defaults to `secret/gitlab`.
.PARAMETER TokenName
GitLab personal access token name used for the local integration bootstrap.
.PARAMETER TokenLifetimeDays
Lifetime of the generated GitLab personal access token.
.PARAMETER Rotate
Forces PAT rotation even when the current Vault secret still verifies cleanly.
.PARAMETER VerifyRegistry
Also verify that the stored or generated `registry-basic` secret can exchange
against GitLab's `/jwt/auth` registry token endpoint.
#>
[CmdletBinding()]
param(
[string]$GitLabUrl = 'http://gitlab.stella-ops.local:8929',
[string]$GitLabRegistryUrl = 'http://gitlab.stella-ops.local:5050',
[string]$GitLabUsername = 'root',
[string]$GitLabPassword = 'Stella2026!',
[string]$VaultUrl = 'http://vault.stella-ops.local:8200',
[string]$VaultToken = 'stellaops-dev-root-token-2026',
[string]$VaultSecretPath = 'secret/gitlab',
[string]$TokenName = 'stella-local-integration',
[ValidateRange(1, 365)]
[int]$TokenLifetimeDays = 30,
[ValidateRange(10, 900)]
[int]$ReadinessTimeoutSeconds = 600,
[switch]$Rotate,
[switch]$VerifyRegistry
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$GitLabUrl = $GitLabUrl.TrimEnd('/')
$GitLabRegistryUrl = $GitLabRegistryUrl.TrimEnd('/')
$VaultUrl = $VaultUrl.TrimEnd('/')
function Get-HttpStatusCode {
param([Parameter(Mandatory)]$ErrorRecord)
$response = $ErrorRecord.Exception.Response
if ($null -eq $response) {
return $null
}
if ($response.PSObject.Properties.Name -contains 'StatusCode') {
$statusCode = $response.StatusCode
if ($statusCode -is [int]) {
return $statusCode
}
try {
return [int]$statusCode
} catch {
return $null
}
}
return $null
}
function Wait-HttpReady {
param(
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$Uri,
[hashtable]$Headers,
[int]$TimeoutSeconds = 300
)
$deadline = (Get-Date).ToUniversalTime().AddSeconds($TimeoutSeconds)
do {
try {
Invoke-RestMethod -Method GET -Uri $Uri -Headers $Headers -TimeoutSec 30 -ErrorAction Stop | Out-Null
return
} catch {
if ((Get-Date).ToUniversalTime() -ge $deadline) {
throw "Timed out waiting for $Name at $Uri. Last error: $($_.Exception.Message)"
}
Start-Sleep -Seconds 5
}
} while ($true)
}
function Split-VaultSecretPath {
param([Parameter(Mandatory)][string]$Path)
$normalized = $Path.Trim('/')
$segments = $normalized -split '/', 2
if ($segments.Count -ne 2 -or [string]::IsNullOrWhiteSpace($segments[0]) -or [string]::IsNullOrWhiteSpace($segments[1])) {
throw "VaultSecretPath must be in '<mount>/<path>' form. Received '$Path'."
}
return [pscustomobject]@{
Mount = $segments[0]
Path = $segments[1]
}
}
function Invoke-VaultJson {
param(
[Parameter(Mandatory)][ValidateSet('GET', 'POST')][string]$Method,
[Parameter(Mandatory)][string]$Path,
[object]$Body
)
$parameters = @{
Method = $Method
Uri = "$VaultUrl/v1/$Path"
Headers = @{ 'X-Vault-Token' = $VaultToken }
TimeoutSec = 30
ErrorAction = 'Stop'
}
if ($null -ne $Body) {
$parameters['ContentType'] = 'application/json'
$parameters['Body'] = $Body | ConvertTo-Json -Depth 10
}
return Invoke-RestMethod @parameters
}
function Get-VaultGitLabSecret {
param([Parameter(Mandatory)]$VaultPathParts)
try {
$response = Invoke-VaultJson -Method GET -Path "$($VaultPathParts.Mount)/data/$($VaultPathParts.Path)"
return $response.data.data
} catch {
$statusCode = Get-HttpStatusCode -ErrorRecord $_
if ($statusCode -eq 404) {
return $null
}
throw
}
}
function Set-VaultGitLabSecret {
param(
[Parameter(Mandatory)]$VaultPathParts,
[Parameter(Mandatory)][hashtable]$Data
)
Invoke-VaultJson -Method POST -Path "$($VaultPathParts.Mount)/data/$($VaultPathParts.Path)" -Body @{ data = $Data } | Out-Null
}
function Get-GitLabOAuthToken {
$response = Invoke-RestMethod -Method POST -Uri "$GitLabUrl/oauth/token" -ContentType 'application/x-www-form-urlencoded' -Body @{
grant_type = 'password'
username = $GitLabUsername
password = $GitLabPassword
} -TimeoutSec 60 -ErrorAction Stop
if ([string]::IsNullOrWhiteSpace($response.access_token)) {
throw "GitLab OAuth password grant did not return an access token."
}
return "$($response.access_token)"
}
function Get-GitLabAdminHeaders {
$deadline = (Get-Date).ToUniversalTime().AddSeconds($ReadinessTimeoutSeconds)
do {
try {
$oauthToken = Get-GitLabOAuthToken
return @{
Authorization = "Bearer $oauthToken"
}
} catch {
if ((Get-Date).ToUniversalTime() -ge $deadline) {
throw "Timed out waiting for GitLab admin login at $GitLabUrl. Last error: $($_.Exception.Message)"
}
Start-Sleep -Seconds 5
}
} while ($true)
}
function Get-GitLabCurrentUser {
param([Parameter(Mandatory)][hashtable]$Headers)
return Invoke-RestMethod -Method GET -Uri "$GitLabUrl/api/v4/user" -Headers $Headers -TimeoutSec 30 -ErrorAction Stop
}
function Get-GitLabPersonalAccessTokens {
param(
[Parameter(Mandatory)][hashtable]$Headers,
[Parameter(Mandatory)][int]$UserId
)
$response = Invoke-RestMethod -Method GET -Uri "$GitLabUrl/api/v4/personal_access_tokens?user_id=$UserId" -Headers $Headers -TimeoutSec 30 -ErrorAction Stop
return @($response)
}
function Revoke-GitLabPersonalAccessToken {
param(
[Parameter(Mandatory)][hashtable]$Headers,
[Parameter(Mandatory)][int]$TokenId
)
Invoke-RestMethod -Method DELETE -Uri "$GitLabUrl/api/v4/personal_access_tokens/$TokenId" -Headers $Headers -TimeoutSec 30 -ErrorAction Stop | Out-Null
}
function New-GitLabPersonalAccessToken {
param(
[Parameter(Mandatory)][hashtable]$Headers,
[Parameter(Mandatory)][int]$UserId,
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string[]]$Scopes,
[Parameter(Mandatory)][string]$ExpiresAt
)
$body = @{
name = $Name
description = 'Stella Ops local integration bootstrap'
expires_at = $ExpiresAt
scopes = $Scopes
} | ConvertTo-Json -Depth 5
return Invoke-RestMethod -Method POST -Uri "$GitLabUrl/api/v4/users/$UserId/personal_access_tokens" -Headers $Headers -ContentType 'application/json' -Body $body -TimeoutSec 30 -ErrorAction Stop
}
function Test-GitLabApiToken {
param([Parameter(Mandatory)][string]$Token)
if ([string]::IsNullOrWhiteSpace($Token)) {
return $false
}
try {
$response = Invoke-RestMethod -Method GET -Uri "$GitLabUrl/api/v4/version" -Headers @{ 'PRIVATE-TOKEN' = $Token } -TimeoutSec 30 -ErrorAction Stop
return -not [string]::IsNullOrWhiteSpace($response.version)
} catch {
return $false
}
}
function Test-GitLabRegistryCredential {
param([Parameter(Mandatory)][string]$RegistryBasic)
if ([string]::IsNullOrWhiteSpace($RegistryBasic) -or -not $RegistryBasic.Contains(':')) {
return $false
}
try {
Invoke-WebRequest -Method GET -Uri "$GitLabRegistryUrl/v2/" -TimeoutSec 30 -ErrorAction Stop | Out-Null
} catch {
$statusCode = Get-HttpStatusCode -ErrorRecord $_
if ($statusCode -ne 401) {
return $false
}
}
try {
$encoded = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($RegistryBasic))
$response = Invoke-RestMethod -Method GET -Uri "$GitLabUrl/jwt/auth?service=container_registry&scope=registry:catalog:*" -Headers @{ Authorization = "Basic $encoded" } -TimeoutSec 30 -ErrorAction Stop
return -not [string]::IsNullOrWhiteSpace($response.token)
} catch {
return $false
}
}
$vaultPathParts = Split-VaultSecretPath -Path $VaultSecretPath
Write-Host "Waiting for Vault and GitLab to become ready..." -ForegroundColor Cyan
Wait-HttpReady -Name 'Vault' -Uri "$VaultUrl/v1/sys/health" -TimeoutSeconds $ReadinessTimeoutSeconds
$existingSecret = Get-VaultGitLabSecret -VaultPathParts $vaultPathParts
if (-not $Rotate -and $null -ne $existingSecret) {
$apiValid = Test-GitLabApiToken -Token "$($existingSecret.'access-token')"
$registryValid = $true
if ($VerifyRegistry) {
$registryValid = Test-GitLabRegistryCredential -RegistryBasic "$($existingSecret.'registry-basic')"
}
if ($apiValid -and $registryValid) {
Write-Host "Existing GitLab bootstrap secret at '$VaultSecretPath' is still valid; reusing it." -ForegroundColor Green
Write-Host "AuthRefs: authref://vault/gitlab#access-token, authref://vault/gitlab#registry-basic"
return
}
Write-Host "Existing GitLab bootstrap secret at '$VaultSecretPath' is stale or incomplete; rotating it." -ForegroundColor Yellow
}
Write-Host "Signing in to GitLab and reconciling the local bootstrap PAT..." -ForegroundColor Cyan
$adminHeaders = Get-GitLabAdminHeaders
$currentUser = Get-GitLabCurrentUser -Headers $adminHeaders
$expiresAt = (Get-Date).ToUniversalTime().AddDays($TokenLifetimeDays).ToString('yyyy-MM-dd')
$tokensToRevoke = Get-GitLabPersonalAccessTokens -Headers $adminHeaders -UserId ([int]$currentUser.id) |
Where-Object { $_.name -eq $TokenName -and $_.active -and -not $_.revoked }
foreach ($token in $tokensToRevoke) {
Revoke-GitLabPersonalAccessToken -Headers $adminHeaders -TokenId ([int]$token.id)
}
$newToken = New-GitLabPersonalAccessToken -Headers $adminHeaders -UserId ([int]$currentUser.id) -Name $TokenName -Scopes @('api', 'read_registry') -ExpiresAt $expiresAt
$registryBasic = "${GitLabUsername}:$($newToken.token)"
Write-Host "Verifying the new GitLab PAT against the API and registry surfaces..." -ForegroundColor Cyan
if (-not (Test-GitLabApiToken -Token "$($newToken.token)")) {
throw "The newly created GitLab personal access token failed the API probe."
}
if ($VerifyRegistry -and -not (Test-GitLabRegistryCredential -RegistryBasic $registryBasic)) {
throw "The newly created GitLab personal access token failed the registry token exchange probe. Ensure the GitLab registry surface is enabled."
}
$secretData = @{
'access-token' = "$($newToken.token)"
'registry-basic' = $registryBasic
'bootstrap-user' = $GitLabUsername
'token-name' = $TokenName
'expires-at' = "$($newToken.expires_at)"
'rotated-at' = (Get-Date).ToUniversalTime().ToString('o')
}
Set-VaultGitLabSecret -VaultPathParts $vaultPathParts -Data $secretData
Write-Host "Bootstrapped GitLab PAT material into Vault path '$VaultSecretPath'." -ForegroundColor Green
Write-Host "AuthRefs: authref://vault/gitlab#access-token, authref://vault/gitlab#registry-basic"
Write-Host "Token name: $TokenName"
Write-Host "Expires at: $($newToken.expires_at)"

View File

@@ -0,0 +1,301 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Registers the locally reachable integration catalog entries for a tenant.
.DESCRIPTION
Uses the live Integrations API exposed by the local Docker stack to create
any missing local-capable providers, then runs test and health checks for
each entry so the catalog converges to a ready local lane.
.PARAMETER Tenant
Tenant identifier used for the catalog operations. Defaults to demo-prod.
.PARAMETER BaseUrl
Base URL for the local Integrations API. Defaults to the host-mapped
integrations-web endpoint.
.PARAMETER IncludeGitLab
Also register the GitLab Server and GitLab CI providers. This requires
authref://vault/gitlab#access-token to be populated in Vault.
.PARAMETER IncludeGitLabRegistry
Also register the GitLab Container Registry provider. This requires the
heavy GitLab profile with registry enabled plus authref://vault/gitlab#registry-basic.
.PARAMETER BootstrapGitLabSecrets
When used with `-IncludeGitLab` or `-IncludeGitLabRegistry`, bootstrap or
rotate the local GitLab PAT material into Vault automatically before the
GitLab-backed integrations are registered.
#>
[CmdletBinding()]
param(
[string]$Tenant = 'demo-prod',
[string]$BaseUrl = 'http://127.1.0.42',
[switch]$IncludeGitLab,
[switch]$IncludeGitLabRegistry,
[switch]$BootstrapGitLabSecrets
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$BaseUrl = $BaseUrl.TrimEnd('/')
$Headers = @{
'X-StellaOps-Tenant' = $Tenant
'X-StellaOps-Actor' = 'local-scratch-setup'
}
if ($BootstrapGitLabSecrets -and ($IncludeGitLab -or $IncludeGitLabRegistry)) {
& (Join-Path $PSScriptRoot 'bootstrap-local-gitlab-secrets.ps1') -VerifyRegistry:$IncludeGitLabRegistry
}
function Invoke-IntegrationApi {
param(
[Parameter(Mandatory)]
[ValidateSet('GET', 'POST')]
[string]$Method,
[Parameter(Mandatory)]
[string]$Path,
[object]$Body
)
$invokeParameters = @{
Method = $Method
Uri = "$BaseUrl$Path"
Headers = $Headers
TimeoutSec = 30
ErrorAction = 'Stop'
}
if ($null -ne $Body) {
$invokeParameters['ContentType'] = 'application/json'
$invokeParameters['Body'] = $Body | ConvertTo-Json -Depth 10
}
return Invoke-RestMethod @invokeParameters
}
function Get-HealthName {
param([int]$Status)
switch ($Status) {
1 { return 'Healthy' }
2 { return 'Degraded' }
3 { return 'Unhealthy' }
default { return 'Unknown' }
}
}
function New-IntegrationDefinition {
param(
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$Description,
[Parameter(Mandatory)][int]$Type,
[Parameter(Mandatory)][int]$Provider,
[Parameter(Mandatory)][string]$Endpoint,
[string]$AuthRefUri,
[string]$OrganizationId,
[hashtable]$ExtendedConfig,
[string[]]$Tags
)
return [ordered]@{
name = $Name
description = $Description
type = $Type
provider = $Provider
endpoint = $Endpoint
authRefUri = $AuthRefUri
organizationId = $OrganizationId
extendedConfig = $ExtendedConfig
tags = $Tags
}
}
$definitions = @(
(New-IntegrationDefinition `
-Name 'Local Harbor Fixture' `
-Description 'Local Harbor mock fixture for registry onboarding and health checks.' `
-Type 1 `
-Provider 100 `
-Endpoint 'http://harbor-fixture.stella-ops.local' `
-OrganizationId 'local-fixtures' `
-ExtendedConfig @{ scheduleType = 'manual' } `
-Tags @('local', 'scratch-setup', 'registry')),
(New-IntegrationDefinition `
-Name 'Local Docker Registry' `
-Description 'Local open OCI registry for catalog and tag probe validation.' `
-Type 1 `
-Provider 104 `
-Endpoint 'http://registry.stella-ops.local:5000' `
-ExtendedConfig @{ scheduleType = 'manual' } `
-Tags @('local', 'scratch-setup', 'registry')),
(New-IntegrationDefinition `
-Name 'Local Nexus Registry' `
-Description 'Local Nexus Repository Manager for registry integration checks.' `
-Type 1 `
-Provider 107 `
-Endpoint 'http://nexus.stella-ops.local:8081' `
-ExtendedConfig @{ scheduleType = 'manual' } `
-Tags @('local', 'scratch-setup', 'registry')),
(New-IntegrationDefinition `
-Name 'Local GitHub App Fixture' `
-Description 'Deterministic GitHub App fixture for SCM integration checks.' `
-Type 2 `
-Provider 200 `
-Endpoint 'http://github-app-fixture.stella-ops.local' `
-OrganizationId 'local-fixtures' `
-ExtendedConfig @{ scheduleType = 'manual' } `
-Tags @('local', 'scratch-setup', 'scm')),
(New-IntegrationDefinition `
-Name 'Local Gitea Server' `
-Description 'Local Gitea service for SCM connectivity and repository discovery.' `
-Type 2 `
-Provider 203 `
-Endpoint 'http://gitea.stella-ops.local:3000' `
-OrganizationId 'local' `
-ExtendedConfig @{ scheduleType = 'manual' } `
-Tags @('local', 'scratch-setup', 'scm')),
(New-IntegrationDefinition `
-Name 'Local Jenkins' `
-Description 'Local Jenkins service for CI/CD integration checks.' `
-Type 3 `
-Provider 302 `
-Endpoint 'http://jenkins.stella-ops.local:8080' `
-ExtendedConfig @{ scheduleType = 'manual' } `
-Tags @('local', 'scratch-setup', 'cicd')),
(New-IntegrationDefinition `
-Name 'Local eBPF Runtime Host' `
-Description 'Local runtime-host fixture exposing the eBPF agent contract.' `
-Type 5 `
-Provider 500 `
-Endpoint 'http://runtime-host-fixture.stella-ops.local' `
-ExtendedConfig @{ scheduleType = 'manual' } `
-Tags @('local', 'scratch-setup', 'runtime-host')),
(New-IntegrationDefinition `
-Name 'Local StellaOps Mirror' `
-Description 'Local Concelier mirror health surface for the StellaOps mirror provider.' `
-Type 6 `
-Provider 600 `
-Endpoint 'http://concelier.stella-ops.local' `
-ExtendedConfig @{ scheduleType = 'manual' } `
-Tags @('local', 'scratch-setup', 'feed-mirror')),
(New-IntegrationDefinition `
-Name 'Local NVD Mirror' `
-Description 'Local Concelier mirror health surface for the NVD mirror provider.' `
-Type 6 `
-Provider 601 `
-Endpoint 'http://concelier.stella-ops.local' `
-ExtendedConfig @{ scheduleType = 'manual' } `
-Tags @('local', 'scratch-setup', 'feed-mirror')),
(New-IntegrationDefinition `
-Name 'Local OSV Mirror' `
-Description 'Local Concelier mirror health surface for the OSV mirror provider.' `
-Type 6 `
-Provider 602 `
-Endpoint 'http://concelier.stella-ops.local' `
-ExtendedConfig @{ scheduleType = 'manual' } `
-Tags @('local', 'scratch-setup', 'feed-mirror')),
(New-IntegrationDefinition `
-Name 'Local Vault' `
-Description 'Local HashiCorp Vault dev server for secrets integration checks.' `
-Type 9 `
-Provider 550 `
-Endpoint 'http://vault.stella-ops.local:8200' `
-ExtendedConfig @{ scheduleType = 'manual' } `
-Tags @('local', 'scratch-setup', 'secrets')),
(New-IntegrationDefinition `
-Name 'Local Consul' `
-Description 'Local Consul server for settings and service-discovery checks.' `
-Type 9 `
-Provider 551 `
-Endpoint 'http://consul.stella-ops.local:8500' `
-ExtendedConfig @{ scheduleType = 'manual' } `
-Tags @('local', 'scratch-setup', 'secrets')),
(New-IntegrationDefinition `
-Name 'Local MinIO' `
-Description 'Local MinIO server for S3-compatible storage integration checks.' `
-Type 10 `
-Provider 450 `
-Endpoint 'http://minio.stella-ops.local:9000' `
-ExtendedConfig @{ scheduleType = 'manual' } `
-Tags @('local', 'scratch-setup', 'storage'))
)
if ($IncludeGitLab) {
$definitions += @(
(New-IntegrationDefinition `
-Name 'Local GitLab Server' `
-Description 'Local GitLab server for SCM connectivity and discovery probes.' `
-Type 2 `
-Provider 201 `
-Endpoint 'http://gitlab.stella-ops.local:8929' `
-AuthRefUri 'authref://vault/gitlab#access-token' `
-ExtendedConfig @{ scheduleType = 'manual' } `
-Tags @('local', 'scratch-setup', 'scm')),
(New-IntegrationDefinition `
-Name 'Local GitLab CI' `
-Description 'Local GitLab CI surface for CI/CD connectivity checks.' `
-Type 3 `
-Provider 301 `
-Endpoint 'http://gitlab.stella-ops.local:8929' `
-AuthRefUri 'authref://vault/gitlab#access-token' `
-ExtendedConfig @{ scheduleType = 'manual' } `
-Tags @('local', 'scratch-setup', 'cicd'))
)
}
if ($IncludeGitLabRegistry) {
$definitions += New-IntegrationDefinition `
-Name 'Local GitLab Container Registry' `
-Description 'Local GitLab container registry surface. Requires authref://vault/gitlab#registry-basic.' `
-Type 1 `
-Provider 109 `
-Endpoint 'http://gitlab.stella-ops.local:5050' `
-AuthRefUri 'authref://vault/gitlab#registry-basic' `
-ExtendedConfig @{ scheduleType = 'manual' } `
-Tags @('local', 'scratch-setup', 'registry')
}
$existingResponse = Invoke-IntegrationApi -Method GET -Path '/api/v1/integrations?pageSize=200'
$existingItems = @($existingResponse.items)
$results = New-Object System.Collections.Generic.List[object]
foreach ($definition in $definitions) {
$match = $existingItems | Where-Object {
$_.provider -eq $definition.provider -and $_.endpoint -eq $definition.endpoint
} | Select-Object -First 1
if ($null -eq $match) {
$created = Invoke-IntegrationApi -Method POST -Path '/api/v1/integrations/' -Body $definition
$id = $created.id
$action = 'created'
} else {
$id = $match.id
$action = 'existing'
}
$test = Invoke-IntegrationApi -Method POST -Path "/api/v1/integrations/$id/test"
$health = Invoke-IntegrationApi -Method GET -Path "/api/v1/integrations/$id/health"
$results.Add([pscustomobject]@{
Name = $definition.name
Provider = $definition.provider
Action = $action
TestSuccess = [bool]$test.success
Health = Get-HealthName -Status ([int]$health.status)
Endpoint = $definition.endpoint
Id = "$id"
})
}
$results |
Sort-Object Name |
Format-Table Name, Action, TestSuccess, Health, Endpoint -AutoSize |
Out-String |
Write-Host
$failures = @($results | Where-Object { -not $_.TestSuccess -or $_.Health -ne 'Healthy' })
if ($failures.Count -gt 0) {
Write-Error "Local integration registration completed with $($failures.Count) failing or non-healthy entry/entries for tenant '$Tenant'."
exit 1
}
Write-Host "Registered and verified $($results.Count) local integration entries for tenant '$Tenant' via $BaseUrl." -ForegroundColor Green