a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
865 lines
29 KiB
PowerShell
865 lines
29 KiB
PowerShell
###############################################################################
|
|
# gitlab-smoke-tests.ps1 - Verify GitLab instance health after upgrades
|
|
#
|
|
# PowerShell port of gitlab-smoke-tests.sh. Zero external dependencies
|
|
# beyond PowerShell 5.1+ and git. Runs on Windows, Linux, and macOS.
|
|
#
|
|
# Author: Phil Connor
|
|
# Contact: contact@mylinux.work
|
|
# License: MIT
|
|
# Version 1.00
|
|
#
|
|
# Usage:
|
|
# $env:GITLAB_URL = "https://gitlab.example.com"
|
|
# $env:GITLAB_TOKEN = "glpat-xxxxxxxxxxxx"
|
|
# .\gitlab-smoke-tests.ps1
|
|
# .\gitlab-smoke-tests.ps1 -SkipGit -SkipRegistry
|
|
# .\gitlab-smoke-tests.ps1 -Insecure -Format junit
|
|
# .\gitlab-smoke-tests.ps1 -Format tap
|
|
###############################################################################
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$GitLabUrl = $env:GITLAB_URL,
|
|
[string]$GitLabToken = $env:GITLAB_TOKEN,
|
|
[string]$GitLabUser = $(if ($env:GITLAB_USER) { $env:GITLAB_USER } else { "root" }),
|
|
[string]$HealthToken = $env:GITLAB_HEALTH_TOKEN,
|
|
[string]$ProjectPrefix = $(if ($env:SMOKE_PROJECT_PREFIX) { $env:SMOKE_PROJECT_PREFIX } else { "smoke-test" }),
|
|
[int]$Timeout = $(if ($env:CURL_TIMEOUT) { [int]$env:CURL_TIMEOUT } else { 10 }),
|
|
[switch]$Insecure,
|
|
[switch]$SkipGit,
|
|
[switch]$SkipRegistry,
|
|
[switch]$SkipCleanup,
|
|
[ValidateSet("text","tap","junit")]
|
|
[string]$Format = "text",
|
|
[string]$JunitFile = "smoke-results.xml",
|
|
[switch]$NoColor
|
|
)
|
|
|
|
$ErrorActionPreference = "Continue"
|
|
|
|
# ============================================================================
|
|
# STATE
|
|
# ============================================================================
|
|
|
|
$script:Pass = 0
|
|
$script:Fail = 0
|
|
$script:Skip = 0
|
|
$script:Total = 0
|
|
$script:Results = @()
|
|
$script:CleanupProjectId = ""
|
|
$script:TmpDir = ""
|
|
$script:StartTime = $null
|
|
$script:GitCloneOk = $false
|
|
|
|
# ============================================================================
|
|
# COLORS
|
|
# ============================================================================
|
|
|
|
function Write-Color {
|
|
param([string]$Text, [string]$Color = "White")
|
|
if ($NoColor) {
|
|
Write-Host $Text
|
|
} else {
|
|
Write-Host $Text -ForegroundColor $Color
|
|
}
|
|
}
|
|
|
|
function Write-Log { param([string]$Msg) Write-Color "[INFO] $Msg" "Cyan" }
|
|
function Write-Warn { param([string]$Msg) Write-Color "[WARN] $Msg" "Yellow" }
|
|
function Write-Err { param([string]$Msg) Write-Color "[ERROR] $Msg" "Red" }
|
|
|
|
# ============================================================================
|
|
# TEST RESULT RECORDING
|
|
# ============================================================================
|
|
|
|
function Record-Pass {
|
|
param([string]$Name, [string]$Detail = "")
|
|
$script:Pass++
|
|
$script:Total++
|
|
$script:Results += [PSCustomObject]@{ Status="PASS"; Name=$Name; Detail=$Detail }
|
|
if ($Format -eq "tap") {
|
|
Write-Host "ok $($script:Total) - $Name"
|
|
} else {
|
|
$msg = " $(if($NoColor){'[PASS]'}else{[char]0x2713}) $Name"
|
|
if ($Detail) { $msg += " - $Detail" }
|
|
Write-Color $msg "Green"
|
|
}
|
|
}
|
|
|
|
function Record-Fail {
|
|
param([string]$Name, [string]$Detail = "")
|
|
$script:Fail++
|
|
$script:Total++
|
|
$script:Results += [PSCustomObject]@{ Status="FAIL"; Name=$Name; Detail=$Detail }
|
|
if ($Format -eq "tap") {
|
|
Write-Host "not ok $($script:Total) - $Name"
|
|
if ($Detail) { Write-Host " # $Detail" }
|
|
} else {
|
|
$msg = " $(if($NoColor){'[FAIL]'}else{[char]0x2717}) $Name"
|
|
if ($Detail) { $msg += " - $Detail" }
|
|
Write-Color $msg "Red"
|
|
}
|
|
}
|
|
|
|
function Record-Skip {
|
|
param([string]$Name, [string]$Reason = "")
|
|
$script:Skip++
|
|
$script:Total++
|
|
$script:Results += [PSCustomObject]@{ Status="SKIP"; Name=$Name; Detail=$Reason }
|
|
if ($Format -eq "tap") {
|
|
Write-Host "ok $($script:Total) - $Name # SKIP $Reason"
|
|
} else {
|
|
$msg = " $(if($NoColor){'[SKIP]'}else{[char]0x2298}) $Name"
|
|
if ($Reason) { $msg += " - $Reason" }
|
|
Write-Color $msg "Yellow"
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# HTTP HELPERS
|
|
# ============================================================================
|
|
|
|
function Invoke-GitLabApi {
|
|
param(
|
|
[string]$Method,
|
|
[string]$Endpoint,
|
|
[string]$Body = $null,
|
|
[switch]$StatusOnly
|
|
)
|
|
|
|
$uri = "$GitLabUrl/api/v4$Endpoint"
|
|
$headers = @{ "Content-Type" = "application/json" }
|
|
if ($GitLabToken) { $headers["PRIVATE-TOKEN"] = $GitLabToken }
|
|
|
|
$params = @{
|
|
Uri = $uri
|
|
Method = $Method
|
|
Headers = $headers
|
|
TimeoutSec = $Timeout
|
|
UseBasicParsing = $true
|
|
ErrorAction = "Stop"
|
|
}
|
|
|
|
if ($Insecure -and $PSVersionTable.PSVersion.Major -ge 7) {
|
|
$params["SkipCertificateCheck"] = $true
|
|
}
|
|
|
|
if ($Body) {
|
|
$params["Body"] = $Body
|
|
}
|
|
|
|
try {
|
|
if ($StatusOnly) {
|
|
$response = Invoke-WebRequest @params
|
|
return [int]$response.StatusCode
|
|
} else {
|
|
return Invoke-RestMethod @params
|
|
}
|
|
} catch {
|
|
if ($StatusOnly) {
|
|
if ($_.Exception.Response) {
|
|
return [int]$_.Exception.Response.StatusCode
|
|
}
|
|
return 0
|
|
}
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Invoke-HealthCheck {
|
|
param([string]$Path)
|
|
|
|
$uri = "$GitLabUrl$Path"
|
|
if ($HealthToken) { $uri += "?token=$HealthToken" }
|
|
|
|
$params = @{
|
|
Uri = $uri
|
|
Method = "GET"
|
|
TimeoutSec = $Timeout
|
|
UseBasicParsing = $true
|
|
ErrorAction = "Stop"
|
|
}
|
|
|
|
if ($Insecure -and $PSVersionTable.PSVersion.Major -ge 7) {
|
|
$params["SkipCertificateCheck"] = $true
|
|
}
|
|
|
|
try {
|
|
$response = Invoke-WebRequest @params
|
|
return [int]$response.StatusCode
|
|
} catch {
|
|
if ($_.Exception.Response) {
|
|
return [int]$_.Exception.Response.StatusCode
|
|
}
|
|
return 0
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# TLS HELPER
|
|
# ============================================================================
|
|
|
|
function Get-TlsCertExpiry {
|
|
param([string]$HostName, [int]$Port = 443)
|
|
|
|
try {
|
|
$tcpClient = New-Object System.Net.Sockets.TcpClient
|
|
$tcpClient.ReceiveTimeout = $Timeout * 1000
|
|
$tcpClient.SendTimeout = $Timeout * 1000
|
|
$tcpClient.Connect($HostName, $Port)
|
|
|
|
$sslStream = New-Object System.Net.Security.SslStream(
|
|
$tcpClient.GetStream(), $false,
|
|
{ param($s,$c,$ch,$e) return $true }
|
|
)
|
|
$sslStream.AuthenticateAsClient($HostName)
|
|
|
|
$cert = $sslStream.RemoteCertificate
|
|
$expiry = [DateTime]$cert.GetExpirationDateString()
|
|
|
|
$sslStream.Dispose()
|
|
$tcpClient.Dispose()
|
|
|
|
return $expiry
|
|
} catch {
|
|
return $null
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# TEST SUITES
|
|
# ============================================================================
|
|
|
|
# -- 1. Connectivity --------------------------------------------------------
|
|
|
|
function Test-Connectivity {
|
|
Write-Host ""
|
|
Write-Color "Connectivity" "White"
|
|
|
|
# 1a. Health endpoint
|
|
$code = Invoke-HealthCheck "/-/health"
|
|
if ($code -eq 200) {
|
|
Record-Pass "GitLab health endpoint reachable" "HTTP $code"
|
|
} else {
|
|
Record-Fail "GitLab health endpoint reachable" "HTTP $code"
|
|
}
|
|
|
|
# 1b. Readiness
|
|
$code = Invoke-HealthCheck "/-/readiness"
|
|
if ($code -eq 200) {
|
|
Record-Pass "GitLab readiness check" "HTTP $code"
|
|
} else {
|
|
Record-Fail "GitLab readiness check" "HTTP $code"
|
|
}
|
|
|
|
# 1c. Liveness
|
|
$code = Invoke-HealthCheck "/-/liveness"
|
|
if ($code -eq 200) {
|
|
Record-Pass "GitLab liveness check" "HTTP $code"
|
|
} else {
|
|
Record-Fail "GitLab liveness check" "HTTP $code"
|
|
}
|
|
|
|
# 1d. TLS certificate
|
|
if ($GitLabUrl -match "^https://") {
|
|
$hostPart = $GitLabUrl -replace "^https://", "" -replace "/.*", "" -replace ":.*", ""
|
|
$portPart = 443
|
|
if ($GitLabUrl -match ":(\d+)") { $portPart = [int]$Matches[1] }
|
|
|
|
$expiry = Get-TlsCertExpiry -HostName $hostPart -Port $portPart
|
|
if ($expiry) {
|
|
$daysLeft = [math]::Floor(($expiry - (Get-Date)).TotalDays)
|
|
if ($daysLeft -gt 30) {
|
|
Record-Pass "TLS certificate valid" "$daysLeft days remaining"
|
|
} elseif ($daysLeft -gt 0) {
|
|
Record-Pass "TLS certificate valid" "$daysLeft days remaining (renew soon)"
|
|
} else {
|
|
Record-Fail "TLS certificate valid" "expired or expiring in $daysLeft days"
|
|
}
|
|
} else {
|
|
Record-Skip "TLS certificate check" "could not retrieve certificate"
|
|
}
|
|
} else {
|
|
Record-Skip "TLS certificate check" "not using HTTPS"
|
|
}
|
|
}
|
|
|
|
# -- 2. API ----------------------------------------------------------------
|
|
|
|
function Test-Api {
|
|
Write-Host ""
|
|
Write-Color "API" "White"
|
|
|
|
# 2a. Version
|
|
$versionData = Invoke-GitLabApi -Method GET -Endpoint "/version"
|
|
if ($versionData -and $versionData.version) {
|
|
Record-Pass "API version endpoint" "GitLab $($versionData.version) ($($versionData.revision))"
|
|
} else {
|
|
Record-Fail "API version endpoint" "no version returned"
|
|
}
|
|
|
|
# 2b. Authentication
|
|
$authStatus = Invoke-GitLabApi -Method GET -Endpoint "/user" -StatusOnly
|
|
if ($authStatus -eq 200) {
|
|
$userData = Invoke-GitLabApi -Method GET -Endpoint "/user"
|
|
Record-Pass "API authentication" "authenticated as $($userData.username)"
|
|
} elseif ($authStatus -eq 401) {
|
|
Record-Fail "API authentication" "token rejected (HTTP 401)"
|
|
} else {
|
|
Record-Fail "API authentication" "HTTP $authStatus"
|
|
}
|
|
|
|
# 2c. List projects
|
|
$projStatus = Invoke-GitLabApi -Method GET -Endpoint "/projects?per_page=1" -StatusOnly
|
|
if ($projStatus -eq 200) {
|
|
Record-Pass "API list projects" "database responding"
|
|
} else {
|
|
Record-Fail "API list projects" "HTTP $projStatus"
|
|
}
|
|
|
|
# 2d. List users
|
|
$userStatus = Invoke-GitLabApi -Method GET -Endpoint "/users?per_page=1" -StatusOnly
|
|
if ($userStatus -eq 200) {
|
|
Record-Pass "API list users" "user directory accessible"
|
|
} else {
|
|
Record-Fail "API list users" "HTTP $userStatus"
|
|
}
|
|
|
|
# 2e. Sidekiq
|
|
$sidekiq = Invoke-GitLabApi -Method GET -Endpoint "/sidekiq/compound_metrics"
|
|
if ($sidekiq -and -not $sidekiq.error) {
|
|
$procCount = 0
|
|
if ($sidekiq.processes) { $procCount = @($sidekiq.processes).Count }
|
|
Record-Pass "Sidekiq running" "$procCount process(es) responding"
|
|
} else {
|
|
Record-Fail "Sidekiq running" "could not query Sidekiq metrics"
|
|
}
|
|
|
|
# 2f. Runners
|
|
$runnerStatus = Invoke-GitLabApi -Method GET -Endpoint "/runners/all?per_page=1" -StatusOnly
|
|
if ($runnerStatus -eq 200) {
|
|
Record-Pass "API runners endpoint" "runner management accessible"
|
|
} elseif ($runnerStatus -eq 403) {
|
|
Record-Skip "API runners endpoint" "token lacks admin scope"
|
|
} else {
|
|
Record-Fail "API runners endpoint" "HTTP $runnerStatus"
|
|
}
|
|
|
|
# 2g. Search
|
|
$searchStatus = Invoke-GitLabApi -Method GET -Endpoint "/search?scope=projects&search=test" -StatusOnly
|
|
if ($searchStatus -eq 200) {
|
|
Record-Pass "API search" "search index responding"
|
|
} elseif ($searchStatus -eq 403) {
|
|
Record-Skip "API search" "search disabled or token lacks scope"
|
|
} else {
|
|
Record-Fail "API search" "HTTP $searchStatus"
|
|
}
|
|
}
|
|
|
|
# -- 3. Git Operations -----------------------------------------------------
|
|
|
|
function Test-Git {
|
|
if ($SkipGit) {
|
|
Write-Host ""
|
|
Write-Color "Git Operations" "White"
|
|
Record-Skip "Git clone" "SkipGit specified"
|
|
Record-Skip "Git push" "SkipGit specified"
|
|
return
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Color "Git Operations" "White"
|
|
|
|
# Create test project
|
|
$projectName = "$ProjectPrefix-$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())"
|
|
$body = @{ name = $projectName; visibility = "private"; initialize_with_readme = $true } | ConvertTo-Json
|
|
$project = Invoke-GitLabApi -Method POST -Endpoint "/projects" -Body $body
|
|
|
|
if (-not $project -or -not $project.id) {
|
|
Record-Fail "Create test project" "API returned no project ID"
|
|
Record-Skip "Git clone" "no test project"
|
|
Record-Skip "Git push" "no test project"
|
|
return
|
|
}
|
|
|
|
$script:CleanupProjectId = $project.id
|
|
Record-Pass "Create test project" "$projectName (ID: $($project.id))"
|
|
|
|
# Build clone URL
|
|
$httpUrl = $project.http_url_to_repo
|
|
if (-not $httpUrl) {
|
|
$httpUrl = "$GitLabUrl/$GitLabUser/$projectName.git"
|
|
}
|
|
|
|
# Rewrite origin if API returns an internal hostname
|
|
$apiOrigin = if ($httpUrl -match "^(https?://[^/]+)") { $Matches[1] } else { "" }
|
|
if ($apiOrigin -and $apiOrigin -ne $GitLabUrl) {
|
|
$httpUrl = $httpUrl -replace [regex]::Escape($apiOrigin), $GitLabUrl
|
|
}
|
|
|
|
# Inject token
|
|
if ($httpUrl -match "^https://") {
|
|
$cloneUrl = $httpUrl -replace "^https://", "https://oauth2:${GitLabToken}@"
|
|
} elseif ($httpUrl -match "^http://") {
|
|
$cloneUrl = $httpUrl -replace "^http://", "http://oauth2:${GitLabToken}@"
|
|
} else {
|
|
$cloneUrl = $httpUrl
|
|
}
|
|
|
|
# Temp directory
|
|
$script:TmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "gitlab-smoke-$([guid]::NewGuid().ToString('N').Substring(0,8))"
|
|
New-Item -ItemType Directory -Path $script:TmpDir -Force | Out-Null
|
|
|
|
# Wait for repo init
|
|
Start-Sleep -Seconds 2
|
|
|
|
# Clone
|
|
$gitArgs = @("clone")
|
|
if ($Insecure) { $env:GIT_SSL_NO_VERIFY = "true" }
|
|
|
|
$repoDir = Join-Path $script:TmpDir "repo"
|
|
$cloneOutput = & git clone $cloneUrl $repoDir 2>&1
|
|
$cloneRc = $LASTEXITCODE
|
|
|
|
if ($cloneRc -eq 0) {
|
|
$script:GitCloneOk = $true
|
|
Record-Pass "Git clone (HTTPS)" "Gitaly responding"
|
|
} else {
|
|
$shortErr = ($cloneOutput | Select-String -Pattern "fatal|error" | Select-Object -First 1) -replace [regex]::Escape($GitLabToken), "[REDACTED]"
|
|
Record-Fail "Git clone (HTTPS)" "$shortErr"
|
|
return
|
|
}
|
|
|
|
# Push
|
|
Push-Location $repoDir
|
|
try {
|
|
& git config user.email "smoke-test@example.com"
|
|
& git config user.name "Smoke Test"
|
|
"smoke test $(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')" | Out-File -FilePath "smoke-test.txt" -Encoding utf8
|
|
|
|
& git add smoke-test.txt
|
|
& git commit -m "smoke test commit" 2>&1 | Out-Null
|
|
|
|
$pushOutput = & git push origin main 2>&1
|
|
$pushRc = $LASTEXITCODE
|
|
if ($pushRc -ne 0) {
|
|
$pushOutput = & git push origin master 2>&1
|
|
$pushRc = $LASTEXITCODE
|
|
}
|
|
|
|
if ($pushRc -eq 0) {
|
|
Record-Pass "Git push (HTTPS)" "write to Gitaly succeeded"
|
|
} else {
|
|
Record-Fail "Git push (HTTPS)" "push failed"
|
|
}
|
|
} finally {
|
|
Pop-Location
|
|
}
|
|
}
|
|
|
|
# -- 4. Container Registry -------------------------------------------------
|
|
|
|
function Test-Registry {
|
|
if ($SkipRegistry) {
|
|
Write-Host ""
|
|
Write-Color "Container Registry" "White"
|
|
Record-Skip "Registry API" "SkipRegistry specified"
|
|
return
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Color "Container Registry" "White"
|
|
|
|
# Check if registry is enabled
|
|
$registryEnabled = ""
|
|
$settings = Invoke-GitLabApi -Method GET -Endpoint "/application/settings"
|
|
if ($settings) {
|
|
$registryEnabled = $settings.container_registry_enabled
|
|
}
|
|
|
|
if ($registryEnabled -eq $false) {
|
|
Record-Skip "Registry API reachable" "container registry disabled in application settings"
|
|
Record-Skip "Registry project endpoint" "container registry disabled in application settings"
|
|
return
|
|
}
|
|
|
|
# Try registry v2 API
|
|
$hostPart = $GitLabUrl -replace "^https?://", "" -replace "/.*", ""
|
|
$registryStatus = 0
|
|
|
|
$registryUrls = @(
|
|
"$GitLabUrl`:5050/v2/",
|
|
"https://${hostPart}:5050/v2/",
|
|
"https://registry.${hostPart}/v2/"
|
|
)
|
|
|
|
foreach ($regUrl in $registryUrls) {
|
|
try {
|
|
$params = @{
|
|
Uri = $regUrl
|
|
Method = "GET"
|
|
TimeoutSec = $Timeout
|
|
UseBasicParsing = $true
|
|
ErrorAction = "Stop"
|
|
}
|
|
if ($Insecure -and $PSVersionTable.PSVersion.Major -ge 7) {
|
|
$params["SkipCertificateCheck"] = $true
|
|
}
|
|
$response = Invoke-WebRequest @params
|
|
$registryStatus = [int]$response.StatusCode
|
|
break
|
|
} catch {
|
|
if ($_.Exception.Response) {
|
|
$registryStatus = [int]$_.Exception.Response.StatusCode
|
|
if ($registryStatus -eq 401) { break }
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($registryStatus -eq 200 -or $registryStatus -eq 401) {
|
|
Record-Pass "Registry API reachable" "HTTP $registryStatus"
|
|
} elseif ($registryStatus -eq 0) {
|
|
if ($registryEnabled -eq $true) {
|
|
Record-Fail "Registry API reachable" "enabled in settings but not reachable at standard ports/hosts"
|
|
} else {
|
|
Record-Skip "Registry API reachable" "not found at standard ports/hosts (settings unreadable - may need admin token)"
|
|
}
|
|
} else {
|
|
Record-Fail "Registry API reachable" "HTTP $registryStatus"
|
|
}
|
|
|
|
# Project-level registry
|
|
if ($script:CleanupProjectId) {
|
|
$regStatus = Invoke-GitLabApi -Method GET -Endpoint "/projects/$($script:CleanupProjectId)/registry/repositories" -StatusOnly
|
|
if ($regStatus -eq 200) {
|
|
Record-Pass "Registry project endpoint" "project registry accessible"
|
|
} elseif ($regStatus -eq 404) {
|
|
Record-Skip "Registry project endpoint" "container registry not enabled for project"
|
|
} else {
|
|
Record-Fail "Registry project endpoint" "HTTP $regStatus"
|
|
}
|
|
}
|
|
}
|
|
|
|
# -- 5. CI/CD --------------------------------------------------------------
|
|
|
|
function Test-CICD {
|
|
Write-Host ""
|
|
Write-Color "CI/CD" "White"
|
|
|
|
# Runners
|
|
$runners = Invoke-GitLabApi -Method GET -Endpoint "/runners/all?per_page=100"
|
|
if ($runners -is [array]) {
|
|
$runnerCount = $runners.Count
|
|
$onlineCount = @($runners | Where-Object { $_.status -eq "online" }).Count
|
|
|
|
if ($onlineCount -gt 0) {
|
|
Record-Pass "CI/CD runners online" "$onlineCount/$runnerCount runners online"
|
|
} elseif ($runnerCount -gt 0) {
|
|
Record-Fail "CI/CD runners online" "0/$runnerCount runners online"
|
|
} else {
|
|
Record-Skip "CI/CD runners online" "no runners registered"
|
|
}
|
|
} else {
|
|
Record-Skip "CI/CD runners" "could not query runners (admin token required)"
|
|
}
|
|
|
|
# CI/CD settings
|
|
$cicdStatus = Invoke-GitLabApi -Method GET -Endpoint "/application/settings" -StatusOnly
|
|
if ($cicdStatus -eq 200) {
|
|
Record-Pass "CI/CD settings accessible" "application settings readable"
|
|
} elseif ($cicdStatus -eq 403) {
|
|
Record-Skip "CI/CD settings accessible" "admin token required"
|
|
} else {
|
|
Record-Fail "CI/CD settings accessible" "HTTP $cicdStatus"
|
|
}
|
|
}
|
|
|
|
# -- 6. Background Migrations ----------------------------------------------
|
|
|
|
function Test-Migrations {
|
|
Write-Host ""
|
|
Write-Color "Background Migrations" "White"
|
|
|
|
$migrations = Invoke-GitLabApi -Method GET -Endpoint "/admin/batched_background_migrations?database=main"
|
|
|
|
if ($migrations -is [array]) {
|
|
$totalMig = $migrations.Count
|
|
$failedMig = @($migrations | Where-Object { $_.status -eq "failed" }).Count
|
|
$activeMig = @($migrations | Where-Object { $_.status -eq "active" }).Count
|
|
$pausedMig = @($migrations | Where-Object { $_.status -eq "paused" }).Count
|
|
$finishedMig = @($migrations | Where-Object { $_.status -eq "finished" }).Count
|
|
|
|
if ($failedMig -gt 0) {
|
|
Record-Fail "Background migrations" "$failedMig failed, $activeMig active, $pausedMig paused, $finishedMig finished of $totalMig"
|
|
} elseif ($pausedMig -gt 0) {
|
|
Record-Fail "Background migrations" "$pausedMig paused, $activeMig active, $finishedMig finished of $totalMig"
|
|
} elseif ($activeMig -gt 0) {
|
|
Record-Pass "Background migrations" "$activeMig active, $finishedMig finished of $totalMig (in progress)"
|
|
} else {
|
|
Record-Pass "Background migrations" "all $totalMig finished"
|
|
}
|
|
} else {
|
|
$migStatus = Invoke-GitLabApi -Method GET -Endpoint "/admin/batched_background_migrations?database=main" -StatusOnly
|
|
if ($migStatus -eq 403) {
|
|
Record-Skip "Background migrations" "admin token required"
|
|
} else {
|
|
Record-Skip "Background migrations" "could not query (HTTP $migStatus)"
|
|
}
|
|
}
|
|
}
|
|
|
|
# -- 7. Components ---------------------------------------------------------
|
|
|
|
function Test-Components {
|
|
Write-Host ""
|
|
Write-Color "Components" "White"
|
|
|
|
# Metadata
|
|
$metadata = Invoke-GitLabApi -Method GET -Endpoint "/metadata"
|
|
if ($metadata -and $metadata.version) {
|
|
$edition = if ($metadata.enterprise -eq $true) { "EE" } else { "CE" }
|
|
Record-Pass "GitLab metadata" "$($metadata.version) $edition"
|
|
} elseif ($metadata) {
|
|
Record-Pass "GitLab metadata" "endpoint reachable"
|
|
} else {
|
|
Record-Skip "GitLab metadata" "metadata endpoint not available"
|
|
}
|
|
|
|
# Statistics
|
|
$stats = Invoke-GitLabApi -Method GET -Endpoint "/application/statistics"
|
|
if ($stats -and $stats.active_users) {
|
|
Record-Pass "Instance statistics" "$($stats.active_users) users, $($stats.projects) projects, $($stats.groups) groups"
|
|
} elseif ($stats) {
|
|
Record-Pass "Instance statistics" "endpoint reachable"
|
|
} else {
|
|
Record-Skip "Instance statistics" "admin token required"
|
|
}
|
|
|
|
# Gitaly (inferred)
|
|
if ($script:GitCloneOk) {
|
|
Record-Pass "Gitaly storage" "project created and cloned successfully"
|
|
} elseif ($script:CleanupProjectId) {
|
|
Record-Skip "Gitaly storage" "project created but clone was not tested or failed"
|
|
}
|
|
|
|
# PostgreSQL (inferred)
|
|
$pgStatus = Invoke-GitLabApi -Method GET -Endpoint "/projects?per_page=1&order_by=updated_at" -StatusOnly
|
|
if ($pgStatus -eq 200) {
|
|
Record-Pass "PostgreSQL" "database queries succeeding"
|
|
} else {
|
|
Record-Fail "PostgreSQL" "sorted query failed (HTTP $pgStatus)"
|
|
}
|
|
|
|
# Redis (inferred)
|
|
$redisStatus = Invoke-GitLabApi -Method GET -Endpoint "/user" -StatusOnly
|
|
if ($redisStatus -eq 200) {
|
|
Record-Pass "Redis" "session/cache operational (auth succeeded)"
|
|
} else {
|
|
Record-Skip "Redis" "cannot verify independently"
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# OUTPUT
|
|
# ============================================================================
|
|
|
|
function Write-Summary {
|
|
$duration = [math]::Floor(((Get-Date) - $script:StartTime).TotalSeconds)
|
|
|
|
Write-Host ""
|
|
$separator = [string]::new([char]0x2500, 40)
|
|
Write-Color $separator "White"
|
|
Write-Color "Summary $GitLabUrl" "White"
|
|
|
|
$summaryLine = " $($script:Pass) passed $($script:Fail) failed $($script:Skip) skipped (${duration}s)"
|
|
Write-Host $summaryLine
|
|
Write-Color $separator "White"
|
|
|
|
if ($script:Fail -eq 0) {
|
|
Write-Color "All tests passed." "Green"
|
|
} else {
|
|
Write-Color "$($script:Fail) test(s) failed." "Red"
|
|
}
|
|
}
|
|
|
|
function Write-TapHeader {
|
|
Write-Host "TAP version 13"
|
|
}
|
|
|
|
function Write-TapFooter {
|
|
Write-Host "1..$($script:Total)"
|
|
Write-Host "# pass $($script:Pass)"
|
|
Write-Host "# fail $($script:Fail)"
|
|
Write-Host "# skip $($script:Skip)"
|
|
}
|
|
|
|
function Write-JunitReport {
|
|
$duration = [math]::Floor(((Get-Date) - $script:StartTime).TotalSeconds)
|
|
|
|
$xml = @"
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<testsuites tests="$($script:Total)" failures="$($script:Fail)" skipped="$($script:Skip)" time="$duration">
|
|
<testsuite name="gitlab-smoke-tests" tests="$($script:Total)" failures="$($script:Fail)" skipped="$($script:Skip)" time="$duration">
|
|
"@
|
|
|
|
foreach ($r in $script:Results) {
|
|
$safeName = $r.Name -replace '&','&' -replace '<','<' -replace '>','>' -replace '"','"'
|
|
$safeDetail = $r.Detail -replace '&','&' -replace '<','<' -replace '>','>' -replace '"','"'
|
|
|
|
switch ($r.Status) {
|
|
"PASS" {
|
|
$xml += "`n <testcase name=`"$safeName`" classname=`"smoke`">"
|
|
if ($r.Detail) { $xml += "`n <system-out>$safeDetail</system-out>" }
|
|
$xml += "`n </testcase>"
|
|
}
|
|
"FAIL" {
|
|
$xml += "`n <testcase name=`"$safeName`" classname=`"smoke`">"
|
|
$xml += "`n <failure message=`"$safeDetail`">FAILED: $safeName - $safeDetail</failure>"
|
|
$xml += "`n </testcase>"
|
|
}
|
|
"SKIP" {
|
|
$xml += "`n <testcase name=`"$safeName`" classname=`"smoke`">"
|
|
$xml += "`n <skipped message=`"$safeDetail`"/>"
|
|
$xml += "`n </testcase>"
|
|
}
|
|
}
|
|
}
|
|
|
|
$xml += "`n </testsuite>"
|
|
$xml += "`n</testsuites>"
|
|
|
|
$xml | Out-File -FilePath $JunitFile -Encoding utf8
|
|
Write-Log "JUnit report written to $JunitFile"
|
|
}
|
|
|
|
# ============================================================================
|
|
# CLEANUP
|
|
# ============================================================================
|
|
|
|
function Invoke-Cleanup {
|
|
if ($script:CleanupProjectId -and -not $SkipCleanup) {
|
|
try {
|
|
Invoke-GitLabApi -Method DELETE -Endpoint "/projects/$($script:CleanupProjectId)" | Out-Null
|
|
} catch { }
|
|
}
|
|
|
|
if ($script:TmpDir -and (Test-Path $script:TmpDir)) {
|
|
Remove-Item -Recurse -Force $script:TmpDir -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
if ($env:GIT_SSL_NO_VERIFY) {
|
|
Remove-Item Env:\GIT_SSL_NO_VERIFY -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# MAIN
|
|
# ============================================================================
|
|
|
|
function Show-Usage {
|
|
@"
|
|
Usage: .\gitlab-smoke-tests.ps1 [OPTIONS]
|
|
|
|
Smoke-test a GitLab instance. PowerShell 5.1+, git only.
|
|
Designed for air-gapped environments.
|
|
|
|
Required environment variables:
|
|
GITLAB_URL GitLab base URL (https://gitlab.example.com)
|
|
GITLAB_TOKEN Personal access token (api scope; admin for full coverage)
|
|
|
|
Optional environment variables:
|
|
GITLAB_HEALTH_TOKEN Health check access token
|
|
GITLAB_USER Username for git operations (default: root)
|
|
|
|
Parameters:
|
|
-SkipGit Skip git clone/push tests
|
|
-SkipRegistry Skip container registry tests
|
|
-SkipCleanup Don't delete the test project after run
|
|
-Insecure Allow self-signed TLS certificates
|
|
-Timeout N HTTP timeout in seconds (default: 10)
|
|
-Format FORMAT Output: text (default), tap, junit
|
|
-JunitFile FILE JUnit output path (default: smoke-results.xml)
|
|
-NoColor Disable colored output
|
|
-Verbose Show debug output
|
|
|
|
Examples:
|
|
`$env:GITLAB_URL = "https://gitlab.example.com"
|
|
`$env:GITLAB_TOKEN = "glpat-xxxxxxxxxxxx"
|
|
.\gitlab-smoke-tests.ps1
|
|
|
|
.\gitlab-smoke-tests.ps1 -Insecure -Format junit
|
|
.\gitlab-smoke-tests.ps1 -SkipGit -SkipRegistry
|
|
.\gitlab-smoke-tests.ps1 -Format tap
|
|
"@
|
|
}
|
|
|
|
# Handle PS 5.1 TLS and self-signed certs
|
|
if ($PSVersionTable.PSVersion.Major -lt 7) {
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
|
|
if ($Insecure) {
|
|
Add-Type @"
|
|
using System.Net;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
public class TrustAll : ICertificatePolicy {
|
|
public bool CheckValidationResult(ServicePoint sp, X509Certificate cert,
|
|
WebRequest req, int problem) { return true; }
|
|
}
|
|
"@ -ErrorAction SilentlyContinue
|
|
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAll
|
|
}
|
|
}
|
|
|
|
# Validate
|
|
if (-not $GitLabUrl) {
|
|
Write-Err "GITLAB_URL is required"
|
|
Write-Host ""
|
|
Show-Usage
|
|
exit 1
|
|
}
|
|
|
|
if (-not $GitLabToken) {
|
|
Write-Err "GITLAB_TOKEN is required"
|
|
Write-Host ""
|
|
Show-Usage
|
|
exit 1
|
|
}
|
|
|
|
$GitLabUrl = $GitLabUrl.TrimEnd("/")
|
|
$script:StartTime = Get-Date
|
|
|
|
if ($Format -eq "tap") {
|
|
Write-TapHeader
|
|
} else {
|
|
Write-Host ""
|
|
Write-Color "GitLab Smoke Tests" "White"
|
|
Write-Host "Target: $GitLabUrl"
|
|
Write-Host "Time: $(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')"
|
|
Write-Host ""
|
|
}
|
|
|
|
try {
|
|
Test-Connectivity
|
|
Test-Api
|
|
Test-Git
|
|
Test-Registry
|
|
Test-CICD
|
|
Test-Migrations
|
|
Test-Components
|
|
} finally {
|
|
Invoke-Cleanup
|
|
}
|
|
|
|
if ($Format -eq "tap") {
|
|
Write-TapFooter
|
|
} elseif ($Format -eq "junit") {
|
|
Write-Summary
|
|
Write-JunitReport
|
|
} else {
|
|
Write-Summary
|
|
}
|
|
|
|
if ($script:Fail -eq 0) { exit 0 } else { exit 1 }
|