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.
1258 lines
45 KiB
HTML
1258 lines
45 KiB
HTML
<!DOCTYPE HTML>
|
|
<html lang="en" class="ayu sidebar-visible" dir="ltr">
|
|
<head>
|
|
<!-- Book generated using mdBook -->
|
|
<meta charset="UTF-8">
|
|
<title>SDKs - 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/sdks.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="sdk-documentation"><a class="header" href="#sdk-documentation">SDK Documentation</a></h1>
|
|
<p>This document provides comprehensive documentation for the official SDKs and client libraries available for provisioning.</p>
|
|
<h2 id="available-sdks"><a class="header" href="#available-sdks">Available SDKs</a></h2>
|
|
<p>Provisioning provides SDKs in multiple languages to facilitate integration:</p>
|
|
<h3 id="official-sdks"><a class="header" href="#official-sdks">Official SDKs</a></h3>
|
|
<ul>
|
|
<li><strong>Python SDK</strong> (<code>provisioning-client</code>) - Full-featured Python client</li>
|
|
<li><strong>JavaScript/TypeScript SDK</strong> (<code>@provisioning/client</code>) - Node.js and browser support</li>
|
|
<li><strong>Go SDK</strong> (<code>go-provisioning-client</code>) - Go client library</li>
|
|
<li><strong>Rust SDK</strong> (<code>provisioning-rs</code>) - Native Rust integration</li>
|
|
</ul>
|
|
<h3 id="community-sdks"><a class="header" href="#community-sdks">Community SDKs</a></h3>
|
|
<ul>
|
|
<li><strong>Java SDK</strong> - Community-maintained Java client</li>
|
|
<li><strong>C# SDK</strong> - .NET client library</li>
|
|
<li><strong>PHP SDK</strong> - PHP client library</li>
|
|
</ul>
|
|
<h2 id="python-sdk"><a class="header" href="#python-sdk">Python SDK</a></h2>
|
|
<h3 id="installation"><a class="header" href="#installation">Installation</a></h3>
|
|
<pre><code class="language-bash"># Install from PyPI
|
|
pip install provisioning-client
|
|
|
|
# Or install development version
|
|
pip install git+https://github.com/provisioning-systems/python-client.git
|
|
</code></pre>
|
|
<h3 id="quick-start"><a class="header" href="#quick-start">Quick Start</a></h3>
|
|
<pre><code class="language-python">from provisioning_client import ProvisioningClient
|
|
import asyncio
|
|
|
|
async def main():
|
|
# Initialize client
|
|
client = ProvisioningClient(
|
|
base_url="http://localhost:9090",
|
|
auth_url="http://localhost:8081",
|
|
username="admin",
|
|
password="your-password"
|
|
)
|
|
|
|
try:
|
|
# Authenticate
|
|
token = await client.authenticate()
|
|
print(f"Authenticated with token: {token[:20]}...")
|
|
|
|
# Create a server workflow
|
|
task_id = client.create_server_workflow(
|
|
infra="production",
|
|
settings="prod-settings.k",
|
|
wait=False
|
|
)
|
|
print(f"Server workflow created: {task_id}")
|
|
|
|
# Wait for completion
|
|
task = client.wait_for_task_completion(task_id, timeout=600)
|
|
print(f"Task completed with status: {task.status}")
|
|
|
|
if task.status == "Completed":
|
|
print(f"Output: {task.output}")
|
|
elif task.status == "Failed":
|
|
print(f"Error: {task.error}")
|
|
|
|
except Exception as e:
|
|
print(f"Error: {e}")
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|
|
</code></pre>
|
|
<h3 id="advanced-usage"><a class="header" href="#advanced-usage">Advanced Usage</a></h3>
|
|
<h4 id="websocket-integration"><a class="header" href="#websocket-integration">WebSocket Integration</a></h4>
|
|
<pre><code class="language-python">async def monitor_workflows():
|
|
client = ProvisioningClient()
|
|
await client.authenticate()
|
|
|
|
# Set up event handlers
|
|
async def on_task_update(event):
|
|
print(f"Task {event['data']['task_id']} status: {event['data']['status']}")
|
|
|
|
async def on_progress_update(event):
|
|
print(f"Progress: {event['data']['progress']}% - {event['data']['current_step']}")
|
|
|
|
client.on_event('TaskStatusChanged', on_task_update)
|
|
client.on_event('WorkflowProgressUpdate', on_progress_update)
|
|
|
|
# Connect to WebSocket
|
|
await client.connect_websocket(['TaskStatusChanged', 'WorkflowProgressUpdate'])
|
|
|
|
# Keep connection alive
|
|
await asyncio.sleep(3600) # Monitor for 1 hour
|
|
</code></pre>
|
|
<h4 id="batch-operations"><a class="header" href="#batch-operations">Batch Operations</a></h4>
|
|
<pre><code class="language-python">async def execute_batch_deployment():
|
|
client = ProvisioningClient()
|
|
await client.authenticate()
|
|
|
|
batch_config = {
|
|
"name": "production_deployment",
|
|
"version": "1.0.0",
|
|
"storage_backend": "surrealdb",
|
|
"parallel_limit": 5,
|
|
"rollback_enabled": True,
|
|
"operations": [
|
|
{
|
|
"id": "servers",
|
|
"type": "server_batch",
|
|
"provider": "upcloud",
|
|
"dependencies": [],
|
|
"config": {
|
|
"server_configs": [
|
|
{"name": "web-01", "plan": "2xCPU-4GB", "zone": "de-fra1"},
|
|
{"name": "web-02", "plan": "2xCPU-4GB", "zone": "de-fra1"}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"id": "kubernetes",
|
|
"type": "taskserv_batch",
|
|
"provider": "upcloud",
|
|
"dependencies": ["servers"],
|
|
"config": {
|
|
"taskservs": ["kubernetes", "cilium", "containerd"]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
# Execute batch operation
|
|
batch_result = await client.execute_batch_operation(batch_config)
|
|
print(f"Batch operation started: {batch_result['batch_id']}")
|
|
|
|
# Monitor progress
|
|
while True:
|
|
status = await client.get_batch_status(batch_result['batch_id'])
|
|
print(f"Batch status: {status['status']} - {status.get('progress', 0)}%")
|
|
|
|
if status['status'] in ['Completed', 'Failed', 'Cancelled']:
|
|
break
|
|
|
|
await asyncio.sleep(10)
|
|
|
|
print(f"Batch operation finished: {status['status']}")
|
|
</code></pre>
|
|
<h4 id="error-handling-with-retries"><a class="header" href="#error-handling-with-retries">Error Handling with Retries</a></h4>
|
|
<pre><code class="language-python">from provisioning_client.exceptions import (
|
|
ProvisioningAPIError,
|
|
AuthenticationError,
|
|
ValidationError,
|
|
RateLimitError
|
|
)
|
|
from tenacity import retry, stop_after_attempt, wait_exponential
|
|
|
|
class RobustProvisioningClient(ProvisioningClient):
|
|
@retry(
|
|
stop=stop_after_attempt(3),
|
|
wait=wait_exponential(multiplier=1, min=4, max=10)
|
|
)
|
|
async def create_server_workflow_with_retry(self, **kwargs):
|
|
try:
|
|
return await self.create_server_workflow(**kwargs)
|
|
except RateLimitError as e:
|
|
print(f"Rate limited, retrying in {e.retry_after} seconds...")
|
|
await asyncio.sleep(e.retry_after)
|
|
raise
|
|
except AuthenticationError:
|
|
print("Authentication failed, re-authenticating...")
|
|
await self.authenticate()
|
|
raise
|
|
except ValidationError as e:
|
|
print(f"Validation error: {e}")
|
|
# Don't retry validation errors
|
|
raise
|
|
except ProvisioningAPIError as e:
|
|
print(f"API error: {e}")
|
|
raise
|
|
|
|
# Usage
|
|
async def robust_workflow():
|
|
client = RobustProvisioningClient()
|
|
|
|
try:
|
|
task_id = await client.create_server_workflow_with_retry(
|
|
infra="production",
|
|
settings="config.k"
|
|
)
|
|
print(f"Workflow created successfully: {task_id}")
|
|
except Exception as e:
|
|
print(f"Failed after retries: {e}")
|
|
</code></pre>
|
|
<h3 id="api-reference"><a class="header" href="#api-reference">API Reference</a></h3>
|
|
<h4 id="provisioningclient-class"><a class="header" href="#provisioningclient-class">ProvisioningClient Class</a></h4>
|
|
<pre><code class="language-python">class ProvisioningClient:
|
|
def __init__(self,
|
|
base_url: str = "http://localhost:9090",
|
|
auth_url: str = "http://localhost:8081",
|
|
username: str = None,
|
|
password: str = None,
|
|
token: str = None):
|
|
"""Initialize the provisioning client"""
|
|
|
|
async def authenticate(self) -> str:
|
|
"""Authenticate and get JWT token"""
|
|
|
|
def create_server_workflow(self,
|
|
infra: str,
|
|
settings: str = "config.k",
|
|
check_mode: bool = False,
|
|
wait: bool = False) -> str:
|
|
"""Create a server provisioning workflow"""
|
|
|
|
def create_taskserv_workflow(self,
|
|
operation: str,
|
|
taskserv: str,
|
|
infra: str,
|
|
settings: str = "config.k",
|
|
check_mode: bool = False,
|
|
wait: bool = False) -> str:
|
|
"""Create a task service workflow"""
|
|
|
|
def get_task_status(self, task_id: str) -> WorkflowTask:
|
|
"""Get the status of a specific task"""
|
|
|
|
def wait_for_task_completion(self,
|
|
task_id: str,
|
|
timeout: int = 300,
|
|
poll_interval: int = 5) -> WorkflowTask:
|
|
"""Wait for a task to complete"""
|
|
|
|
async def connect_websocket(self, event_types: List[str] = None):
|
|
"""Connect to WebSocket for real-time updates"""
|
|
|
|
def on_event(self, event_type: str, handler: Callable):
|
|
"""Register an event handler"""
|
|
</code></pre>
|
|
<h2 id="javascripttypescript-sdk"><a class="header" href="#javascripttypescript-sdk">JavaScript/TypeScript SDK</a></h2>
|
|
<h3 id="installation-1"><a class="header" href="#installation-1">Installation</a></h3>
|
|
<pre><code class="language-bash"># npm
|
|
npm install @provisioning/client
|
|
|
|
# yarn
|
|
yarn add @provisioning/client
|
|
|
|
# pnpm
|
|
pnpm add @provisioning/client
|
|
</code></pre>
|
|
<h3 id="quick-start-1"><a class="header" href="#quick-start-1">Quick Start</a></h3>
|
|
<pre><code class="language-typescript">import { ProvisioningClient } from '@provisioning/client';
|
|
|
|
async function main() {
|
|
const client = new ProvisioningClient({
|
|
baseUrl: 'http://localhost:9090',
|
|
authUrl: 'http://localhost:8081',
|
|
username: 'admin',
|
|
password: 'your-password'
|
|
});
|
|
|
|
try {
|
|
// Authenticate
|
|
await client.authenticate();
|
|
console.log('Authentication successful');
|
|
|
|
// Create server workflow
|
|
const taskId = await client.createServerWorkflow({
|
|
infra: 'production',
|
|
settings: 'prod-settings.k'
|
|
});
|
|
console.log(`Server workflow created: ${taskId}`);
|
|
|
|
// Wait for completion
|
|
const task = await client.waitForTaskCompletion(taskId);
|
|
console.log(`Task completed with status: ${task.status}`);
|
|
|
|
} catch (error) {
|
|
console.error('Error:', error.message);
|
|
}
|
|
}
|
|
|
|
main();
|
|
</code></pre>
|
|
<h3 id="react-integration"><a class="header" href="#react-integration">React Integration</a></h3>
|
|
<pre><code class="language-tsx">import React, { useState, useEffect } from 'react';
|
|
import { ProvisioningClient } from '@provisioning/client';
|
|
|
|
interface Task {
|
|
id: string;
|
|
name: string;
|
|
status: string;
|
|
progress?: number;
|
|
}
|
|
|
|
const WorkflowDashboard: React.FC = () => {
|
|
const [client] = useState(() => new ProvisioningClient({
|
|
baseUrl: process.env.REACT_APP_API_URL,
|
|
username: process.env.REACT_APP_USERNAME,
|
|
password: process.env.REACT_APP_PASSWORD
|
|
}));
|
|
|
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
const [connected, setConnected] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const initClient = async () => {
|
|
try {
|
|
await client.authenticate();
|
|
|
|
// Set up WebSocket event handlers
|
|
client.on('TaskStatusChanged', (event: any) => {
|
|
setTasks(prev => prev.map(task =>
|
|
task.id === event.data.task_id
|
|
? { ...task, status: event.data.status, progress: event.data.progress }
|
|
: task
|
|
));
|
|
});
|
|
|
|
client.on('websocketConnected', () => {
|
|
setConnected(true);
|
|
});
|
|
|
|
client.on('websocketDisconnected', () => {
|
|
setConnected(false);
|
|
});
|
|
|
|
// Connect WebSocket
|
|
await client.connectWebSocket(['TaskStatusChanged', 'WorkflowProgressUpdate']);
|
|
|
|
// Load initial tasks
|
|
const initialTasks = await client.listTasks();
|
|
setTasks(initialTasks);
|
|
|
|
} catch (error) {
|
|
console.error('Failed to initialize client:', error);
|
|
}
|
|
};
|
|
|
|
initClient();
|
|
|
|
return () => {
|
|
client.disconnectWebSocket();
|
|
};
|
|
}, [client]);
|
|
|
|
const createServerWorkflow = async () => {
|
|
try {
|
|
const taskId = await client.createServerWorkflow({
|
|
infra: 'production',
|
|
settings: 'config.k'
|
|
});
|
|
|
|
// Add to tasks list
|
|
setTasks(prev => [...prev, {
|
|
id: taskId,
|
|
name: 'Server Creation',
|
|
status: 'Pending'
|
|
}]);
|
|
|
|
} catch (error) {
|
|
console.error('Failed to create workflow:', error);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="workflow-dashboard">
|
|
<div className="header">
|
|
<h1>Workflow Dashboard</h1>
|
|
<div className={`connection-status ${connected ? 'connected' : 'disconnected'}`}>
|
|
{connected ? '🟢 Connected' : '🔴 Disconnected'}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="controls">
|
|
<button onClick={createServerWorkflow}>
|
|
Create Server Workflow
|
|
</button>
|
|
</div>
|
|
|
|
<div className="tasks">
|
|
{tasks.map(task => (
|
|
<div key={task.id} className="task-card">
|
|
<h3>{task.name}</h3>
|
|
<div className="task-status">
|
|
<span className={`status ${task.status.toLowerCase()}`}>
|
|
{task.status}
|
|
</span>
|
|
{task.progress && (
|
|
<div className="progress-bar">
|
|
<div
|
|
className="progress-fill"
|
|
style={{ width: `${task.progress}%` }}
|
|
/>
|
|
<span className="progress-text">{task.progress}%</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default WorkflowDashboard;
|
|
</code></pre>
|
|
<h3 id="nodejs-cli-tool"><a class="header" href="#nodejs-cli-tool">Node.js CLI Tool</a></h3>
|
|
<pre><code class="language-typescript">#!/usr/bin/env node
|
|
|
|
import { Command } from 'commander';
|
|
import { ProvisioningClient } from '@provisioning/client';
|
|
import chalk from 'chalk';
|
|
import ora from 'ora';
|
|
|
|
const program = new Command();
|
|
|
|
program
|
|
.name('provisioning-cli')
|
|
.description('CLI tool for provisioning')
|
|
.version('1.0.0');
|
|
|
|
program
|
|
.command('create-server')
|
|
.description('Create a server workflow')
|
|
.requiredOption('-i, --infra <infra>', 'Infrastructure target')
|
|
.option('-s, --settings <settings>', 'Settings file', 'config.k')
|
|
.option('-c, --check', 'Check mode only')
|
|
.option('-w, --wait', 'Wait for completion')
|
|
.action(async (options) => {
|
|
const client = new ProvisioningClient({
|
|
baseUrl: process.env.PROVISIONING_API_URL,
|
|
username: process.env.PROVISIONING_USERNAME,
|
|
password: process.env.PROVISIONING_PASSWORD
|
|
});
|
|
|
|
const spinner = ora('Authenticating...').start();
|
|
|
|
try {
|
|
await client.authenticate();
|
|
spinner.text = 'Creating server workflow...';
|
|
|
|
const taskId = await client.createServerWorkflow({
|
|
infra: options.infra,
|
|
settings: options.settings,
|
|
check_mode: options.check,
|
|
wait: false
|
|
});
|
|
|
|
spinner.succeed(`Server workflow created: ${chalk.green(taskId)}`);
|
|
|
|
if (options.wait) {
|
|
spinner.start('Waiting for completion...');
|
|
|
|
// Set up progress updates
|
|
client.on('TaskStatusChanged', (event: any) => {
|
|
if (event.data.task_id === taskId) {
|
|
spinner.text = `Status: ${event.data.status}`;
|
|
}
|
|
});
|
|
|
|
client.on('WorkflowProgressUpdate', (event: any) => {
|
|
if (event.data.workflow_id === taskId) {
|
|
spinner.text = `${event.data.progress}% - ${event.data.current_step}`;
|
|
}
|
|
});
|
|
|
|
await client.connectWebSocket(['TaskStatusChanged', 'WorkflowProgressUpdate']);
|
|
|
|
const task = await client.waitForTaskCompletion(taskId);
|
|
|
|
if (task.status === 'Completed') {
|
|
spinner.succeed(chalk.green('Workflow completed successfully!'));
|
|
if (task.output) {
|
|
console.log(chalk.gray('Output:'), task.output);
|
|
}
|
|
} else {
|
|
spinner.fail(chalk.red(`Workflow failed: ${task.error}`));
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
spinner.fail(chalk.red(`Error: ${error.message}`));
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
program
|
|
.command('list-tasks')
|
|
.description('List all tasks')
|
|
.option('-s, --status <status>', 'Filter by status')
|
|
.action(async (options) => {
|
|
const client = new ProvisioningClient();
|
|
|
|
try {
|
|
await client.authenticate();
|
|
const tasks = await client.listTasks(options.status);
|
|
|
|
console.log(chalk.bold('Tasks:'));
|
|
tasks.forEach(task => {
|
|
const statusColor = task.status === 'Completed' ? 'green' :
|
|
task.status === 'Failed' ? 'red' :
|
|
task.status === 'Running' ? 'yellow' : 'gray';
|
|
|
|
console.log(` ${task.id} - ${task.name} [${chalk[statusColor](task.status)}]`);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
program
|
|
.command('monitor')
|
|
.description('Monitor workflows in real-time')
|
|
.action(async () => {
|
|
const client = new ProvisioningClient();
|
|
|
|
try {
|
|
await client.authenticate();
|
|
|
|
console.log(chalk.bold('🔍 Monitoring workflows...'));
|
|
console.log(chalk.gray('Press Ctrl+C to stop'));
|
|
|
|
client.on('TaskStatusChanged', (event: any) => {
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
const statusColor = event.data.status === 'Completed' ? 'green' :
|
|
event.data.status === 'Failed' ? 'red' :
|
|
event.data.status === 'Running' ? 'yellow' : 'gray';
|
|
|
|
console.log(`[${chalk.gray(timestamp)}] Task ${event.data.task_id} → ${chalk[statusColor](event.data.status)}`);
|
|
});
|
|
|
|
client.on('WorkflowProgressUpdate', (event: any) => {
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
console.log(`[${chalk.gray(timestamp)}] ${event.data.workflow_id}: ${event.data.progress}% - ${event.data.current_step}`);
|
|
});
|
|
|
|
await client.connectWebSocket(['TaskStatusChanged', 'WorkflowProgressUpdate']);
|
|
|
|
// Keep the process running
|
|
process.on('SIGINT', () => {
|
|
console.log(chalk.yellow('\nStopping monitor...'));
|
|
client.disconnectWebSocket();
|
|
process.exit(0);
|
|
});
|
|
|
|
// Keep alive
|
|
setInterval(() => {}, 1000);
|
|
|
|
} catch (error) {
|
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
program.parse();
|
|
</code></pre>
|
|
<h3 id="api-reference-1"><a class="header" href="#api-reference-1">API Reference</a></h3>
|
|
<pre><code class="language-typescript">interface ProvisioningClientOptions {
|
|
baseUrl?: string;
|
|
authUrl?: string;
|
|
username?: string;
|
|
password?: string;
|
|
token?: string;
|
|
}
|
|
|
|
class ProvisioningClient extends EventEmitter {
|
|
constructor(options: ProvisioningClientOptions);
|
|
|
|
async authenticate(): Promise<string>;
|
|
|
|
async createServerWorkflow(config: {
|
|
infra: string;
|
|
settings?: string;
|
|
check_mode?: boolean;
|
|
wait?: boolean;
|
|
}): Promise<string>;
|
|
|
|
async createTaskservWorkflow(config: {
|
|
operation: string;
|
|
taskserv: string;
|
|
infra: string;
|
|
settings?: string;
|
|
check_mode?: boolean;
|
|
wait?: boolean;
|
|
}): Promise<string>;
|
|
|
|
async getTaskStatus(taskId: string): Promise<Task>;
|
|
|
|
async listTasks(statusFilter?: string): Promise<Task[]>;
|
|
|
|
async waitForTaskCompletion(
|
|
taskId: string,
|
|
timeout?: number,
|
|
pollInterval?: number
|
|
): Promise<Task>;
|
|
|
|
async connectWebSocket(eventTypes?: string[]): Promise<void>;
|
|
|
|
disconnectWebSocket(): void;
|
|
|
|
async executeBatchOperation(batchConfig: BatchConfig): Promise<any>;
|
|
|
|
async getBatchStatus(batchId: string): Promise<any>;
|
|
}
|
|
</code></pre>
|
|
<h2 id="go-sdk"><a class="header" href="#go-sdk">Go SDK</a></h2>
|
|
<h3 id="installation-2"><a class="header" href="#installation-2">Installation</a></h3>
|
|
<pre><code class="language-bash">go get github.com/provisioning-systems/go-client
|
|
</code></pre>
|
|
<h3 id="quick-start-2"><a class="header" href="#quick-start-2">Quick Start</a></h3>
|
|
<pre><code class="language-go">package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/provisioning-systems/go-client"
|
|
)
|
|
|
|
func main() {
|
|
// Initialize client
|
|
client, err := provisioning.NewClient(&provisioning.Config{
|
|
BaseURL: "http://localhost:9090",
|
|
AuthURL: "http://localhost:8081",
|
|
Username: "admin",
|
|
Password: "your-password",
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("Failed to create client: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// Authenticate
|
|
token, err := client.Authenticate(ctx)
|
|
if err != nil {
|
|
log.Fatalf("Authentication failed: %v", err)
|
|
}
|
|
fmt.Printf("Authenticated with token: %.20s...\n", token)
|
|
|
|
// Create server workflow
|
|
taskID, err := client.CreateServerWorkflow(ctx, &provisioning.CreateServerRequest{
|
|
Infra: "production",
|
|
Settings: "prod-settings.k",
|
|
Wait: false,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("Failed to create workflow: %v", err)
|
|
}
|
|
fmt.Printf("Server workflow created: %s\n", taskID)
|
|
|
|
// Wait for completion
|
|
task, err := client.WaitForTaskCompletion(ctx, taskID, 10*time.Minute)
|
|
if err != nil {
|
|
log.Fatalf("Failed to wait for completion: %v", err)
|
|
}
|
|
|
|
fmt.Printf("Task completed with status: %s\n", task.Status)
|
|
if task.Status == "Completed" {
|
|
fmt.Printf("Output: %s\n", task.Output)
|
|
} else if task.Status == "Failed" {
|
|
fmt.Printf("Error: %s\n", task.Error)
|
|
}
|
|
}
|
|
</code></pre>
|
|
<h3 id="websocket-integration-1"><a class="header" href="#websocket-integration-1">WebSocket Integration</a></h3>
|
|
<pre><code class="language-go">package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
|
|
"github.com/provisioning-systems/go-client"
|
|
)
|
|
|
|
func main() {
|
|
client, err := provisioning.NewClient(&provisioning.Config{
|
|
BaseURL: "http://localhost:9090",
|
|
Username: "admin",
|
|
Password: "password",
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("Failed to create client: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// Authenticate
|
|
_, err = client.Authenticate(ctx)
|
|
if err != nil {
|
|
log.Fatalf("Authentication failed: %v", err)
|
|
}
|
|
|
|
// Set up WebSocket connection
|
|
ws, err := client.ConnectWebSocket(ctx, []string{
|
|
"TaskStatusChanged",
|
|
"WorkflowProgressUpdate",
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect WebSocket: %v", err)
|
|
}
|
|
defer ws.Close()
|
|
|
|
// Handle events
|
|
go func() {
|
|
for event := range ws.Events() {
|
|
switch event.Type {
|
|
case "TaskStatusChanged":
|
|
fmt.Printf("Task %s status changed to: %s\n",
|
|
event.Data["task_id"], event.Data["status"])
|
|
case "WorkflowProgressUpdate":
|
|
fmt.Printf("Workflow progress: %v%% - %s\n",
|
|
event.Data["progress"], event.Data["current_step"])
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Wait for interrupt
|
|
c := make(chan os.Signal, 1)
|
|
signal.Notify(c, os.Interrupt)
|
|
<-c
|
|
|
|
fmt.Println("Shutting down...")
|
|
}
|
|
</code></pre>
|
|
<h3 id="http-client-with-retry-logic"><a class="header" href="#http-client-with-retry-logic">HTTP Client with Retry Logic</a></h3>
|
|
<pre><code class="language-go">package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/provisioning-systems/go-client"
|
|
"github.com/cenkalti/backoff/v4"
|
|
)
|
|
|
|
type ResilientClient struct {
|
|
*provisioning.Client
|
|
}
|
|
|
|
func NewResilientClient(config *provisioning.Config) (*ResilientClient, error) {
|
|
client, err := provisioning.NewClient(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ResilientClient{Client: client}, nil
|
|
}
|
|
|
|
func (c *ResilientClient) CreateServerWorkflowWithRetry(
|
|
ctx context.Context,
|
|
req *provisioning.CreateServerRequest,
|
|
) (string, error) {
|
|
var taskID string
|
|
|
|
operation := func() error {
|
|
var err error
|
|
taskID, err = c.CreateServerWorkflow(ctx, req)
|
|
|
|
// Don't retry validation errors
|
|
if provisioning.IsValidationError(err) {
|
|
return backoff.Permanent(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
exponentialBackoff := backoff.NewExponentialBackOff()
|
|
exponentialBackoff.MaxElapsedTime = 5 * time.Minute
|
|
|
|
err := backoff.Retry(operation, exponentialBackoff)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed after retries: %w", err)
|
|
}
|
|
|
|
return taskID, nil
|
|
}
|
|
|
|
func main() {
|
|
client, err := NewResilientClient(&provisioning.Config{
|
|
BaseURL: "http://localhost:9090",
|
|
Username: "admin",
|
|
Password: "password",
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("Failed to create client: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// Authenticate with retry
|
|
_, err = client.Authenticate(ctx)
|
|
if err != nil {
|
|
log.Fatalf("Authentication failed: %v", err)
|
|
}
|
|
|
|
// Create workflow with retry
|
|
taskID, err := client.CreateServerWorkflowWithRetry(ctx, &provisioning.CreateServerRequest{
|
|
Infra: "production",
|
|
Settings: "config.k",
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("Failed to create workflow: %v", err)
|
|
}
|
|
|
|
fmt.Printf("Workflow created successfully: %s\n", taskID)
|
|
}
|
|
</code></pre>
|
|
<h2 id="rust-sdk"><a class="header" href="#rust-sdk">Rust SDK</a></h2>
|
|
<h3 id="installation-3"><a class="header" href="#installation-3">Installation</a></h3>
|
|
<p>Add to your <code>Cargo.toml</code>:</p>
|
|
<pre><code class="language-toml">[dependencies]
|
|
provisioning-rs = "2.0.0"
|
|
tokio = { version = "1.0", features = ["full"] }
|
|
</code></pre>
|
|
<h3 id="quick-start-3"><a class="header" href="#quick-start-3">Quick Start</a></h3>
|
|
<pre><code class="language-rust">use provisioning_rs::{ProvisioningClient, Config, CreateServerRequest};
|
|
use tokio;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
// Initialize client
|
|
let config = Config {
|
|
base_url: "http://localhost:9090".to_string(),
|
|
auth_url: Some("http://localhost:8081".to_string()),
|
|
username: Some("admin".to_string()),
|
|
password: Some("your-password".to_string()),
|
|
token: None,
|
|
};
|
|
|
|
let mut client = ProvisioningClient::new(config);
|
|
|
|
// Authenticate
|
|
let token = client.authenticate().await?;
|
|
println!("Authenticated with token: {}...", &token[..20]);
|
|
|
|
// Create server workflow
|
|
let request = CreateServerRequest {
|
|
infra: "production".to_string(),
|
|
settings: Some("prod-settings.k".to_string()),
|
|
check_mode: false,
|
|
wait: false,
|
|
};
|
|
|
|
let task_id = client.create_server_workflow(request).await?;
|
|
println!("Server workflow created: {}", task_id);
|
|
|
|
// Wait for completion
|
|
let task = client.wait_for_task_completion(&task_id, std::time::Duration::from_secs(600)).await?;
|
|
|
|
println!("Task completed with status: {:?}", task.status);
|
|
match task.status {
|
|
TaskStatus::Completed => {
|
|
if let Some(output) = task.output {
|
|
println!("Output: {}", output);
|
|
}
|
|
},
|
|
TaskStatus::Failed => {
|
|
if let Some(error) = task.error {
|
|
println!("Error: {}", error);
|
|
}
|
|
},
|
|
_ => {}
|
|
}
|
|
|
|
Ok(())
|
|
}</code></pre>
|
|
<h3 id="websocket-integration-2"><a class="header" href="#websocket-integration-2">WebSocket Integration</a></h3>
|
|
<pre><code class="language-rust">use provisioning_rs::{ProvisioningClient, Config, WebSocketEvent};
|
|
use futures_util::StreamExt;
|
|
use tokio;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let config = Config {
|
|
base_url: "http://localhost:9090".to_string(),
|
|
username: Some("admin".to_string()),
|
|
password: Some("password".to_string()),
|
|
..Default::default()
|
|
};
|
|
|
|
let mut client = ProvisioningClient::new(config);
|
|
|
|
// Authenticate
|
|
client.authenticate().await?;
|
|
|
|
// Connect WebSocket
|
|
let mut ws = client.connect_websocket(vec![
|
|
"TaskStatusChanged".to_string(),
|
|
"WorkflowProgressUpdate".to_string(),
|
|
]).await?;
|
|
|
|
// Handle events
|
|
tokio::spawn(async move {
|
|
while let Some(event) = ws.next().await {
|
|
match event {
|
|
Ok(WebSocketEvent::TaskStatusChanged { data }) => {
|
|
println!("Task {} status changed to: {}", data.task_id, data.status);
|
|
},
|
|
Ok(WebSocketEvent::WorkflowProgressUpdate { data }) => {
|
|
println!("Workflow progress: {}% - {}", data.progress, data.current_step);
|
|
},
|
|
Ok(WebSocketEvent::SystemHealthUpdate { data }) => {
|
|
println!("System health: {}", data.overall_status);
|
|
},
|
|
Err(e) => {
|
|
eprintln!("WebSocket error: {}", e);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Keep the main thread alive
|
|
tokio::signal::ctrl_c().await?;
|
|
println!("Shutting down...");
|
|
|
|
Ok(())
|
|
}</code></pre>
|
|
<h3 id="batch-operations-1"><a class="header" href="#batch-operations-1">Batch Operations</a></h3>
|
|
<pre><code class="language-rust">use provisioning_rs::{BatchOperationRequest, BatchOperation};
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = ProvisioningClient::new(config);
|
|
client.authenticate().await?;
|
|
|
|
// Define batch operation
|
|
let batch_request = BatchOperationRequest {
|
|
name: "production_deployment".to_string(),
|
|
version: "1.0.0".to_string(),
|
|
storage_backend: "surrealdb".to_string(),
|
|
parallel_limit: 5,
|
|
rollback_enabled: true,
|
|
operations: vec![
|
|
BatchOperation {
|
|
id: "servers".to_string(),
|
|
operation_type: "server_batch".to_string(),
|
|
provider: "upcloud".to_string(),
|
|
dependencies: vec![],
|
|
config: serde_json::json!({
|
|
"server_configs": [
|
|
{"name": "web-01", "plan": "2xCPU-4GB", "zone": "de-fra1"},
|
|
{"name": "web-02", "plan": "2xCPU-4GB", "zone": "de-fra1"}
|
|
]
|
|
}),
|
|
},
|
|
BatchOperation {
|
|
id: "kubernetes".to_string(),
|
|
operation_type: "taskserv_batch".to_string(),
|
|
provider: "upcloud".to_string(),
|
|
dependencies: vec!["servers".to_string()],
|
|
config: serde_json::json!({
|
|
"taskservs": ["kubernetes", "cilium", "containerd"]
|
|
}),
|
|
},
|
|
],
|
|
};
|
|
|
|
// Execute batch operation
|
|
let batch_result = client.execute_batch_operation(batch_request).await?;
|
|
println!("Batch operation started: {}", batch_result.batch_id);
|
|
|
|
// Monitor progress
|
|
loop {
|
|
let status = client.get_batch_status(&batch_result.batch_id).await?;
|
|
println!("Batch status: {} - {}%", status.status, status.progress.unwrap_or(0.0));
|
|
|
|
match status.status.as_str() {
|
|
"Completed" | "Failed" | "Cancelled" => break,
|
|
_ => tokio::time::sleep(std::time::Duration::from_secs(10)).await,
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}</code></pre>
|
|
<h2 id="best-practices"><a class="header" href="#best-practices">Best Practices</a></h2>
|
|
<h3 id="authentication-and-security"><a class="header" href="#authentication-and-security">Authentication and Security</a></h3>
|
|
<ol>
|
|
<li><strong>Token Management</strong>: Store tokens securely and implement automatic refresh</li>
|
|
<li><strong>Environment Variables</strong>: Use environment variables for credentials</li>
|
|
<li><strong>HTTPS</strong>: Always use HTTPS in production environments</li>
|
|
<li><strong>Token Expiration</strong>: Handle token expiration gracefully</li>
|
|
</ol>
|
|
<h3 id="error-handling"><a class="header" href="#error-handling">Error Handling</a></h3>
|
|
<ol>
|
|
<li><strong>Specific Exceptions</strong>: Handle specific error types appropriately</li>
|
|
<li><strong>Retry Logic</strong>: Implement exponential backoff for transient failures</li>
|
|
<li><strong>Circuit Breakers</strong>: Use circuit breakers for resilient integrations</li>
|
|
<li><strong>Logging</strong>: Log errors with appropriate context</li>
|
|
</ol>
|
|
<h3 id="performance-optimization"><a class="header" href="#performance-optimization">Performance Optimization</a></h3>
|
|
<ol>
|
|
<li><strong>Connection Pooling</strong>: Reuse HTTP connections</li>
|
|
<li><strong>Async Operations</strong>: Use asynchronous operations where possible</li>
|
|
<li><strong>Batch Operations</strong>: Group related operations for efficiency</li>
|
|
<li><strong>Caching</strong>: Cache frequently accessed data appropriately</li>
|
|
</ol>
|
|
<h3 id="websocket-connections"><a class="header" href="#websocket-connections">WebSocket Connections</a></h3>
|
|
<ol>
|
|
<li><strong>Reconnection</strong>: Implement automatic reconnection with backoff</li>
|
|
<li><strong>Event Filtering</strong>: Subscribe only to needed event types</li>
|
|
<li><strong>Error Handling</strong>: Handle WebSocket errors gracefully</li>
|
|
<li><strong>Resource Cleanup</strong>: Properly close WebSocket connections</li>
|
|
</ol>
|
|
<h3 id="testing"><a class="header" href="#testing">Testing</a></h3>
|
|
<ol>
|
|
<li><strong>Unit Tests</strong>: Test SDK functionality with mocked responses</li>
|
|
<li><strong>Integration Tests</strong>: Test against real API endpoints</li>
|
|
<li><strong>Error Scenarios</strong>: Test error handling paths</li>
|
|
<li><strong>Load Testing</strong>: Validate performance under load</li>
|
|
</ol>
|
|
<p>This comprehensive SDK documentation provides developers with everything needed to integrate with provisioning using their preferred programming language, complete with examples, best practices, and detailed API references.</p>
|
|
|
|
</main>
|
|
|
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
|
<!-- Mobile navigation buttons -->
|
|
<a rel="prev" href="../api/extensions.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/integration-examples.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/extensions.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/integration-examples.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>
|