param( [string]$BackendBaseUrl = "http://192.168.200.61:8080/api/v1", [string]$DockerContext = "test-ubuntu", [string]$AdminEmail = "windows-smoke@example.local", [string]$AdminPassword = "SmokePass!123", [int]$Count = 3, [string]$ImageTag = "rap-node-agent:control-panel-test" ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).ProviderPath $stateRoot = Join-Path "C:\work" "rap-fabric-test-nodes" New-Item -ItemType Directory -Force -Path $stateRoot | Out-Null function Invoke-Docker { param([string[]]$Arguments) docker @Arguments if ($LASTEXITCODE -ne 0) { throw "docker $($Arguments -join ' ') failed with exit code $LASTEXITCODE" } } function Invoke-Api { param( [string]$Method, [string]$Path, [object]$Body = $null ) $uri = "$BackendBaseUrl$Path" if ($null -eq $Body) { return Invoke-RestMethod -Method $Method -Uri $uri -TimeoutSec 30 } return Invoke-RestMethod -Method $Method -Uri $uri -ContentType "application/json" -Body ($Body | ConvertTo-Json -Depth 20) -TimeoutSec 30 } Write-Host "Building node-agent image on Docker context $DockerContext..." Invoke-Docker -Arguments @("--context", $DockerContext, "build", "-f", "$repoRoot\agents\rap-node-agent\Dockerfile", "-t", $ImageTag, "$repoRoot") $login = Invoke-Api -Method Post -Path "/auth/login" -Body @{ email = $AdminEmail password = $AdminPassword device_fingerprint = "fabric-test-nodes-script" device_label = "Fabric Test Nodes Script" trust_device = $true } $actorUserID = $login.user.id $clusters = Invoke-Api -Method Get -Path "/clusters/?actor_user_id=$actorUserID" $cluster = @($clusters.clusters)[0] if ($null -eq $cluster) { throw "No cluster available for test node deployment." } $clusterID = $cluster.id Write-Host "Using cluster $($cluster.name) $clusterID" Invoke-Api -Method Put -Path "/fabric/testing-flags" -Body @{ actor_user_id = $actorUserID scope_type = "platform" scope_id = $null cluster_id = $null enabled = $true telemetry_enabled = $true synthetic_links_enabled = $true history_retention_hours = 24 metadata = @{ source = "deploy-test-nodes.ps1"; runtime_mesh_enabled = $false } } | Out-Null $joinToken = Invoke-Api -Method Post -Path "/clusters/$clusterID/join-tokens" -Body @{ actor_user_id = $actorUserID scope = @{ purpose = "fabric-test-nodes"; roles = @("core-mesh", "relay-node") } expires_at = (Get-Date).ToUniversalTime().AddHours(2).ToString("o") max_uses = $Count } $nodes = @() for ($i = 1; $i -le $Count; $i++) { $nodeName = "fabric-test-node-$i" $fingerprint = "rap-node-fp_$([guid]::NewGuid().ToString('N'))" $publicKey = "rap-node-pub_$([guid]::NewGuid().ToString('N'))" $joinRequest = Invoke-Api -Method Post -Path "/clusters/$clusterID/join-requests" -Body @{ join_token = $joinToken.join_token.token node_name = $nodeName node_fingerprint = $fingerprint public_key = $publicKey reported_capabilities = @{ can_accept_node_ingress = $true can_route_mesh = $true can_run_rdp_worker = $false testing_node = $true } reported_facts = @{ os = "linux" runtime = "docker-test" source = "deploy-test-nodes.ps1" } requested_roles = @("core-mesh", "relay-node") } $approved = Invoke-Api -Method Post -Path "/clusters/$clusterID/join-requests/$($joinRequest.join_request.id)/approve" -Body @{ actor_user_id = $actorUserID node_key = $fingerprint ownership_type = "platform_managed" owner_organization_id = $null } $nodeID = $approved.node_bootstrap.node_id $nodes += [pscustomobject]@{ name = $nodeName; id = $nodeID; fingerprint = $fingerprint; public_key = $publicKey } $nodeStateDir = Join-Path $stateRoot $nodeName New-Item -ItemType Directory -Force -Path $nodeStateDir | Out-Null @{ node_id = $nodeID cluster_id = $clusterID node_name = $nodeName node_fingerprint = $fingerprint public_key = $publicKey identity_status = "active" created_at = (Get-Date).ToUniversalTime().ToString("o") updated_at = (Get-Date).ToUniversalTime().ToString("o") } | ConvertTo-Json -Depth 10 | Set-Content -Encoding UTF8 -Path (Join-Path $nodeStateDir "identity.json") $containerName = "rap_fabric_test_node_$i" docker --context $DockerContext rm -f $containerName 2>$null | Out-Null Invoke-Docker -Arguments @( "--context", $DockerContext, "create", "--name", $containerName, "--network", "host", "-e", "RAP_BACKEND_URL=$BackendBaseUrl", "-e", "RAP_NODE_STATE_DIR=/tmp/state", "-e", "RAP_HEARTBEAT_INTERVAL_SECONDS=5", $ImageTag, "-backend-url", $BackendBaseUrl, "-state-dir", "/tmp/state", "-heartbeat-interval", "5s" ) | Out-Null Invoke-Docker -Arguments @("--context", $DockerContext, "cp", "$nodeStateDir\.", "$containerName`:/tmp/state") Invoke-Docker -Arguments @("--context", $DockerContext, "start", $containerName) | Out-Null Write-Host "Started $containerName for $nodeName $nodeID" } Start-Sleep -Seconds 12 for ($i = 0; $i -lt $nodes.Count; $i++) { for ($j = 0; $j -lt $nodes.Count; $j++) { if ($i -eq $j) { continue } $latency = 2 + (($i + $j) % 7) Invoke-Api -Method Post -Path "/clusters/$clusterID/mesh/links" -Body @{ source_node_id = $nodes[$i].id target_node_id = $nodes[$j].id link_status = "reachable" latency_ms = $latency quality_score = 95 - $latency metadata = @{ source = "deploy-test-nodes.ps1" synthetic = $true runtime_mesh_enabled = $false } } | Out-Null } } $summary = Invoke-Api -Method Get -Path "/cluster-admin-summaries?actor_user_id=$actorUserID" $links = Invoke-Api -Method Get -Path "/clusters/$clusterID/mesh/links?actor_user_id=$actorUserID" [pscustomobject]@{ cluster_id = $clusterID nodes_started = $nodes.Count cluster_summaries = $summary.cluster_summaries mesh_link_count = @($links.mesh_links).Count state_root = $stateRoot } | ConvertTo-Json -Depth 20