mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 01:59:04 +02:00
ci: expand Windows feature fixtures and test Server 2022 (#2503)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
72e10af92d
commit
56137ffdc1
+478
-289
@@ -299,7 +299,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- package
|
||||
- test
|
||||
- windows-test
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
@@ -317,9 +317,18 @@ jobs:
|
||||
windows_exporter-*.msi \
|
||||
sha256sums.txt
|
||||
|
||||
test:
|
||||
runs-on: windows-2025
|
||||
windows-test:
|
||||
name: Windows tests (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 25
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Server 2025 runners are much slower to provision the same fixtures.
|
||||
- os: windows-2022
|
||||
nanoserver: ltsc2022
|
||||
smtp: true
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
@@ -328,248 +337,246 @@ jobs:
|
||||
CRI_ENDPOINT: "npipe:////./pipe/containerd-containerd"
|
||||
CRITOOLS_VERSION: "1.37.0"
|
||||
KUBERNETES_VERSION: "v1.37.1"
|
||||
NANOSERVER_IMAGE: mcr.microsoft.com/windows/nanoserver:${{ matrix.nanoserver }}
|
||||
SMTP_ENABLED: ${{ matrix.smtp }}
|
||||
WINDOWS_EXPORTER_TEST_RDP_USER: rdp-ci
|
||||
WINDOWS_TEST_OS: ${{ matrix.os }}
|
||||
# These collectors must work on the fixtures below; missing features are failures.
|
||||
WINDOWS_EXPORTER_TEST_COLLECTORS: "cache,container,cpu,cpu_info,dhcp,diskdrive,dns,fsrmquota,hyperv,iis,license,logical_disk,memory,mscluster,msmq,net,netframework,nps,os,pagefile,physical_disk,printer,process,scheduled_task,service,smb,smbclient,system,tcp,terminal_services,time,udp,update"
|
||||
WINDOWS_EXPORTER_TEST_COLLECTORS: "cache,container,cpu,cpu_info,dhcp,diskdrive,dns,fsrmquota,hyperv,iis,license,logical_disk,memory,mscluster,msmq,mssql,net,netframework,nps,os,pagefile,physical_disk,printer,process,scheduled_task,service,smb,smbclient,system,tcp,terminal_services,time,udp,update${{ matrix.smtp && ',smtp' || '' }}"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: 'false'
|
||||
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
# Skip the SQL Server download and extraction when the setup files are cached.
|
||||
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
# renovate: golang=go
|
||||
go-version: '1.27.1'
|
||||
path: ${{ runner.temp }}/sql-server/setup
|
||||
key: sql-server-2025-express-fa7e1fabc9a2-${{ runner.os }}
|
||||
|
||||
- parallel:
|
||||
- name: Install Containers feature
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
# Steps that do not register services run in the background while Windows features install.
|
||||
- name: Install FreeRDP client
|
||||
id: install-freerdp
|
||||
background: true
|
||||
shell: powershell
|
||||
run: |
|
||||
foreach ($attempt in 1..3) {
|
||||
choco install freerdp.portable --version=3.28.0 --yes --no-progress
|
||||
if ($LASTEXITCODE -eq 0) { break }
|
||||
if ($attempt -eq 3) { throw "Failed to install FreeRDP" }
|
||||
Start-Sleep -Seconds 10
|
||||
}
|
||||
Get-Command wfreerdp.exe -ErrorAction Stop
|
||||
|
||||
$features = @(
|
||||
"Containers"
|
||||
"Failover-Clustering"
|
||||
"RSAT-Clustering-PowerShell"
|
||||
)
|
||||
$result = Install-WindowsFeature -Name $features
|
||||
$result | Format-List Success, RestartNeeded, ExitCode, FeatureResult
|
||||
|
||||
if (-not $result.Success -or $result.RestartNeeded -ne "No") {
|
||||
throw "Containers feature could not be installed without a restart"
|
||||
}
|
||||
|
||||
Get-WindowsFeature -Name $features |
|
||||
Format-Table DisplayName, Name, InstallState
|
||||
|
||||
- name: Setup storage pool
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$existingDiskIds = @(Get-PhysicalDisk | ForEach-Object ObjectId)
|
||||
$disk1 = Join-Path $env:RUNNER_TEMP "storage-pool-1.vhdx"
|
||||
$disk2 = Join-Path $env:RUNNER_TEMP "storage-pool-2.vhdx"
|
||||
$diskpartScript = Join-Path $env:RUNNER_TEMP "storage-pool-diskpart.txt"
|
||||
|
||||
@"
|
||||
create vdisk file="$disk1" maximum=10240 type=expandable
|
||||
attach vdisk
|
||||
create vdisk file="$disk2" maximum=10240 type=expandable
|
||||
attach vdisk
|
||||
"@ | Set-Content -Path $diskpartScript -Encoding ascii
|
||||
|
||||
diskpart /s $diskpartScript
|
||||
Update-HostStorageCache
|
||||
|
||||
$newDisks = @(Get-PhysicalDisk | Where-Object { $_.ObjectId -notin $existingDiskIds })
|
||||
$newDisks | Format-Table FriendlyName, DeviceId, MediaType, BusType, Size, CanPool, CannotPoolReason
|
||||
|
||||
$poolableDisks = @($newDisks | Where-Object CanPool)
|
||||
|
||||
if ($poolableDisks.Count -ne 2) {
|
||||
throw "Expected two poolable VHDX disks, found $($poolableDisks.Count)"
|
||||
}
|
||||
|
||||
New-StoragePool `
|
||||
-FriendlyName "GitHubActions" `
|
||||
-StorageSubsystemFriendlyName "Windows Storage*" `
|
||||
-PhysicalDisks $poolableDisks
|
||||
|
||||
$pool = Get-StoragePool -FriendlyName "GitHubActions" -ErrorAction Stop
|
||||
$pool | Format-List FriendlyName, HealthStatus, OperationalStatus, Size, AllocatedSize
|
||||
|
||||
if ($pool.IsPrimordial) {
|
||||
throw "Expected a concrete storage pool"
|
||||
}
|
||||
|
||||
New-VirtualDisk -StoragePoolFriendlyName "GitHubActions" `
|
||||
-FriendlyName "CIVirtualDisk" -Size 1GB -ResiliencySettingName Simple
|
||||
|
||||
- name: Download container prerequisites
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$cacheDir = Join-Path $env:RUNNER_TEMP "container-prereqs"
|
||||
New-Item -ItemType Directory -Force -Path $cacheDir | Out-Null
|
||||
|
||||
$downloads = @{
|
||||
"Install-Containerd.ps1" = "https://raw.githubusercontent.com/kubernetes-sigs/sig-windows-tools/877cfb9ae91749ab9d73314337504ac962d6f90e/hostprocess/Install-Containerd.ps1"
|
||||
"containerd.tar.gz" = "https://github.com/containerd/containerd/releases/download/v$($env:CONTAINERD_VERSION)/containerd-$($env:CONTAINERD_VERSION)-windows-amd64.tar.gz"
|
||||
"crictl.tar.gz" = "https://github.com/kubernetes-sigs/cri-tools/releases/download/v$($env:CRITOOLS_VERSION)/crictl-v$($env:CRITOOLS_VERSION)-windows-amd64.tar.gz"
|
||||
"cni-plugins.tgz" = "https://github.com/containernetworking/plugins/releases/download/v$($env:CNI_VERSION)/cni-plugins-windows-amd64-v$($env:CNI_VERSION).tgz"
|
||||
"kubelet.exe" = "https://dl.k8s.io/release/$($env:KUBERNETES_VERSION)/bin/windows/amd64/kubelet.exe"
|
||||
"kubelet.exe.sha256" = "https://dl.k8s.io/release/$($env:KUBERNETES_VERSION)/bin/windows/amd64/kubelet.exe.sha256"
|
||||
"hns.psm1" = "https://raw.githubusercontent.com/microsoft/SDN/32df9732ef6dfab07dba205440e30bdfbd8e579a/Kubernetes/windows/hns.psm1"
|
||||
}
|
||||
|
||||
foreach ($download in $downloads.GetEnumerator()) {
|
||||
Invoke-WebRequest `
|
||||
-Uri $download.Value `
|
||||
-OutFile (Join-Path $cacheDir $download.Key)
|
||||
}
|
||||
|
||||
$installer = Join-Path $cacheDir "Install-Containerd.ps1"
|
||||
$installerContent = Get-Content -Raw $installer
|
||||
$installerContent = $installerContent.Replace(
|
||||
' curl.exe --silent --fail -Lo $destination $source',
|
||||
' if (Test-Path $destination) { Write-Host "Using pre-downloaded $destination"; return }' + "`r`n" + ' curl.exe --silent --fail -Lo $destination $source'
|
||||
)
|
||||
Set-Content -Path $installer -Value $installerContent -Encoding utf8
|
||||
- name: Start Hyper-V VM
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Import-Module Hyper-V
|
||||
Start-Service vmms
|
||||
|
||||
New-VMSwitch -Name "CISwitch" -SwitchType Internal
|
||||
New-VM `
|
||||
-Name "GitHubActions" `
|
||||
-Generation 1 `
|
||||
-MemoryStartupBytes 256MB `
|
||||
-NewVHDPath (Join-Path $env:RUNNER_TEMP "hyperv-ci.vhdx") `
|
||||
-NewVHDSizeBytes 1GB `
|
||||
-SwitchName "CISwitch"
|
||||
|
||||
Add-VMNetworkAdapter -VMName "GitHubActions" -Name "CILegacy" `
|
||||
-IsLegacy $true -SwitchName "CISwitch"
|
||||
Start-VM -Name "GitHubActions"
|
||||
|
||||
$vm = Get-VM -Name "GitHubActions"
|
||||
$vm | Format-List Name, State, Status, Generation, Uptime
|
||||
|
||||
if ($vm.State -ne "Running") {
|
||||
throw "Expected Hyper-V VM to be running, got $($vm.State)"
|
||||
}
|
||||
|
||||
- name: Setup local RDP session
|
||||
id: setup-rdp
|
||||
continue-on-error: true
|
||||
- name: Setup BitLocker volumes
|
||||
id: setup-bitlocker
|
||||
background: true
|
||||
shell: powershell
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$userName = "rdp-ci"
|
||||
$password = "WindowsExporter-CI-2026!"
|
||||
$securePassword = ConvertTo-SecureString $password -AsPlainText -Force
|
||||
. ./tools/vhd-lock.ps1
|
||||
|
||||
if ($null -eq (Get-LocalUser -Name $userName -ErrorAction SilentlyContinue)) {
|
||||
New-LocalUser `
|
||||
-Name $userName `
|
||||
-Password $securePassword `
|
||||
-PasswordNeverExpires `
|
||||
-AccountNeverExpires | Out-Null
|
||||
}
|
||||
# Hosted images ship the BitLocker feature; data volumes need no reboot.
|
||||
function New-BitLockerVolume([string]$Name) {
|
||||
$vhd = Join-Path $env:RUNNER_TEMP "$Name.vhdx"
|
||||
New-VHD -Path $vhd -Dynamic -SizeBytes 512MB | Out-Null
|
||||
$volume = Invoke-WithVhdLock {
|
||||
Mount-VHD $vhd -PassThru | Get-Disk |
|
||||
Initialize-Disk -PartitionStyle GPT -PassThru |
|
||||
New-Partition -UseMaximumSize -AssignDriveLetter |
|
||||
Format-Volume -FileSystem NTFS -NewFileSystemLabel $Name -Confirm:$false
|
||||
}
|
||||
$mountPoint = "$($volume.DriveLetter):"
|
||||
|
||||
Add-LocalGroupMember `
|
||||
-Group "Remote Desktop Users" `
|
||||
-Member $userName `
|
||||
-ErrorAction SilentlyContinue
|
||||
$password = ConvertTo-SecureString ("CI-" + [Guid]::NewGuid().ToString('N')) -AsPlainText -Force
|
||||
Enable-BitLocker -MountPoint $mountPoint -PasswordProtector -Password $password -UsedSpaceOnly | Out-Null
|
||||
|
||||
Set-ItemProperty `
|
||||
-Path "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server" `
|
||||
-Name fDenyTSConnections `
|
||||
-Value 0
|
||||
|
||||
Start-Service TermService
|
||||
(Get-Service TermService).WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
|
||||
|
||||
choco install freerdp.portable `
|
||||
--version=3.28.0 `
|
||||
--yes `
|
||||
--no-progress
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to install FreeRDP"
|
||||
}
|
||||
|
||||
$wfreerdp = (Get-Command wfreerdp.exe -ErrorAction Stop).Source
|
||||
$freeRdp = Start-Process `
|
||||
-FilePath $wfreerdp `
|
||||
-ArgumentList @(
|
||||
"/v:127.0.0.1"
|
||||
"/u:$env:COMPUTERNAME\$userName"
|
||||
"/p:$password"
|
||||
"/cert:ignore"
|
||||
"/size:800x600"
|
||||
) `
|
||||
-PassThru
|
||||
|
||||
Write-Host "Started wfreerdp.exe with PID $($freeRdp.Id)"
|
||||
|
||||
$deadline = (Get-Date).AddSeconds(60)
|
||||
$rdpSession = $null
|
||||
|
||||
do {
|
||||
$sessions = qwinsta.exe
|
||||
$sessions | ForEach-Object { Write-Host $_ }
|
||||
|
||||
$rdpSession = $sessions |
|
||||
Where-Object { $_ -match "RDP-Tcp#" } |
|
||||
Select-Object -First 1
|
||||
|
||||
if ($null -ne $rdpSession) {
|
||||
break
|
||||
$deadline = (Get-Date).AddMinutes(2)
|
||||
while ((Get-BitLockerVolume -MountPoint $mountPoint).VolumeStatus -ne "FullyEncrypted") {
|
||||
if ((Get-Date) -ge $deadline) { throw "BitLocker volume $Name did not finish encryption" }
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
|
||||
if ($freeRdp.HasExited) {
|
||||
throw "wfreerdp.exe exited before an RDP session was created"
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
|
||||
if ($null -eq $rdpSession) {
|
||||
throw "No RDP-Tcp session was created by FreeRDP"
|
||||
return $mountPoint
|
||||
}
|
||||
|
||||
Write-Host "RDP session created: $rdpSession"
|
||||
$unlocked = New-BitLockerVolume CIBitLocker
|
||||
$locked = New-BitLockerVolume CIBitLockerLocked
|
||||
Lock-BitLocker -MountPoint $locked -ForceDismount | Out-Null
|
||||
|
||||
$remoteFxNetwork = Get-Counter -ListSet "RemoteFX Network" -ErrorAction Stop
|
||||
$remoteFxGraphics = Get-Counter -ListSet "RemoteFX Graphics" -ErrorAction Stop
|
||||
Get-BitLockerVolume -MountPoint $unlocked, $locked |
|
||||
Format-Table MountPoint, VolumeStatus, ProtectionStatus, LockStatus
|
||||
|
||||
$networkInstances = @($remoteFxNetwork.PathsWithInstances |
|
||||
Where-Object { $_ -match "RDP-Tcp#" })
|
||||
$graphicsInstances = @($remoteFxGraphics.PathsWithInstances |
|
||||
Where-Object { $_ -match "RDP-Tcp#" })
|
||||
@{ Unlocked = $unlocked; Locked = $locked } | ConvertTo-Json |
|
||||
Set-Content (Join-Path $env:RUNNER_TEMP "bitlocker-fixture.json")
|
||||
|
||||
Write-Host "RemoteFX Network instances:"
|
||||
$networkInstances | ForEach-Object { Write-Host $_ }
|
||||
- name: Setup storage pool
|
||||
id: setup-storage-pool
|
||||
background: true
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "RemoteFX Graphics instances:"
|
||||
$graphicsInstances | ForEach-Object { Write-Host $_ }
|
||||
$disk1 = Join-Path $env:RUNNER_TEMP "storage-pool-1.vhdx"
|
||||
$disk2 = Join-Path $env:RUNNER_TEMP "storage-pool-2.vhdx"
|
||||
$diskpartScript = Join-Path $env:RUNNER_TEMP "storage-pool-diskpart.txt"
|
||||
|
||||
if ($networkInstances.Count -eq 0) {
|
||||
Write-Warning "No RemoteFX Network RDP-Tcp instance was created"
|
||||
@"
|
||||
create vdisk file="$disk1" maximum=10240 type=expandable
|
||||
attach vdisk
|
||||
create vdisk file="$disk2" maximum=10240 type=expandable
|
||||
attach vdisk
|
||||
"@ | Set-Content -Path $diskpartScript -Encoding ascii
|
||||
|
||||
. ./tools/vhd-lock.ps1
|
||||
Invoke-WithVhdLock {
|
||||
diskpart /s $diskpartScript
|
||||
if ($LASTEXITCODE -ne 0) { throw "diskpart failed to attach the storage pool disks" }
|
||||
Update-HostStorageCache
|
||||
}
|
||||
|
||||
if ($graphicsInstances.Count -eq 0) {
|
||||
Write-Warning "No RemoteFX Graphics RDP-Tcp instance was created"
|
||||
# Background steps attach their own VHDX files concurrently.
|
||||
$diskNumbers = @($disk1, $disk2 | ForEach-Object { (Get-DiskImage -ImagePath $_).Number })
|
||||
$newDisks = @(Get-PhysicalDisk | Where-Object { [int]$_.DeviceId -in $diskNumbers })
|
||||
$newDisks | Format-Table FriendlyName, DeviceId, MediaType, BusType, Size, CanPool, CannotPoolReason
|
||||
|
||||
$poolableDisks = @($newDisks | Where-Object CanPool)
|
||||
|
||||
if ($poolableDisks.Count -ne 2) {
|
||||
throw "Expected two poolable VHDX disks, found $($poolableDisks.Count)"
|
||||
}
|
||||
|
||||
New-StoragePool `
|
||||
-FriendlyName "GitHubActions" `
|
||||
-StorageSubsystemFriendlyName "Windows Storage*" `
|
||||
-PhysicalDisks $poolableDisks
|
||||
|
||||
$pool = Get-StoragePool -FriendlyName "GitHubActions" -ErrorAction Stop
|
||||
$pool | Format-List FriendlyName, HealthStatus, OperationalStatus, Size, AllocatedSize
|
||||
|
||||
if ($pool.IsPrimordial) {
|
||||
throw "Expected a concrete storage pool"
|
||||
}
|
||||
|
||||
New-VirtualDisk -StoragePoolFriendlyName "GitHubActions" `
|
||||
-FriendlyName "CIVirtualDisk" -Size 1GB -ResiliencySettingName Simple
|
||||
|
||||
- name: Start Hyper-V VM
|
||||
id: start-hyperv-vm
|
||||
background: true
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Import-Module Hyper-V
|
||||
Start-Service vmms
|
||||
|
||||
New-VMSwitch -Name "CISwitch" -SwitchType Internal
|
||||
New-VM `
|
||||
-Name "GitHubActions" `
|
||||
-Generation 1 `
|
||||
-MemoryStartupBytes 256MB `
|
||||
-NewVHDPath (Join-Path $env:RUNNER_TEMP "hyperv-ci.vhdx") `
|
||||
-NewVHDSizeBytes 1GB `
|
||||
-SwitchName "CISwitch"
|
||||
|
||||
Add-VMNetworkAdapter -VMName "GitHubActions" -Name "CILegacy" `
|
||||
-IsLegacy $true -SwitchName "CISwitch"
|
||||
Start-VM -Name "GitHubActions"
|
||||
|
||||
$vm = Get-VM -Name "GitHubActions"
|
||||
$vm | Format-List Name, State, Status, Generation, Uptime
|
||||
|
||||
if ($vm.State -ne "Running") {
|
||||
throw "Expected Hyper-V VM to be running, got $($vm.State)"
|
||||
}
|
||||
|
||||
- name: Download container prerequisites
|
||||
id: download-container-prereqs
|
||||
background: true
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$cacheDir = Join-Path $env:RUNNER_TEMP "container-prereqs"
|
||||
New-Item -ItemType Directory -Force -Path $cacheDir | Out-Null
|
||||
|
||||
$downloads = @{
|
||||
"Install-Containerd.ps1" = "https://raw.githubusercontent.com/kubernetes-sigs/sig-windows-tools/877cfb9ae91749ab9d73314337504ac962d6f90e/hostprocess/Install-Containerd.ps1"
|
||||
"containerd.tar.gz" = "https://github.com/containerd/containerd/releases/download/v$($env:CONTAINERD_VERSION)/containerd-$($env:CONTAINERD_VERSION)-windows-amd64.tar.gz"
|
||||
"crictl.tar.gz" = "https://github.com/kubernetes-sigs/cri-tools/releases/download/v$($env:CRITOOLS_VERSION)/crictl-v$($env:CRITOOLS_VERSION)-windows-amd64.tar.gz"
|
||||
"cni-plugins.tgz" = "https://github.com/containernetworking/plugins/releases/download/v$($env:CNI_VERSION)/cni-plugins-windows-amd64-v$($env:CNI_VERSION).tgz"
|
||||
"kubelet.exe" = "https://dl.k8s.io/release/$($env:KUBERNETES_VERSION)/bin/windows/amd64/kubelet.exe"
|
||||
"kubelet.exe.sha256" = "https://dl.k8s.io/release/$($env:KUBERNETES_VERSION)/bin/windows/amd64/kubelet.exe.sha256"
|
||||
"hns.psm1" = "https://raw.githubusercontent.com/microsoft/SDN/32df9732ef6dfab07dba205440e30bdfbd8e579a/Kubernetes/windows/hns.psm1"
|
||||
}
|
||||
|
||||
foreach ($download in $downloads.GetEnumerator()) {
|
||||
Invoke-WebRequest `
|
||||
-Uri $download.Value `
|
||||
-OutFile (Join-Path $cacheDir $download.Key) `
|
||||
-MaximumRetryCount 5 `
|
||||
-RetryIntervalSec 5
|
||||
}
|
||||
|
||||
$installer = Join-Path $cacheDir "Install-Containerd.ps1"
|
||||
$installerContent = Get-Content -Raw $installer
|
||||
$installerContent = $installerContent.Replace(
|
||||
' curl.exe --silent --fail -Lo $destination $source',
|
||||
' if (Test-Path $destination) { Write-Host "Using pre-downloaded $destination"; return }' + "`r`n" + ' curl.exe --silent --fail -Lo $destination $source'
|
||||
)
|
||||
Set-Content -Path $installer -Value $installerContent -Encoding utf8
|
||||
|
||||
# Install every Windows feature in one foreground step. Steps that register
|
||||
# services, such as SQL Server setup, New-Cluster, and containerd, start only
|
||||
# afterwards: changing service configuration during the install aborts its
|
||||
# registry transaction, and the rollback then requires a restart.
|
||||
- name: Install Windows features
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$features = @(
|
||||
"Containers"
|
||||
"DHCP"
|
||||
"DNS"
|
||||
"Failover-Clustering"
|
||||
"FS-FileServer"
|
||||
"FS-Resource-Manager"
|
||||
"MSMQ-Server"
|
||||
"NPAS"
|
||||
"Print-Server"
|
||||
"RSAT-Clustering-PowerShell"
|
||||
"RSAT-DHCP"
|
||||
"RSAT-DNS-Server"
|
||||
"RSAT-Print-Services"
|
||||
"Web-Server"
|
||||
)
|
||||
|
||||
# SMTP was removed from Windows Server 2025.
|
||||
if ($env:SMTP_ENABLED -eq 'true') {
|
||||
$features += "SMTP-Server"
|
||||
}
|
||||
|
||||
$result = Install-WindowsFeature -Name $features
|
||||
$result | Format-List Success, RestartNeeded, ExitCode, FeatureResult
|
||||
|
||||
if (-not $result.Success -or $result.RestartNeeded -ne "No") {
|
||||
Get-WindowsPackage -Online | Where-Object PackageState -eq InstallPending |
|
||||
Format-Table PackageName, PackageState
|
||||
throw "Required Windows features could not be installed without a restart"
|
||||
}
|
||||
|
||||
Get-WindowsFeature -Name $features |
|
||||
Format-Table DisplayName, Name, InstallState
|
||||
|
||||
- name: Setup SQL Server Express
|
||||
id: setup-mssql
|
||||
background: true
|
||||
shell: powershell
|
||||
run: ./tools/setup-sql-server.ps1
|
||||
|
||||
- name: Setup MSCluster
|
||||
id: setup-mscluster
|
||||
# The Go test requires the cluster and virtual disk.
|
||||
@@ -610,36 +617,8 @@ jobs:
|
||||
Get-ClusterNode | Format-Table Name, State, NodeWeight, DynamicWeight
|
||||
Get-ClusterNetwork | Format-Table Name, State, Role, Address
|
||||
|
||||
- name: Install Windows roles
|
||||
id: install-windows-roles
|
||||
background: true
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$features = @(
|
||||
"DHCP"
|
||||
"DNS"
|
||||
"FS-FileServer"
|
||||
"FS-Resource-Manager"
|
||||
"MSMQ-Server"
|
||||
"NPAS"
|
||||
"Print-Server"
|
||||
"RSAT-DHCP"
|
||||
"RSAT-DNS-Server"
|
||||
"RSAT-Print-Services"
|
||||
"Web-Server"
|
||||
)
|
||||
|
||||
$result = Install-WindowsFeature -Name $features
|
||||
$result | Format-List Success, RestartNeeded, ExitCode, FeatureResult
|
||||
|
||||
if (-not $result.Success -or $result.RestartNeeded -ne "No") {
|
||||
throw "Required Windows roles could not be installed without a restart"
|
||||
}
|
||||
|
||||
Get-WindowsFeature -Name $features |
|
||||
Format-Table DisplayName, Name, InstallState
|
||||
- name: Wait for container prerequisites
|
||||
wait: [download-container-prereqs]
|
||||
|
||||
- name: Prepare container runtime
|
||||
shell: pwsh
|
||||
@@ -763,13 +742,13 @@ jobs:
|
||||
spec:
|
||||
containers:
|
||||
- name: nanoserver
|
||||
image: mcr.microsoft.com/windows/nanoserver:ltsc2025
|
||||
image: __NANOSERVER_IMAGE__
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- ping.exe
|
||||
- -t
|
||||
- 127.0.0.1
|
||||
'@ | Set-Content `
|
||||
'@.Replace('__NANOSERVER_IMAGE__', $env:NANOSERVER_IMAGE) | Set-Content `
|
||||
-Path (Join-Path $manifestDir "process-isolated.yaml") `
|
||||
-Encoding ascii
|
||||
|
||||
@@ -807,10 +786,10 @@ jobs:
|
||||
$crictl = Join-Path $env:ProgramFiles "containerd\crictl.exe"
|
||||
& $crictl `
|
||||
--runtime-endpoint $env:CRI_ENDPOINT `
|
||||
pull mcr.microsoft.com/windows/nanoserver:ltsc2025
|
||||
pull $env:NANOSERVER_IMAGE
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to pull Windows Server 2025 Nano Server image"
|
||||
throw "Failed to pull Nano Server image $env:NANOSERVER_IMAGE"
|
||||
}
|
||||
|
||||
- name: Setup container network
|
||||
@@ -858,7 +837,7 @@ jobs:
|
||||
$imageNames = @($images.images.repoTags)
|
||||
|
||||
$hostProcessReady = $imageNames -contains "mcr.microsoft.com/oss/kubernetes/windows-host-process-containers-base-image:v1.0.0"
|
||||
$nanoServerReady = $imageNames -contains "mcr.microsoft.com/windows/nanoserver:ltsc2025"
|
||||
$nanoServerReady = $imageNames -contains $env:NANOSERVER_IMAGE
|
||||
|
||||
if ($networkReady -and $hostProcessReady -and $nanoServerReady) {
|
||||
break
|
||||
@@ -920,10 +899,135 @@ jobs:
|
||||
throw "Static pods did not reach Running state"
|
||||
}
|
||||
|
||||
- name: Wait for Windows roles
|
||||
wait: [install-windows-roles]
|
||||
- name: Wait for FreeRDP client
|
||||
wait: [install-freerdp]
|
||||
|
||||
- parallel:
|
||||
- name: Setup local RDP session
|
||||
shell: powershell
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$userName = "rdp-ci"
|
||||
$password = "WindowsExporter-CI-2026!"
|
||||
$securePassword = ConvertTo-SecureString $password -AsPlainText -Force
|
||||
|
||||
if ($null -eq (Get-LocalUser -Name $userName -ErrorAction SilentlyContinue)) {
|
||||
New-LocalUser `
|
||||
-Name $userName `
|
||||
-Password $securePassword `
|
||||
-PasswordNeverExpires `
|
||||
-AccountNeverExpires | Out-Null
|
||||
}
|
||||
|
||||
Add-LocalGroupMember `
|
||||
-Group "Remote Desktop Users" `
|
||||
-Member $userName `
|
||||
-ErrorAction SilentlyContinue
|
||||
|
||||
Set-ItemProperty `
|
||||
-Path "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server" `
|
||||
-Name fDenyTSConnections `
|
||||
-Value 0
|
||||
|
||||
Start-Service TermService
|
||||
(Get-Service TermService).WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
|
||||
|
||||
$wfreerdp = (Get-Command wfreerdp.exe -ErrorAction Stop).Source
|
||||
$stdout = Join-Path $env:RUNNER_TEMP "rdp-client.stdout.log"
|
||||
$stderr = Join-Path $env:RUNNER_TEMP "rdp-client.stderr.log"
|
||||
# Ending a step can kill its process tree. Task Scheduler starts the
|
||||
# client in the runner user's console session instead, so it keeps the
|
||||
# RDP session active after this step. Runners are disposable, so
|
||||
# nothing stops it.
|
||||
$clientArguments = @(
|
||||
"/v:127.0.0.1"
|
||||
"/u:$env:COMPUTERNAME\$userName"
|
||||
"/p:$password"
|
||||
"/cert:ignore"
|
||||
"/size:800x600"
|
||||
"/log-level:INFO"
|
||||
) -join " "
|
||||
$action = New-ScheduledTaskAction -Execute "cmd.exe" `
|
||||
-Argument "/c $wfreerdp $clientArguments > $stdout 2> $stderr"
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" `
|
||||
-LogonType Interactive -RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 2)
|
||||
Register-ScheduledTask -TaskName "CIFreeRDP" -Action $action -Principal $principal `
|
||||
-Settings $settings -Force | Out-Null
|
||||
Start-ScheduledTask -TaskName "CIFreeRDP"
|
||||
$deadline = (Get-Date).AddSeconds(15)
|
||||
while ((Get-ScheduledTask -TaskName "CIFreeRDP").State -ne "Running") {
|
||||
if ((Get-Date) -ge $deadline) { throw "Scheduled task CIFreeRDP did not start" }
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
Write-Host "Started wfreerdp.exe through scheduled task CIFreeRDP"
|
||||
|
||||
$deadline = (Get-Date).AddSeconds(120)
|
||||
$rdpSession = $null
|
||||
|
||||
do {
|
||||
$sessions = qwinsta.exe
|
||||
$sessions | ForEach-Object { Write-Host $_ }
|
||||
|
||||
$rdpSession = $sessions |
|
||||
Where-Object { $_ -match "^\s*>?\s*rdp-tcp#\d+\s+$userName\s+\d+\s+Active\b" } |
|
||||
Select-Object -First 1
|
||||
|
||||
if ($null -ne $rdpSession) {
|
||||
break
|
||||
}
|
||||
|
||||
if ((Get-ScheduledTask -TaskName "CIFreeRDP").State -ne "Running") {
|
||||
Get-Content $stdout,$stderr -ErrorAction SilentlyContinue
|
||||
throw "wfreerdp.exe exited before the RDP user became active"
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
|
||||
if ($null -eq $rdpSession) {
|
||||
Get-Content $stdout,$stderr -ErrorAction SilentlyContinue
|
||||
throw "No authenticated active RDP session was created by FreeRDP"
|
||||
}
|
||||
|
||||
Write-Host "Active RDP session: $rdpSession"
|
||||
$sessionName = [regex]::Match($rdpSession, 'rdp-tcp#\d+', 'IgnoreCase').Value.Replace('#', ' ')
|
||||
|
||||
$remoteFxNetwork = Get-Counter -ListSet "RemoteFX Network" -ErrorAction Stop
|
||||
$remoteFxGraphics = Get-Counter -ListSet "RemoteFX Graphics" -ErrorAction Stop
|
||||
|
||||
$networkInstances = @($remoteFxNetwork.PathsWithInstances |
|
||||
Where-Object { $_ -match "RDP-Tcp[# ]\d+" })
|
||||
$graphicsInstances = @($remoteFxGraphics.PathsWithInstances |
|
||||
Where-Object { $_ -match "RDP-Tcp[# ]\d+" })
|
||||
|
||||
Write-Host "RemoteFX Network instances:"
|
||||
$networkInstances | ForEach-Object { Write-Host $_ }
|
||||
|
||||
Write-Host "RemoteFX Graphics instances:"
|
||||
$graphicsInstances | ForEach-Object { Write-Host $_ }
|
||||
|
||||
if ($networkInstances.Count -eq 0) {
|
||||
Write-Warning "No RemoteFX Network RDP-Tcp instance was created"
|
||||
}
|
||||
|
||||
if ($graphicsInstances.Count -eq 0) {
|
||||
Write-Warning "No RemoteFX Graphics RDP-Tcp instance was created"
|
||||
}
|
||||
@{
|
||||
SessionName = $sessionName
|
||||
RemoteFX = ($networkInstances.Count -gt 0 -and $graphicsInstances.Count -gt 0)
|
||||
} | ConvertTo-Json | Set-Content (Join-Path $env:RUNNER_TEMP "rdp-ready.json")
|
||||
|
||||
- name: Start SMTP server
|
||||
if: ${{ matrix.smtp }}
|
||||
shell: powershell
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
Start-Service SMTPSVC
|
||||
(Get-Service SMTPSVC).WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
|
||||
|
||||
- name: Setup IIS
|
||||
shell: pwsh
|
||||
run: |
|
||||
@@ -977,6 +1081,7 @@ jobs:
|
||||
Select-Object -ExpandProperty Paths
|
||||
|
||||
- name: Enable Windows Update
|
||||
timeout-minutes: 5
|
||||
shell: powershell
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
@@ -993,6 +1098,19 @@ jobs:
|
||||
$historyCount = $searcher.GetTotalHistoryCount()
|
||||
Write-Host "Windows Update history count: $historyCount"
|
||||
|
||||
# Some runner images have no usable cached catalog. Search online
|
||||
# once to populate it; the Go collector still searches offline.
|
||||
# Searching only retrieves metadata, without installing updates.
|
||||
try {
|
||||
$result = $searcher.Search("IsInstalled=0 and IsHidden=0")
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Offline catalog search failed: $($_.Exception.ToString())"
|
||||
$searcher.Online = $true
|
||||
$result = $searcher.Search("IsInstalled=0 and IsHidden=0")
|
||||
}
|
||||
Write-Host "Windows Update catalog contains $($result.Updates.Count) pending updates"
|
||||
|
||||
Get-Service wuauserv |
|
||||
Format-Table Name, Status, StartType
|
||||
|
||||
@@ -1141,6 +1259,33 @@ jobs:
|
||||
|
||||
Get-SmbShare -Name "CIShare"
|
||||
Get-SmbConnection | Format-Table ServerName, ShareName, Dialect, NumOpens
|
||||
|
||||
# Keep an SMB file open and the .NET runtime loaded during the Go tests.
|
||||
# Task Scheduler keeps the workload alive after this step.
|
||||
$workload = Join-Path $env:RUNNER_TEMP "netframework-workload.ps1"
|
||||
@'
|
||||
$data = New-Object byte[] 1MB
|
||||
$path = "\\$env:COMPUTERNAME\CIShare\test.txt"
|
||||
$stream = [System.IO.File]::Open($path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::ReadWrite)
|
||||
try {
|
||||
while ($true) {
|
||||
$stream.Position = 0
|
||||
$stream.WriteByte(65)
|
||||
$stream.Flush()
|
||||
[System.GC]::KeepAlive($data)
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
} finally {
|
||||
$stream.Dispose()
|
||||
}
|
||||
'@ | Set-Content $workload
|
||||
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -File $workload"
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" `
|
||||
-LogonType Interactive -RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 2)
|
||||
Register-ScheduledTask -TaskName "CIWorkload" -Action $action -Principal $principal `
|
||||
-Settings $settings -Force | Out-Null
|
||||
Start-ScheduledTask -TaskName "CIWorkload"
|
||||
- name: Setup DNS and DHCP
|
||||
shell: pwsh
|
||||
run: |
|
||||
@@ -1194,45 +1339,77 @@ jobs:
|
||||
}
|
||||
|
||||
|
||||
- name: Wait for background setup
|
||||
wait: [pull-hostprocess, pull-nanoserver, setup-mscluster, setup-rdp]
|
||||
# The foreground waits for SQL Server here anyway, so Go setup costs nothing.
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
# renovate: golang=go
|
||||
go-version: '1.27.1'
|
||||
|
||||
- name: Start .NET Framework and SMB workloads
|
||||
- name: Wait for background setup
|
||||
wait:
|
||||
- pull-hostprocess
|
||||
- pull-nanoserver
|
||||
- setup-bitlocker
|
||||
- setup-mscluster
|
||||
- setup-mssql
|
||||
- setup-storage-pool
|
||||
- start-hyperv-vm
|
||||
|
||||
- name: Export background fixtures
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$workload = Join-Path $env:RUNNER_TEMP "netframework-workload.ps1"
|
||||
@'
|
||||
$data = New-Object byte[] 1MB
|
||||
$path = "\\$env:COMPUTERNAME\CIShare\test.txt"
|
||||
$stream = [System.IO.File]::Open($path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::ReadWrite)
|
||||
try {
|
||||
while ($true) {
|
||||
$stream.Position = 0
|
||||
$stream.WriteByte(65)
|
||||
$stream.Flush()
|
||||
[System.GC]::KeepAlive($data)
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
} finally {
|
||||
$stream.Dispose()
|
||||
$readyFile = Join-Path $env:RUNNER_TEMP "rdp-ready.json"
|
||||
if (-not (Test-Path $readyFile)) {
|
||||
Get-Content (Join-Path $env:RUNNER_TEMP "rdp-client.*.log") -ErrorAction SilentlyContinue
|
||||
throw "RDP fixture did not become ready"
|
||||
}
|
||||
'@ | Set-Content $workload
|
||||
$process = Start-Process powershell.exe -PassThru `
|
||||
-ArgumentList "-NoProfile", "-File", $workload
|
||||
$process.Id | Set-Content (Join-Path $env:RUNNER_TEMP "netframework.pid")
|
||||
$fixture = Get-Content $readyFile -Raw | ConvertFrom-Json
|
||||
"WINDOWS_EXPORTER_TEST_RDP_SESSION=$($fixture.SessionName)" | Add-Content $env:GITHUB_ENV
|
||||
if ($fixture.RemoteFX) {
|
||||
"WINDOWS_EXPORTER_TEST_COLLECTORS=$env:WINDOWS_EXPORTER_TEST_COLLECTORS,remote_fx" |
|
||||
Add-Content $env:GITHUB_ENV
|
||||
}
|
||||
|
||||
$bitlocker = Get-Content (Join-Path $env:RUNNER_TEMP "bitlocker-fixture.json") -Raw | ConvertFrom-Json
|
||||
"WINDOWS_EXPORTER_TEST_BITLOCKER_VOLUME=$($bitlocker.Unlocked)" | Add-Content $env:GITHUB_ENV
|
||||
"WINDOWS_EXPORTER_TEST_BITLOCKER_LOCKED_VOLUME=$($bitlocker.Locked)" | Add-Content $env:GITHUB_ENV
|
||||
|
||||
- name: Resync WMI performance counters
|
||||
shell: pwsh
|
||||
run: |
|
||||
winmgmt.exe /resyncperf
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to synchronize WMI performance counters" }
|
||||
|
||||
- name: Go tests
|
||||
id: go-tests
|
||||
shell: pwsh
|
||||
run: |
|
||||
go test -v -count=1 -race -timeout=10m ./... 2>&1 | Tee-Object test-output.txt
|
||||
go test -json -count=1 -race -timeout=10m ./... 2>&1 | Tee-Object test-results.jsonl |
|
||||
ForEach-Object {
|
||||
$line = $_.ToString()
|
||||
try {
|
||||
$event = $line | ConvertFrom-Json -ErrorAction Stop
|
||||
if ($event.Output) { [Console]::Write($event.Output) }
|
||||
} catch {
|
||||
Write-Host $line
|
||||
}
|
||||
}
|
||||
$testExitCode = $LASTEXITCODE
|
||||
Select-String -Path test-output.txt -Pattern '^--- SKIP:' |
|
||||
ForEach-Object Line | Add-Content $env:GITHUB_STEP_SUMMARY
|
||||
"exit_code=$testExitCode" | Add-Content $env:GITHUB_OUTPUT
|
||||
exit $testExitCode
|
||||
|
||||
- name: Summarize Windows feature tests
|
||||
if: ${{ always() }}
|
||||
shell: pwsh
|
||||
env:
|
||||
TEST_EXIT_CODE: ${{ steps.go-tests.outputs.exit_code || '1' }}
|
||||
run: |
|
||||
Copy-Item "$env:windir/Logs/DISM/dism.log" (Join-Path $env:RUNNER_TEMP "dism.log") -ErrorAction SilentlyContinue
|
||||
# CBS.log names the files that make role installation request a restart.
|
||||
Copy-Item "$env:windir/Logs/CBS/CBS.log" (Join-Path $env:RUNNER_TEMP "cbs.log") -ErrorAction SilentlyContinue
|
||||
./tools/test-summary.ps1 -TestExitCode ([int]$env:TEST_EXIT_CODE)
|
||||
|
||||
- name: Build and smoke test exporter
|
||||
shell: pwsh
|
||||
run: |
|
||||
@@ -1244,24 +1421,19 @@ jobs:
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: windows-test-results
|
||||
name: windows-test-results-${{ matrix.os }}
|
||||
path: |
|
||||
test-output.txt
|
||||
test-results.jsonl
|
||||
feature-summary.md
|
||||
${{ runner.temp }}/windows_exporter.*.log
|
||||
${{ runner.temp }}/kubelet.*.log
|
||||
${{ runner.temp }}/sql-server-summary.txt
|
||||
${{ runner.temp }}/rdp-client.*.log
|
||||
${{ runner.temp }}/dism.log
|
||||
${{ runner.temp }}/cbs.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Stop test workloads
|
||||
if: ${{ always() }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$pidFile = Join-Path $env:RUNNER_TEMP "netframework.pid"
|
||||
if (Test-Path $pidFile) {
|
||||
Stop-Process -Id (Get-Content $pidFile) -ErrorAction SilentlyContinue
|
||||
}
|
||||
Get-Process kubelet -ErrorAction SilentlyContinue | Stop-Process -ErrorAction SilentlyContinue
|
||||
Get-VM -Name GitHubActions -ErrorAction SilentlyContinue | Stop-VM -TurnOff -Force -ErrorAction SilentlyContinue
|
||||
|
||||
lint:
|
||||
runs-on: windows-2025
|
||||
permissions:
|
||||
@@ -1332,3 +1504,20 @@ jobs:
|
||||
# VALIDATE_XML: true
|
||||
# VALIDATE_YAML: true
|
||||
|
||||
# Require this job in the merge rules. It is skipped, which counts as passing,
|
||||
# unless a needed job failed or was cancelled.
|
||||
fail:
|
||||
if: ${{ cancelled() || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'failure') }}
|
||||
needs:
|
||||
- build
|
||||
- package
|
||||
- docker
|
||||
- windows-test
|
||||
- lint
|
||||
- super-linter
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Verify checks passed
|
||||
run: |
|
||||
echo "Some checks have failed!"
|
||||
exit 1
|
||||
|
||||
@@ -152,6 +152,8 @@ func waitUntilListening(tb testing.TB, network, address string) error {
|
||||
|
||||
deadline := time.Now().Add(30 * time.Second)
|
||||
|
||||
// Before the listener starts, dials fail with connection refused or, on a
|
||||
// busy Windows host, with a dial timeout. Retry both until the deadline.
|
||||
for time.Now().Before(deadline) {
|
||||
conn, err = dialer.DialContext(tb.Context(), network, address)
|
||||
if err == nil {
|
||||
@@ -160,13 +162,11 @@ func waitUntilListening(tb testing.TB, network, address string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if errors.Is(err, windows.Errno(10061)) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
continue
|
||||
if tb.Context().Err() != nil {
|
||||
break
|
||||
}
|
||||
|
||||
break
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
|
||||
if winErr, ok := errors.AsType[windows.Errno](err); ok {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Windows feature tests
|
||||
|
||||
The CI Windows job prepares real Windows roles and workloads before running
|
||||
`go test -json -count=1 -race -timeout=10m ./...`. Collector tests validate the
|
||||
metrics from those workloads. A final exporter smoke check builds and starts the
|
||||
binary, requests `/metrics` once, and checks for a nonempty HTTP 200 response.
|
||||
|
||||
The job runs on Windows Server 2022; Server 2025 runners take much longer to
|
||||
provision the same fixtures. It provisions Containers, Hyper-V, SQL Server
|
||||
Express, IIS, SMTP, MSMQ, NPS, FSRM, SMB, DNS, DHCP, a workgroup failover
|
||||
cluster, printer and storage fixtures, unlocked and locked BitLocker volumes,
|
||||
and an authenticated RDP session.
|
||||
|
||||
`WINDOWS_EXPORTER_TEST_COLLECTORS` lists required collectors. Their tests fail
|
||||
when setup is missing or collection fails instead of skipping. Fixture assertions
|
||||
check known instances, including the active RDP user's session and SQL database.
|
||||
RemoteFX is required when both network and graphics instances are available.
|
||||
|
||||
Each Windows job publishes a feature table in its Actions summary and a
|
||||
`windows-test-results-<OS>` artifact containing `feature-summary.md`, structured
|
||||
Go test events, readable test output, and setup diagnostics. The table distinguishes
|
||||
required fixtures, unavailable collectors, and empty optional instance groups.
|
||||
This records Windows feature availability rather than Go statement coverage.
|
||||
@@ -32,4 +32,5 @@ func BenchmarkCollector(b *testing.B) {
|
||||
func TestCollector(t *testing.T) {
|
||||
metrics := testutils.TestCollector(t, hyperv.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_dynamic_memory_vm_physical_bytes", prometheus.Labels{"vm": "GitHubActions"})
|
||||
testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_vm_processor_count", prometheus.Labels{"vm": "GitHubActions"})
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ const (
|
||||
Name = "logical_disk"
|
||||
subCollectorMetrics = "metrics"
|
||||
subCollectorBitlocker = "bitlocker_status"
|
||||
|
||||
bitlockerStatusLocked = 6
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -514,46 +516,86 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
|
||||
}
|
||||
|
||||
if slices.Contains(c.config.CollectorsEnabled, subCollectorBitlocker) {
|
||||
c.bitlockerReqCh <- data.Name
|
||||
c.collectBitlocker(ch, data.Name, false)
|
||||
}
|
||||
}
|
||||
|
||||
bitlockerStatus := <-c.bitlockerResCh
|
||||
|
||||
if bitlockerStatus.err != nil {
|
||||
c.logger.Warn("failed to get BitLocker status for "+data.Name,
|
||||
slog.Any("err", bitlockerStatus.err),
|
||||
)
|
||||
if slices.Contains(c.config.CollectorsEnabled, subCollectorBitlocker) {
|
||||
// Locked BitLocker volumes have no LogicalDisk performance instance.
|
||||
seen := make(map[string]struct{}, len(c.perfDataObject))
|
||||
for _, data := range c.perfDataObject {
|
||||
seen[strings.ToUpper(data.Name)] = struct{}{}
|
||||
}
|
||||
|
||||
for mountPoint := range volumes {
|
||||
if len(mountPoint) != 2 || mountPoint[1] != ':' {
|
||||
continue
|
||||
}
|
||||
|
||||
if bitlockerStatus.status == -1 {
|
||||
c.logger.Debug("BitLocker status for "+data.Name+" is unknown",
|
||||
slog.Int("status", bitlockerStatus.status),
|
||||
)
|
||||
|
||||
if _, ok := seen[strings.ToUpper(mountPoint)]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for i, status := range []string{"disabled", "on", "off", "encrypting", "decrypting", "suspended", "locked", "unknown", "waiting_for_activation"} {
|
||||
val := 0.0
|
||||
if bitlockerStatus.status == i {
|
||||
val = 1.0
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.bitlockerStatus,
|
||||
prometheus.GaugeValue,
|
||||
val,
|
||||
data.Name,
|
||||
status,
|
||||
)
|
||||
if c.config.VolumeExclude.MatchString(mountPoint) || !c.config.VolumeInclude.MatchString(mountPoint) {
|
||||
continue
|
||||
}
|
||||
|
||||
c.collectBitlocker(ch, mountPoint, true)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectBitlocker emits the BitLocker status of a volume. With lockedOnly,
|
||||
// volumes in any other state are skipped.
|
||||
func (c *Collector) collectBitlocker(ch chan<- prometheus.Metric, volume string, lockedOnly bool) {
|
||||
c.bitlockerReqCh <- volume
|
||||
|
||||
bitlockerStatus := <-c.bitlockerResCh
|
||||
|
||||
if bitlockerStatus.err != nil {
|
||||
level := slog.LevelWarn
|
||||
if lockedOnly {
|
||||
// Drive letters without performance data include empty media drives.
|
||||
level = slog.LevelDebug
|
||||
}
|
||||
|
||||
c.logger.Log(context.Background(), level, "failed to get BitLocker status for "+volume,
|
||||
slog.Any("err", bitlockerStatus.err),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if bitlockerStatus.status == -1 {
|
||||
c.logger.Debug("BitLocker status for "+volume+" is unknown",
|
||||
slog.Int("status", bitlockerStatus.status),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if lockedOnly && bitlockerStatus.status != bitlockerStatusLocked {
|
||||
return
|
||||
}
|
||||
|
||||
for i, status := range []string{"disabled", "on", "off", "encrypting", "decrypting", "suspended", "locked", "unknown", "waiting_for_activation"} {
|
||||
val := 0.0
|
||||
if bitlockerStatus.status == i {
|
||||
val = 1.0
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.bitlockerStatus,
|
||||
prometheus.GaugeValue,
|
||||
val,
|
||||
volume,
|
||||
status,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func getDriveType(driveType uint32) string {
|
||||
switch driveType {
|
||||
case windows.DRIVE_UNKNOWN:
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/logical_disk"
|
||||
"github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -45,6 +46,31 @@ func TestCollector(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCollectorBitlocker(t *testing.T) {
|
||||
metrics := testutils.TestCollector(t, logical_disk.New, &logical_disk.Config{
|
||||
CollectorsEnabled: []string{"metrics", "bitlocker_status"},
|
||||
VolumeInclude: types.RegExpAny,
|
||||
VolumeExclude: types.RegExpEmpty,
|
||||
})
|
||||
|
||||
for env, want := range map[string]string{
|
||||
"WINDOWS_EXPORTER_TEST_BITLOCKER_VOLUME": "on",
|
||||
"WINDOWS_EXPORTER_TEST_BITLOCKER_LOCKED_VOLUME": "locked",
|
||||
} {
|
||||
volume := os.Getenv(env)
|
||||
if volume == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
status := testutils.RequireFixtureMetric(t, metrics, logical_disk.Name, "windows_logical_disk_bitlocker_status", prometheus.Labels{
|
||||
"volume": volume,
|
||||
"status": want,
|
||||
})
|
||||
require.NotNil(t, status)
|
||||
require.InDelta(t, 1, status.GetGauge().GetValue(), 0, "BitLocker fixture volume %s is not reported as %s", volume, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectorVolumeFilters(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -32,4 +32,5 @@ func BenchmarkCollector(b *testing.B) {
|
||||
func TestCollector(t *testing.T) {
|
||||
metrics := testutils.TestCollector(t, mscluster.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, mscluster.Name, "windows_mscluster_virtualdisk_size_bytes", prometheus.Labels{"name": "CIVirtualDisk"})
|
||||
testutils.RequireFixtureMetric(t, metrics, mscluster.Name, "windows_mscluster_storagepool_size_bytes", prometheus.Labels{"name": "GitHubActions"})
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/mssql"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,5 +30,7 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, mssql.New, nil)
|
||||
metrics := testutils.TestCollector(t, mssql.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, mssql.Name, "windows_mssql_instance_info", prometheus.Labels{"mssql_instance": "CISQL"})
|
||||
testutils.RequireFixtureMetric(t, metrics, mssql.Name, "windows_mssql_databases_data_files_size_bytes", prometheus.Labels{"mssql_instance": "CISQL", "database": "CIWindowsExporter"})
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@
|
||||
package remote_fx_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/remote_fx"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,5 +31,8 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, remote_fx.New, nil)
|
||||
metrics := testutils.TestCollector(t, remote_fx.New, nil)
|
||||
labels := prometheus.Labels{"session_name": os.Getenv("WINDOWS_EXPORTER_TEST_RDP_SESSION")}
|
||||
testutils.RequireFixtureMetric(t, metrics, remote_fx.Name, "windows_remote_fx_net_sent_bytes_total", labels)
|
||||
testutils.RequireFixtureMetric(t, metrics, remote_fx.Name, "windows_remote_fx_gfx_output_frames_total", labels)
|
||||
}
|
||||
|
||||
@@ -18,10 +18,14 @@
|
||||
package smtp_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
netsmtp "net/smtp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/smtp"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,5 +33,16 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, smtp.New, nil)
|
||||
metrics := testutils.TestCollector(t, smtp.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, smtp.Name, "windows_smtp_inbound_connections_total", nil)
|
||||
|
||||
// Exercise the configured virtual server, including its SMTP greeting.
|
||||
dialer := net.Dialer{Timeout: 5 * time.Second}
|
||||
conn, err := dialer.DialContext(t.Context(), "tcp", "127.0.0.1:25")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = conn.Close() })
|
||||
require.NoError(t, conn.SetDeadline(time.Now().Add(5*time.Second)))
|
||||
client, err := netsmtp.NewClient(conn, "localhost")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Quit())
|
||||
}
|
||||
|
||||
@@ -18,10 +18,13 @@
|
||||
package terminal_services_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/terminal_services"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,5 +32,23 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, terminal_services.New, nil)
|
||||
metrics := testutils.TestCollector(t, terminal_services.New, nil)
|
||||
|
||||
userName := os.Getenv("WINDOWS_EXPORTER_TEST_RDP_USER")
|
||||
if userName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
sessionName := os.Getenv("WINDOWS_EXPORTER_TEST_RDP_SESSION")
|
||||
require.NotEmpty(t, sessionName, "RDP fixture did not reach an authenticated active session")
|
||||
info := testutils.RequireFixtureMetric(t, metrics, terminal_services.Name, "windows_terminal_services_session_info", prometheus.Labels{
|
||||
"user": os.Getenv("COMPUTERNAME") + `\` + userName,
|
||||
"session_name": sessionName,
|
||||
"state": "active",
|
||||
})
|
||||
require.NotNil(t, info)
|
||||
require.InDelta(t, 1, info.GetGauge().GetValue(), 0, "RDP fixture is no longer active")
|
||||
handles := testutils.RequireFixtureMetric(t, metrics, terminal_services.Name, "windows_terminal_services_handles", prometheus.Labels{"session_name": sessionName})
|
||||
require.NotNil(t, handles)
|
||||
require.Positive(t, handles.GetGauge().GetValue(), "RDP fixture has no session processes")
|
||||
}
|
||||
|
||||
+27
-29
@@ -250,6 +250,14 @@ func Test_MI_Query_Unmarshal(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_MI_FD_Leak(t *testing.T) {
|
||||
// A leak of one handle per query grows far beyond this; MI and the Go
|
||||
// runtime may still open a few handles for worker threads.
|
||||
const (
|
||||
warmupQueries = 20
|
||||
queries = 300
|
||||
maxHandleIncrease = 50
|
||||
)
|
||||
|
||||
application, err := mi.ApplicationInitialize()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, application)
|
||||
@@ -258,46 +266,34 @@ func Test_MI_FD_Leak(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, session)
|
||||
|
||||
currentFileHandle, err := testutils.GetProcessHandleCount(windows.CurrentProcess())
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("Current File Handle Count: ", currentFileHandle)
|
||||
|
||||
queryPrinter, err := mi.NewQuery("SELECT Name FROM Win32_Process")
|
||||
require.NoError(t, err)
|
||||
|
||||
for range 300 {
|
||||
query := func() {
|
||||
var processes []win32Process
|
||||
|
||||
err := session.Query(&processes, mi.NamespaceRootCIMv2, queryPrinter, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
currentFileHandle, err = testutils.GetProcessHandleCount(windows.CurrentProcess())
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("Current File Handle Count: ", currentFileHandle)
|
||||
require.NoError(t, session.Query(&processes, mi.NamespaceRootCIMv2, queryPrinter, -1))
|
||||
}
|
||||
|
||||
currentFileHandle, err = testutils.GetProcessHandleCount(windows.CurrentProcess())
|
||||
// The first queries initialize MI caches and thread pools.
|
||||
for range warmupQueries {
|
||||
query()
|
||||
}
|
||||
|
||||
baseline, err := testutils.GetProcessHandleCount(windows.CurrentProcess())
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("Current File Handle Count: ", currentFileHandle)
|
||||
for range queries {
|
||||
query()
|
||||
}
|
||||
|
||||
err = session.Close()
|
||||
current, err := testutils.GetProcessHandleCount(windows.CurrentProcess())
|
||||
require.NoError(t, err)
|
||||
require.LessOrEqual(t, int64(current), int64(baseline)+maxHandleIncrease,
|
||||
"handle count grew from %d to %d after %d queries", baseline, current, queries)
|
||||
|
||||
currentFileHandle, err = testutils.GetProcessHandleCount(windows.CurrentProcess())
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("Current File Handle Count: ", currentFileHandle)
|
||||
|
||||
err = application.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
currentFileHandle, err = testutils.GetProcessHandleCount(windows.CurrentProcess())
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("Current File Handle Count: ", currentFileHandle)
|
||||
require.NoError(t, session.Close())
|
||||
require.NoError(t, application.Close())
|
||||
}
|
||||
|
||||
func Test_MI_QueryTimeout(t *testing.T) {
|
||||
@@ -326,7 +322,9 @@ func Test_MI_QueryTimeout(t *testing.T) {
|
||||
err = operationOptions.SetTimeout(1 * time.Millisecond)
|
||||
require.NoError(t, err)
|
||||
|
||||
operation, err := session.QueryInstances(mi.OperationFlagsStandardRTTI, operationOptions, mi.NamespaceRootCIMv2, mi.QueryDialectWQL, "select Name from win32_process where handle = 0")
|
||||
// A query for a single process can finish within the timeout; reading every
|
||||
// property of every process cannot.
|
||||
operation, err := session.QueryInstances(mi.OperationFlagsStandardRTTI, operationOptions, mi.NamespaceRootCIMv2, mi.QueryDialectWQL, "select * from win32_process")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, operation)
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ func FuncBenchmarkCollector[C collector.Collector](b *testing.B, name string, co
|
||||
func TestCollector[C collector.Collector, V any](t *testing.T, fn func(*V) C, conf *V) map[string]*dto.MetricFamily {
|
||||
t.Helper()
|
||||
|
||||
logger := slog.New(slog.DiscardHandler)
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn}))
|
||||
c := fn(conf)
|
||||
required := slices.Contains(strings.Split(os.Getenv("WINDOWS_EXPORTER_TEST_COLLECTORS"), ","), c.GetName())
|
||||
|
||||
@@ -209,11 +209,11 @@ func (m collectedMetrics) Collect(ch chan<- prometheus.Metric) {
|
||||
|
||||
// RequireFixtureMetric checks a known CI fixture without requiring it on a
|
||||
// developer's machine. Labels match exactly, ignoring case for Windows names.
|
||||
func RequireFixtureMetric(t *testing.T, families map[string]*dto.MetricFamily, collectorName, metricName string, labels prometheus.Labels) {
|
||||
func RequireFixtureMetric(t *testing.T, families map[string]*dto.MetricFamily, collectorName, metricName string, labels prometheus.Labels) *dto.Metric {
|
||||
t.Helper()
|
||||
|
||||
if !slices.Contains(strings.Split(os.Getenv("WINDOWS_EXPORTER_TEST_COLLECTORS"), ","), collectorName) {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
require.Contains(t, families, metricName)
|
||||
@@ -236,11 +236,13 @@ func RequireFixtureMetric(t *testing.T, families map[string]*dto.MetricFamily, c
|
||||
}
|
||||
|
||||
if matched {
|
||||
return
|
||||
return metric
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("metric %s with fixture labels %v was not emitted; got %s", metricName, labels, families[metricName])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func noDataOnly(err error) bool {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# Install a real Database Engine instance; LocalDB does not expose server counters.
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$mediaDir = Join-Path $env:RUNNER_TEMP "sql-server"
|
||||
New-Item -ItemType Directory -Force -Path $mediaDir | Out-Null
|
||||
$setupDir = Join-Path $mediaDir "setup"
|
||||
# CI caches the extracted setup files; download and extract only on a cache miss.
|
||||
if (-not (Test-Path (Join-Path $setupDir "setup.exe"))) {
|
||||
$installer = Join-Path $mediaDir "SQL2025-SSEI-Expr.exe"
|
||||
# Microsoft download endpoints occasionally reset hosted runner connections.
|
||||
curl.exe --fail --location --retry 5 --retry-all-errors --silent --show-error --output $installer `
|
||||
"https://download.microsoft.com/download/ffd82b4c-9955-47c0-8efe-6290f7795cf6/SQL2025-SSEI-Expr.exe"
|
||||
if ($LASTEXITCODE -ne 0) { throw "SQL Server bootstrapper download failed" }
|
||||
$expectedHash = "fa7e1fabc9a2e9c9cdab0d1512bcb30d2949133147db057ac530f510e5270680"
|
||||
if ((Get-FileHash $installer -Algorithm SHA256).Hash -ne $expectedHash) {
|
||||
throw "SQL Server bootstrapper SHA256 mismatch"
|
||||
}
|
||||
|
||||
$media = Join-Path $mediaDir "SQLEXPR_x64_ENU.exe"
|
||||
foreach ($attempt in 1..3) {
|
||||
$download = Start-Process $installer -Wait -PassThru `
|
||||
-ArgumentList "/ACTION=Download", "/MEDIATYPE=Core", "/QUIET", "/MEDIAPATH=$mediaDir"
|
||||
if ($download.ExitCode -eq 0 -and (Test-Path $media)) { break }
|
||||
if ($attempt -eq 3) { throw "SQL Server media download failed: $($download.ExitCode)" }
|
||||
Write-Warning "SQL Server media download attempt $attempt failed: $($download.ExitCode)"
|
||||
Start-Sleep -Seconds 10
|
||||
}
|
||||
|
||||
$extract = Start-Process $media -Wait -PassThru `
|
||||
-ArgumentList "/Q", "/X:$setupDir"
|
||||
if ($extract.ExitCode -ne 0) {
|
||||
throw "SQL Server media extraction failed: $($extract.ExitCode)"
|
||||
}
|
||||
}
|
||||
|
||||
# Azure runners can expose sectors larger than SQL Server's supported 4 KB.
|
||||
# Keep every instance file on a disk with a known, compatible sector size.
|
||||
Import-Module Hyper-V
|
||||
$diskPath = Join-Path $env:RUNNER_TEMP "sql-server.vhdx"
|
||||
New-VHD -Path $diskPath -Dynamic -SizeBytes 8GB `
|
||||
-LogicalSectorSizeBytes 512 -PhysicalSectorSizeBytes 4096 | Out-Null
|
||||
. (Join-Path $PSScriptRoot "vhd-lock.ps1")
|
||||
$volume = Invoke-WithVhdLock {
|
||||
$disk = Mount-VHD -Path $diskPath -PassThru | Get-Disk
|
||||
if ($disk.PhysicalSectorSize -gt 4096) {
|
||||
throw "SQL Server fixture disk exposes unsupported physical sectors"
|
||||
}
|
||||
$disk | Initialize-Disk -PartitionStyle GPT -PassThru |
|
||||
New-Partition -UseMaximumSize -AssignDriveLetter |
|
||||
Format-Volume -FileSystem NTFS -NewFileSystemLabel CISQL -Confirm:$false
|
||||
}
|
||||
$instanceDir = "$($volume.DriveLetter):\SQLServer"
|
||||
fsutil.exe fsinfo sectorinfo "$($volume.DriveLetter):"
|
||||
|
||||
$setup = Start-Process (Join-Path $setupDir "setup.exe") -Wait -PassThru -ArgumentList @(
|
||||
"/Q"
|
||||
"/IACCEPTSQLSERVERLICENSETERMS"
|
||||
"/ACTION=Install"
|
||||
"/FEATURES=SQLEngine"
|
||||
"/INSTANCENAME=CISQL"
|
||||
"/INSTANCEDIR=$instanceDir"
|
||||
"/INSTALLSQLDATADIR=$instanceDir"
|
||||
'/SQLSYSADMINACCOUNTS="BUILTIN\Administrators"'
|
||||
"/UPDATEENABLED=False"
|
||||
"/TCPENABLED=0"
|
||||
"/NPENABLED=0"
|
||||
)
|
||||
$summaryLog = Join-Path $env:RUNNER_TEMP "sql-server-summary.txt"
|
||||
Copy-Item "C:\Program Files\Microsoft SQL Server\170\Setup Bootstrap\Log\Summary.txt" `
|
||||
$summaryLog -ErrorAction SilentlyContinue
|
||||
if ($setup.ExitCode -ne 0) {
|
||||
Get-Content $summaryLog -ErrorAction SilentlyContinue
|
||||
throw "SQL Server setup failed or requires a restart: $($setup.ExitCode)"
|
||||
}
|
||||
|
||||
$connection = New-Object System.Data.SqlClient.SqlConnection
|
||||
$connection.ConnectionString = "Data Source=lpc:.\CISQL;Integrated Security=True;Connect Timeout=30"
|
||||
try {
|
||||
$connection.Open()
|
||||
$command = $connection.CreateCommand()
|
||||
$command.CommandText = "CREATE DATABASE CIWindowsExporter"
|
||||
$command.ExecuteNonQuery() | Out-Null
|
||||
# Express can close idle databases and remove their performance counter instances.
|
||||
$command.CommandText = "ALTER DATABASE CIWindowsExporter SET AUTO_CLOSE OFF"
|
||||
$command.ExecuteNonQuery() | Out-Null
|
||||
$connection.ChangeDatabase("CIWindowsExporter")
|
||||
$command.CommandText = @'
|
||||
CREATE TABLE dbo.Fixture (ID int NOT NULL, Data char(8000) NOT NULL);
|
||||
INSERT INTO dbo.Fixture SELECT TOP (1000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), 'fixture' FROM sys.all_objects;
|
||||
CHECKPOINT;
|
||||
'@
|
||||
$command.ExecuteNonQuery() | Out-Null
|
||||
} finally {
|
||||
$connection.Dispose()
|
||||
}
|
||||
Get-Service 'MSSQL$CISQL' | Format-Table Name, Status
|
||||
@@ -0,0 +1,64 @@
|
||||
param([int]$TestExitCode)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$prefix = "github.com/prometheus-community/windows_exporter/internal/collector/"
|
||||
$required = @($env:WINDOWS_EXPORTER_TEST_COLLECTORS.Split(','))
|
||||
$results = @{}
|
||||
$text = [System.Text.StringBuilder]::new()
|
||||
|
||||
$lines = if (Test-Path test-results.jsonl) { Get-Content test-results.jsonl } else { @() }
|
||||
foreach ($line in $lines) {
|
||||
try {
|
||||
$event = $line | ConvertFrom-Json -ErrorAction Stop
|
||||
} catch {
|
||||
[void]$text.AppendLine($line)
|
||||
continue
|
||||
}
|
||||
if ($event.Output) { [void]$text.Append($event.Output) }
|
||||
if (-not $event.Package -or -not $event.Package.StartsWith($prefix) -or $event.Test -ne "TestCollector") {
|
||||
continue
|
||||
}
|
||||
$name = $event.Package.Substring($prefix.Length)
|
||||
if (-not $results.ContainsKey($name)) {
|
||||
$results[$name] = @{ State = "not completed"; Notes = @() }
|
||||
}
|
||||
if ($event.Action -in @("pass", "skip", "fail")) {
|
||||
$results[$name].State = $event.Action
|
||||
}
|
||||
if ($event.Output -match 'collector \S+ (?:is not supported|has empty optional instance groups): (.+)') {
|
||||
$results[$name].Notes += $Matches[1].Trim()
|
||||
}
|
||||
}
|
||||
$text.ToString() | Set-Content test-output.txt -NoNewline -Encoding utf8
|
||||
|
||||
# Aborted tests must leave required fixtures visible as not run.
|
||||
foreach ($name in $required) {
|
||||
if (-not $results.ContainsKey($name)) {
|
||||
$results[$name] = @{ State = "not run"; Notes = @() }
|
||||
}
|
||||
}
|
||||
$testState = if (-not $lines) { "not run" } elseif ($TestExitCode -eq 0) { "passed" } else { "failed" }
|
||||
$summary = @(
|
||||
"### Windows feature tests: $env:WINDOWS_TEST_OS"
|
||||
""
|
||||
"Go suite: **$testState**. Required fixtures cannot skip. Empty optional instance groups remain listed below."
|
||||
""
|
||||
"| Collector | Required | Live test | Unavailable features / notes |"
|
||||
"| --- | --- | --- | --- |"
|
||||
)
|
||||
foreach ($name in $results.Keys | Sort-Object) {
|
||||
$fixture = if ($name -in $required) { "yes" } else { "no" }
|
||||
$state = switch ($results[$name].State) {
|
||||
"pass" { "passed" }
|
||||
"skip" { "unavailable" }
|
||||
"fail" { "failed" }
|
||||
default { $results[$name].State }
|
||||
}
|
||||
$notes = ($results[$name].Notes | Sort-Object -Unique) -join '; '
|
||||
$notes = $notes.Replace('|', '\|')
|
||||
$summary += "| $name | $fixture | $state | $notes |"
|
||||
}
|
||||
$summary | Set-Content feature-summary.md -Encoding utf8
|
||||
if ($env:GITHUB_STEP_SUMMARY) {
|
||||
$summary | Add-Content $env:GITHUB_STEP_SUMMARY
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# Background CI steps attach virtual disks at the same time, which makes
|
||||
# Mount-VHD fail with 0x800703E3. Serialize attaching and formatting them.
|
||||
function Invoke-WithVhdLock([scriptblock]$ScriptBlock) {
|
||||
$mutex = [System.Threading.Mutex]::new($false, "Global\windows-exporter-ci-vhd")
|
||||
try {
|
||||
try {
|
||||
[void]$mutex.WaitOne()
|
||||
} catch [System.Threading.AbandonedMutexException] {
|
||||
# The previous owner exited without releasing; the lock is ours now.
|
||||
}
|
||||
|
||||
try {
|
||||
& $ScriptBlock
|
||||
} finally {
|
||||
$mutex.ReleaseMutex()
|
||||
}
|
||||
} finally {
|
||||
$mutex.Dispose()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user