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.
1118 lines
42 KiB
HTML
1118 lines
42 KiB
HTML
<!DOCTYPE HTML>
|
||
<html lang="en" class="ayu sidebar-visible" dir="ltr">
|
||
<head>
|
||
<!-- Book generated using mdBook -->
|
||
<meta charset="UTF-8">
|
||
<title>Workflow - 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/development/workflow.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="development-workflow-guide"><a class="header" href="#development-workflow-guide">Development Workflow Guide</a></h1>
|
||
<p>This document outlines the recommended development workflows, coding practices, testing strategies, and debugging techniques for the provisioning project.</p>
|
||
<h2 id="table-of-contents"><a class="header" href="#table-of-contents">Table of Contents</a></h2>
|
||
<ol>
|
||
<li><a href="#overview">Overview</a></li>
|
||
<li><a href="#development-setup">Development Setup</a></li>
|
||
<li><a href="#daily-development-workflow">Daily Development Workflow</a></li>
|
||
<li><a href="#code-organization">Code Organization</a></li>
|
||
<li><a href="#testing-strategies">Testing Strategies</a></li>
|
||
<li><a href="#debugging-techniques">Debugging Techniques</a></li>
|
||
<li><a href="#integration-workflows">Integration Workflows</a></li>
|
||
<li><a href="#collaboration-guidelines">Collaboration Guidelines</a></li>
|
||
<li><a href="#quality-assurance">Quality Assurance</a></li>
|
||
<li><a href="#best-practices">Best Practices</a></li>
|
||
</ol>
|
||
<h2 id="overview"><a class="header" href="#overview">Overview</a></h2>
|
||
<p>The provisioning project employs a multi-language, multi-component architecture requiring specific development workflows to maintain consistency, quality, and efficiency.</p>
|
||
<p><strong>Key Technologies</strong>:</p>
|
||
<ul>
|
||
<li><strong>Nushell</strong>: Primary scripting and automation language</li>
|
||
<li><strong>Rust</strong>: High-performance system components</li>
|
||
<li><strong>KCL</strong>: Configuration language and schemas</li>
|
||
<li><strong>TOML</strong>: Configuration files</li>
|
||
<li><strong>Jinja2</strong>: Template engine</li>
|
||
</ul>
|
||
<p><strong>Development Principles</strong>:</p>
|
||
<ul>
|
||
<li><strong>Configuration-Driven</strong>: Never hardcode, always configure</li>
|
||
<li><strong>Hybrid Architecture</strong>: Rust for performance, Nushell for flexibility</li>
|
||
<li><strong>Test-First</strong>: Comprehensive testing at all levels</li>
|
||
<li><strong>Documentation-Driven</strong>: Code and APIs are self-documenting</li>
|
||
</ul>
|
||
<h2 id="development-setup"><a class="header" href="#development-setup">Development Setup</a></h2>
|
||
<h3 id="initial-environment-setup"><a class="header" href="#initial-environment-setup">Initial Environment Setup</a></h3>
|
||
<p><strong>1. Clone and Navigate</strong>:</p>
|
||
<pre><code class="language-bash"># Clone repository
|
||
git clone https://github.com/company/provisioning-system.git
|
||
cd provisioning-system
|
||
|
||
# Navigate to workspace
|
||
cd workspace/tools
|
||
</code></pre>
|
||
<p><strong>2. Initialize Workspace</strong>:</p>
|
||
<pre><code class="language-bash"># Initialize development workspace
|
||
nu workspace.nu init --user-name $USER --infra-name dev-env
|
||
|
||
# Check workspace health
|
||
nu workspace.nu health --detailed --fix-issues
|
||
</code></pre>
|
||
<p><strong>3. Configure Development Environment</strong>:</p>
|
||
<pre><code class="language-bash"># Create user configuration
|
||
cp workspace/config/local-overrides.toml.example workspace/config/$USER.toml
|
||
|
||
# Edit configuration for development
|
||
$EDITOR workspace/config/$USER.toml
|
||
</code></pre>
|
||
<p><strong>4. Set Up Build System</strong>:</p>
|
||
<pre><code class="language-bash"># Navigate to build tools
|
||
cd src/tools
|
||
|
||
# Check build prerequisites
|
||
make info
|
||
|
||
# Perform initial build
|
||
make dev-build
|
||
</code></pre>
|
||
<h3 id="tool-installation"><a class="header" href="#tool-installation">Tool Installation</a></h3>
|
||
<p><strong>Required Tools</strong>:</p>
|
||
<pre><code class="language-bash"># Install Nushell
|
||
cargo install nu
|
||
|
||
# Install KCL
|
||
cargo install kcl-cli
|
||
|
||
# Install additional tools
|
||
cargo install cross # Cross-compilation
|
||
cargo install cargo-audit # Security auditing
|
||
cargo install cargo-watch # File watching
|
||
</code></pre>
|
||
<p><strong>Optional Development Tools</strong>:</p>
|
||
<pre><code class="language-bash"># Install development enhancers
|
||
cargo install nu_plugin_tera # Template plugin
|
||
cargo install sops # Secrets management
|
||
brew install k9s # Kubernetes management
|
||
</code></pre>
|
||
<h3 id="ide-configuration"><a class="header" href="#ide-configuration">IDE Configuration</a></h3>
|
||
<p><strong>VS Code Setup</strong> (<code>.vscode/settings.json</code>):</p>
|
||
<pre><code class="language-json">{
|
||
"files.associations": {
|
||
"*.nu": "shellscript",
|
||
"*.k": "kcl",
|
||
"*.toml": "toml"
|
||
},
|
||
"nushell.shellPath": "/usr/local/bin/nu",
|
||
"rust-analyzer.cargo.features": "all",
|
||
"editor.formatOnSave": true,
|
||
"editor.rulers": [100],
|
||
"files.trimTrailingWhitespace": true
|
||
}
|
||
</code></pre>
|
||
<p><strong>Recommended Extensions</strong>:</p>
|
||
<ul>
|
||
<li>Nushell Language Support</li>
|
||
<li>Rust Analyzer</li>
|
||
<li>KCL Language Support</li>
|
||
<li>TOML Language Support</li>
|
||
<li>Better TOML</li>
|
||
</ul>
|
||
<h2 id="daily-development-workflow"><a class="header" href="#daily-development-workflow">Daily Development Workflow</a></h2>
|
||
<h3 id="morning-routine"><a class="header" href="#morning-routine">Morning Routine</a></h3>
|
||
<p><strong>1. Sync and Update</strong>:</p>
|
||
<pre><code class="language-bash"># Sync with upstream
|
||
git pull origin main
|
||
|
||
# Update workspace
|
||
cd workspace/tools
|
||
nu workspace.nu health --fix-issues
|
||
|
||
# Check for updates
|
||
nu workspace.nu status --detailed
|
||
</code></pre>
|
||
<p><strong>2. Review Current State</strong>:</p>
|
||
<pre><code class="language-bash"># Check current infrastructure
|
||
provisioning show servers
|
||
provisioning show settings
|
||
|
||
# Review workspace status
|
||
nu workspace.nu status
|
||
</code></pre>
|
||
<h3 id="development-cycle"><a class="header" href="#development-cycle">Development Cycle</a></h3>
|
||
<p><strong>1. Feature Development</strong>:</p>
|
||
<pre><code class="language-bash"># Create feature branch
|
||
git checkout -b feature/new-provider-support
|
||
|
||
# Start development environment
|
||
cd workspace/tools
|
||
nu workspace.nu init --workspace-type development
|
||
|
||
# Begin development
|
||
$EDITOR workspace/extensions/providers/new-provider/nulib/provider.nu
|
||
</code></pre>
|
||
<p><strong>2. Incremental Testing</strong>:</p>
|
||
<pre><code class="language-bash"># Test syntax during development
|
||
nu --check workspace/extensions/providers/new-provider/nulib/provider.nu
|
||
|
||
# Run unit tests
|
||
nu workspace/extensions/providers/new-provider/tests/unit/basic-test.nu
|
||
|
||
# Integration testing
|
||
nu workspace.nu tools test-extension providers/new-provider
|
||
</code></pre>
|
||
<p><strong>3. Build and Validate</strong>:</p>
|
||
<pre><code class="language-bash"># Quick development build
|
||
cd src/tools
|
||
make dev-build
|
||
|
||
# Validate changes
|
||
make validate-all
|
||
|
||
# Test distribution
|
||
make test-dist
|
||
</code></pre>
|
||
<h3 id="testing-during-development"><a class="header" href="#testing-during-development">Testing During Development</a></h3>
|
||
<p><strong>Unit Testing</strong>:</p>
|
||
<pre><code class="language-nushell"># Add test examples to functions
|
||
def create-server [name: string] -> record {
|
||
# @test: "test-server" -> {name: "test-server", status: "created"}
|
||
# Implementation here
|
||
}
|
||
</code></pre>
|
||
<p><strong>Integration Testing</strong>:</p>
|
||
<pre><code class="language-bash"># Test with real infrastructure
|
||
nu workspace/extensions/providers/new-provider/nulib/provider.nu \
|
||
create-server test-server --dry-run
|
||
|
||
# Test with workspace isolation
|
||
PROVISIONING_WORKSPACE_USER=$USER provisioning server create test-server --check
|
||
</code></pre>
|
||
<h3 id="end-of-day-routine"><a class="header" href="#end-of-day-routine">End-of-Day Routine</a></h3>
|
||
<p><strong>1. Commit Progress</strong>:</p>
|
||
<pre><code class="language-bash"># Stage changes
|
||
git add .
|
||
|
||
# Commit with descriptive message
|
||
git commit -m "feat(provider): add new cloud provider support
|
||
|
||
- Implement basic server creation
|
||
- Add configuration schema
|
||
- Include unit tests
|
||
- Update documentation"
|
||
|
||
# Push to feature branch
|
||
git push origin feature/new-provider-support
|
||
</code></pre>
|
||
<p><strong>2. Workspace Maintenance</strong>:</p>
|
||
<pre><code class="language-bash"># Clean up development data
|
||
nu workspace.nu cleanup --type cache --age 1d
|
||
|
||
# Backup current state
|
||
nu workspace.nu backup --auto-name --components config,extensions
|
||
|
||
# Check workspace health
|
||
nu workspace.nu health
|
||
</code></pre>
|
||
<h2 id="code-organization"><a class="header" href="#code-organization">Code Organization</a></h2>
|
||
<h3 id="nushell-code-structure"><a class="header" href="#nushell-code-structure">Nushell Code Structure</a></h3>
|
||
<p><strong>File Organization</strong>:</p>
|
||
<pre><code>Extension Structure:
|
||
├── nulib/
|
||
│ ├── main.nu # Main entry point
|
||
│ ├── core/ # Core functionality
|
||
│ │ ├── api.nu # API interactions
|
||
│ │ ├── config.nu # Configuration handling
|
||
│ │ └── utils.nu # Utility functions
|
||
│ ├── commands/ # User commands
|
||
│ │ ├── create.nu # Create operations
|
||
│ │ ├── delete.nu # Delete operations
|
||
│ │ └── list.nu # List operations
|
||
│ └── tests/ # Test files
|
||
│ ├── unit/ # Unit tests
|
||
│ └── integration/ # Integration tests
|
||
└── templates/ # Template files
|
||
├── config.j2 # Configuration templates
|
||
└── manifest.j2 # Manifest templates
|
||
</code></pre>
|
||
<p><strong>Function Naming Conventions</strong>:</p>
|
||
<pre><code class="language-nushell"># Use kebab-case for commands
|
||
def create-server [name: string] -> record { ... }
|
||
def validate-config [config: record] -> bool { ... }
|
||
|
||
# Use snake_case for internal functions
|
||
def get_api_client [] -> record { ... }
|
||
def parse_config_file [path: string] -> record { ... }
|
||
|
||
# Use descriptive prefixes
|
||
def check-server-status [server: string] -> string { ... }
|
||
def get-server-info [server: string] -> record { ... }
|
||
def list-available-zones [] -> list<string> { ... }
|
||
</code></pre>
|
||
<p><strong>Error Handling Pattern</strong>:</p>
|
||
<pre><code class="language-nushell">def create-server [
|
||
name: string
|
||
--dry-run: bool = false
|
||
] -> record {
|
||
# 1. Validate inputs
|
||
if ($name | str length) == 0 {
|
||
error make {
|
||
msg: "Server name cannot be empty"
|
||
label: {
|
||
text: "empty name provided"
|
||
span: (metadata $name).span
|
||
}
|
||
}
|
||
}
|
||
|
||
# 2. Check prerequisites
|
||
let config = try {
|
||
get-provider-config
|
||
} catch {
|
||
error make {msg: "Failed to load provider configuration"}
|
||
}
|
||
|
||
# 3. Perform operation
|
||
if $dry_run {
|
||
return {action: "create", server: $name, status: "dry-run"}
|
||
}
|
||
|
||
# 4. Return result
|
||
{server: $name, status: "created", id: (generate-id)}
|
||
}
|
||
</code></pre>
|
||
<h3 id="rust-code-structure"><a class="header" href="#rust-code-structure">Rust Code Structure</a></h3>
|
||
<p><strong>Project Organization</strong>:</p>
|
||
<pre><code>src/
|
||
├── lib.rs # Library root
|
||
├── main.rs # Binary entry point
|
||
├── config/ # Configuration handling
|
||
│ ├── mod.rs
|
||
│ ├── loader.rs # Config loading
|
||
│ └── validation.rs # Config validation
|
||
├── api/ # HTTP API
|
||
│ ├── mod.rs
|
||
│ ├── handlers.rs # Request handlers
|
||
│ └── middleware.rs # Middleware components
|
||
└── orchestrator/ # Orchestration logic
|
||
├── mod.rs
|
||
├── workflow.rs # Workflow management
|
||
└── task_queue.rs # Task queue management
|
||
</code></pre>
|
||
<p><strong>Error Handling</strong>:</p>
|
||
<pre><code class="language-rust">use anyhow::{Context, Result};
|
||
use thiserror::Error;
|
||
|
||
#[derive(Error, Debug)]
|
||
pub enum ProvisioningError {
|
||
#[error("Configuration error: {message}")]
|
||
Config { message: String },
|
||
|
||
#[error("Network error: {source}")]
|
||
Network {
|
||
#[from]
|
||
source: reqwest::Error,
|
||
},
|
||
|
||
#[error("Validation failed: {field}")]
|
||
Validation { field: String },
|
||
}
|
||
|
||
pub fn create_server(name: &str) -> Result<ServerInfo> {
|
||
let config = load_config()
|
||
.context("Failed to load configuration")?;
|
||
|
||
validate_server_name(name)
|
||
.context("Server name validation failed")?;
|
||
|
||
let server = provision_server(name, &config)
|
||
.context("Failed to provision server")?;
|
||
|
||
Ok(server)
|
||
}</code></pre>
|
||
<h3 id="kcl-schema-organization"><a class="header" href="#kcl-schema-organization">KCL Schema Organization</a></h3>
|
||
<p><strong>Schema Structure</strong>:</p>
|
||
<pre><code class="language-kcl"># Base schema definitions
|
||
schema ServerConfig:
|
||
name: str
|
||
plan: str
|
||
zone: str
|
||
tags?: {str: str} = {}
|
||
|
||
check:
|
||
len(name) > 0, "Server name cannot be empty"
|
||
plan in ["1xCPU-2GB", "2xCPU-4GB", "4xCPU-8GB"], "Invalid plan"
|
||
|
||
# Provider-specific extensions
|
||
schema UpCloudServerConfig(ServerConfig):
|
||
template?: str = "Ubuntu Server 22.04 LTS (Jammy Jellyfish)"
|
||
storage?: int = 25
|
||
|
||
check:
|
||
storage >= 10, "Minimum storage is 10GB"
|
||
storage <= 2048, "Maximum storage is 2TB"
|
||
|
||
# Composition schemas
|
||
schema InfrastructureConfig:
|
||
servers: [ServerConfig]
|
||
networks?: [NetworkConfig] = []
|
||
load_balancers?: [LoadBalancerConfig] = []
|
||
|
||
check:
|
||
len(servers) > 0, "At least one server required"
|
||
</code></pre>
|
||
<h2 id="testing-strategies"><a class="header" href="#testing-strategies">Testing Strategies</a></h2>
|
||
<h3 id="test-driven-development"><a class="header" href="#test-driven-development">Test-Driven Development</a></h3>
|
||
<p><strong>TDD Workflow</strong>:</p>
|
||
<ol>
|
||
<li><strong>Write Test First</strong>: Define expected behavior</li>
|
||
<li><strong>Run Test (Fail)</strong>: Confirm test fails as expected</li>
|
||
<li><strong>Write Code</strong>: Implement minimal code to pass</li>
|
||
<li><strong>Run Test (Pass)</strong>: Confirm test now passes</li>
|
||
<li><strong>Refactor</strong>: Improve code while keeping tests green</li>
|
||
</ol>
|
||
<h3 id="nushell-testing"><a class="header" href="#nushell-testing">Nushell Testing</a></h3>
|
||
<p><strong>Unit Test Pattern</strong>:</p>
|
||
<pre><code class="language-nushell"># Function with embedded test
|
||
def validate-server-name [name: string] -> bool {
|
||
# @test: "valid-name" -> true
|
||
# @test: "" -> false
|
||
# @test: "name-with-spaces" -> false
|
||
|
||
if ($name | str length) == 0 {
|
||
return false
|
||
}
|
||
|
||
if ($name | str contains " ") {
|
||
return false
|
||
}
|
||
|
||
true
|
||
}
|
||
|
||
# Separate test file
|
||
# tests/unit/server-validation-test.nu
|
||
def test_validate_server_name [] {
|
||
# Valid cases
|
||
assert (validate-server-name "valid-name")
|
||
assert (validate-server-name "server123")
|
||
|
||
# Invalid cases
|
||
assert not (validate-server-name "")
|
||
assert not (validate-server-name "name with spaces")
|
||
assert not (validate-server-name "name@with!special")
|
||
|
||
print "✅ validate-server-name tests passed"
|
||
}
|
||
</code></pre>
|
||
<p><strong>Integration Test Pattern</strong>:</p>
|
||
<pre><code class="language-nushell"># tests/integration/server-lifecycle-test.nu
|
||
def test_complete_server_lifecycle [] {
|
||
# Setup
|
||
let test_server = "test-server-" + (date now | format date "%Y%m%d%H%M%S")
|
||
|
||
try {
|
||
# Test creation
|
||
let create_result = (create-server $test_server --dry-run)
|
||
assert ($create_result.status == "dry-run")
|
||
|
||
# Test validation
|
||
let validate_result = (validate-server-config $test_server)
|
||
assert $validate_result
|
||
|
||
print $"✅ Server lifecycle test passed for ($test_server)"
|
||
} catch { |e|
|
||
print $"❌ Server lifecycle test failed: ($e.msg)"
|
||
exit 1
|
||
}
|
||
}
|
||
</code></pre>
|
||
<h3 id="rust-testing"><a class="header" href="#rust-testing">Rust Testing</a></h3>
|
||
<p><strong>Unit Testing</strong>:</p>
|
||
<pre><code class="language-rust">#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use tokio_test;
|
||
|
||
#[test]
|
||
fn test_validate_server_name() {
|
||
assert!(validate_server_name("valid-name"));
|
||
assert!(validate_server_name("server123"));
|
||
|
||
assert!(!validate_server_name(""));
|
||
assert!(!validate_server_name("name with spaces"));
|
||
assert!(!validate_server_name("name@special"));
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_server_creation() {
|
||
let config = test_config();
|
||
let result = create_server("test-server", &config).await;
|
||
|
||
assert!(result.is_ok());
|
||
let server = result.unwrap();
|
||
assert_eq!(server.name, "test-server");
|
||
assert_eq!(server.status, "created");
|
||
}
|
||
}</code></pre>
|
||
<p><strong>Integration Testing</strong>:</p>
|
||
<pre><code class="language-rust">#[cfg(test)]
|
||
mod integration_tests {
|
||
use super::*;
|
||
use testcontainers::*;
|
||
|
||
#[tokio::test]
|
||
async fn test_full_workflow() {
|
||
// Setup test environment
|
||
let docker = clients::Cli::default();
|
||
let postgres = docker.run(images::postgres::Postgres::default());
|
||
|
||
let config = TestConfig {
|
||
database_url: format!("postgresql://localhost:{}/test",
|
||
postgres.get_host_port_ipv4(5432))
|
||
};
|
||
|
||
// Test complete workflow
|
||
let workflow = create_workflow(&config).await.unwrap();
|
||
let result = execute_workflow(workflow).await.unwrap();
|
||
|
||
assert_eq!(result.status, WorkflowStatus::Completed);
|
||
}
|
||
}</code></pre>
|
||
<h3 id="kcl-testing"><a class="header" href="#kcl-testing">KCL Testing</a></h3>
|
||
<p><strong>Schema Validation Testing</strong>:</p>
|
||
<pre><code class="language-bash"># Test KCL schemas
|
||
kcl test kcl/
|
||
|
||
# Validate specific schemas
|
||
kcl check kcl/server.k --data test-data.yaml
|
||
|
||
# Test with examples
|
||
kcl run kcl/server.k -D name="test-server" -D plan="2xCPU-4GB"
|
||
</code></pre>
|
||
<h3 id="test-automation"><a class="header" href="#test-automation">Test Automation</a></h3>
|
||
<p><strong>Continuous Testing</strong>:</p>
|
||
<pre><code class="language-bash"># Watch for changes and run tests
|
||
cargo watch -x test -x check
|
||
|
||
# Watch Nushell files
|
||
find . -name "*.nu" | entr -r nu tests/run-all-tests.nu
|
||
|
||
# Automated testing in workspace
|
||
nu workspace.nu tools test-all --watch
|
||
</code></pre>
|
||
<h2 id="debugging-techniques"><a class="header" href="#debugging-techniques">Debugging Techniques</a></h2>
|
||
<h3 id="debug-configuration"><a class="header" href="#debug-configuration">Debug Configuration</a></h3>
|
||
<p><strong>Enable Debug Mode</strong>:</p>
|
||
<pre><code class="language-bash"># Environment variables
|
||
export PROVISIONING_DEBUG=true
|
||
export PROVISIONING_LOG_LEVEL=debug
|
||
export RUST_LOG=debug
|
||
export RUST_BACKTRACE=1
|
||
|
||
# Workspace debug
|
||
export PROVISIONING_WORKSPACE_USER=$USER
|
||
</code></pre>
|
||
<h3 id="nushell-debugging"><a class="header" href="#nushell-debugging">Nushell Debugging</a></h3>
|
||
<p><strong>Debug Techniques</strong>:</p>
|
||
<pre><code class="language-nushell"># Debug prints
|
||
def debug-server-creation [name: string] {
|
||
print $"🐛 Creating server: ($name)"
|
||
|
||
let config = get-provider-config
|
||
print $"🐛 Config loaded: ($config | to json)"
|
||
|
||
let result = try {
|
||
create-server-api $name $config
|
||
} catch { |e|
|
||
print $"🐛 API call failed: ($e.msg)"
|
||
$e
|
||
}
|
||
|
||
print $"🐛 Result: ($result | to json)"
|
||
$result
|
||
}
|
||
|
||
# Conditional debugging
|
||
def create-server [name: string] {
|
||
if $env.PROVISIONING_DEBUG? == "true" {
|
||
print $"Debug: Creating server ($name)"
|
||
}
|
||
|
||
# Implementation
|
||
}
|
||
|
||
# Interactive debugging
|
||
def debug-interactive [] {
|
||
print "🐛 Entering debug mode..."
|
||
print "Available commands: $env.PATH"
|
||
print "Current config: " (get-config | to json)
|
||
|
||
# Drop into interactive shell
|
||
nu --interactive
|
||
}
|
||
</code></pre>
|
||
<p><strong>Error Investigation</strong>:</p>
|
||
<pre><code class="language-nushell"># Comprehensive error handling
|
||
def safe-server-creation [name: string] {
|
||
try {
|
||
create-server $name
|
||
} catch { |e|
|
||
# Log error details
|
||
{
|
||
timestamp: (date now | format date "%Y-%m-%d %H:%M:%S"),
|
||
operation: "create-server",
|
||
input: $name,
|
||
error: $e.msg,
|
||
debug: $e.debug?,
|
||
env: {
|
||
user: $env.USER,
|
||
workspace: $env.PROVISIONING_WORKSPACE_USER?,
|
||
debug: $env.PROVISIONING_DEBUG?
|
||
}
|
||
} | save --append logs/error-debug.json
|
||
|
||
# Re-throw with context
|
||
error make {
|
||
msg: $"Server creation failed: ($e.msg)",
|
||
label: {text: "failed here", span: $e.span?}
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<h3 id="rust-debugging"><a class="header" href="#rust-debugging">Rust Debugging</a></h3>
|
||
<p><strong>Debug Logging</strong>:</p>
|
||
<pre><code class="language-rust">use tracing::{debug, info, warn, error, instrument};
|
||
|
||
#[instrument]
|
||
pub async fn create_server(name: &str) -> Result<ServerInfo> {
|
||
debug!("Starting server creation for: {}", name);
|
||
|
||
let config = load_config()
|
||
.map_err(|e| {
|
||
error!("Failed to load config: {:?}", e);
|
||
e
|
||
})?;
|
||
|
||
info!("Configuration loaded successfully");
|
||
debug!("Config details: {:?}", config);
|
||
|
||
let server = provision_server(name, &config).await
|
||
.map_err(|e| {
|
||
error!("Provisioning failed for {}: {:?}", name, e);
|
||
e
|
||
})?;
|
||
|
||
info!("Server {} created successfully", name);
|
||
Ok(server)
|
||
}</code></pre>
|
||
<p><strong>Interactive Debugging</strong>:</p>
|
||
<pre><code class="language-rust">// Use debugger breakpoints
|
||
#[cfg(debug_assertions)]
|
||
{
|
||
println!("Debug: server creation starting");
|
||
dbg!(&config);
|
||
// Add breakpoint here in IDE
|
||
}</code></pre>
|
||
<h3 id="log-analysis"><a class="header" href="#log-analysis">Log Analysis</a></h3>
|
||
<p><strong>Log Monitoring</strong>:</p>
|
||
<pre><code class="language-bash"># Follow all logs
|
||
tail -f workspace/runtime/logs/$USER/*.log
|
||
|
||
# Filter for errors
|
||
grep -i error workspace/runtime/logs/$USER/*.log
|
||
|
||
# Monitor specific component
|
||
tail -f workspace/runtime/logs/$USER/orchestrator.log | grep -i workflow
|
||
|
||
# Structured log analysis
|
||
jq '.level == "ERROR"' workspace/runtime/logs/$USER/structured.jsonl
|
||
</code></pre>
|
||
<p><strong>Debug Log Levels</strong>:</p>
|
||
<pre><code class="language-bash"># Different verbosity levels
|
||
PROVISIONING_LOG_LEVEL=trace provisioning server create test
|
||
PROVISIONING_LOG_LEVEL=debug provisioning server create test
|
||
PROVISIONING_LOG_LEVEL=info provisioning server create test
|
||
</code></pre>
|
||
<h2 id="integration-workflows"><a class="header" href="#integration-workflows">Integration Workflows</a></h2>
|
||
<h3 id="existing-system-integration"><a class="header" href="#existing-system-integration">Existing System Integration</a></h3>
|
||
<p><strong>Working with Legacy Components</strong>:</p>
|
||
<pre><code class="language-bash"># Test integration with existing system
|
||
provisioning --version # Legacy system
|
||
src/core/nulib/provisioning --version # New system
|
||
|
||
# Test workspace integration
|
||
PROVISIONING_WORKSPACE_USER=$USER provisioning server list
|
||
|
||
# Validate configuration compatibility
|
||
provisioning validate config
|
||
nu workspace.nu config validate
|
||
</code></pre>
|
||
<h3 id="api-integration-testing"><a class="header" href="#api-integration-testing">API Integration Testing</a></h3>
|
||
<p><strong>REST API Testing</strong>:</p>
|
||
<pre><code class="language-bash"># Test orchestrator API
|
||
curl -X GET http://localhost:9090/health
|
||
curl -X GET http://localhost:9090/tasks
|
||
|
||
# Test workflow creation
|
||
curl -X POST http://localhost:9090/workflows/servers/create \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"name": "test-server", "plan": "2xCPU-4GB"}'
|
||
|
||
# Monitor workflow
|
||
curl -X GET http://localhost:9090/workflows/batch/status/workflow-id
|
||
</code></pre>
|
||
<h3 id="database-integration"><a class="header" href="#database-integration">Database Integration</a></h3>
|
||
<p><strong>SurrealDB Integration</strong>:</p>
|
||
<pre><code class="language-nushell"># Test database connectivity
|
||
use core/nulib/lib_provisioning/database/surreal.nu
|
||
let db = (connect-database)
|
||
(test-connection $db)
|
||
|
||
# Workflow state testing
|
||
let workflow_id = (create-workflow-record "test-workflow")
|
||
let status = (get-workflow-status $workflow_id)
|
||
assert ($status.status == "pending")
|
||
</code></pre>
|
||
<h3 id="external-tool-integration"><a class="header" href="#external-tool-integration">External Tool Integration</a></h3>
|
||
<p><strong>Container Integration</strong>:</p>
|
||
<pre><code class="language-bash"># Test with Docker
|
||
docker run --rm -v $(pwd):/work provisioning:dev provisioning --version
|
||
|
||
# Test with Kubernetes
|
||
kubectl apply -f manifests/test-pod.yaml
|
||
kubectl logs test-pod
|
||
|
||
# Validate in different environments
|
||
make test-dist PLATFORM=docker
|
||
make test-dist PLATFORM=kubernetes
|
||
</code></pre>
|
||
<h2 id="collaboration-guidelines"><a class="header" href="#collaboration-guidelines">Collaboration Guidelines</a></h2>
|
||
<h3 id="branch-strategy"><a class="header" href="#branch-strategy">Branch Strategy</a></h3>
|
||
<p><strong>Branch Naming</strong>:</p>
|
||
<ul>
|
||
<li><code>feature/description</code> - New features</li>
|
||
<li><code>fix/description</code> - Bug fixes</li>
|
||
<li><code>docs/description</code> - Documentation updates</li>
|
||
<li><code>refactor/description</code> - Code refactoring</li>
|
||
<li><code>test/description</code> - Test improvements</li>
|
||
</ul>
|
||
<p><strong>Workflow</strong>:</p>
|
||
<pre><code class="language-bash"># Start new feature
|
||
git checkout main
|
||
git pull origin main
|
||
git checkout -b feature/new-provider-support
|
||
|
||
# Regular commits
|
||
git add .
|
||
git commit -m "feat(provider): implement server creation API"
|
||
|
||
# Push and create PR
|
||
git push origin feature/new-provider-support
|
||
gh pr create --title "Add new provider support" --body "..."
|
||
</code></pre>
|
||
<h3 id="code-review-process"><a class="header" href="#code-review-process">Code Review Process</a></h3>
|
||
<p><strong>Review Checklist</strong>:</p>
|
||
<ul>
|
||
<li><input disabled="" type="checkbox"/>
|
||
Code follows project conventions</li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
Tests are included and passing</li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
Documentation is updated</li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
No hardcoded values</li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
Error handling is comprehensive</li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
Performance considerations addressed</li>
|
||
</ul>
|
||
<p><strong>Review Commands</strong>:</p>
|
||
<pre><code class="language-bash"># Test PR locally
|
||
gh pr checkout 123
|
||
cd src/tools && make ci-test
|
||
|
||
# Run specific tests
|
||
nu workspace/extensions/providers/new-provider/tests/run-all.nu
|
||
|
||
# Check code quality
|
||
cargo clippy -- -D warnings
|
||
nu --check $(find . -name "*.nu")
|
||
</code></pre>
|
||
<h3 id="documentation-requirements"><a class="header" href="#documentation-requirements">Documentation Requirements</a></h3>
|
||
<p><strong>Code Documentation</strong>:</p>
|
||
<pre><code class="language-nushell"># Function documentation
|
||
def create-server [
|
||
name: string # Server name (must be unique)
|
||
plan: string # Server plan (e.g., "2xCPU-4GB")
|
||
--dry-run: bool # Show what would be created without doing it
|
||
] -> record { # Returns server creation result
|
||
# Creates a new server with the specified configuration
|
||
#
|
||
# Examples:
|
||
# create-server "web-01" "2xCPU-4GB"
|
||
# create-server "test" "1xCPU-2GB" --dry-run
|
||
|
||
# Implementation
|
||
}
|
||
</code></pre>
|
||
<h3 id="communication"><a class="header" href="#communication">Communication</a></h3>
|
||
<p><strong>Progress Updates</strong>:</p>
|
||
<ul>
|
||
<li>Daily standup participation</li>
|
||
<li>Weekly architecture reviews</li>
|
||
<li>PR descriptions with context</li>
|
||
<li>Issue tracking with details</li>
|
||
</ul>
|
||
<p><strong>Knowledge Sharing</strong>:</p>
|
||
<ul>
|
||
<li>Technical blog posts</li>
|
||
<li>Architecture decision records</li>
|
||
<li>Code review discussions</li>
|
||
<li>Team documentation updates</li>
|
||
</ul>
|
||
<h2 id="quality-assurance"><a class="header" href="#quality-assurance">Quality Assurance</a></h2>
|
||
<h3 id="code-quality-checks"><a class="header" href="#code-quality-checks">Code Quality Checks</a></h3>
|
||
<p><strong>Automated Quality Gates</strong>:</p>
|
||
<pre><code class="language-bash"># Pre-commit hooks
|
||
pre-commit install
|
||
|
||
# Manual quality check
|
||
cd src/tools
|
||
make validate-all
|
||
|
||
# Security audit
|
||
cargo audit
|
||
</code></pre>
|
||
<p><strong>Quality Metrics</strong>:</p>
|
||
<ul>
|
||
<li>Code coverage > 80%</li>
|
||
<li>No critical security vulnerabilities</li>
|
||
<li>All tests passing</li>
|
||
<li>Documentation coverage complete</li>
|
||
<li>Performance benchmarks met</li>
|
||
</ul>
|
||
<h3 id="performance-monitoring"><a class="header" href="#performance-monitoring">Performance Monitoring</a></h3>
|
||
<p><strong>Performance Testing</strong>:</p>
|
||
<pre><code class="language-bash"># Benchmark builds
|
||
make benchmark
|
||
|
||
# Performance profiling
|
||
cargo flamegraph --bin provisioning-orchestrator
|
||
|
||
# Load testing
|
||
ab -n 1000 -c 10 http://localhost:9090/health
|
||
</code></pre>
|
||
<p><strong>Resource Monitoring</strong>:</p>
|
||
<pre><code class="language-bash"># Monitor during development
|
||
nu workspace/tools/runtime-manager.nu monitor --duration 5m
|
||
|
||
# Check resource usage
|
||
du -sh workspace/runtime/
|
||
df -h
|
||
</code></pre>
|
||
<h2 id="best-practices"><a class="header" href="#best-practices">Best Practices</a></h2>
|
||
<h3 id="configuration-management"><a class="header" href="#configuration-management">Configuration Management</a></h3>
|
||
<p><strong>Never Hardcode</strong>:</p>
|
||
<pre><code class="language-nushell"># Bad
|
||
def get-api-url [] { "https://api.upcloud.com" }
|
||
|
||
# Good
|
||
def get-api-url [] {
|
||
get-config-value "providers.upcloud.api_url" "https://api.upcloud.com"
|
||
}
|
||
</code></pre>
|
||
<h3 id="error-handling"><a class="header" href="#error-handling">Error Handling</a></h3>
|
||
<p><strong>Comprehensive Error Context</strong>:</p>
|
||
<pre><code class="language-nushell">def create-server [name: string] {
|
||
try {
|
||
validate-server-name $name
|
||
} catch { |e|
|
||
error make {
|
||
msg: $"Invalid server name '($name)': ($e.msg)",
|
||
label: {text: "server name validation failed", span: $e.span?}
|
||
}
|
||
}
|
||
|
||
try {
|
||
provision-server $name
|
||
} catch { |e|
|
||
error make {
|
||
msg: $"Server provisioning failed for '($name)': ($e.msg)",
|
||
help: "Check provider credentials and quota limits"
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<h3 id="resource-management"><a class="header" href="#resource-management">Resource Management</a></h3>
|
||
<p><strong>Clean Up Resources</strong>:</p>
|
||
<pre><code class="language-nushell">def with-temporary-server [name: string, action: closure] {
|
||
let server = (create-server $name)
|
||
|
||
try {
|
||
do $action $server
|
||
} catch { |e|
|
||
# Clean up on error
|
||
delete-server $name
|
||
$e
|
||
}
|
||
|
||
# Clean up on success
|
||
delete-server $name
|
||
}
|
||
</code></pre>
|
||
<h3 id="testing-best-practices"><a class="header" href="#testing-best-practices">Testing Best Practices</a></h3>
|
||
<p><strong>Test Isolation</strong>:</p>
|
||
<pre><code class="language-nushell">def test-with-isolation [test_name: string, test_action: closure] {
|
||
let test_workspace = $"test-($test_name)-(date now | format date '%Y%m%d%H%M%S')"
|
||
|
||
try {
|
||
# Set up isolated environment
|
||
$env.PROVISIONING_WORKSPACE_USER = $test_workspace
|
||
nu workspace.nu init --user-name $test_workspace
|
||
|
||
# Run test
|
||
do $test_action
|
||
|
||
print $"✅ Test ($test_name) passed"
|
||
} catch { |e|
|
||
print $"❌ Test ($test_name) failed: ($e.msg)"
|
||
exit 1
|
||
} finally {
|
||
# Clean up test environment
|
||
nu workspace.nu cleanup --user-name $test_workspace --type all --force
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>This development workflow provides a comprehensive framework for efficient, quality-focused development while maintaining the project’s architectural principles and ensuring smooth collaboration across the team.</p>
|
||
|
||
</main>
|
||
|
||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||
<!-- Mobile navigation buttons -->
|
||
<a rel="prev" href="../development/project-structure.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="../development/integration.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="../development/project-structure.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="../development/integration.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>
|