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:
351
scripts/bootstrap-local-gitlab-secrets.ps1
Normal file
351
scripts/bootstrap-local-gitlab-secrets.ps1
Normal 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)"
|
||||
301
scripts/register-local-integrations.ps1
Normal file
301
scripts/register-local-integrations.ps1
Normal 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
|
||||
Reference in New Issue
Block a user