provisioning/docs/book/api/websocket.html
Jesús Pérez 6a59d34bb1
chore: update provisioning configuration and documentation
Update configuration files, templates, and internal documentation
for the provisioning repository system.

Configuration Updates:
- KMS configuration modernization
- Plugin system settings
- Service port mappings
- Test cluster topologies
- Installation configuration examples
- VM configuration defaults
- Cedar authorization policies

Documentation Updates:
- Library module documentation
- Extension API guides
- AI system documentation
- Service management guides
- Test environment setup
- Plugin usage guides
- Validator configuration documentation

All changes are backward compatible.
2025-12-11 21:50:42 +00:00

1047 lines
38 KiB
HTML

<!DOCTYPE HTML>
<html lang="en" class="ayu sidebar-visible" dir="ltr">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>WebSocket API - Provisioning Platform Documentation</title>
<!-- Custom HTML head -->
<meta name="description" content="Complete documentation for the Provisioning Platform - Infrastructure automation with Nushell, KCL, and Rust">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="icon" href="../favicon.svg">
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<link rel="stylesheet" href="../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" id="highlight-css" href="../highlight.css">
<link rel="stylesheet" id="tomorrow-night-css" href="../tomorrow-night.css">
<link rel="stylesheet" id="ayu-highlight-css" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
<!-- Provide site root and default themes to javascript -->
<script>
const path_to_root = "../";
const default_light_theme = "ayu";
const default_dark_theme = "navy";
</script>
<!-- Start loading toc.js asap -->
<script src="../toc.js"></script>
</head>
<body>
<div id="mdbook-help-container">
<div id="mdbook-help-popup">
<h2 class="mdbook-help-title">Keyboard shortcuts</h2>
<div>
<p>Press <kbd></kbd> or <kbd></kbd> to navigate between chapters</p>
<p>Press <kbd>S</kbd> or <kbd>/</kbd> to search in the book</p>
<p>Press <kbd>?</kbd> to show this help</p>
<p>Press <kbd>Esc</kbd> to hide this help</p>
</div>
</div>
</div>
<div id="body-container">
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
let theme = localStorage.getItem('mdbook-theme');
let sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
const default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? default_dark_theme : default_light_theme;
let theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
const html = document.documentElement;
html.classList.remove('ayu')
html.classList.add(theme);
html.classList.add("js");
</script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
let sidebar = null;
const sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
sidebar_toggle.checked = sidebar === 'visible';
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<!-- populated by js -->
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
<noscript>
<iframe class="sidebar-iframe-outer" src="../toc.html"></iframe>
</noscript>
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</label>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="default_theme">Auto</button></li>
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search (`/`)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="/ s" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Provisioning Platform Documentation</h1>
<div class="right-buttons">
<a href="../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
<a href="https://github.com/provisioning/provisioning-platform" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
<a href="https://github.com/provisioning/provisioning-platform/edit/main/provisioning/docs/src/api/websocket.md" title="Suggest an edit" aria-label="Suggest an edit">
<i id="git-edit-button" class="fa fa-edit"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1 id="websocket-api-reference"><a class="header" href="#websocket-api-reference">WebSocket API Reference</a></h1>
<p>This document provides comprehensive documentation for the WebSocket API used for real-time monitoring, event streaming, and live updates in provisioning.</p>
<h2 id="overview"><a class="header" href="#overview">Overview</a></h2>
<p>The WebSocket API enables real-time communication between clients and the provisioning orchestrator, providing:</p>
<ul>
<li>Live workflow progress updates</li>
<li>System health monitoring</li>
<li>Event streaming</li>
<li>Real-time metrics</li>
<li>Interactive debugging sessions</li>
</ul>
<h2 id="websocket-endpoints"><a class="header" href="#websocket-endpoints">WebSocket Endpoints</a></h2>
<h3 id="primary-websocket-endpoint"><a class="header" href="#primary-websocket-endpoint">Primary WebSocket Endpoint</a></h3>
<h4 id="wslocalhost9090ws"><a class="header" href="#wslocalhost9090ws"><code>ws://localhost:9090/ws</code></a></h4>
<p>The main WebSocket endpoint for real-time events and monitoring.</p>
<p><strong>Connection Parameters:</strong></p>
<ul>
<li><code>token</code>: JWT authentication token (required)</li>
<li><code>events</code>: Comma-separated list of event types to subscribe to (optional)</li>
<li><code>batch_size</code>: Maximum number of events per message (default: 10)</li>
<li><code>compression</code>: Enable message compression (default: false)</li>
</ul>
<p><strong>Example Connection:</strong></p>
<pre><code class="language-javascript">const ws = new WebSocket('ws://localhost:9090/ws?token=jwt-token&amp;events=task,batch,system');
</code></pre>
<h3 id="specialized-websocket-endpoints"><a class="header" href="#specialized-websocket-endpoints">Specialized WebSocket Endpoints</a></h3>
<h4 id="wslocalhost9090metrics"><a class="header" href="#wslocalhost9090metrics"><code>ws://localhost:9090/metrics</code></a></h4>
<p>Real-time metrics streaming endpoint.</p>
<p><strong>Features:</strong></p>
<ul>
<li>Live system metrics</li>
<li>Performance data</li>
<li>Resource utilization</li>
<li>Custom metric streams</li>
</ul>
<h4 id="wslocalhost9090logs"><a class="header" href="#wslocalhost9090logs"><code>ws://localhost:9090/logs</code></a></h4>
<p>Live log streaming endpoint.</p>
<p><strong>Features:</strong></p>
<ul>
<li>Real-time log tailing</li>
<li>Log level filtering</li>
<li>Component-specific logs</li>
<li>Search and filtering</li>
</ul>
<h2 id="authentication"><a class="header" href="#authentication">Authentication</a></h2>
<h3 id="jwt-token-authentication"><a class="header" href="#jwt-token-authentication">JWT Token Authentication</a></h3>
<p>All WebSocket connections require authentication via JWT token:</p>
<pre><code class="language-javascript">// Include token in connection URL
const ws = new WebSocket('ws://localhost:9090/ws?token=' + jwtToken);
// Or send token after connection
ws.onopen = function() {
ws.send(JSON.stringify({
type: 'auth',
token: jwtToken
}));
};
</code></pre>
<h3 id="connection-authentication-flow"><a class="header" href="#connection-authentication-flow">Connection Authentication Flow</a></h3>
<ol>
<li><strong>Initial Connection</strong>: Client connects with token parameter</li>
<li><strong>Token Validation</strong>: Server validates JWT token</li>
<li><strong>Authorization</strong>: Server checks token permissions</li>
<li><strong>Subscription</strong>: Client subscribes to event types</li>
<li><strong>Event Stream</strong>: Server begins streaming events</li>
</ol>
<h2 id="event-types-and-schemas"><a class="header" href="#event-types-and-schemas">Event Types and Schemas</a></h2>
<h3 id="core-event-types"><a class="header" href="#core-event-types">Core Event Types</a></h3>
<h4 id="task-status-changed"><a class="header" href="#task-status-changed">Task Status Changed</a></h4>
<p>Fired when a workflow task status changes.</p>
<pre><code class="language-json">{
"event_type": "TaskStatusChanged",
"timestamp": "2025-09-26T10:00:00Z",
"data": {
"task_id": "uuid-string",
"name": "create_servers",
"status": "Running",
"previous_status": "Pending",
"progress": 45.5
},
"metadata": {
"task_id": "uuid-string",
"workflow_type": "server_creation",
"infra": "production"
}
}
</code></pre>
<h4 id="batch-operation-update"><a class="header" href="#batch-operation-update">Batch Operation Update</a></h4>
<p>Fired when batch operation status changes.</p>
<pre><code class="language-json">{
"event_type": "BatchOperationUpdate",
"timestamp": "2025-09-26T10:00:00Z",
"data": {
"batch_id": "uuid-string",
"name": "multi_cloud_deployment",
"status": "Running",
"progress": 65.0,
"operations": [
{
"id": "upcloud_servers",
"status": "Completed",
"progress": 100.0
},
{
"id": "aws_taskservs",
"status": "Running",
"progress": 30.0
}
]
},
"metadata": {
"total_operations": 5,
"completed_operations": 2,
"failed_operations": 0
}
}
</code></pre>
<h4 id="system-health-update"><a class="header" href="#system-health-update">System Health Update</a></h4>
<p>Fired when system health status changes.</p>
<pre><code class="language-json">{
"event_type": "SystemHealthUpdate",
"timestamp": "2025-09-26T10:00:00Z",
"data": {
"overall_status": "Healthy",
"components": {
"storage": {
"status": "Healthy",
"last_check": "2025-09-26T09:59:55Z"
},
"batch_coordinator": {
"status": "Warning",
"last_check": "2025-09-26T09:59:55Z",
"message": "High memory usage"
}
},
"metrics": {
"cpu_usage": 45.2,
"memory_usage": 2048,
"disk_usage": 75.5,
"active_workflows": 5
}
},
"metadata": {
"check_interval": 30,
"next_check": "2025-09-26T10:00:30Z"
}
}
</code></pre>
<h4 id="workflow-progress-update"><a class="header" href="#workflow-progress-update">Workflow Progress Update</a></h4>
<p>Fired when workflow progress changes.</p>
<pre><code class="language-json">{
"event_type": "WorkflowProgressUpdate",
"timestamp": "2025-09-26T10:00:00Z",
"data": {
"workflow_id": "uuid-string",
"name": "kubernetes_deployment",
"progress": 75.0,
"current_step": "Installing CNI",
"total_steps": 8,
"completed_steps": 6,
"estimated_time_remaining": 120,
"step_details": {
"step_name": "Installing CNI",
"step_progress": 45.0,
"step_message": "Downloading Cilium components"
}
},
"metadata": {
"infra": "production",
"provider": "upcloud",
"started_at": "2025-09-26T09:45:00Z"
}
}
</code></pre>
<h4 id="log-entry"><a class="header" href="#log-entry">Log Entry</a></h4>
<p>Real-time log streaming.</p>
<pre><code class="language-json">{
"event_type": "LogEntry",
"timestamp": "2025-09-26T10:00:00Z",
"data": {
"level": "INFO",
"message": "Server web-01 created successfully",
"component": "server-manager",
"task_id": "uuid-string",
"details": {
"server_id": "server-uuid",
"hostname": "web-01",
"ip_address": "10.0.1.100"
}
},
"metadata": {
"source": "orchestrator",
"thread": "worker-1"
}
}
</code></pre>
<h4 id="metric-update"><a class="header" href="#metric-update">Metric Update</a></h4>
<p>Real-time metrics streaming.</p>
<pre><code class="language-json">{
"event_type": "MetricUpdate",
"timestamp": "2025-09-26T10:00:00Z",
"data": {
"metric_name": "workflow_duration",
"metric_type": "histogram",
"value": 180.5,
"labels": {
"workflow_type": "server_creation",
"status": "completed",
"infra": "production"
}
},
"metadata": {
"interval": 15,
"aggregation": "average"
}
}
</code></pre>
<h3 id="custom-event-types"><a class="header" href="#custom-event-types">Custom Event Types</a></h3>
<p>Applications can define custom event types:</p>
<pre><code class="language-json">{
"event_type": "CustomApplicationEvent",
"timestamp": "2025-09-26T10:00:00Z",
"data": {
// Custom event data
},
"metadata": {
"custom_field": "custom_value"
}
}
</code></pre>
<h2 id="client-side-javascript-api"><a class="header" href="#client-side-javascript-api">Client-Side JavaScript API</a></h2>
<h3 id="connection-management"><a class="header" href="#connection-management">Connection Management</a></h3>
<pre><code class="language-javascript">class ProvisioningWebSocket {
constructor(baseUrl, token, options = {}) {
this.baseUrl = baseUrl;
this.token = token;
this.options = {
reconnect: true,
reconnectInterval: 5000,
maxReconnectAttempts: 10,
...options
};
this.ws = null;
this.reconnectAttempts = 0;
this.eventHandlers = new Map();
}
connect() {
const wsUrl = `${this.baseUrl}/ws?token=${this.token}`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = (event) =&gt; {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
this.emit('connected', event);
};
this.ws.onmessage = (event) =&gt; {
try {
const message = JSON.parse(event.data);
this.handleMessage(message);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
this.ws.onclose = (event) =&gt; {
console.log('WebSocket disconnected');
this.emit('disconnected', event);
if (this.options.reconnect &amp;&amp; this.reconnectAttempts &lt; this.options.maxReconnectAttempts) {
setTimeout(() =&gt; {
this.reconnectAttempts++;
console.log(`Reconnecting... (${this.reconnectAttempts}/${this.options.maxReconnectAttempts})`);
this.connect();
}, this.options.reconnectInterval);
}
};
this.ws.onerror = (error) =&gt; {
console.error('WebSocket error:', error);
this.emit('error', error);
};
}
handleMessage(message) {
if (message.event_type) {
this.emit(message.event_type, message);
this.emit('message', message);
}
}
on(eventType, handler) {
if (!this.eventHandlers.has(eventType)) {
this.eventHandlers.set(eventType, []);
}
this.eventHandlers.get(eventType).push(handler);
}
off(eventType, handler) {
const handlers = this.eventHandlers.get(eventType);
if (handlers) {
const index = handlers.indexOf(handler);
if (index &gt; -1) {
handlers.splice(index, 1);
}
}
}
emit(eventType, data) {
const handlers = this.eventHandlers.get(eventType);
if (handlers) {
handlers.forEach(handler =&gt; {
try {
handler(data);
} catch (error) {
console.error(`Error in event handler for ${eventType}:`, error);
}
});
}
}
send(message) {
if (this.ws &amp;&amp; this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
} else {
console.warn('WebSocket not connected, message not sent');
}
}
disconnect() {
this.options.reconnect = false;
if (this.ws) {
this.ws.close();
}
}
subscribe(eventTypes) {
this.send({
type: 'subscribe',
events: Array.isArray(eventTypes) ? eventTypes : [eventTypes]
});
}
unsubscribe(eventTypes) {
this.send({
type: 'unsubscribe',
events: Array.isArray(eventTypes) ? eventTypes : [eventTypes]
});
}
}
// Usage example
const ws = new ProvisioningWebSocket('ws://localhost:9090', 'your-jwt-token');
ws.on('TaskStatusChanged', (event) =&gt; {
console.log(`Task ${event.data.task_id} status: ${event.data.status}`);
updateTaskUI(event.data);
});
ws.on('WorkflowProgressUpdate', (event) =&gt; {
console.log(`Workflow progress: ${event.data.progress}%`);
updateProgressBar(event.data.progress);
});
ws.on('SystemHealthUpdate', (event) =&gt; {
console.log('System health:', event.data.overall_status);
updateHealthIndicator(event.data);
});
ws.connect();
// Subscribe to specific events
ws.subscribe(['TaskStatusChanged', 'WorkflowProgressUpdate']);
</code></pre>
<h3 id="real-time-dashboard-example"><a class="header" href="#real-time-dashboard-example">Real-Time Dashboard Example</a></h3>
<pre><code class="language-javascript">class ProvisioningDashboard {
constructor(wsUrl, token) {
this.ws = new ProvisioningWebSocket(wsUrl, token);
this.setupEventHandlers();
this.connect();
}
setupEventHandlers() {
this.ws.on('TaskStatusChanged', this.handleTaskUpdate.bind(this));
this.ws.on('BatchOperationUpdate', this.handleBatchUpdate.bind(this));
this.ws.on('SystemHealthUpdate', this.handleHealthUpdate.bind(this));
this.ws.on('WorkflowProgressUpdate', this.handleProgressUpdate.bind(this));
this.ws.on('LogEntry', this.handleLogEntry.bind(this));
}
connect() {
this.ws.connect();
}
handleTaskUpdate(event) {
const taskCard = document.getElementById(`task-${event.data.task_id}`);
if (taskCard) {
taskCard.querySelector('.status').textContent = event.data.status;
taskCard.querySelector('.status').className = `status ${event.data.status.toLowerCase()}`;
if (event.data.progress) {
const progressBar = taskCard.querySelector('.progress-bar');
progressBar.style.width = `${event.data.progress}%`;
}
}
}
handleBatchUpdate(event) {
const batchCard = document.getElementById(`batch-${event.data.batch_id}`);
if (batchCard) {
batchCard.querySelector('.batch-progress').style.width = `${event.data.progress}%`;
event.data.operations.forEach(op =&gt; {
const opElement = batchCard.querySelector(`[data-operation="${op.id}"]`);
if (opElement) {
opElement.querySelector('.operation-status').textContent = op.status;
opElement.querySelector('.operation-progress').style.width = `${op.progress}%`;
}
});
}
}
handleHealthUpdate(event) {
const healthIndicator = document.getElementById('health-indicator');
healthIndicator.className = `health-indicator ${event.data.overall_status.toLowerCase()}`;
healthIndicator.textContent = event.data.overall_status;
const metricsPanel = document.getElementById('metrics-panel');
metricsPanel.innerHTML = `
&lt;div class="metric"&gt;CPU: ${event.data.metrics.cpu_usage}%&lt;/div&gt;
&lt;div class="metric"&gt;Memory: ${Math.round(event.data.metrics.memory_usage / 1024 / 1024)}MB&lt;/div&gt;
&lt;div class="metric"&gt;Disk: ${event.data.metrics.disk_usage}%&lt;/div&gt;
&lt;div class="metric"&gt;Active Workflows: ${event.data.metrics.active_workflows}&lt;/div&gt;
`;
}
handleProgressUpdate(event) {
const workflowCard = document.getElementById(`workflow-${event.data.workflow_id}`);
if (workflowCard) {
const progressBar = workflowCard.querySelector('.workflow-progress');
const stepInfo = workflowCard.querySelector('.step-info');
progressBar.style.width = `${event.data.progress}%`;
stepInfo.textContent = `${event.data.current_step} (${event.data.completed_steps}/${event.data.total_steps})`;
if (event.data.estimated_time_remaining) {
const timeRemaining = workflowCard.querySelector('.time-remaining');
timeRemaining.textContent = `${Math.round(event.data.estimated_time_remaining / 60)} min remaining`;
}
}
}
handleLogEntry(event) {
const logContainer = document.getElementById('log-container');
const logEntry = document.createElement('div');
logEntry.className = `log-entry log-${event.data.level.toLowerCase()}`;
logEntry.innerHTML = `
&lt;span class="log-timestamp"&gt;${new Date(event.timestamp).toLocaleTimeString()}&lt;/span&gt;
&lt;span class="log-level"&gt;${event.data.level}&lt;/span&gt;
&lt;span class="log-component"&gt;${event.data.component}&lt;/span&gt;
&lt;span class="log-message"&gt;${event.data.message}&lt;/span&gt;
`;
logContainer.appendChild(logEntry);
// Auto-scroll to bottom
logContainer.scrollTop = logContainer.scrollHeight;
// Limit log entries to prevent memory issues
const maxLogEntries = 1000;
if (logContainer.children.length &gt; maxLogEntries) {
logContainer.removeChild(logContainer.firstChild);
}
}
}
// Initialize dashboard
const dashboard = new ProvisioningDashboard('ws://localhost:9090', jwtToken);
</code></pre>
<h2 id="server-side-implementation"><a class="header" href="#server-side-implementation">Server-Side Implementation</a></h2>
<h3 id="rust-websocket-handler"><a class="header" href="#rust-websocket-handler">Rust WebSocket Handler</a></h3>
<p>The orchestrator implements WebSocket support using Axum and Tokio:</p>
<pre><code class="language-rust">use axum::{
extract::{ws::WebSocket, ws::WebSocketUpgrade, Query, State},
response::Response,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio::sync::broadcast;
#[derive(Debug, Deserialize)]
pub struct WsQuery {
token: String,
events: Option&lt;String&gt;,
batch_size: Option&lt;usize&gt;,
compression: Option&lt;bool&gt;,
}
#[derive(Debug, Clone, Serialize)]
pub struct WebSocketMessage {
pub event_type: String,
pub timestamp: chrono::DateTime&lt;chrono::Utc&gt;,
pub data: serde_json::Value,
pub metadata: HashMap&lt;String, String&gt;,
}
pub async fn websocket_handler(
ws: WebSocketUpgrade,
Query(params): Query&lt;WsQuery&gt;,
State(state): State&lt;SharedState&gt;,
) -&gt; Response {
// Validate JWT token
let claims = match state.auth_service.validate_token(&amp;params.token) {
Ok(claims) =&gt; claims,
Err(_) =&gt; return Response::builder()
.status(401)
.body("Unauthorized".into())
.unwrap(),
};
ws.on_upgrade(move |socket| handle_socket(socket, params, claims, state))
}
async fn handle_socket(
socket: WebSocket,
params: WsQuery,
claims: Claims,
state: SharedState,
) {
let (mut sender, mut receiver) = socket.split();
// Subscribe to event stream
let mut event_rx = state.monitoring_system.subscribe_to_events().await;
// Parse requested event types
let requested_events: Vec&lt;String&gt; = params.events
.unwrap_or_default()
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
// Handle incoming messages from client
let sender_task = tokio::spawn(async move {
while let Some(msg) = receiver.next().await {
if let Ok(msg) = msg {
if let Ok(text) = msg.to_text() {
if let Ok(client_msg) = serde_json::from_str::&lt;ClientMessage&gt;(text) {
handle_client_message(client_msg, &amp;state).await;
}
}
}
}
});
// Handle outgoing messages to client
let receiver_task = tokio::spawn(async move {
let mut batch = Vec::new();
let batch_size = params.batch_size.unwrap_or(10);
while let Ok(event) = event_rx.recv().await {
// Filter events based on subscription
if !requested_events.is_empty() &amp;&amp; !requested_events.contains(&amp;event.event_type) {
continue;
}
// Check permissions
if !has_event_permission(&amp;claims, &amp;event.event_type) {
continue;
}
batch.push(event);
// Send batch when full or after timeout
if batch.len() &gt;= batch_size {
send_event_batch(&amp;mut sender, &amp;batch).await;
batch.clear();
}
}
});
// Wait for either task to complete
tokio::select! {
_ = sender_task =&gt; {},
_ = receiver_task =&gt; {},
}
}
#[derive(Debug, Deserialize)]
struct ClientMessage {
#[serde(rename = "type")]
msg_type: String,
token: Option&lt;String&gt;,
events: Option&lt;Vec&lt;String&gt;&gt;,
}
async fn handle_client_message(msg: ClientMessage, state: &amp;SharedState) {
match msg.msg_type.as_str() {
"subscribe" =&gt; {
// Handle event subscription
},
"unsubscribe" =&gt; {
// Handle event unsubscription
},
"auth" =&gt; {
// Handle re-authentication
},
_ =&gt; {
// Unknown message type
}
}
}
async fn send_event_batch(sender: &amp;mut SplitSink&lt;WebSocket, Message&gt;, batch: &amp;[WebSocketMessage]) {
let batch_msg = serde_json::json!({
"type": "batch",
"events": batch
});
if let Ok(msg_text) = serde_json::to_string(&amp;batch_msg) {
if let Err(e) = sender.send(Message::Text(msg_text)).await {
eprintln!("Failed to send WebSocket message: {}", e);
}
}
}
fn has_event_permission(claims: &amp;Claims, event_type: &amp;str) -&gt; bool {
// Check if user has permission to receive this event type
match event_type {
"SystemHealthUpdate" =&gt; claims.role.contains(&amp;"admin".to_string()),
"LogEntry" =&gt; claims.role.contains(&amp;"admin".to_string()) ||
claims.role.contains(&amp;"developer".to_string()),
_ =&gt; true, // Most events are accessible to all authenticated users
}
}</code></pre>
<h2 id="event-filtering-and-subscriptions"><a class="header" href="#event-filtering-and-subscriptions">Event Filtering and Subscriptions</a></h2>
<h3 id="client-side-filtering"><a class="header" href="#client-side-filtering">Client-Side Filtering</a></h3>
<pre><code class="language-javascript">// Subscribe to specific event types
ws.subscribe(['TaskStatusChanged', 'WorkflowProgressUpdate']);
// Subscribe with filters
ws.send({
type: 'subscribe',
events: ['TaskStatusChanged'],
filters: {
task_name: 'create_servers',
status: ['Running', 'Completed', 'Failed']
}
});
// Advanced filtering
ws.send({
type: 'subscribe',
events: ['LogEntry'],
filters: {
level: ['ERROR', 'WARN'],
component: ['server-manager', 'batch-coordinator'],
since: '2025-09-26T10:00:00Z'
}
});
</code></pre>
<h3 id="server-side-event-filtering"><a class="header" href="#server-side-event-filtering">Server-Side Event Filtering</a></h3>
<p>Events can be filtered on the server side based on:</p>
<ul>
<li>User permissions and roles</li>
<li>Event type subscriptions</li>
<li>Custom filter criteria</li>
<li>Rate limiting</li>
</ul>
<h2 id="error-handling-and-reconnection"><a class="header" href="#error-handling-and-reconnection">Error Handling and Reconnection</a></h2>
<h3 id="connection-errors"><a class="header" href="#connection-errors">Connection Errors</a></h3>
<pre><code class="language-javascript">ws.on('error', (error) =&gt; {
console.error('WebSocket error:', error);
// Handle specific error types
if (error.code === 1006) {
// Abnormal closure, attempt reconnection
setTimeout(() =&gt; ws.connect(), 5000);
} else if (error.code === 1008) {
// Policy violation, check token
refreshTokenAndReconnect();
}
});
ws.on('disconnected', (event) =&gt; {
console.log(`WebSocket disconnected: ${event.code} - ${event.reason}`);
// Handle different close codes
switch (event.code) {
case 1000: // Normal closure
console.log('Connection closed normally');
break;
case 1001: // Going away
console.log('Server is shutting down');
break;
case 4001: // Custom: Token expired
refreshTokenAndReconnect();
break;
default:
// Attempt reconnection for other errors
if (shouldReconnect()) {
scheduleReconnection();
}
}
});
</code></pre>
<h3 id="heartbeat-and-keep-alive"><a class="header" href="#heartbeat-and-keep-alive">Heartbeat and Keep-Alive</a></h3>
<pre><code class="language-javascript">class ProvisioningWebSocket {
constructor(baseUrl, token, options = {}) {
// ... existing code ...
this.heartbeatInterval = options.heartbeatInterval || 30000;
this.heartbeatTimer = null;
}
connect() {
// ... existing connection code ...
this.ws.onopen = (event) =&gt; {
console.log('WebSocket connected');
this.startHeartbeat();
this.emit('connected', event);
};
this.ws.onclose = (event) =&gt; {
this.stopHeartbeat();
// ... existing close handling ...
};
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() =&gt; {
if (this.ws &amp;&amp; this.ws.readyState === WebSocket.OPEN) {
this.send({ type: 'ping' });
}
}, this.heartbeatInterval);
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
handleMessage(message) {
if (message.type === 'pong') {
// Heartbeat response received
return;
}
// ... existing message handling ...
}
}
</code></pre>
<h2 id="performance-considerations"><a class="header" href="#performance-considerations">Performance Considerations</a></h2>
<h3 id="message-batching"><a class="header" href="#message-batching">Message Batching</a></h3>
<p>To improve performance, the server can batch multiple events into single WebSocket messages:</p>
<pre><code class="language-json">{
"type": "batch",
"timestamp": "2025-09-26T10:00:00Z",
"events": [
{
"event_type": "TaskStatusChanged",
"data": { ... }
},
{
"event_type": "WorkflowProgressUpdate",
"data": { ... }
}
]
}
</code></pre>
<h3 id="compression"><a class="header" href="#compression">Compression</a></h3>
<p>Enable message compression for large events:</p>
<pre><code class="language-javascript">const ws = new WebSocket('ws://localhost:9090/ws?token=jwt&amp;compression=true');
</code></pre>
<h3 id="rate-limiting"><a class="header" href="#rate-limiting">Rate Limiting</a></h3>
<p>The server implements rate limiting to prevent abuse:</p>
<ul>
<li>Maximum connections per user: 10</li>
<li>Maximum messages per second: 100</li>
<li>Maximum subscription events: 50</li>
</ul>
<h2 id="security-considerations"><a class="header" href="#security-considerations">Security Considerations</a></h2>
<h3 id="authentication-and-authorization"><a class="header" href="#authentication-and-authorization">Authentication and Authorization</a></h3>
<ul>
<li>All connections require valid JWT tokens</li>
<li>Tokens are validated on connection and periodically renewed</li>
<li>Event access is controlled by user roles and permissions</li>
</ul>
<h3 id="message-validation"><a class="header" href="#message-validation">Message Validation</a></h3>
<ul>
<li>All incoming messages are validated against schemas</li>
<li>Malformed messages are rejected</li>
<li>Rate limiting prevents DoS attacks</li>
</ul>
<h3 id="data-sanitization"><a class="header" href="#data-sanitization">Data Sanitization</a></h3>
<ul>
<li>All event data is sanitized before transmission</li>
<li>Sensitive information is filtered based on user permissions</li>
<li>PII and secrets are never transmitted</li>
</ul>
<p>This WebSocket API provides a robust, real-time communication channel for monitoring and managing provisioning with comprehensive security and performance features.</p>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../api/rest-api.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../api/nushell-api.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../api/rest-api.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../api/nushell-api.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<!-- Livereload script (if served using the cli tool) -->
<script>
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsAddress = wsProtocol + "//" + location.host + "/" + "__livereload";
const socket = new WebSocket(wsAddress);
socket.onmessage = function (event) {
if (event.data === "reload") {
socket.close();
location.reload();
}
};
window.onbeforeunload = function() {
socket.close();
}
</script>
<script>
window.playground_copyable = true;
</script>
<script src="../elasticlunr.min.js"></script>
<script src="../mark.min.js"></script>
<script src="../searcher.js"></script>
<script src="../clipboard.min.js"></script>
<script src="../highlight.js"></script>
<script src="../book.js"></script>
<!-- Custom JS scripts -->
</div>
</body>
</html>