Compare commits
	
		
			47 Commits
		
	
	
		
			ef8cc74d2b
			...
			network_se
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | da3da0ae30 | ||
|  | 784f87db97 | ||
|  | 773db2238d | ||
|  | e8a369e3a2 | ||
|  | 4b4f3371b0 | ||
|  | 1bb731711b | ||
|  | af89ef0149 | ||
|  | 768e3e176d | ||
|  | aa0248ef17 | ||
|  | aab2b6f128 | ||
|  | d735316b7f | ||
|  | d1c80863b8 | ||
|  | 169c62da47 | ||
|  | 33a5f24981 | ||
|  | d7562ce466 | ||
| ca736d62f3 | |||
|  | 078c6f723b | ||
|  | 9fdb8d8845 | ||
| 8203a3b1ff | |||
| 1770ac561e | |||
|  | eed6dbf8dc | ||
| 4cd4e04028 | |||
| 8cc828fc0e | |||
| 56af312aad | |||
| dfd6931c5b | |||
| 6e01f99958 | |||
| 0c02d0e99f | |||
| 7856fc0a4e | |||
|  | 758e59e921 | ||
| f1806eb788 | |||
|  | 6e5d9b35e8 | ||
| 61f5331804 | |||
|  | 423b7bfa7e | ||
| fc2830da31 | |||
|  | 6b12001ca2 | ||
|  | 99e121b0d8 | ||
|  | 502e345f91 | ||
|  | 352e846410 | ||
|  | b72c50bed9 | ||
|  | 95122dffee | ||
|  | a63cbe2bd9 | ||
|  | 1e4c0ac41a | ||
|  | 0e49be8d71 | ||
|  | 32339e6063 | ||
|  | 131d978450 | ||
|  | 46ad848e7e | ||
|  | b4e370b668 | 
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -62,4 +62,5 @@ docusaurus.config.ts | ||||
| sidebars.ts | ||||
|  | ||||
| tsconfig.json | ||||
| Cargo.toml.bak | ||||
| Cargo.toml.bak | ||||
| for_augment | ||||
							
								
								
									
										115
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										115
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -12,21 +12,25 @@ readme = "README.md" | ||||
|  | ||||
| [workspace] | ||||
| members = [ | ||||
|     ".", | ||||
|     "vault", | ||||
|     "git", | ||||
|     "redisclient", | ||||
|     "mycelium", | ||||
|     "text", | ||||
|     "os", | ||||
|     "net", | ||||
|     "zinit_client", | ||||
|     "process", | ||||
|     "virt", | ||||
|     "postgresclient", | ||||
|     "kubernetes", | ||||
|     "packages/clients/myceliumclient", | ||||
|     "packages/clients/postgresclient", | ||||
|     "packages/clients/redisclient", | ||||
|     "packages/clients/zinitclient", | ||||
|     "packages/core/net", | ||||
|     "packages/core/text", | ||||
|     "packages/crypt/vault", | ||||
|     "packages/data/ourdb", | ||||
|     "packages/data/radixtree", | ||||
|     "packages/data/tst", | ||||
|     "packages/system/git", | ||||
|     "packages/system/kubernetes", | ||||
|     "packages/system/os", | ||||
|     "packages/system/process", | ||||
|     "packages/system/virt", | ||||
|     "rhai", | ||||
|     "rhailib", | ||||
|     "herodo", | ||||
|     "packages/clients/hetznerclient", | ||||
| ] | ||||
| resolver = "2" | ||||
|  | ||||
| @@ -48,7 +52,7 @@ log = "0.4" | ||||
| once_cell = "1.18.0" | ||||
| rand = "0.8.5" | ||||
| regex = "1.8.1" | ||||
| reqwest = { version = "0.12.15", features = ["json"] } | ||||
| reqwest = { version = "0.12.15", features = ["json", "blocking"] } | ||||
| rhai = { version = "1.12.0", features = ["sync"] } | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
| serde_json = "1.0" | ||||
| @@ -69,6 +73,10 @@ chacha20poly1305 = "0.10.1" | ||||
| k256 = { version = "0.13.4", features = ["ecdsa", "ecdh"] } | ||||
| sha2 = "0.10.7" | ||||
| hex = "0.4" | ||||
| bincode = { version = "2.0.1", features = ["serde"] } | ||||
| pbkdf2 = "0.12.2" | ||||
| getrandom = { version = "0.3.3", features = ["wasm_js"] } | ||||
| tera = "1.19.0" | ||||
|  | ||||
| # Ethereum dependencies | ||||
| ethers = { version = "2.0.7", features = ["legacy"] } | ||||
| @@ -82,27 +90,55 @@ windows = { version = "0.61.1", features = [ | ||||
| ] } | ||||
|  | ||||
| # Specialized dependencies | ||||
| zinit-client = "0.3.0" | ||||
| zinit-client = "0.4.0" | ||||
| urlencoding = "2.1.3" | ||||
| tokio-test = "0.4.4" | ||||
| kube = { version = "0.95.0", features = ["client", "config", "derive"] } | ||||
| k8s-openapi = { version = "0.23.0", features = ["latest"] } | ||||
| tokio-retry = "0.3.0" | ||||
| governor = "0.6.3" | ||||
| tower = { version = "0.5.2", features = ["timeout", "limit"] } | ||||
| serde_yaml = "0.9" | ||||
| postgres-types = "0.2.5" | ||||
| r2d2 = "0.8.10" | ||||
|  | ||||
| # SAL dependencies | ||||
| sal-git = { path = "packages/system/git" } | ||||
| sal-kubernetes = { path = "packages/system/kubernetes" } | ||||
| sal-redisclient = { path = "packages/clients/redisclient" } | ||||
| sal-mycelium = { path = "packages/clients/myceliumclient" } | ||||
| sal-hetzner = { path = "packages/clients/hetznerclient" } | ||||
| sal-text = { path = "packages/core/text" } | ||||
| sal-os = { path = "packages/system/os" } | ||||
| sal-net = { path = "packages/core/net" } | ||||
| sal-zinit-client = { path = "packages/clients/zinitclient" } | ||||
| sal-process = { path = "packages/system/process" } | ||||
| sal-virt = { path = "packages/system/virt" } | ||||
| sal-postgresclient = { path = "packages/clients/postgresclient" } | ||||
| sal-vault = { path = "packages/crypt/vault" } | ||||
| sal-rhai = { path = "rhai" } | ||||
| sal-service-manager = { path = "_archive/service_manager" } | ||||
|  | ||||
| [dependencies] | ||||
| thiserror = "2.0.12" # For error handling in the main Error enum | ||||
| thiserror = { workspace = true } | ||||
| tokio = { workspace = true } | ||||
|  | ||||
| # Optional dependencies - users can choose which modules to include | ||||
| sal-git = { path = "git", optional = true } | ||||
| sal-kubernetes = { path = "kubernetes", optional = true } | ||||
| sal-redisclient = { path = "redisclient", optional = true } | ||||
| sal-mycelium = { path = "mycelium", optional = true } | ||||
| sal-text = { path = "text", optional = true } | ||||
| sal-os = { path = "os", optional = true } | ||||
| sal-net = { path = "net", optional = true } | ||||
| sal-zinit-client = { path = "zinit_client", optional = true } | ||||
| sal-process = { path = "process", optional = true } | ||||
| sal-virt = { path = "virt", optional = true } | ||||
| sal-postgresclient = { path = "postgresclient", optional = true } | ||||
| sal-vault = { path = "vault", optional = true } | ||||
| sal-rhai = { path = "rhai", optional = true } | ||||
| sal-git = { workspace = true, optional = true } | ||||
| sal-kubernetes = { workspace = true, optional = true } | ||||
| sal-redisclient = { workspace = true, optional = true } | ||||
| sal-mycelium = { workspace = true, optional = true } | ||||
| sal-hetzner = { workspace = true, optional = true } | ||||
| sal-text = { workspace = true, optional = true } | ||||
| sal-os = { workspace = true, optional = true } | ||||
| sal-net = { workspace = true, optional = true } | ||||
| sal-zinit-client = { workspace = true, optional = true } | ||||
| sal-process = { workspace = true, optional = true } | ||||
| sal-virt = { workspace = true, optional = true } | ||||
| sal-postgresclient = { workspace = true, optional = true } | ||||
| sal-vault = { workspace = true, optional = true } | ||||
| sal-rhai = { workspace = true, optional = true } | ||||
| sal-service-manager = { workspace = true, optional = true } | ||||
|  | ||||
| [features] | ||||
| default = [] | ||||
| @@ -112,6 +148,7 @@ git = ["dep:sal-git"] | ||||
| kubernetes = ["dep:sal-kubernetes"] | ||||
| redisclient = ["dep:sal-redisclient"] | ||||
| mycelium = ["dep:sal-mycelium"] | ||||
| hetzner = ["dep:sal-hetzner"] | ||||
| text = ["dep:sal-text"] | ||||
| os = ["dep:sal-os"] | ||||
| net = ["dep:sal-net"] | ||||
| @@ -121,10 +158,11 @@ virt = ["dep:sal-virt"] | ||||
| postgresclient = ["dep:sal-postgresclient"] | ||||
| vault = ["dep:sal-vault"] | ||||
| rhai = ["dep:sal-rhai"] | ||||
| # service_manager is removed as it's not a direct member anymore | ||||
|  | ||||
| # Convenience feature groups | ||||
| core = ["os", "process", "text", "net"] | ||||
| clients = ["redisclient", "postgresclient", "zinit_client", "mycelium"] | ||||
| clients = ["redisclient", "postgresclient", "zinit_client", "mycelium", "hetzner"] | ||||
| infrastructure = ["git", "vault", "kubernetes", "virt"] | ||||
| scripting = ["rhai"] | ||||
| all = [ | ||||
| @@ -132,6 +170,7 @@ all = [ | ||||
|     "kubernetes", | ||||
|     "redisclient", | ||||
|     "mycelium", | ||||
|     "hetzner", | ||||
|     "text", | ||||
|     "os", | ||||
|     "net", | ||||
| @@ -142,3 +181,19 @@ all = [ | ||||
|     "vault", | ||||
|     "rhai", | ||||
| ] | ||||
|  | ||||
| # Examples | ||||
| [[example]] | ||||
| name = "postgres_cluster" | ||||
| path = "examples/kubernetes/clusters/postgres.rs" | ||||
| required-features = ["kubernetes"] | ||||
|  | ||||
| [[example]] | ||||
| name = "redis_cluster" | ||||
| path = "examples/kubernetes/clusters/redis.rs" | ||||
| required-features = ["kubernetes"] | ||||
|  | ||||
| [[example]] | ||||
| name = "generic_cluster" | ||||
| path = "examples/kubernetes/clusters/generic.rs" | ||||
| required-features = ["kubernetes"] | ||||
|   | ||||
							
								
								
									
										456
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										456
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,404 +1,136 @@ | ||||
| # SAL (System Abstraction Layer) | ||||
| # Herocode Herolib Rust Repository | ||||
|  | ||||
| **Version: 0.1.0** | ||||
| ## Overview | ||||
|  | ||||
| SAL is a comprehensive Rust library designed to provide a unified and simplified interface for a wide array of system-level operations and interactions. It abstracts platform-specific details, enabling developers to write robust, cross-platform code with greater ease. SAL also includes `herodo`, a powerful command-line tool for executing Rhai scripts that leverage SAL's capabilities for automation and system management tasks. | ||||
| This repository contains the **Herocode Herolib** Rust library and a collection of scripts, examples, and utilities for building, testing, and publishing the SAL (System Abstraction Layer) crates. The repository includes: | ||||
|  | ||||
| ## 🏗️ **Cargo Workspace Structure** | ||||
| - **Rust crates** for various system components (e.g., `os`, `process`, `text`, `git`, `vault`, `kubernetes`, etc.). | ||||
| - **Rhai scripts** and test suites for each crate. | ||||
| - **Utility scripts** to automate common development tasks. | ||||
|  | ||||
| SAL is organized as a **Cargo workspace** with 15 specialized crates: | ||||
| ## Scripts | ||||
|  | ||||
| - **Root Package**: `sal` - Umbrella crate that re-exports all modules | ||||
| - **12 Library Crates**: Core SAL modules (os, process, text, net, git, vault, kubernetes, virt, redisclient, postgresclient, zinit_client, mycelium) | ||||
| - **1 Binary Crate**: `herodo` - Rhai script execution engine | ||||
| - **1 Integration Crate**: `rhai` - Rhai scripting integration layer | ||||
| The repository provides three primary helper scripts located in the repository root: | ||||
|  | ||||
| This workspace structure provides excellent build performance, dependency management, and maintainability. | ||||
| | Script | Description | Typical Usage | | ||||
| |--------|-------------|--------------| | ||||
| | `scripts/publish-all.sh` | Publishes all SAL crates to **crates.io** in the correct dependency order. Handles version bumping, dependency updates, dry‑run mode, and rate‑limiting. | `./scripts/publish-all.sh [--dry-run] [--wait <seconds>] [--version <ver>]` | | ||||
| | `build_herodo.sh` | Builds the `herodo` binary from the `herodo` package and optionally runs a specified Rhai script. | `./build_herodo.sh [script_name]` | | ||||
| | `run_rhai_tests.sh` | Executes all Rhai test suites across the repository, logging results and providing a summary. | `./run_rhai_tests.sh` | | ||||
|  | ||||
| ### **🚀 Workspace Benefits** | ||||
| - **Unified Dependency Management**: Shared dependencies across all crates with consistent versions | ||||
| - **Optimized Build Performance**: Parallel compilation and shared build artifacts | ||||
| - **Simplified Testing**: Run tests across all modules with a single command | ||||
| - **Modular Architecture**: Each module is independently maintainable while sharing common infrastructure | ||||
| - **Production Ready**: 100% test coverage with comprehensive Rhai integration tests | ||||
| Below are detailed usage instructions for each script. | ||||
|  | ||||
| ## 📦 Installation | ||||
| --- | ||||
|  | ||||
| SAL is designed to be modular - install only the components you need! | ||||
| ## 1. `scripts/publish-all.sh` | ||||
|  | ||||
| ### Option 1: Individual Crates (Recommended) | ||||
| ### Purpose | ||||
|  | ||||
| Install only the modules you need: | ||||
| - Publishes each SAL crate in the correct dependency order. | ||||
| - Updates crate versions (if `--version` is supplied). | ||||
| - Updates path dependencies to version dependencies before publishing. | ||||
| - Supports **dry‑run** mode to preview actions without publishing. | ||||
| - Handles rate‑limiting between crate publishes. | ||||
|  | ||||
| ### Options | ||||
|  | ||||
| | Option | Description | | ||||
| |--------|-------------| | ||||
| | `--dry-run` | Shows what would be published without actually publishing. | | ||||
| | `--wait <seconds>` | Wait time between publishes (default: 15 s). | | ||||
| | `--version <ver>` | Set a new version for all crates (updates `Cargo.toml` files). | | ||||
| | `-h, --help` | Show help message. | | ||||
|  | ||||
| ### Example Usage | ||||
|  | ||||
| ```bash | ||||
| # Currently available packages | ||||
| cargo add sal-os sal-process sal-text sal-net sal-git sal-vault sal-kubernetes sal-virt | ||||
| # Dry run – no crates will be published | ||||
| ./scripts/publish-all.sh --dry-run | ||||
|  | ||||
| # Coming soon (rate limited) | ||||
| # cargo add sal-redisclient sal-postgresclient sal-zinit-client sal-mycelium sal-rhai | ||||
| # Publish with a custom wait time and version bump | ||||
| ./scripts/publish-all.sh --wait 30 --version 1.2.3 | ||||
|  | ||||
| # Normal publish (no dry‑run) | ||||
| ./scripts/publish-all.sh | ||||
| ``` | ||||
|  | ||||
| ### Option 2: Meta-crate with Features | ||||
| ### Notes | ||||
|  | ||||
| Use the main `sal` crate with specific features: | ||||
| - Must be run from the repository root (where `Cargo.toml` lives). | ||||
| - Requires `cargo` and a logged‑in `cargo` session (`cargo login`). | ||||
| - The script automatically updates dependencies in each crate’s `Cargo.toml` to use the new version before publishing. | ||||
|  | ||||
| ```bash | ||||
| # Coming soon - meta-crate with features (rate limited) | ||||
| # cargo add sal --features os,process,text | ||||
| # cargo add sal --features core              # os, process, text, net | ||||
| # cargo add sal --features infrastructure    # git, vault, kubernetes, virt | ||||
| # cargo add sal --features all | ||||
| --- | ||||
|  | ||||
| # For now, use individual crates (see Option 1 above) | ||||
| ``` | ||||
| ## 2. `build_herodo.sh` | ||||
|  | ||||
| ### Quick Start Examples | ||||
| ### Purpose | ||||
|  | ||||
| #### Using Individual Crates (Recommended) | ||||
|  | ||||
| ```rust | ||||
| use sal_os::fs; | ||||
| use sal_process::run; | ||||
|  | ||||
| fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     // File system operations | ||||
|     let files = fs::list_files(".")?; | ||||
|     println!("Found {} files", files.len()); | ||||
|  | ||||
|     // Process execution | ||||
|     let result = run::command("echo hello")?; | ||||
|     println!("Output: {}", result.stdout); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Using Meta-crate with Features | ||||
|  | ||||
| ```rust | ||||
| // In Cargo.toml: sal = { version = "0.1.0", features = ["os", "process"] } | ||||
| use sal::os::fs; | ||||
| use sal::process::run; | ||||
|  | ||||
| fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     // File system operations | ||||
|     let files = fs::list_files(".")?; | ||||
|     println!("Found {} files", files.len()); | ||||
|  | ||||
|     // Process execution | ||||
|     let result = run::command("echo hello")?; | ||||
|     println!("Output: {}", result.stdout); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Using Herodo for Scripting | ||||
|  | ||||
| ```bash | ||||
| # Build and install herodo | ||||
| git clone https://github.com/PlanetFirst/sal.git | ||||
| cd sal | ||||
| ./build_herodo.sh | ||||
|  | ||||
| # Create a script file | ||||
| cat > example.rhai << 'EOF' | ||||
| // File operations | ||||
| let files = find_files(".", "*.rs"); | ||||
| print("Found " + files.len() + " Rust files"); | ||||
|  | ||||
| // Process execution | ||||
| let result = run("echo 'Hello from SAL!'"); | ||||
| print("Output: " + result.stdout); | ||||
|  | ||||
| // Network operations | ||||
| let reachable = http_check("https://github.com"); | ||||
| print("GitHub reachable: " + reachable); | ||||
| EOF | ||||
|  | ||||
| # Execute the script | ||||
| herodo example.rhai | ||||
| ``` | ||||
|  | ||||
| ## 📦 Available Packages | ||||
|  | ||||
| SAL is published as individual crates, allowing you to install only what you need: | ||||
|  | ||||
| | Package | Description | Install Command | | ||||
| |---------|-------------|-----------------| | ||||
| | [`sal-os`](https://crates.io/crates/sal-os) | Operating system operations | `cargo add sal-os` | | ||||
| | [`sal-process`](https://crates.io/crates/sal-process) | Process management | `cargo add sal-process` | | ||||
| | [`sal-text`](https://crates.io/crates/sal-text) | Text processing utilities | `cargo add sal-text` | | ||||
| | [`sal-net`](https://crates.io/crates/sal-net) | Network operations | `cargo add sal-net` | | ||||
| | [`sal-git`](https://crates.io/crates/sal-git) | Git repository management | `cargo add sal-git` | | ||||
| | [`sal-vault`](https://crates.io/crates/sal-vault) | Cryptographic operations | `cargo add sal-vault` | | ||||
| | [`sal-kubernetes`](https://crates.io/crates/sal-kubernetes) | Kubernetes management | `cargo add sal-kubernetes` | | ||||
| | [`sal-virt`](https://crates.io/crates/sal-virt) | Virtualization tools | `cargo add sal-virt` | | ||||
| | `sal-redisclient` | Redis database client | `cargo add sal-redisclient` ⏳ | | ||||
| | `sal-postgresclient` | PostgreSQL client | `cargo add sal-postgresclient` ⏳ | | ||||
| | `sal-zinit-client` | Zinit process supervisor | `cargo add sal-zinit-client` ⏳ | | ||||
| | `sal-mycelium` | Mycelium network client | `cargo add sal-mycelium` ⏳ | | ||||
| | `sal-rhai` | Rhai scripting integration | `cargo add sal-rhai` ⏳ | | ||||
| | `sal` | Meta-crate with features | `cargo add sal --features all` ⏳ | | ||||
| | `herodo` | Script executor binary | Build from source ⏳ | | ||||
|  | ||||
| **Legend**: ✅ Published | ⏳ Publishing soon (rate limited) | ||||
|  | ||||
| ### 📢 **Publishing Status** | ||||
|  | ||||
| **Currently Available on crates.io:** | ||||
| - ✅ [`sal-os`](https://crates.io/crates/sal-os) - Operating system operations | ||||
| - ✅ [`sal-process`](https://crates.io/crates/sal-process) - Process management | ||||
| - ✅ [`sal-text`](https://crates.io/crates/sal-text) - Text processing utilities | ||||
| - ✅ [`sal-net`](https://crates.io/crates/sal-net) - Network operations | ||||
| - ✅ [`sal-git`](https://crates.io/crates/sal-git) - Git repository management | ||||
| - ✅ [`sal-vault`](https://crates.io/crates/sal-vault) - Cryptographic operations | ||||
| - ✅ [`sal-kubernetes`](https://crates.io/crates/sal-kubernetes) - Kubernetes management | ||||
| - ✅ [`sal-virt`](https://crates.io/crates/sal-virt) - Virtualization tools | ||||
|  | ||||
| **Publishing Soon** (hit crates.io rate limit): | ||||
| - ⏳ `sal-redisclient`, `sal-postgresclient`, `sal-zinit-client`, `sal-mycelium` | ||||
| - ⏳ `sal-rhai` | ||||
| - ⏳ `sal` (meta-crate), `herodo` (binary) | ||||
|  | ||||
| **Estimated Timeline**: Remaining packages will be published within 24 hours once the rate limit resets. | ||||
|  | ||||
| ## Core Features | ||||
|  | ||||
| SAL offers a broad spectrum of functionalities, including: | ||||
|  | ||||
| - **System Operations**: File and directory management, environment variable access, system information retrieval, and OS-specific commands. | ||||
| - **Process Management**: Create, monitor, control, and interact with system processes. | ||||
| - **Containerization Tools**:  | ||||
|     - Integration with **Buildah** for building OCI/Docker-compatible container images. | ||||
|     - Integration with **nerdctl** for managing containers (run, stop, list, build, etc.). | ||||
| - **Version Control**: Programmatic interaction with Git repositories (clone, commit, push, pull, status, etc.). | ||||
| - **Database Clients**: | ||||
|     - **Redis**: Robust client for interacting with Redis servers. | ||||
|     - **PostgreSQL**: Client for executing queries and managing PostgreSQL databases. | ||||
| - **Scripting Engine**: In-built support for the **Rhai** scripting language, allowing SAL functionalities to be scripted and automated, primarily through the `herodo` tool. | ||||
| - **Networking & Services**: | ||||
|     - **Mycelium**: Tools for Mycelium network peer management and message passing. | ||||
|     - **Zinit**: Client for interacting with the Zinit process supervision system. | ||||
|     - **RFS (Remote/Virtual Filesystem)**: Mount, manage, pack, and unpack various types of filesystems (local, SSH, S3, WebDAV). | ||||
| - **Text Processing**: A suite of utilities for text manipulation, formatting, and regular expressions. | ||||
| - **Cryptography (`vault`)**: Functions for common cryptographic operations. | ||||
|  | ||||
| ## `herodo`: The SAL Scripting Tool | ||||
|  | ||||
| `herodo` is a command-line utility bundled with SAL that executes Rhai scripts. It empowers users to automate tasks and orchestrate complex workflows by leveraging SAL's diverse modules directly from scripts. | ||||
| - Builds the `herodo` binary from the `herodo` package. | ||||
| - Copies the binary to a system‑wide location (`/usr/local/bin`) if run as root, otherwise to `~/hero/bin`. | ||||
| - Optionally runs a specified Rhai script after building. | ||||
|  | ||||
| ### Usage | ||||
|  | ||||
| ```bash | ||||
| # Execute a single Rhai script | ||||
| herodo script.rhai | ||||
| # Build only | ||||
| ./build_herodo.sh | ||||
|  | ||||
| # Execute a script with arguments | ||||
| herodo script.rhai arg1 arg2 | ||||
|  | ||||
| # Execute all .rhai scripts in a directory | ||||
| herodo /path/to/scripts/ | ||||
| # Build and run a specific Rhai script (e.g., `example`): | ||||
| ./build_herodo.sh example | ||||
| ``` | ||||
|  | ||||
| If a directory is provided, `herodo` will execute all `.rhai` scripts within that directory (and its subdirectories) in alphabetical order. | ||||
| ### Details | ||||
|  | ||||
| ### Scriptable SAL Modules via `herodo` | ||||
| - The script changes to its own directory, builds the `herodo` crate (`cargo build`), and copies the binary. | ||||
| - If a script name is provided, it looks for the script in: | ||||
|   - `src/rhaiexamples/<name>.rhai` | ||||
|   - `src/herodo/scripts/<name>.rhai` | ||||
| - If the script is not found, the script exits with an error. | ||||
|  | ||||
| The following SAL modules and functionalities are exposed to the Rhai scripting environment through `herodo`: | ||||
| --- | ||||
|  | ||||
| - **OS (`os`)**: Comprehensive file system operations, file downloading & installation, and system package management. [Documentation](os/README.md) | ||||
| - **Process (`process`)**: Robust command and script execution, plus process management (listing, finding, killing, checking command existence). [Documentation](process/README.md) | ||||
| - **Text (`text`)**: String manipulation, prefixing, path/name fixing, text replacement, and templating. [Documentation](text/README.md) | ||||
| - **Net (`net`)**: Network operations, HTTP requests, and connectivity utilities. [Documentation](net/README.md) | ||||
| - **Git (`git`)**: High-level repository management and generic Git command execution with Redis-backed authentication (clone, pull, push, commit, etc.). [Documentation](git/README.md) | ||||
| - **Vault (`vault`)**: Cryptographic operations, keypair management, encryption, decryption, hashing, etc. [Documentation](vault/README.md) | ||||
| - **Redis Client (`redisclient`)**: Execute Redis commands (`redis_get`, `redis_set`, `redis_execute`, etc.). [Documentation](redisclient/README.md) | ||||
| - **PostgreSQL Client (`postgresclient`)**: Execute SQL queries against PostgreSQL databases. [Documentation](postgresclient/README.md) | ||||
| - **Zinit (`zinit_client`)**: Client for Zinit process supervisor (service management, logs). [Documentation](zinit_client/README.md) | ||||
| - **Mycelium (`mycelium`)**: Client for Mycelium decentralized networking API (node info, peer management, messaging). [Documentation](mycelium/README.md) | ||||
| - **Virtualization (`virt`)**: | ||||
|   - **Buildah**: OCI/Docker image building functions. [Documentation](virt/README.md) | ||||
|   - **nerdctl**: Container lifecycle management (`nerdctl_run`, `nerdctl_stop`, `nerdctl_images`, `nerdctl_image_build`, etc.) | ||||
|   - **RFS**: Mount various filesystems (local, SSH, S3, etc.), pack/unpack filesystem layers. | ||||
| ## 3. `run_rhai_tests.sh` | ||||
|  | ||||
| ### Example `herodo` Rhai Script | ||||
| ### Purpose | ||||
|  | ||||
| ```rhai | ||||
| // file: /opt/scripts/example_task.rhai | ||||
| - Runs **all** Rhai test suites across the repository. | ||||
| - Supports both the legacy `rhai_tests` directory and the newer `*/tests/rhai` layout. | ||||
| - Logs output to `run_rhai_tests.log` and prints a summary. | ||||
|  | ||||
| // OS operations | ||||
| println("Checking for /tmp/my_app_data..."); | ||||
| if !exist("/tmp/my_app_data") { | ||||
|     mkdir("/tmp/my_app_data"); | ||||
|     println("Created directory /tmp/my_app_data"); | ||||
| } | ||||
|  | ||||
| // Redis operations | ||||
| println("Setting Redis key 'app_status' to 'running'"); | ||||
| redis_set("app_status", "running"); | ||||
| let status = redis_get("app_status"); | ||||
| println("Current app_status from Redis: " + status); | ||||
|  | ||||
| // Process execution | ||||
| println("Listing files in /tmp:"); | ||||
| let output = run("ls -la /tmp"); | ||||
| println(output.stdout); | ||||
|  | ||||
| println("Script finished."); | ||||
| ``` | ||||
|  | ||||
| Run with: `herodo /opt/scripts/example_task.rhai` | ||||
|  | ||||
| For more examples, check the individual module test directories (e.g., `text/tests/rhai/`, `os/tests/rhai/`, etc.) in this repository. | ||||
|  | ||||
| ## Using SAL as a Rust Library | ||||
|  | ||||
| ### Option 1: Individual Crates (Recommended) | ||||
|  | ||||
| Add only the SAL modules you need: | ||||
|  | ||||
| ```toml | ||||
| [dependencies] | ||||
| sal-os = "0.1.0" | ||||
| sal-process = "0.1.0" | ||||
| sal-text = "0.1.0" | ||||
| ``` | ||||
|  | ||||
| ```rust | ||||
| use sal_os::fs; | ||||
| use sal_process::run; | ||||
| use sal_text::template; | ||||
|  | ||||
| fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     // File operations | ||||
|     let files = fs::list_files(".")?; | ||||
|     println!("Found {} files", files.len()); | ||||
|  | ||||
|     // Process execution | ||||
|     let result = run::command("echo 'Hello SAL!'")?; | ||||
|     println!("Output: {}", result.stdout); | ||||
|  | ||||
|     // Text templating | ||||
|     let template_str = "Hello {{name}}!"; | ||||
|     let mut vars = std::collections::HashMap::new(); | ||||
|     vars.insert("name".to_string(), "World".to_string()); | ||||
|     let rendered = template::render(template_str, &vars)?; | ||||
|     println!("Rendered: {}", rendered); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Option 2: Meta-crate with Features (Coming Soon) | ||||
|  | ||||
| ```toml | ||||
| [dependencies] | ||||
| sal = { version = "0.1.0", features = ["os", "process", "text"] } | ||||
| ``` | ||||
|  | ||||
| ```rust | ||||
| use sal::os::fs; | ||||
| use sal::process::run; | ||||
| use sal::text::template; | ||||
|  | ||||
| // Same code as above, but using the meta-crate | ||||
| ``` | ||||
|  | ||||
| *(Note: The meta-crate `sal` will be available once all individual packages are published.)* | ||||
|  | ||||
| ## 🎯 **Why Choose SAL?** | ||||
|  | ||||
| ### **Modular Architecture** | ||||
| - **Install Only What You Need**: Each package is independent - no bloated dependencies | ||||
| - **Faster Compilation**: Smaller dependency trees mean faster build times | ||||
| - **Smaller Binaries**: Only include the functionality you actually use | ||||
| - **Clear Dependencies**: Explicit about what functionality your project uses | ||||
|  | ||||
| ### **Developer Experience** | ||||
| - **Consistent APIs**: All packages follow the same design patterns and conventions | ||||
| - **Comprehensive Documentation**: Each package has detailed documentation and examples | ||||
| - **Real-World Tested**: All functionality is production-tested, no placeholder code | ||||
| - **Type Safety**: Leverages Rust's type system for safe, reliable operations | ||||
|  | ||||
| ### **Scripting Power** | ||||
| - **Herodo Integration**: Execute Rhai scripts with full access to SAL functionality | ||||
| - **Cross-Platform**: Works consistently across Windows, macOS, and Linux | ||||
| - **Automation Ready**: Perfect for DevOps, CI/CD, and system administration tasks | ||||
|  | ||||
| ## 📦 **Workspace Modules Overview** | ||||
|  | ||||
| SAL is organized as a Cargo workspace with the following crates: | ||||
|  | ||||
| ### **Core Library Modules** | ||||
| - **`sal-os`**: Core OS interactions, file system operations, environment access | ||||
| - **`sal-process`**: Process creation, management, and control | ||||
| - **`sal-text`**: Utilities for text processing and manipulation | ||||
| - **`sal-net`**: Network operations, HTTP requests, and connectivity utilities | ||||
|  | ||||
| ### **Integration Modules** | ||||
| - **`sal-git`**: Git repository management and operations | ||||
| - **`sal-vault`**: Cryptographic functions and keypair management | ||||
| - **`sal-rhai`**: Integration layer for the Rhai scripting engine, used by `herodo` | ||||
|  | ||||
| ### **Client Modules** | ||||
| - **`sal-redisclient`**: Client for Redis database interactions | ||||
| - **`sal-postgresclient`**: Client for PostgreSQL database interactions | ||||
| - **`sal-zinit-client`**: Client for Zinit process supervisor | ||||
| - **`sal-mycelium`**: Client for Mycelium network operations | ||||
|  | ||||
| ### **Specialized Modules** | ||||
| - **`sal-virt`**: Virtualization-related utilities (buildah, nerdctl, rfs) | ||||
|  | ||||
| ### **Root Package & Binary** | ||||
| - **`sal`**: Root umbrella crate that re-exports all modules | ||||
| - **`herodo`**: Command-line binary for executing Rhai scripts | ||||
|  | ||||
| ## 🔨 **Building SAL** | ||||
|  | ||||
| Build the entire workspace (all crates) using Cargo: | ||||
| ### Usage | ||||
|  | ||||
| ```bash | ||||
| # Build all workspace members | ||||
| cargo build --workspace | ||||
|  | ||||
| # Build for release | ||||
| cargo build --workspace --release | ||||
|  | ||||
| # Build specific crate | ||||
| cargo build -p sal-text | ||||
| cargo build -p herodo | ||||
| ``` | ||||
|  | ||||
| The `herodo` executable will be located at `target/debug/herodo` or `target/release/herodo`. | ||||
|  | ||||
| ## 🧪 **Running Tests** | ||||
|  | ||||
| ### **Rust Unit Tests** | ||||
| ```bash | ||||
| # Run all workspace tests | ||||
| cargo test --workspace | ||||
|  | ||||
| # Run tests for specific crate | ||||
| cargo test -p sal-text | ||||
| cargo test -p sal-os | ||||
|  | ||||
| # Run only library tests (faster) | ||||
| cargo test --workspace --lib | ||||
| ``` | ||||
|  | ||||
| ### **Rhai Integration Tests** | ||||
| Run comprehensive Rhai script tests that exercise `herodo` and SAL's scripted functionalities: | ||||
|  | ||||
| ```bash | ||||
| # Run all Rhai integration tests (16 modules) | ||||
| # Run all tests | ||||
| ./run_rhai_tests.sh | ||||
|  | ||||
| # Results: 16/16 modules pass with 100% success rate | ||||
| ``` | ||||
|  | ||||
| The Rhai tests validate real-world functionality across all SAL modules and provide comprehensive integration testing. | ||||
| ### Output | ||||
|  | ||||
| - Colored console output for readability. | ||||
| - Log file (`run_rhai_tests.log`) contains full output for later review. | ||||
| - Summary includes total modules, passed, and failed counts. | ||||
| - Exit code `0` if all tests pass, `1` otherwise. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## General Development Workflow | ||||
|  | ||||
| 1. **Build**: Use `build_herodo.sh` to compile the `herodo` binary. | ||||
| 2. **Test**: Run `run_rhai_tests.sh` to ensure all Rhai scripts pass. | ||||
| 3. **Publish**: When ready to release, use `scripts/publish-all.sh` (with `--dry-run` first to verify). | ||||
|  | ||||
| ## Prerequisites | ||||
|  | ||||
| - **Rust toolchain** (`cargo`, `rustc`) installed. | ||||
| - **Rhai** interpreter (`herodo`) built and available. | ||||
| - **Git** for version control. | ||||
| - **Cargo login** for publishing to crates.io. | ||||
|  | ||||
| ## License | ||||
|  | ||||
| SAL is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details. | ||||
| See `LICENSE` for details. | ||||
|  | ||||
| --- | ||||
|  | ||||
| **Happy coding!** | ||||
|   | ||||
							
								
								
									
										43
									
								
								_archive/service_manager/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								_archive/service_manager/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| [package] | ||||
| name = "sal-service-manager" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| authors = ["PlanetFirst <info@incubaid.com>"] | ||||
| description = "SAL Service Manager - Cross-platform service management for dynamic worker deployment" | ||||
| repository = "https://git.threefold.info/herocode/sal" | ||||
| license = "Apache-2.0" | ||||
|  | ||||
| [dependencies] | ||||
| # Use workspace dependencies for consistency | ||||
| thiserror = "1.0" | ||||
| tokio = { workspace = true } | ||||
| log = { workspace = true } | ||||
| serde = { workspace = true } | ||||
| serde_json = { workspace = true } | ||||
| futures = { workspace = true } | ||||
| once_cell = { workspace = true } | ||||
| # Use base zinit-client instead of SAL wrapper | ||||
| zinit-client = { version = "0.4.0" } | ||||
| # Optional Rhai integration | ||||
| rhai = { workspace = true, optional = true } | ||||
|  | ||||
|  | ||||
| [target.'cfg(target_os = "macos")'.dependencies] | ||||
| # macOS-specific dependencies for launchctl | ||||
| plist = "1.6" | ||||
|  | ||||
| [features] | ||||
| default = ["zinit"] | ||||
| zinit = [] | ||||
| rhai = ["dep:rhai"] | ||||
|  | ||||
| # Enable zinit feature for tests | ||||
| [dev-dependencies] | ||||
| tokio-test = "0.4" | ||||
| rhai = { workspace = true } | ||||
| tempfile = { workspace = true } | ||||
| env_logger = "0.10" | ||||
|  | ||||
| [[test]] | ||||
| name = "zinit_integration_tests" | ||||
| required-features = ["zinit"] | ||||
							
								
								
									
										198
									
								
								_archive/service_manager/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								_archive/service_manager/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,198 @@ | ||||
| # SAL Service Manager | ||||
|  | ||||
| [](https://crates.io/crates/sal-service-manager) | ||||
| [](https://docs.rs/sal-service-manager) | ||||
|  | ||||
| A cross-platform service management library for the System Abstraction Layer (SAL). This crate provides a unified interface for managing system services across different platforms, enabling dynamic deployment of workers and services. | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - **Cross-platform service management** - Unified API across macOS and Linux | ||||
| - **Dynamic worker deployment** - Perfect for circle workers and on-demand services | ||||
| - **Platform-specific implementations**: | ||||
|   - **macOS**: Uses `launchctl` with plist management | ||||
|   - **Linux**: Uses `zinit` for lightweight service management (systemd also available) | ||||
| - **Complete lifecycle management** - Start, stop, restart, status monitoring, and log retrieval | ||||
| - **Service configuration** - Environment variables, working directories, auto-restart | ||||
| - **Production-ready** - Comprehensive error handling and resource management | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| Add this to your `Cargo.toml`: | ||||
|  | ||||
| ```toml | ||||
| [dependencies] | ||||
| sal-service-manager = "0.1.0" | ||||
| ``` | ||||
|  | ||||
| Or use it as part of the SAL ecosystem: | ||||
|  | ||||
| ```toml | ||||
| [dependencies] | ||||
| sal = { version = "0.1.0", features = ["service_manager"] } | ||||
| ``` | ||||
|  | ||||
| ## Primary Use Case: Dynamic Circle Worker Management | ||||
|  | ||||
| This service manager was designed specifically for dynamic deployment of circle workers in freezone environments. When a new resident registers, you can instantly launch a dedicated circle worker: | ||||
|  | ||||
| ```rust,no_run | ||||
| use sal_service_manager::{create_service_manager, ServiceConfig}; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| // New resident registration triggers worker creation | ||||
| fn deploy_circle_worker(resident_id: &str) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let manager = create_service_manager(); | ||||
|  | ||||
|     let mut env = HashMap::new(); | ||||
|     env.insert("RESIDENT_ID".to_string(), resident_id.to_string()); | ||||
|     env.insert("WORKER_TYPE".to_string(), "circle".to_string()); | ||||
|  | ||||
|     let config = ServiceConfig { | ||||
|         name: format!("circle-worker-{}", resident_id), | ||||
|         binary_path: "/usr/bin/circle-worker".to_string(), | ||||
|         args: vec!["--resident".to_string(), resident_id.to_string()], | ||||
|         working_directory: Some("/var/lib/circle-workers".to_string()), | ||||
|         environment: env, | ||||
|         auto_restart: true, | ||||
|     }; | ||||
|  | ||||
|     // Deploy the worker | ||||
|     manager.start(&config)?; | ||||
|     println!("✅ Circle worker deployed for resident: {}", resident_id); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Basic Usage Example | ||||
|  | ||||
| Here is an example of the core service management API: | ||||
|  | ||||
| ```rust,no_run | ||||
| use sal_service_manager::{create_service_manager, ServiceConfig}; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let service_manager = create_service_manager(); | ||||
|  | ||||
|     let config = ServiceConfig { | ||||
|         name: "my-service".to_string(), | ||||
|         binary_path: "/usr/local/bin/my-service-executable".to_string(), | ||||
|         args: vec!["--config".to_string(), "/etc/my-service.conf".to_string()], | ||||
|         working_directory: Some("/var/tmp".to_string()), | ||||
|         environment: HashMap::new(), | ||||
|         auto_restart: true, | ||||
|     }; | ||||
|  | ||||
|     // Start a new service | ||||
|     service_manager.start(&config)?; | ||||
|  | ||||
|     // Get the status of the service | ||||
|     let status = service_manager.status("my-service")?; | ||||
|     println!("Service status: {:?}", status); | ||||
|  | ||||
|     // Stop the service | ||||
|     service_manager.stop("my-service")?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Examples | ||||
|  | ||||
| Comprehensive examples are available in the SAL examples directory: | ||||
|  | ||||
| ### Circle Worker Manager Example | ||||
|  | ||||
| The primary use case - dynamically launching circle workers for new freezone residents: | ||||
|  | ||||
| ```bash | ||||
| # Run the circle worker management example | ||||
| herodo examples/service_manager/circle_worker_manager.rhai | ||||
| ``` | ||||
|  | ||||
| This example demonstrates: | ||||
| - Creating service configurations for circle workers | ||||
| - Complete service lifecycle management | ||||
| - Error handling and status monitoring | ||||
| - Service cleanup and removal | ||||
|  | ||||
| ### Basic Usage Example | ||||
|  | ||||
| A simpler example showing the core API: | ||||
|  | ||||
| ```bash | ||||
| # Run the basic usage example | ||||
| herodo examples/service_manager/basic_usage.rhai | ||||
| ``` | ||||
|  | ||||
| See `examples/service_manager/README.md` for detailed documentation. | ||||
|  | ||||
| ## Testing | ||||
|  | ||||
| Run the test suite: | ||||
|  | ||||
| ```bash | ||||
| cargo test -p sal-service-manager | ||||
| ``` | ||||
|  | ||||
| For Rhai integration tests: | ||||
|  | ||||
| ```bash | ||||
| cargo test -p sal-service-manager --features rhai | ||||
| ``` | ||||
|  | ||||
| ### Testing with Herodo | ||||
|  | ||||
| To test the service manager with real Rhai scripts using herodo, first build herodo: | ||||
|  | ||||
| ```bash | ||||
| ./build_herodo.sh | ||||
| ``` | ||||
|  | ||||
| Then run Rhai scripts that use the service manager: | ||||
|  | ||||
| ```bash | ||||
| herodo your_service_script.rhai | ||||
| ``` | ||||
|  | ||||
| ## Prerequisites | ||||
|  | ||||
| ### Linux (zinit/systemd) | ||||
|  | ||||
| The service manager automatically discovers running zinit servers and falls back to systemd if none are found. | ||||
|  | ||||
| **For zinit (recommended):** | ||||
|  | ||||
| ```bash | ||||
| # Start zinit with default socket | ||||
| zinit -s /tmp/zinit.sock init | ||||
|  | ||||
| # Or with a custom socket path | ||||
| zinit -s /var/run/zinit.sock init | ||||
| ``` | ||||
|  | ||||
| **Socket Discovery:** | ||||
| The service manager will automatically find running zinit servers by checking: | ||||
| 1. `ZINIT_SOCKET_PATH` environment variable (if set) | ||||
| 2. Common socket locations: `/var/run/zinit.sock`, `/tmp/zinit.sock`, `/run/zinit.sock`, `./zinit.sock` | ||||
|  | ||||
| **Custom socket path:** | ||||
| ```bash | ||||
| # Set custom socket path | ||||
| export ZINIT_SOCKET_PATH=/your/custom/path/zinit.sock | ||||
| ``` | ||||
|  | ||||
| **Systemd fallback:** | ||||
| If no zinit server is detected, the service manager automatically falls back to systemd. | ||||
|  | ||||
| ### macOS (launchctl) | ||||
|  | ||||
| No additional setup required - uses the built-in launchctl system. | ||||
|  | ||||
| ## Platform Support | ||||
|  | ||||
| - **macOS**: Full support using `launchctl` for service management | ||||
| - **Linux**: Full support using `zinit` for service management (systemd also available as alternative) | ||||
| - **Windows**: Not currently supported | ||||
							
								
								
									
										47
									
								
								_archive/service_manager/examples/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								_archive/service_manager/examples/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| # Service Manager Examples | ||||
|  | ||||
| This directory contains examples demonstrating the usage of the `sal-service-manager` crate. | ||||
|  | ||||
| ## Running Examples | ||||
|  | ||||
| To run any example, use the following command structure from the `service_manager` crate's root directory: | ||||
|  | ||||
| ```sh | ||||
| cargo run --example <EXAMPLE_NAME> | ||||
| ``` | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### 1. `simple_service` | ||||
|  | ||||
| This example demonstrates the ideal, clean lifecycle of a service using the separated `create` and `start` steps. | ||||
|  | ||||
| **Behavior:** | ||||
| 1.  Creates a new service definition. | ||||
| 2.  Starts the newly created service. | ||||
| 3.  Checks its status to confirm it's running. | ||||
| 4.  Stops the service. | ||||
| 5.  Checks its status again to confirm it's stopped. | ||||
| 6.  Removes the service definition. | ||||
|  | ||||
| **Run it:** | ||||
| ```sh | ||||
| cargo run --example simple_service | ||||
| ``` | ||||
|  | ||||
| ### 2. `service_spaghetti` | ||||
|  | ||||
| This example demonstrates how the service manager handles "messy" or improper sequences of operations, showcasing its error handling and robustness. | ||||
|  | ||||
| **Behavior:** | ||||
| 1.  Creates a service. | ||||
| 2.  Starts the service. | ||||
| 3.  Tries to start the **same service again** (which should fail as it's already running). | ||||
| 4.  Removes the service **without stopping it first** (the manager should handle this gracefully). | ||||
| 5.  Tries to stop the **already removed** service (which should fail). | ||||
| 6.  Tries to remove the service **again** (which should also fail). | ||||
|  | ||||
| **Run it:** | ||||
| ```sh | ||||
| cargo run --example service_spaghetti | ||||
| ``` | ||||
							
								
								
									
										109
									
								
								_archive/service_manager/examples/service_spaghetti.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								_archive/service_manager/examples/service_spaghetti.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| //! service_spaghetti - An example of messy service management. | ||||
| //! | ||||
| //! This example demonstrates how the service manager behaves when commands | ||||
| //! are issued in a less-than-ideal order, such as starting a service that's | ||||
| //! already running or removing a service that hasn't been stopped. | ||||
|  | ||||
| use sal_service_manager::{create_service_manager, ServiceConfig}; | ||||
| use std::collections::HashMap; | ||||
| use std::thread; | ||||
| use std::time::Duration; | ||||
|  | ||||
| fn main() { | ||||
|     // Initialize logging to see socket discovery in action | ||||
|     env_logger::init(); | ||||
|  | ||||
|     let manager = match create_service_manager() { | ||||
|         Ok(manager) => manager, | ||||
|         Err(e) => { | ||||
|             eprintln!("Error: Failed to create service manager: {}", e); | ||||
|             return; | ||||
|         } | ||||
|     }; | ||||
|     let service_name = "com.herocode.examples.spaghetti"; | ||||
|  | ||||
|     let service_config = ServiceConfig { | ||||
|         name: service_name.to_string(), | ||||
|         binary_path: "/bin/sh".to_string(), | ||||
|         args: vec![ | ||||
|             "-c".to_string(), | ||||
|             "while true; do echo 'Spaghetti service is running...'; sleep 5; done".to_string(), | ||||
|         ], | ||||
|         working_directory: None, | ||||
|         environment: HashMap::new(), | ||||
|         auto_restart: false, | ||||
|     }; | ||||
|  | ||||
|     println!("--- Service Spaghetti Example ---"); | ||||
|     println!("This example demonstrates messy, error-prone service management."); | ||||
|  | ||||
|     // Cleanup from previous runs to ensure a clean slate | ||||
|     if let Ok(true) = manager.exists(service_name) { | ||||
|         println!( | ||||
|             "\nService '{}' found from a previous run. Cleaning up first.", | ||||
|             service_name | ||||
|         ); | ||||
|         let _ = manager.stop(service_name); | ||||
|         let _ = manager.remove(service_name); | ||||
|         println!("Cleanup complete."); | ||||
|     } | ||||
|  | ||||
|     // 1. Start the service (creates and starts in one step) | ||||
|     println!("\n1. Starting the service for the first time..."); | ||||
|     match manager.start(&service_config) { | ||||
|         Ok(()) => println!("   -> Success: Service '{}' started.", service_name), | ||||
|         Err(e) => { | ||||
|             eprintln!( | ||||
|                 "   -> Error: Failed to start service: {}. Halting example.", | ||||
|                 e | ||||
|             ); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     thread::sleep(Duration::from_secs(2)); | ||||
|  | ||||
|     // 2. Try to start the service again while it's already running | ||||
|     println!("\n2. Trying to start the *same service* again..."); | ||||
|     match manager.start(&service_config) { | ||||
|         Ok(()) => println!("   -> Unexpected Success: Service started again."), | ||||
|         Err(e) => eprintln!( | ||||
|             "   -> Expected Error: {}. The manager should detect it is already running.", | ||||
|             e | ||||
|         ), | ||||
|     } | ||||
|  | ||||
|     // 3. Let it run for a bit | ||||
|     println!("\n3. Letting the service run for 5 seconds..."); | ||||
|     thread::sleep(Duration::from_secs(5)); | ||||
|  | ||||
|     // 4. Remove the service without stopping it first | ||||
|     // The `remove` function is designed to stop the service if it's running. | ||||
|     println!("\n4. Removing the service without explicitly stopping it first..."); | ||||
|     match manager.remove(service_name) { | ||||
|         Ok(()) => println!("   -> Success: Service was stopped and removed."), | ||||
|         Err(e) => eprintln!("   -> Error: Failed to remove service: {}", e), | ||||
|     } | ||||
|  | ||||
|     // 5. Try to stop the service after it has been removed | ||||
|     println!("\n5. Trying to stop the service that was just removed..."); | ||||
|     match manager.stop(service_name) { | ||||
|         Ok(()) => println!("   -> Unexpected Success: Stopped a removed service."), | ||||
|         Err(e) => eprintln!( | ||||
|             "   -> Expected Error: {}. The manager knows the service is gone.", | ||||
|             e | ||||
|         ), | ||||
|     } | ||||
|  | ||||
|     // 6. Try to remove the service again | ||||
|     println!("\n6. Trying to remove the service again..."); | ||||
|     match manager.remove(service_name) { | ||||
|         Ok(()) => println!("   -> Unexpected Success: Removed a non-existent service."), | ||||
|         Err(e) => eprintln!( | ||||
|             "   -> Expected Error: {}. The manager correctly reports it's not found.", | ||||
|             e | ||||
|         ), | ||||
|     } | ||||
|  | ||||
|     println!("\n--- Spaghetti Example Finished ---"); | ||||
| } | ||||
							
								
								
									
										110
									
								
								_archive/service_manager/examples/simple_service.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								_archive/service_manager/examples/simple_service.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| use sal_service_manager::{create_service_manager, ServiceConfig}; | ||||
| use std::collections::HashMap; | ||||
| use std::thread; | ||||
| use std::time::Duration; | ||||
|  | ||||
| fn main() { | ||||
|     // Initialize logging to see socket discovery in action | ||||
|     env_logger::init(); | ||||
|  | ||||
|     // 1. Create a service manager for the current platform | ||||
|     let manager = match create_service_manager() { | ||||
|         Ok(manager) => manager, | ||||
|         Err(e) => { | ||||
|             eprintln!("Error: Failed to create service manager: {}", e); | ||||
|             return; | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // 2. Define the configuration for our new service | ||||
|     let service_name = "com.herocode.examples.simpleservice"; | ||||
|     let service_config = ServiceConfig { | ||||
|         name: service_name.to_string(), | ||||
|         // A simple command that runs in a loop | ||||
|         binary_path: "/bin/sh".to_string(), | ||||
|         args: vec![ | ||||
|             "-c".to_string(), | ||||
|             "while true; do echo 'Simple service is running...'; date; sleep 5; done".to_string(), | ||||
|         ], | ||||
|         working_directory: None, | ||||
|         environment: HashMap::new(), | ||||
|         auto_restart: false, | ||||
|     }; | ||||
|  | ||||
|     println!("--- Service Manager Example ---"); | ||||
|  | ||||
|     // Cleanup from previous runs, if necessary | ||||
|     if let Ok(true) = manager.exists(service_name) { | ||||
|         println!( | ||||
|             "Service '{}' already exists. Cleaning up before starting.", | ||||
|             service_name | ||||
|         ); | ||||
|         if let Err(e) = manager.stop(service_name) { | ||||
|             println!( | ||||
|                 "Note: could not stop existing service (it might not be running): {}", | ||||
|                 e | ||||
|             ); | ||||
|         } | ||||
|         if let Err(e) = manager.remove(service_name) { | ||||
|             eprintln!("Error: failed to remove existing service: {}", e); | ||||
|             return; | ||||
|         } | ||||
|         println!("Cleanup complete."); | ||||
|     } | ||||
|  | ||||
|     // 3. Start the service (creates and starts in one step) | ||||
|     println!("\n1. Starting service: '{}'", service_name); | ||||
|     match manager.start(&service_config) { | ||||
|         Ok(()) => println!("Service '{}' started successfully.", service_name), | ||||
|         Err(e) => { | ||||
|             eprintln!("Error: Failed to start service '{}': {}", service_name, e); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Give it a moment to run | ||||
|     println!("\nWaiting for 2 seconds for the service to initialize..."); | ||||
|     thread::sleep(Duration::from_secs(2)); | ||||
|  | ||||
|     // 4. Check the status of the service | ||||
|     println!("\n2. Checking service status..."); | ||||
|     match manager.status(service_name) { | ||||
|         Ok(status) => println!("Service status: {:?}", status), | ||||
|         Err(e) => eprintln!( | ||||
|             "Error: Failed to get status for service '{}': {}", | ||||
|             service_name, e | ||||
|         ), | ||||
|     } | ||||
|  | ||||
|     println!("\nLetting the service run for 10 seconds. Check logs if you can."); | ||||
|     thread::sleep(Duration::from_secs(10)); | ||||
|  | ||||
|     // 5. Stop the service | ||||
|     println!("\n3. Stopping service: '{}'", service_name); | ||||
|     match manager.stop(service_name) { | ||||
|         Ok(()) => println!("Service '{}' stopped successfully.", service_name), | ||||
|         Err(e) => eprintln!("Error: Failed to stop service '{}': {}", service_name, e), | ||||
|     } | ||||
|  | ||||
|     println!("\nWaiting for 2 seconds for the service to stop..."); | ||||
|     thread::sleep(Duration::from_secs(2)); | ||||
|  | ||||
|     // Check status again | ||||
|     println!("\n4. Checking status after stopping..."); | ||||
|     match manager.status(service_name) { | ||||
|         Ok(status) => println!("Service status: {:?}", status), | ||||
|         Err(e) => eprintln!( | ||||
|             "Error: Failed to get status for service '{}': {}", | ||||
|             service_name, e | ||||
|         ), | ||||
|     } | ||||
|  | ||||
|     // 6. Remove the service | ||||
|     println!("\n5. Removing service: '{}'", service_name); | ||||
|     match manager.remove(service_name) { | ||||
|         Ok(()) => println!("Service '{}' removed successfully.", service_name), | ||||
|         Err(e) => eprintln!("Error: Failed to remove service '{}': {}", service_name, e), | ||||
|     } | ||||
|  | ||||
|     println!("\n--- Example Finished ---"); | ||||
| } | ||||
							
								
								
									
										47
									
								
								_archive/service_manager/examples/socket_discovery_test.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								_archive/service_manager/examples/socket_discovery_test.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| //! Socket Discovery Test | ||||
| //! | ||||
| //! This example demonstrates the zinit socket discovery functionality. | ||||
| //! It shows how the service manager finds available zinit sockets. | ||||
|  | ||||
| use sal_service_manager::create_service_manager; | ||||
|  | ||||
| fn main() { | ||||
|     // Initialize logging to see socket discovery in action | ||||
|     env_logger::init(); | ||||
|      | ||||
|     println!("=== Zinit Socket Discovery Test ==="); | ||||
|     println!("This test demonstrates how the service manager discovers zinit sockets."); | ||||
|     println!(); | ||||
|      | ||||
|     // Test environment variable | ||||
|     if let Ok(socket_path) = std::env::var("ZINIT_SOCKET_PATH") { | ||||
|         println!("🔍 ZINIT_SOCKET_PATH environment variable set to: {}", socket_path); | ||||
|     } else { | ||||
|         println!("🔍 ZINIT_SOCKET_PATH environment variable not set"); | ||||
|     } | ||||
|     println!(); | ||||
|      | ||||
|     println!("🚀 Creating service manager..."); | ||||
|     match create_service_manager() { | ||||
|         Ok(_manager) => { | ||||
|             println!("✅ Service manager created successfully!"); | ||||
|              | ||||
|             #[cfg(target_os = "macos")] | ||||
|             println!("📱 Platform: macOS - Using launchctl"); | ||||
|              | ||||
|             #[cfg(target_os = "linux")] | ||||
|             println!("🐧 Platform: Linux - Check logs above for socket discovery details"); | ||||
|         } | ||||
|         Err(e) => { | ||||
|             println!("❌ Failed to create service manager: {}", e); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     println!(); | ||||
|     println!("=== Test Complete ==="); | ||||
|     println!(); | ||||
|     println!("To test zinit socket discovery on Linux:"); | ||||
|     println!("1. Start zinit: zinit -s /tmp/zinit.sock init"); | ||||
|     println!("2. Run with logging: RUST_LOG=debug cargo run --example socket_discovery_test -p sal-service-manager"); | ||||
|     println!("3. Or set custom path: ZINIT_SOCKET_PATH=/custom/path.sock RUST_LOG=debug cargo run --example socket_discovery_test -p sal-service-manager"); | ||||
| } | ||||
							
								
								
									
										492
									
								
								_archive/service_manager/src/launchctl.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										492
									
								
								_archive/service_manager/src/launchctl.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,492 @@ | ||||
| use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus}; | ||||
| use once_cell::sync::Lazy; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::collections::HashMap; | ||||
| use std::path::PathBuf; | ||||
| use tokio::process::Command; | ||||
| use tokio::runtime::Runtime; | ||||
|  | ||||
| // Shared runtime for async operations - production-safe initialization | ||||
| static ASYNC_RUNTIME: Lazy<Option<Runtime>> = Lazy::new(|| Runtime::new().ok()); | ||||
|  | ||||
| /// Get the async runtime, creating a temporary one if the static runtime failed | ||||
| fn get_runtime() -> Result<Runtime, ServiceManagerError> { | ||||
|     // Try to use the static runtime first | ||||
|     if let Some(_runtime) = ASYNC_RUNTIME.as_ref() { | ||||
|         // We can't return a reference to the static runtime because we need ownership | ||||
|         // for block_on, so we create a new one. This is a reasonable trade-off for safety. | ||||
|         Runtime::new().map_err(|e| { | ||||
|             ServiceManagerError::Other(format!("Failed to create async runtime: {}", e)) | ||||
|         }) | ||||
|     } else { | ||||
|         // Static runtime failed, try to create a new one | ||||
|         Runtime::new().map_err(|e| { | ||||
|             ServiceManagerError::Other(format!("Failed to create async runtime: {}", e)) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct LaunchctlServiceManager { | ||||
|     service_prefix: String, | ||||
| } | ||||
|  | ||||
| #[derive(Serialize, Deserialize)] | ||||
| struct LaunchDaemon { | ||||
|     #[serde(rename = "Label")] | ||||
|     label: String, | ||||
|     #[serde(rename = "ProgramArguments")] | ||||
|     program_arguments: Vec<String>, | ||||
|     #[serde(rename = "WorkingDirectory", skip_serializing_if = "Option::is_none")] | ||||
|     working_directory: Option<String>, | ||||
|     #[serde( | ||||
|         rename = "EnvironmentVariables", | ||||
|         skip_serializing_if = "Option::is_none" | ||||
|     )] | ||||
|     environment_variables: Option<HashMap<String, String>>, | ||||
|     #[serde(rename = "KeepAlive", skip_serializing_if = "Option::is_none")] | ||||
|     keep_alive: Option<bool>, | ||||
|     #[serde(rename = "RunAtLoad")] | ||||
|     run_at_load: bool, | ||||
|     #[serde(rename = "StandardOutPath", skip_serializing_if = "Option::is_none")] | ||||
|     standard_out_path: Option<String>, | ||||
|     #[serde(rename = "StandardErrorPath", skip_serializing_if = "Option::is_none")] | ||||
|     standard_error_path: Option<String>, | ||||
| } | ||||
|  | ||||
| impl LaunchctlServiceManager { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             service_prefix: "tf.ourworld.circles".to_string(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn get_service_label(&self, service_name: &str) -> String { | ||||
|         format!("{}.{}", self.service_prefix, service_name) | ||||
|     } | ||||
|  | ||||
|     fn get_plist_path(&self, service_name: &str) -> PathBuf { | ||||
|         let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); | ||||
|         PathBuf::from(home) | ||||
|             .join("Library") | ||||
|             .join("LaunchAgents") | ||||
|             .join(format!("{}.plist", self.get_service_label(service_name))) | ||||
|     } | ||||
|  | ||||
|     fn get_log_path(&self, service_name: &str) -> PathBuf { | ||||
|         let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); | ||||
|         PathBuf::from(home) | ||||
|             .join("Library") | ||||
|             .join("Logs") | ||||
|             .join("circles") | ||||
|             .join(format!("{}.log", service_name)) | ||||
|     } | ||||
|  | ||||
|     async fn create_plist(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> { | ||||
|         let label = self.get_service_label(&config.name); | ||||
|         let plist_path = self.get_plist_path(&config.name); | ||||
|         let log_path = self.get_log_path(&config.name); | ||||
|  | ||||
|         // Ensure the LaunchAgents directory exists | ||||
|         if let Some(parent) = plist_path.parent() { | ||||
|             tokio::fs::create_dir_all(parent).await?; | ||||
|         } | ||||
|  | ||||
|         // Ensure the logs directory exists | ||||
|         if let Some(parent) = log_path.parent() { | ||||
|             tokio::fs::create_dir_all(parent).await?; | ||||
|         } | ||||
|  | ||||
|         let mut program_arguments = vec![config.binary_path.clone()]; | ||||
|         program_arguments.extend(config.args.clone()); | ||||
|  | ||||
|         let launch_daemon = LaunchDaemon { | ||||
|             label: label.clone(), | ||||
|             program_arguments, | ||||
|             working_directory: config.working_directory.clone(), | ||||
|             environment_variables: if config.environment.is_empty() { | ||||
|                 None | ||||
|             } else { | ||||
|                 Some(config.environment.clone()) | ||||
|             }, | ||||
|             keep_alive: if config.auto_restart { | ||||
|                 Some(true) | ||||
|             } else { | ||||
|                 None | ||||
|             }, | ||||
|             run_at_load: true, | ||||
|             standard_out_path: Some(log_path.to_string_lossy().to_string()), | ||||
|             standard_error_path: Some(log_path.to_string_lossy().to_string()), | ||||
|         }; | ||||
|  | ||||
|         let mut plist_content = Vec::new(); | ||||
|         plist::to_writer_xml(&mut plist_content, &launch_daemon) | ||||
|             .map_err(|e| ServiceManagerError::Other(format!("Failed to serialize plist: {}", e)))?; | ||||
|         let plist_content = String::from_utf8(plist_content).map_err(|e| { | ||||
|             ServiceManagerError::Other(format!("Failed to convert plist to string: {}", e)) | ||||
|         })?; | ||||
|  | ||||
|         tokio::fs::write(&plist_path, plist_content).await?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     async fn run_launchctl(&self, args: &[&str]) -> Result<String, ServiceManagerError> { | ||||
|         let output = Command::new("launchctl").args(args).output().await?; | ||||
|  | ||||
|         if !output.status.success() { | ||||
|             let stderr = String::from_utf8_lossy(&output.stderr); | ||||
|             return Err(ServiceManagerError::Other(format!( | ||||
|                 "launchctl command failed: {}", | ||||
|                 stderr | ||||
|             ))); | ||||
|         } | ||||
|  | ||||
|         Ok(String::from_utf8_lossy(&output.stdout).to_string()) | ||||
|     } | ||||
|  | ||||
|     async fn wait_for_service_status( | ||||
|         &self, | ||||
|         service_name: &str, | ||||
|         timeout_secs: u64, | ||||
|     ) -> Result<(), ServiceManagerError> { | ||||
|         use tokio::time::{sleep, timeout, Duration}; | ||||
|  | ||||
|         let timeout_duration = Duration::from_secs(timeout_secs); | ||||
|         let poll_interval = Duration::from_millis(500); | ||||
|  | ||||
|         let result = timeout(timeout_duration, async { | ||||
|             loop { | ||||
|                 match self.status(service_name) { | ||||
|                     Ok(ServiceStatus::Running) => { | ||||
|                         return Ok(()); | ||||
|                     } | ||||
|                     Ok(ServiceStatus::Failed) => { | ||||
|                         // Service failed, get error details from logs | ||||
|                         let logs = self.logs(service_name, Some(20)).unwrap_or_default(); | ||||
|                         let error_msg = if logs.is_empty() { | ||||
|                             "Service failed to start (no logs available)".to_string() | ||||
|                         } else { | ||||
|                             // Extract error lines from logs | ||||
|                             let error_lines: Vec<&str> = logs | ||||
|                                 .lines() | ||||
|                                 .filter(|line| { | ||||
|                                     line.to_lowercase().contains("error") | ||||
|                                         || line.to_lowercase().contains("failed") | ||||
|                                 }) | ||||
|                                 .take(3) | ||||
|                                 .collect(); | ||||
|  | ||||
|                             if error_lines.is_empty() { | ||||
|                                 format!( | ||||
|                                     "Service failed to start. Recent logs:\n{}", | ||||
|                                     logs.lines() | ||||
|                                         .rev() | ||||
|                                         .take(5) | ||||
|                                         .collect::<Vec<_>>() | ||||
|                                         .into_iter() | ||||
|                                         .rev() | ||||
|                                         .collect::<Vec<_>>() | ||||
|                                         .join("\n") | ||||
|                                 ) | ||||
|                             } else { | ||||
|                                 format!( | ||||
|                                     "Service failed to start. Errors:\n{}", | ||||
|                                     error_lines.join("\n") | ||||
|                                 ) | ||||
|                             } | ||||
|                         }; | ||||
|                         return Err(ServiceManagerError::StartFailed( | ||||
|                             service_name.to_string(), | ||||
|                             error_msg, | ||||
|                         )); | ||||
|                     } | ||||
|                     Ok(ServiceStatus::Stopped) | Ok(ServiceStatus::Unknown) => { | ||||
|                         // Still starting, continue polling | ||||
|                         sleep(poll_interval).await; | ||||
|                     } | ||||
|                     Err(ServiceManagerError::ServiceNotFound(_)) => { | ||||
|                         return Err(ServiceManagerError::ServiceNotFound( | ||||
|                             service_name.to_string(), | ||||
|                         )); | ||||
|                     } | ||||
|                     Err(e) => { | ||||
|                         return Err(e); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|         .await; | ||||
|  | ||||
|         match result { | ||||
|             Ok(Ok(())) => Ok(()), | ||||
|             Ok(Err(e)) => Err(e), | ||||
|             Err(_) => Err(ServiceManagerError::StartFailed( | ||||
|                 service_name.to_string(), | ||||
|                 format!("Service did not start within {} seconds", timeout_secs), | ||||
|             )), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ServiceManager for LaunchctlServiceManager { | ||||
|     fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> { | ||||
|         let plist_path = self.get_plist_path(service_name); | ||||
|         Ok(plist_path.exists()) | ||||
|     } | ||||
|  | ||||
|     fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> { | ||||
|         // Use production-safe runtime for async operations | ||||
|         let runtime = get_runtime()?; | ||||
|         runtime.block_on(async { | ||||
|             let label = self.get_service_label(&config.name); | ||||
|  | ||||
|             // Check if service is already loaded | ||||
|             let list_output = self.run_launchctl(&["list"]).await?; | ||||
|             if list_output.contains(&label) { | ||||
|                 return Err(ServiceManagerError::ServiceAlreadyExists( | ||||
|                     config.name.clone(), | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             // Create the plist file | ||||
|             self.create_plist(config).await?; | ||||
|  | ||||
|             // Load the service | ||||
|             let plist_path = self.get_plist_path(&config.name); | ||||
|             self.run_launchctl(&["load", &plist_path.to_string_lossy()]) | ||||
|                 .await | ||||
|                 .map_err(|e| { | ||||
|                     ServiceManagerError::StartFailed(config.name.clone(), e.to_string()) | ||||
|                 })?; | ||||
|  | ||||
|             Ok(()) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         let runtime = get_runtime()?; | ||||
|         runtime.block_on(async { | ||||
|             let label = self.get_service_label(service_name); | ||||
|             let plist_path = self.get_plist_path(service_name); | ||||
|  | ||||
|             // Check if plist file exists | ||||
|             if !plist_path.exists() { | ||||
|                 return Err(ServiceManagerError::ServiceNotFound( | ||||
|                     service_name.to_string(), | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             // Check if service is already loaded and running | ||||
|             let list_output = self.run_launchctl(&["list"]).await?; | ||||
|             if list_output.contains(&label) { | ||||
|                 // Service is loaded, check if it's running | ||||
|                 match self.status(service_name)? { | ||||
|                     ServiceStatus::Running => { | ||||
|                         return Ok(()); // Already running, nothing to do | ||||
|                     } | ||||
|                     _ => { | ||||
|                         // Service is loaded but not running, try to start it | ||||
|                         self.run_launchctl(&["start", &label]).await.map_err(|e| { | ||||
|                             ServiceManagerError::StartFailed( | ||||
|                                 service_name.to_string(), | ||||
|                                 e.to_string(), | ||||
|                             ) | ||||
|                         })?; | ||||
|                         return Ok(()); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Service is not loaded, load it | ||||
|             self.run_launchctl(&["load", &plist_path.to_string_lossy()]) | ||||
|                 .await | ||||
|                 .map_err(|e| { | ||||
|                     ServiceManagerError::StartFailed(service_name.to_string(), e.to_string()) | ||||
|                 })?; | ||||
|  | ||||
|             Ok(()) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn start_and_confirm( | ||||
|         &self, | ||||
|         config: &ServiceConfig, | ||||
|         timeout_secs: u64, | ||||
|     ) -> Result<(), ServiceManagerError> { | ||||
|         // First start the service | ||||
|         self.start(config)?; | ||||
|  | ||||
|         // Then wait for confirmation using production-safe runtime | ||||
|         let runtime = get_runtime()?; | ||||
|         runtime.block_on(async { | ||||
|             self.wait_for_service_status(&config.name, timeout_secs) | ||||
|                 .await | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn start_existing_and_confirm( | ||||
|         &self, | ||||
|         service_name: &str, | ||||
|         timeout_secs: u64, | ||||
|     ) -> Result<(), ServiceManagerError> { | ||||
|         // First start the existing service | ||||
|         self.start_existing(service_name)?; | ||||
|  | ||||
|         // Then wait for confirmation using production-safe runtime | ||||
|         let runtime = get_runtime()?; | ||||
|         runtime.block_on(async { | ||||
|             self.wait_for_service_status(service_name, timeout_secs) | ||||
|                 .await | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         let runtime = get_runtime()?; | ||||
|         runtime.block_on(async { | ||||
|             let _label = self.get_service_label(service_name); | ||||
|             let plist_path = self.get_plist_path(service_name); | ||||
|  | ||||
|             // Unload the service | ||||
|             self.run_launchctl(&["unload", &plist_path.to_string_lossy()]) | ||||
|                 .await | ||||
|                 .map_err(|e| { | ||||
|                     ServiceManagerError::StopFailed(service_name.to_string(), e.to_string()) | ||||
|                 })?; | ||||
|  | ||||
|             Ok(()) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         // For launchctl, we stop and start | ||||
|         if let Err(e) = self.stop(service_name) { | ||||
|             // If stop fails because service doesn't exist, that's ok for restart | ||||
|             if !matches!(e, ServiceManagerError::ServiceNotFound(_)) { | ||||
|                 return Err(ServiceManagerError::RestartFailed( | ||||
|                     service_name.to_string(), | ||||
|                     e.to_string(), | ||||
|                 )); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // We need the config to restart, but we don't have it stored | ||||
|         // For now, return an error - in a real implementation we might store configs | ||||
|         Err(ServiceManagerError::RestartFailed( | ||||
|             service_name.to_string(), | ||||
|             "Restart requires re-providing service configuration".to_string(), | ||||
|         )) | ||||
|     } | ||||
|  | ||||
|     fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> { | ||||
|         let runtime = get_runtime()?; | ||||
|         runtime.block_on(async { | ||||
|             let label = self.get_service_label(service_name); | ||||
|             let plist_path = self.get_plist_path(service_name); | ||||
|  | ||||
|             // First check if the plist file exists | ||||
|             if !plist_path.exists() { | ||||
|                 return Err(ServiceManagerError::ServiceNotFound( | ||||
|                     service_name.to_string(), | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             let list_output = self.run_launchctl(&["list"]).await?; | ||||
|  | ||||
|             if !list_output.contains(&label) { | ||||
|                 return Ok(ServiceStatus::Stopped); | ||||
|             } | ||||
|  | ||||
|             // Get detailed status | ||||
|             match self.run_launchctl(&["list", &label]).await { | ||||
|                 Ok(output) => { | ||||
|                     if output.contains("\"PID\" = ") { | ||||
|                         Ok(ServiceStatus::Running) | ||||
|                     } else if output.contains("\"LastExitStatus\" = ") { | ||||
|                         Ok(ServiceStatus::Failed) | ||||
|                     } else { | ||||
|                         Ok(ServiceStatus::Unknown) | ||||
|                     } | ||||
|                 } | ||||
|                 Err(_) => Ok(ServiceStatus::Stopped), | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn logs( | ||||
|         &self, | ||||
|         service_name: &str, | ||||
|         lines: Option<usize>, | ||||
|     ) -> Result<String, ServiceManagerError> { | ||||
|         let runtime = get_runtime()?; | ||||
|         runtime.block_on(async { | ||||
|             let log_path = self.get_log_path(service_name); | ||||
|  | ||||
|             if !log_path.exists() { | ||||
|                 return Ok(String::new()); | ||||
|             } | ||||
|  | ||||
|             match lines { | ||||
|                 Some(n) => { | ||||
|                     let output = Command::new("tail") | ||||
|                         .args(&["-n", &n.to_string(), &log_path.to_string_lossy()]) | ||||
|                         .output() | ||||
|                         .await?; | ||||
|                     Ok(String::from_utf8_lossy(&output.stdout).to_string()) | ||||
|                 } | ||||
|                 None => { | ||||
|                     let content = tokio::fs::read_to_string(&log_path).await?; | ||||
|                     Ok(content) | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn list(&self) -> Result<Vec<String>, ServiceManagerError> { | ||||
|         let runtime = get_runtime()?; | ||||
|         runtime.block_on(async { | ||||
|             let list_output = self.run_launchctl(&["list"]).await?; | ||||
|  | ||||
|             let services: Vec<String> = list_output | ||||
|                 .lines() | ||||
|                 .filter_map(|line| { | ||||
|                     if line.contains(&self.service_prefix) { | ||||
|                         // Extract service name from label | ||||
|                         line.split_whitespace() | ||||
|                             .last() | ||||
|                             .and_then(|label| { | ||||
|                                 label.strip_prefix(&format!("{}.", self.service_prefix)) | ||||
|                             }) | ||||
|                             .map(|s| s.to_string()) | ||||
|                     } else { | ||||
|                         None | ||||
|                     } | ||||
|                 }) | ||||
|                 .collect(); | ||||
|  | ||||
|             Ok(services) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         // Try to stop the service first, but don't fail if it's already stopped or doesn't exist | ||||
|         if let Err(e) = self.stop(service_name) { | ||||
|             // Log the error but continue with removal | ||||
|             log::warn!( | ||||
|                 "Failed to stop service '{}' before removal: {}", | ||||
|                 service_name, | ||||
|                 e | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         // Remove the plist file using production-safe runtime | ||||
|         let runtime = get_runtime()?; | ||||
|         runtime.block_on(async { | ||||
|             let plist_path = self.get_plist_path(service_name); | ||||
|             if plist_path.exists() { | ||||
|                 tokio::fs::remove_file(&plist_path).await?; | ||||
|             } | ||||
|             Ok(()) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										301
									
								
								_archive/service_manager/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										301
									
								
								_archive/service_manager/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,301 @@ | ||||
| use std::collections::HashMap; | ||||
| use thiserror::Error; | ||||
|  | ||||
| #[derive(Error, Debug)] | ||||
| pub enum ServiceManagerError { | ||||
|     #[error("Service '{0}' not found")] | ||||
|     ServiceNotFound(String), | ||||
|     #[error("Service '{0}' already exists")] | ||||
|     ServiceAlreadyExists(String), | ||||
|     #[error("Failed to start service '{0}': {1}")] | ||||
|     StartFailed(String, String), | ||||
|     #[error("Failed to stop service '{0}': {1}")] | ||||
|     StopFailed(String, String), | ||||
|     #[error("Failed to restart service '{0}': {1}")] | ||||
|     RestartFailed(String, String), | ||||
|     #[error("Failed to get logs for service '{0}': {1}")] | ||||
|     LogsFailed(String, String), | ||||
|     #[error("IO error: {0}")] | ||||
|     IoError(#[from] std::io::Error), | ||||
|     #[error("Service manager error: {0}")] | ||||
|     Other(String), | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct ServiceConfig { | ||||
|     pub name: String, | ||||
|     pub binary_path: String, | ||||
|     pub args: Vec<String>, | ||||
|     pub working_directory: Option<String>, | ||||
|     pub environment: HashMap<String, String>, | ||||
|     pub auto_restart: bool, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, PartialEq)] | ||||
| pub enum ServiceStatus { | ||||
|     Running, | ||||
|     Stopped, | ||||
|     Failed, | ||||
|     Unknown, | ||||
| } | ||||
|  | ||||
| pub trait ServiceManager: Send + Sync { | ||||
|     /// Check if a service exists | ||||
|     fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError>; | ||||
|  | ||||
|     /// Start a service with the given configuration | ||||
|     fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError>; | ||||
|  | ||||
|     /// Start an existing service by name (load existing plist/config) | ||||
|     fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError>; | ||||
|  | ||||
|     /// Start a service and wait for confirmation that it's running or failed | ||||
|     fn start_and_confirm( | ||||
|         &self, | ||||
|         config: &ServiceConfig, | ||||
|         timeout_secs: u64, | ||||
|     ) -> Result<(), ServiceManagerError>; | ||||
|  | ||||
|     /// Start an existing service and wait for confirmation that it's running or failed | ||||
|     fn start_existing_and_confirm( | ||||
|         &self, | ||||
|         service_name: &str, | ||||
|         timeout_secs: u64, | ||||
|     ) -> Result<(), ServiceManagerError>; | ||||
|  | ||||
|     /// Stop a service by name | ||||
|     fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError>; | ||||
|  | ||||
|     /// Restart a service by name | ||||
|     fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError>; | ||||
|  | ||||
|     /// Get the status of a service | ||||
|     fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError>; | ||||
|  | ||||
|     /// Get logs for a service | ||||
|     fn logs(&self, service_name: &str, lines: Option<usize>) | ||||
|         -> Result<String, ServiceManagerError>; | ||||
|  | ||||
|     /// List all managed services | ||||
|     fn list(&self) -> Result<Vec<String>, ServiceManagerError>; | ||||
|  | ||||
|     /// Remove a service configuration (stop if running) | ||||
|     fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError>; | ||||
| } | ||||
|  | ||||
| // Platform-specific implementations | ||||
| #[cfg(target_os = "macos")] | ||||
| mod launchctl; | ||||
| #[cfg(target_os = "macos")] | ||||
| pub use launchctl::LaunchctlServiceManager; | ||||
|  | ||||
| #[cfg(target_os = "linux")] | ||||
| mod systemd; | ||||
| #[cfg(target_os = "linux")] | ||||
| pub use systemd::SystemdServiceManager; | ||||
|  | ||||
| mod zinit; | ||||
| pub use zinit::ZinitServiceManager; | ||||
|  | ||||
| #[cfg(feature = "rhai")] | ||||
| pub mod rhai; | ||||
|  | ||||
| /// Discover available zinit socket paths | ||||
| /// | ||||
| /// This function checks for zinit sockets in the following order: | ||||
| /// 1. Environment variable ZINIT_SOCKET_PATH (if set) | ||||
| /// 2. Common socket locations with connectivity testing | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// Returns the first working socket path found, or None if no working zinit server is detected. | ||||
| #[cfg(target_os = "linux")] | ||||
| fn discover_zinit_socket() -> Option<String> { | ||||
|     // First check environment variable | ||||
|     if let Ok(env_socket_path) = std::env::var("ZINIT_SOCKET_PATH") { | ||||
|         log::debug!("Checking ZINIT_SOCKET_PATH: {}", env_socket_path); | ||||
|         if test_zinit_socket(&env_socket_path) { | ||||
|             log::info!( | ||||
|                 "Using zinit socket from ZINIT_SOCKET_PATH: {}", | ||||
|                 env_socket_path | ||||
|             ); | ||||
|             return Some(env_socket_path); | ||||
|         } else { | ||||
|             log::warn!( | ||||
|                 "ZINIT_SOCKET_PATH specified but socket is not accessible: {}", | ||||
|                 env_socket_path | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Try common socket locations | ||||
|     let common_paths = [ | ||||
|         "/var/run/zinit.sock", | ||||
|         "/tmp/zinit.sock", | ||||
|         "/run/zinit.sock", | ||||
|         "./zinit.sock", | ||||
|     ]; | ||||
|  | ||||
|     log::debug!("Discovering zinit socket from common locations..."); | ||||
|     for path in &common_paths { | ||||
|         log::debug!("Testing socket path: {}", path); | ||||
|         if test_zinit_socket(path) { | ||||
|             log::info!("Found working zinit socket at: {}", path); | ||||
|             return Some(path.to_string()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     log::debug!("No working zinit socket found"); | ||||
|     None | ||||
| } | ||||
|  | ||||
| /// Test if a zinit socket is accessible and responsive | ||||
| /// | ||||
| /// This function attempts to create a ZinitServiceManager and perform a basic | ||||
| /// connectivity test by listing services. | ||||
| #[cfg(target_os = "linux")] | ||||
| fn test_zinit_socket(socket_path: &str) -> bool { | ||||
|     // Check if socket file exists first | ||||
|     if !std::path::Path::new(socket_path).exists() { | ||||
|         log::debug!("Socket file does not exist: {}", socket_path); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     // Try to create a manager and test basic connectivity | ||||
|     match ZinitServiceManager::new(socket_path) { | ||||
|         Ok(manager) => { | ||||
|             // Test basic connectivity by trying to list services | ||||
|             match manager.list() { | ||||
|                 Ok(_) => { | ||||
|                     log::debug!("Socket {} is responsive", socket_path); | ||||
|                     true | ||||
|                 } | ||||
|                 Err(e) => { | ||||
|                     log::debug!("Socket {} exists but not responsive: {}", socket_path, e); | ||||
|                     false | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Err(e) => { | ||||
|             log::debug!("Failed to create manager for socket {}: {}", socket_path, e); | ||||
|             false | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Create a service manager appropriate for the current platform | ||||
| /// | ||||
| /// - On macOS: Uses launchctl for service management | ||||
| /// - On Linux: Uses zinit for service management with systemd fallback | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// Returns a Result containing the service manager or an error if initialization fails. | ||||
| /// On Linux, it first tries to discover a working zinit socket. If no zinit server is found, | ||||
| /// it will fall back to systemd. | ||||
| /// | ||||
| /// # Environment Variables | ||||
| /// | ||||
| /// - `ZINIT_SOCKET_PATH`: Specifies the zinit socket path (Linux only) | ||||
| /// | ||||
| /// # Errors | ||||
| /// | ||||
| /// Returns `ServiceManagerError` if: | ||||
| /// - The platform is not supported (Windows, etc.) | ||||
| /// - Service manager initialization fails on all available backends | ||||
| pub fn create_service_manager() -> Result<Box<dyn ServiceManager>, ServiceManagerError> { | ||||
|     #[cfg(target_os = "macos")] | ||||
|     { | ||||
|         Ok(Box::new(LaunchctlServiceManager::new())) | ||||
|     } | ||||
|     #[cfg(target_os = "linux")] | ||||
|     { | ||||
|         // Try to discover a working zinit socket | ||||
|         if let Some(socket_path) = discover_zinit_socket() { | ||||
|             match ZinitServiceManager::new(&socket_path) { | ||||
|                 Ok(zinit_manager) => { | ||||
|                     log::info!("Using zinit service manager with socket: {}", socket_path); | ||||
|                     return Ok(Box::new(zinit_manager)); | ||||
|                 } | ||||
|                 Err(zinit_error) => { | ||||
|                     log::warn!( | ||||
|                         "Failed to create zinit manager for discovered socket {}: {}", | ||||
|                         socket_path, | ||||
|                         zinit_error | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             log::info!("No running zinit server detected. To use zinit, start it with: zinit -s /tmp/zinit.sock init"); | ||||
|         } | ||||
|  | ||||
|         // Fallback to systemd | ||||
|         log::info!("Falling back to systemd service manager"); | ||||
|         Ok(Box::new(SystemdServiceManager::new())) | ||||
|     } | ||||
|     #[cfg(not(any(target_os = "macos", target_os = "linux")))] | ||||
|     { | ||||
|         Err(ServiceManagerError::Other( | ||||
|             "Service manager not implemented for this platform".to_string(), | ||||
|         )) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Create a service manager for zinit with a custom socket path | ||||
| /// | ||||
| /// This is useful when zinit is running with a non-default socket path | ||||
| pub fn create_zinit_service_manager( | ||||
|     socket_path: &str, | ||||
| ) -> Result<Box<dyn ServiceManager>, ServiceManagerError> { | ||||
|     Ok(Box::new(ZinitServiceManager::new(socket_path)?)) | ||||
| } | ||||
|  | ||||
| /// Create a service manager for systemd (Linux alternative) | ||||
| /// | ||||
| /// This creates a systemd-based service manager as an alternative to zinit on Linux | ||||
| #[cfg(target_os = "linux")] | ||||
| pub fn create_systemd_service_manager() -> Box<dyn ServiceManager> { | ||||
|     Box::new(SystemdServiceManager::new()) | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
|  | ||||
|     #[test] | ||||
|     fn test_create_service_manager() { | ||||
|         // This test ensures the service manager can be created without panicking | ||||
|         let result = create_service_manager(); | ||||
|         assert!(result.is_ok(), "Service manager creation should succeed"); | ||||
|     } | ||||
|  | ||||
|     #[cfg(target_os = "linux")] | ||||
|     #[test] | ||||
|     fn test_socket_discovery_with_env_var() { | ||||
|         // Test that environment variable is respected | ||||
|         std::env::set_var("ZINIT_SOCKET_PATH", "/test/path.sock"); | ||||
|  | ||||
|         // The discover function should check the env var first | ||||
|         // Since the socket doesn't exist, it should return None, but we can't test | ||||
|         // the actual discovery logic without a real socket | ||||
|  | ||||
|         std::env::remove_var("ZINIT_SOCKET_PATH"); | ||||
|     } | ||||
|  | ||||
|     #[cfg(target_os = "linux")] | ||||
|     #[test] | ||||
|     fn test_socket_discovery_without_env_var() { | ||||
|         // Ensure env var is not set | ||||
|         std::env::remove_var("ZINIT_SOCKET_PATH"); | ||||
|  | ||||
|         // The discover function should try common paths | ||||
|         // Since no zinit is running, it should return None | ||||
|         let result = discover_zinit_socket(); | ||||
|  | ||||
|         // This is expected to be None in test environment | ||||
|         assert!( | ||||
|             result.is_none(), | ||||
|             "Should return None when no zinit server is running" | ||||
|         ); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										256
									
								
								_archive/service_manager/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								_archive/service_manager/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,256 @@ | ||||
| //! Rhai integration for the service manager module | ||||
| //! | ||||
| //! This module provides Rhai scripting support for service management operations. | ||||
|  | ||||
| use crate::{create_service_manager, ServiceConfig, ServiceManager}; | ||||
| use rhai::{Engine, EvalAltResult, Map}; | ||||
| use std::collections::HashMap; | ||||
| use std::sync::Arc; | ||||
|  | ||||
| /// A wrapper around ServiceManager that can be used in Rhai | ||||
| #[derive(Clone)] | ||||
| pub struct RhaiServiceManager { | ||||
|     inner: Arc<Box<dyn ServiceManager>>, | ||||
| } | ||||
|  | ||||
| impl RhaiServiceManager { | ||||
|     pub fn new() -> Result<Self, Box<EvalAltResult>> { | ||||
|         let manager = create_service_manager() | ||||
|             .map_err(|e| format!("Failed to create service manager: {}", e))?; | ||||
|         Ok(Self { | ||||
|             inner: Arc::new(manager), | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Register the service manager module with a Rhai engine | ||||
| pub fn register_service_manager_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { | ||||
|     // Factory function to create service manager | ||||
|     engine.register_type::<RhaiServiceManager>(); | ||||
|     engine.register_fn( | ||||
|         "create_service_manager", | ||||
|         || -> Result<RhaiServiceManager, Box<EvalAltResult>> { RhaiServiceManager::new() }, | ||||
|     ); | ||||
|  | ||||
|     // Service management functions | ||||
|     engine.register_fn( | ||||
|         "start", | ||||
|         |manager: &mut RhaiServiceManager, config: Map| -> Result<(), Box<EvalAltResult>> { | ||||
|             let service_config = map_to_service_config(config)?; | ||||
|             manager | ||||
|                 .inner | ||||
|                 .start(&service_config) | ||||
|                 .map_err(|e| format!("Failed to start service: {}", e).into()) | ||||
|         }, | ||||
|     ); | ||||
|  | ||||
|     engine.register_fn( | ||||
|         "stop", | ||||
|         |manager: &mut RhaiServiceManager, | ||||
|          service_name: String| | ||||
|          -> Result<(), Box<EvalAltResult>> { | ||||
|             manager | ||||
|                 .inner | ||||
|                 .stop(&service_name) | ||||
|                 .map_err(|e| format!("Failed to stop service: {}", e).into()) | ||||
|         }, | ||||
|     ); | ||||
|  | ||||
|     engine.register_fn( | ||||
|         "restart", | ||||
|         |manager: &mut RhaiServiceManager, | ||||
|          service_name: String| | ||||
|          -> Result<(), Box<EvalAltResult>> { | ||||
|             manager | ||||
|                 .inner | ||||
|                 .restart(&service_name) | ||||
|                 .map_err(|e| format!("Failed to restart service: {}", e).into()) | ||||
|         }, | ||||
|     ); | ||||
|  | ||||
|     engine.register_fn( | ||||
|         "status", | ||||
|         |manager: &mut RhaiServiceManager, | ||||
|          service_name: String| | ||||
|          -> Result<String, Box<EvalAltResult>> { | ||||
|             let status = manager | ||||
|                 .inner | ||||
|                 .status(&service_name) | ||||
|                 .map_err(|e| format!("Failed to get service status: {}", e))?; | ||||
|             Ok(format!("{:?}", status)) | ||||
|         }, | ||||
|     ); | ||||
|  | ||||
|     engine.register_fn( | ||||
|         "logs", | ||||
|         |manager: &mut RhaiServiceManager, | ||||
|          service_name: String, | ||||
|          lines: i64| | ||||
|          -> Result<String, Box<EvalAltResult>> { | ||||
|             let lines_opt = if lines > 0 { | ||||
|                 Some(lines as usize) | ||||
|             } else { | ||||
|                 None | ||||
|             }; | ||||
|             manager | ||||
|                 .inner | ||||
|                 .logs(&service_name, lines_opt) | ||||
|                 .map_err(|e| format!("Failed to get service logs: {}", e).into()) | ||||
|         }, | ||||
|     ); | ||||
|  | ||||
|     engine.register_fn( | ||||
|         "list", | ||||
|         |manager: &mut RhaiServiceManager| -> Result<Vec<String>, Box<EvalAltResult>> { | ||||
|             manager | ||||
|                 .inner | ||||
|                 .list() | ||||
|                 .map_err(|e| format!("Failed to list services: {}", e).into()) | ||||
|         }, | ||||
|     ); | ||||
|  | ||||
|     engine.register_fn( | ||||
|         "remove", | ||||
|         |manager: &mut RhaiServiceManager, | ||||
|          service_name: String| | ||||
|          -> Result<(), Box<EvalAltResult>> { | ||||
|             manager | ||||
|                 .inner | ||||
|                 .remove(&service_name) | ||||
|                 .map_err(|e| format!("Failed to remove service: {}", e).into()) | ||||
|         }, | ||||
|     ); | ||||
|  | ||||
|     engine.register_fn( | ||||
|         "exists", | ||||
|         |manager: &mut RhaiServiceManager, | ||||
|          service_name: String| | ||||
|          -> Result<bool, Box<EvalAltResult>> { | ||||
|             manager | ||||
|                 .inner | ||||
|                 .exists(&service_name) | ||||
|                 .map_err(|e| format!("Failed to check if service exists: {}", e).into()) | ||||
|         }, | ||||
|     ); | ||||
|  | ||||
|     engine.register_fn( | ||||
|         "start_and_confirm", | ||||
|         |manager: &mut RhaiServiceManager, | ||||
|          config: Map, | ||||
|          timeout_secs: i64| | ||||
|          -> Result<(), Box<EvalAltResult>> { | ||||
|             let service_config = map_to_service_config(config)?; | ||||
|             let timeout = if timeout_secs > 0 { | ||||
|                 timeout_secs as u64 | ||||
|             } else { | ||||
|                 30 | ||||
|             }; | ||||
|             manager | ||||
|                 .inner | ||||
|                 .start_and_confirm(&service_config, timeout) | ||||
|                 .map_err(|e| format!("Failed to start and confirm service: {}", e).into()) | ||||
|         }, | ||||
|     ); | ||||
|  | ||||
|     engine.register_fn( | ||||
|         "start_existing_and_confirm", | ||||
|         |manager: &mut RhaiServiceManager, | ||||
|          service_name: String, | ||||
|          timeout_secs: i64| | ||||
|          -> Result<(), Box<EvalAltResult>> { | ||||
|             let timeout = if timeout_secs > 0 { | ||||
|                 timeout_secs as u64 | ||||
|             } else { | ||||
|                 30 | ||||
|             }; | ||||
|             manager | ||||
|                 .inner | ||||
|                 .start_existing_and_confirm(&service_name, timeout) | ||||
|                 .map_err(|e| format!("Failed to start existing service and confirm: {}", e).into()) | ||||
|         }, | ||||
|     ); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Convert a Rhai Map to a ServiceConfig | ||||
| fn map_to_service_config(map: Map) -> Result<ServiceConfig, Box<EvalAltResult>> { | ||||
|     let name = map | ||||
|         .get("name") | ||||
|         .and_then(|v| v.clone().into_string().ok()) | ||||
|         .ok_or("Service config must have a 'name' field")?; | ||||
|  | ||||
|     let binary_path = map | ||||
|         .get("binary_path") | ||||
|         .and_then(|v| v.clone().into_string().ok()) | ||||
|         .ok_or("Service config must have a 'binary_path' field")?; | ||||
|  | ||||
|     let args = map | ||||
|         .get("args") | ||||
|         .and_then(|v| v.clone().try_cast::<rhai::Array>()) | ||||
|         .map(|arr| { | ||||
|             arr.into_iter() | ||||
|                 .filter_map(|v| v.into_string().ok()) | ||||
|                 .collect::<Vec<String>>() | ||||
|         }) | ||||
|         .unwrap_or_default(); | ||||
|  | ||||
|     let working_directory = map | ||||
|         .get("working_directory") | ||||
|         .and_then(|v| v.clone().into_string().ok()); | ||||
|  | ||||
|     let environment = map | ||||
|         .get("environment") | ||||
|         .and_then(|v| v.clone().try_cast::<Map>()) | ||||
|         .map(|env_map| { | ||||
|             env_map | ||||
|                 .into_iter() | ||||
|                 .filter_map(|(k, v)| v.into_string().ok().map(|val| (k.to_string(), val))) | ||||
|                 .collect::<HashMap<String, String>>() | ||||
|         }) | ||||
|         .unwrap_or_default(); | ||||
|  | ||||
|     let auto_restart = map | ||||
|         .get("auto_restart") | ||||
|         .and_then(|v| v.as_bool().ok()) | ||||
|         .unwrap_or(false); | ||||
|  | ||||
|     Ok(ServiceConfig { | ||||
|         name, | ||||
|         binary_path, | ||||
|         args, | ||||
|         working_directory, | ||||
|         environment, | ||||
|         auto_restart, | ||||
|     }) | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
|     use rhai::{Engine, Map}; | ||||
|  | ||||
|     #[test] | ||||
|     fn test_register_service_manager_module() { | ||||
|         let mut engine = Engine::new(); | ||||
|         register_service_manager_module(&mut engine).unwrap(); | ||||
|  | ||||
|         // Test that the functions are registered | ||||
|         // Note: Rhai doesn't expose a public API to check if functions are registered | ||||
|         // So we'll just verify the module registration doesn't panic | ||||
|         assert!(true); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_map_to_service_config() { | ||||
|         let mut map = Map::new(); | ||||
|         map.insert("name".into(), "test-service".into()); | ||||
|         map.insert("binary_path".into(), "/bin/echo".into()); | ||||
|         map.insert("auto_restart".into(), true.into()); | ||||
|  | ||||
|         let config = map_to_service_config(map).unwrap(); | ||||
|         assert_eq!(config.name, "test-service"); | ||||
|         assert_eq!(config.binary_path, "/bin/echo"); | ||||
|         assert_eq!(config.auto_restart, true); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										434
									
								
								_archive/service_manager/src/systemd.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										434
									
								
								_archive/service_manager/src/systemd.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,434 @@ | ||||
| use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus}; | ||||
| use std::fs; | ||||
| use std::path::PathBuf; | ||||
| use std::process::Command; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct SystemdServiceManager { | ||||
|     service_prefix: String, | ||||
|     user_mode: bool, | ||||
| } | ||||
|  | ||||
| impl SystemdServiceManager { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             service_prefix: "sal".to_string(), | ||||
|             user_mode: true, // Default to user services for safety | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn new_system() -> Self { | ||||
|         Self { | ||||
|             service_prefix: "sal".to_string(), | ||||
|             user_mode: false, // System-wide services (requires root) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn get_service_name(&self, service_name: &str) -> String { | ||||
|         format!("{}-{}.service", self.service_prefix, service_name) | ||||
|     } | ||||
|  | ||||
|     fn get_unit_file_path(&self, service_name: &str) -> PathBuf { | ||||
|         let service_file = self.get_service_name(service_name); | ||||
|         if self.user_mode { | ||||
|             // User service directory | ||||
|             let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); | ||||
|             PathBuf::from(home) | ||||
|                 .join(".config") | ||||
|                 .join("systemd") | ||||
|                 .join("user") | ||||
|                 .join(service_file) | ||||
|         } else { | ||||
|             // System service directory | ||||
|             PathBuf::from("/etc/systemd/system").join(service_file) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn run_systemctl(&self, args: &[&str]) -> Result<String, ServiceManagerError> { | ||||
|         let mut cmd = Command::new("systemctl"); | ||||
|  | ||||
|         if self.user_mode { | ||||
|             cmd.arg("--user"); | ||||
|         } | ||||
|  | ||||
|         cmd.args(args); | ||||
|  | ||||
|         let output = cmd | ||||
|             .output() | ||||
|             .map_err(|e| ServiceManagerError::Other(format!("Failed to run systemctl: {}", e)))?; | ||||
|  | ||||
|         if !output.status.success() { | ||||
|             let stderr = String::from_utf8_lossy(&output.stderr); | ||||
|             return Err(ServiceManagerError::Other(format!( | ||||
|                 "systemctl command failed: {}", | ||||
|                 stderr | ||||
|             ))); | ||||
|         } | ||||
|  | ||||
|         Ok(String::from_utf8_lossy(&output.stdout).to_string()) | ||||
|     } | ||||
|  | ||||
|     fn create_unit_file(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> { | ||||
|         let unit_path = self.get_unit_file_path(&config.name); | ||||
|  | ||||
|         // Ensure the directory exists | ||||
|         if let Some(parent) = unit_path.parent() { | ||||
|             fs::create_dir_all(parent).map_err(|e| { | ||||
|                 ServiceManagerError::Other(format!("Failed to create unit directory: {}", e)) | ||||
|             })?; | ||||
|         } | ||||
|  | ||||
|         // Create the unit file content | ||||
|         let mut unit_content = String::new(); | ||||
|         unit_content.push_str("[Unit]\n"); | ||||
|         unit_content.push_str(&format!("Description={} service\n", config.name)); | ||||
|         unit_content.push_str("After=network.target\n\n"); | ||||
|  | ||||
|         unit_content.push_str("[Service]\n"); | ||||
|         unit_content.push_str("Type=simple\n"); | ||||
|  | ||||
|         // Build the ExecStart command | ||||
|         let mut exec_start = config.binary_path.clone(); | ||||
|         for arg in &config.args { | ||||
|             exec_start.push(' '); | ||||
|             exec_start.push_str(arg); | ||||
|         } | ||||
|         unit_content.push_str(&format!("ExecStart={}\n", exec_start)); | ||||
|  | ||||
|         if let Some(working_dir) = &config.working_directory { | ||||
|             unit_content.push_str(&format!("WorkingDirectory={}\n", working_dir)); | ||||
|         } | ||||
|  | ||||
|         // Add environment variables | ||||
|         for (key, value) in &config.environment { | ||||
|             unit_content.push_str(&format!("Environment=\"{}={}\"\n", key, value)); | ||||
|         } | ||||
|  | ||||
|         if config.auto_restart { | ||||
|             unit_content.push_str("Restart=always\n"); | ||||
|             unit_content.push_str("RestartSec=5\n"); | ||||
|         } | ||||
|  | ||||
|         unit_content.push_str("\n[Install]\n"); | ||||
|         unit_content.push_str("WantedBy=default.target\n"); | ||||
|  | ||||
|         // Write the unit file | ||||
|         fs::write(&unit_path, unit_content) | ||||
|             .map_err(|e| ServiceManagerError::Other(format!("Failed to write unit file: {}", e)))?; | ||||
|  | ||||
|         // Reload systemd to pick up the new unit file | ||||
|         self.run_systemctl(&["daemon-reload"])?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ServiceManager for SystemdServiceManager { | ||||
|     fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> { | ||||
|         let unit_path = self.get_unit_file_path(service_name); | ||||
|         Ok(unit_path.exists()) | ||||
|     } | ||||
|  | ||||
|     fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> { | ||||
|         let service_name = self.get_service_name(&config.name); | ||||
|  | ||||
|         // Check if service already exists and is running | ||||
|         if self.exists(&config.name)? { | ||||
|             match self.status(&config.name)? { | ||||
|                 ServiceStatus::Running => { | ||||
|                     return Err(ServiceManagerError::ServiceAlreadyExists( | ||||
|                         config.name.clone(), | ||||
|                     )); | ||||
|                 } | ||||
|                 _ => { | ||||
|                     // Service exists but not running, we can start it | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             // Create the unit file | ||||
|             self.create_unit_file(config)?; | ||||
|         } | ||||
|  | ||||
|         // Enable and start the service | ||||
|         self.run_systemctl(&["enable", &service_name]) | ||||
|             .map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?; | ||||
|  | ||||
|         self.run_systemctl(&["start", &service_name]) | ||||
|             .map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         let service_unit = self.get_service_name(service_name); | ||||
|  | ||||
|         // Check if unit file exists | ||||
|         if !self.exists(service_name)? { | ||||
|             return Err(ServiceManagerError::ServiceNotFound( | ||||
|                 service_name.to_string(), | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         // Check if already running | ||||
|         match self.status(service_name)? { | ||||
|             ServiceStatus::Running => { | ||||
|                 return Ok(()); // Already running, nothing to do | ||||
|             } | ||||
|             _ => { | ||||
|                 // Start the service | ||||
|                 self.run_systemctl(&["start", &service_unit]).map_err(|e| { | ||||
|                     ServiceManagerError::StartFailed(service_name.to_string(), e.to_string()) | ||||
|                 })?; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn start_and_confirm( | ||||
|         &self, | ||||
|         config: &ServiceConfig, | ||||
|         timeout_secs: u64, | ||||
|     ) -> Result<(), ServiceManagerError> { | ||||
|         // Start the service first | ||||
|         self.start(config)?; | ||||
|  | ||||
|         // Wait for confirmation with timeout | ||||
|         let start_time = std::time::Instant::now(); | ||||
|         let timeout_duration = std::time::Duration::from_secs(timeout_secs); | ||||
|  | ||||
|         while start_time.elapsed() < timeout_duration { | ||||
|             match self.status(&config.name) { | ||||
|                 Ok(ServiceStatus::Running) => return Ok(()), | ||||
|                 Ok(ServiceStatus::Failed) => { | ||||
|                     return Err(ServiceManagerError::StartFailed( | ||||
|                         config.name.clone(), | ||||
|                         "Service failed to start".to_string(), | ||||
|                     )); | ||||
|                 } | ||||
|                 Ok(_) => { | ||||
|                     // Still starting, wait a bit | ||||
|                     std::thread::sleep(std::time::Duration::from_millis(100)); | ||||
|                 } | ||||
|                 Err(_) => { | ||||
|                     // Service might not exist yet, wait a bit | ||||
|                     std::thread::sleep(std::time::Duration::from_millis(100)); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Err(ServiceManagerError::StartFailed( | ||||
|             config.name.clone(), | ||||
|             format!("Service did not start within {} seconds", timeout_secs), | ||||
|         )) | ||||
|     } | ||||
|  | ||||
|     fn start_existing_and_confirm( | ||||
|         &self, | ||||
|         service_name: &str, | ||||
|         timeout_secs: u64, | ||||
|     ) -> Result<(), ServiceManagerError> { | ||||
|         // Start the existing service first | ||||
|         self.start_existing(service_name)?; | ||||
|  | ||||
|         // Wait for confirmation with timeout | ||||
|         let start_time = std::time::Instant::now(); | ||||
|         let timeout_duration = std::time::Duration::from_secs(timeout_secs); | ||||
|  | ||||
|         while start_time.elapsed() < timeout_duration { | ||||
|             match self.status(service_name) { | ||||
|                 Ok(ServiceStatus::Running) => return Ok(()), | ||||
|                 Ok(ServiceStatus::Failed) => { | ||||
|                     return Err(ServiceManagerError::StartFailed( | ||||
|                         service_name.to_string(), | ||||
|                         "Service failed to start".to_string(), | ||||
|                     )); | ||||
|                 } | ||||
|                 Ok(_) => { | ||||
|                     // Still starting, wait a bit | ||||
|                     std::thread::sleep(std::time::Duration::from_millis(100)); | ||||
|                 } | ||||
|                 Err(_) => { | ||||
|                     // Service might not exist yet, wait a bit | ||||
|                     std::thread::sleep(std::time::Duration::from_millis(100)); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Err(ServiceManagerError::StartFailed( | ||||
|             service_name.to_string(), | ||||
|             format!("Service did not start within {} seconds", timeout_secs), | ||||
|         )) | ||||
|     } | ||||
|  | ||||
|     fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         let service_unit = self.get_service_name(service_name); | ||||
|  | ||||
|         // Check if service exists | ||||
|         if !self.exists(service_name)? { | ||||
|             return Err(ServiceManagerError::ServiceNotFound( | ||||
|                 service_name.to_string(), | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         // Stop the service | ||||
|         self.run_systemctl(&["stop", &service_unit]).map_err(|e| { | ||||
|             ServiceManagerError::StopFailed(service_name.to_string(), e.to_string()) | ||||
|         })?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         let service_unit = self.get_service_name(service_name); | ||||
|  | ||||
|         // Check if service exists | ||||
|         if !self.exists(service_name)? { | ||||
|             return Err(ServiceManagerError::ServiceNotFound( | ||||
|                 service_name.to_string(), | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         // Restart the service | ||||
|         self.run_systemctl(&["restart", &service_unit]) | ||||
|             .map_err(|e| { | ||||
|                 ServiceManagerError::RestartFailed(service_name.to_string(), e.to_string()) | ||||
|             })?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> { | ||||
|         let service_unit = self.get_service_name(service_name); | ||||
|  | ||||
|         // Check if service exists | ||||
|         if !self.exists(service_name)? { | ||||
|             return Err(ServiceManagerError::ServiceNotFound( | ||||
|                 service_name.to_string(), | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         // Get service status | ||||
|         let output = self | ||||
|             .run_systemctl(&["is-active", &service_unit]) | ||||
|             .unwrap_or_else(|_| "unknown".to_string()); | ||||
|  | ||||
|         let status = match output.trim() { | ||||
|             "active" => ServiceStatus::Running, | ||||
|             "inactive" => ServiceStatus::Stopped, | ||||
|             "failed" => ServiceStatus::Failed, | ||||
|             _ => ServiceStatus::Unknown, | ||||
|         }; | ||||
|  | ||||
|         Ok(status) | ||||
|     } | ||||
|  | ||||
|     fn logs( | ||||
|         &self, | ||||
|         service_name: &str, | ||||
|         lines: Option<usize>, | ||||
|     ) -> Result<String, ServiceManagerError> { | ||||
|         let service_unit = self.get_service_name(service_name); | ||||
|  | ||||
|         // Check if service exists | ||||
|         if !self.exists(service_name)? { | ||||
|             return Err(ServiceManagerError::ServiceNotFound( | ||||
|                 service_name.to_string(), | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         // Build journalctl command | ||||
|         let mut args = vec!["--unit", &service_unit, "--no-pager"]; | ||||
|         let lines_arg; | ||||
|         if let Some(n) = lines { | ||||
|             lines_arg = format!("--lines={}", n); | ||||
|             args.push(&lines_arg); | ||||
|         } | ||||
|  | ||||
|         // Use journalctl to get logs | ||||
|         let mut cmd = std::process::Command::new("journalctl"); | ||||
|         if self.user_mode { | ||||
|             cmd.arg("--user"); | ||||
|         } | ||||
|         cmd.args(&args); | ||||
|  | ||||
|         let output = cmd.output().map_err(|e| { | ||||
|             ServiceManagerError::LogsFailed( | ||||
|                 service_name.to_string(), | ||||
|                 format!("Failed to run journalctl: {}", e), | ||||
|             ) | ||||
|         })?; | ||||
|  | ||||
|         if !output.status.success() { | ||||
|             let stderr = String::from_utf8_lossy(&output.stderr); | ||||
|             return Err(ServiceManagerError::LogsFailed( | ||||
|                 service_name.to_string(), | ||||
|                 format!("journalctl command failed: {}", stderr), | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         Ok(String::from_utf8_lossy(&output.stdout).to_string()) | ||||
|     } | ||||
|  | ||||
|     fn list(&self) -> Result<Vec<String>, ServiceManagerError> { | ||||
|         // List all services with our prefix | ||||
|         let output = | ||||
|             self.run_systemctl(&["list-units", "--type=service", "--all", "--no-pager"])?; | ||||
|  | ||||
|         let mut services = Vec::new(); | ||||
|         for line in output.lines() { | ||||
|             if line.contains(&format!("{}-", self.service_prefix)) { | ||||
|                 // Extract service name from the line | ||||
|                 if let Some(unit_name) = line.split_whitespace().next() { | ||||
|                     if let Some(service_name) = unit_name.strip_suffix(".service") { | ||||
|                         if let Some(name) = | ||||
|                             service_name.strip_prefix(&format!("{}-", self.service_prefix)) | ||||
|                         { | ||||
|                             services.push(name.to_string()); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(services) | ||||
|     } | ||||
|  | ||||
|     fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         let service_unit = self.get_service_name(service_name); | ||||
|  | ||||
|         // Check if service exists | ||||
|         if !self.exists(service_name)? { | ||||
|             return Err(ServiceManagerError::ServiceNotFound( | ||||
|                 service_name.to_string(), | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         // Try to stop the service first, but don't fail if it's already stopped | ||||
|         if let Err(e) = self.stop(service_name) { | ||||
|             log::warn!( | ||||
|                 "Failed to stop service '{}' before removal: {}", | ||||
|                 service_name, | ||||
|                 e | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         // Disable the service | ||||
|         if let Err(e) = self.run_systemctl(&["disable", &service_unit]) { | ||||
|             log::warn!("Failed to disable service '{}': {}", service_name, e); | ||||
|         } | ||||
|  | ||||
|         // Remove the unit file | ||||
|         let unit_path = self.get_unit_file_path(service_name); | ||||
|         if unit_path.exists() { | ||||
|             std::fs::remove_file(&unit_path).map_err(|e| { | ||||
|                 ServiceManagerError::Other(format!("Failed to remove unit file: {}", e)) | ||||
|             })?; | ||||
|         } | ||||
|  | ||||
|         // Reload systemd to pick up the changes | ||||
|         self.run_systemctl(&["daemon-reload"])?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										379
									
								
								_archive/service_manager/src/zinit.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										379
									
								
								_archive/service_manager/src/zinit.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,379 @@ | ||||
| use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus}; | ||||
| use once_cell::sync::Lazy; | ||||
| use serde_json::json; | ||||
| use std::sync::Arc; | ||||
| use std::time::Duration; | ||||
| use tokio::runtime::Runtime; | ||||
| use tokio::time::timeout; | ||||
| use zinit_client::{ServiceStatus as ZinitServiceStatus, ZinitClient, ZinitError}; | ||||
|  | ||||
| // Shared runtime for async operations - production-safe initialization | ||||
| static ASYNC_RUNTIME: Lazy<Option<Runtime>> = Lazy::new(|| Runtime::new().ok()); | ||||
|  | ||||
| /// Get the async runtime, creating a temporary one if the static runtime failed | ||||
| fn get_runtime() -> Result<Runtime, ServiceManagerError> { | ||||
|     // Try to use the static runtime first | ||||
|     if let Some(_runtime) = ASYNC_RUNTIME.as_ref() { | ||||
|         // We can't return a reference to the static runtime because we need ownership | ||||
|         // for block_on, so we create a new one. This is a reasonable trade-off for safety. | ||||
|         Runtime::new().map_err(|e| { | ||||
|             ServiceManagerError::Other(format!("Failed to create async runtime: {}", e)) | ||||
|         }) | ||||
|     } else { | ||||
|         // Static runtime failed, try to create a new one | ||||
|         Runtime::new().map_err(|e| { | ||||
|             ServiceManagerError::Other(format!("Failed to create async runtime: {}", e)) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct ZinitServiceManager { | ||||
|     client: Arc<ZinitClient>, | ||||
| } | ||||
|  | ||||
| impl ZinitServiceManager { | ||||
|     pub fn new(socket_path: &str) -> Result<Self, ServiceManagerError> { | ||||
|         // Create the base zinit client directly | ||||
|         let client = Arc::new(ZinitClient::new(socket_path)); | ||||
|  | ||||
|         Ok(ZinitServiceManager { client }) | ||||
|     } | ||||
|  | ||||
|     /// Execute an async operation using the shared runtime or current context | ||||
|     fn execute_async<F, T>(&self, operation: F) -> Result<T, ServiceManagerError> | ||||
|     where | ||||
|         F: std::future::Future<Output = Result<T, ZinitError>> + Send + 'static, | ||||
|         T: Send + 'static, | ||||
|     { | ||||
|         // Check if we're already in a tokio runtime context | ||||
|         if let Ok(_handle) = tokio::runtime::Handle::try_current() { | ||||
|             // We're in an async context, use spawn_blocking to avoid nested runtime | ||||
|             let result = std::thread::spawn( | ||||
|                 move || -> Result<Result<T, ZinitError>, ServiceManagerError> { | ||||
|                     let rt = Runtime::new().map_err(|e| { | ||||
|                         ServiceManagerError::Other(format!("Failed to create runtime: {}", e)) | ||||
|                     })?; | ||||
|                     Ok(rt.block_on(operation)) | ||||
|                 }, | ||||
|             ) | ||||
|             .join() | ||||
|             .map_err(|_| ServiceManagerError::Other("Thread join failed".to_string()))?; | ||||
|             result?.map_err(|e| ServiceManagerError::Other(e.to_string())) | ||||
|         } else { | ||||
|             // No current runtime, use production-safe runtime | ||||
|             let runtime = get_runtime()?; | ||||
|             runtime | ||||
|                 .block_on(operation) | ||||
|                 .map_err(|e| ServiceManagerError::Other(e.to_string())) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Execute an async operation with timeout using the shared runtime or current context | ||||
|     fn execute_async_with_timeout<F, T>( | ||||
|         &self, | ||||
|         operation: F, | ||||
|         timeout_secs: u64, | ||||
|     ) -> Result<T, ServiceManagerError> | ||||
|     where | ||||
|         F: std::future::Future<Output = Result<T, ZinitError>> + Send + 'static, | ||||
|         T: Send + 'static, | ||||
|     { | ||||
|         let timeout_duration = Duration::from_secs(timeout_secs); | ||||
|         let timeout_op = timeout(timeout_duration, operation); | ||||
|  | ||||
|         // Check if we're already in a tokio runtime context | ||||
|         if let Ok(_handle) = tokio::runtime::Handle::try_current() { | ||||
|             // We're in an async context, use spawn_blocking to avoid nested runtime | ||||
|             let result = std::thread::spawn(move || { | ||||
|                 let rt = tokio::runtime::Runtime::new().unwrap(); | ||||
|                 rt.block_on(timeout_op) | ||||
|             }) | ||||
|             .join() | ||||
|             .map_err(|_| ServiceManagerError::Other("Thread join failed".to_string()))?; | ||||
|  | ||||
|             result | ||||
|                 .map_err(|_| { | ||||
|                     ServiceManagerError::Other(format!( | ||||
|                         "Operation timed out after {} seconds", | ||||
|                         timeout_secs | ||||
|                     )) | ||||
|                 })? | ||||
|                 .map_err(|e| ServiceManagerError::Other(e.to_string())) | ||||
|         } else { | ||||
|             // No current runtime, use production-safe runtime | ||||
|             let runtime = get_runtime()?; | ||||
|             runtime | ||||
|                 .block_on(timeout_op) | ||||
|                 .map_err(|_| { | ||||
|                     ServiceManagerError::Other(format!( | ||||
|                         "Operation timed out after {} seconds", | ||||
|                         timeout_secs | ||||
|                     )) | ||||
|                 })? | ||||
|                 .map_err(|e| ServiceManagerError::Other(e.to_string())) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ServiceManager for ZinitServiceManager { | ||||
|     fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> { | ||||
|         let status_res = self.status(service_name); | ||||
|         match status_res { | ||||
|             Ok(_) => Ok(true), | ||||
|             Err(ServiceManagerError::ServiceNotFound(_)) => Ok(false), | ||||
|             Err(e) => Err(e), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> { | ||||
|         // Build the exec command with args | ||||
|         let mut exec_command = config.binary_path.clone(); | ||||
|         if !config.args.is_empty() { | ||||
|             exec_command.push(' '); | ||||
|             exec_command.push_str(&config.args.join(" ")); | ||||
|         } | ||||
|  | ||||
|         // Create zinit-compatible service configuration | ||||
|         let mut service_config = json!({ | ||||
|             "exec": exec_command, | ||||
|             "oneshot": !config.auto_restart,  // zinit uses oneshot, not restart | ||||
|             "env": config.environment, | ||||
|         }); | ||||
|  | ||||
|         // Add optional fields if present | ||||
|         if let Some(ref working_dir) = config.working_directory { | ||||
|             // Zinit doesn't support working_directory directly, so we need to modify the exec command | ||||
|             let cd_command = format!("cd {} && {}", working_dir, exec_command); | ||||
|             service_config["exec"] = json!(cd_command); | ||||
|         } | ||||
|  | ||||
|         let client = Arc::clone(&self.client); | ||||
|         let service_name = config.name.clone(); | ||||
|         self.execute_async( | ||||
|             async move { client.create_service(&service_name, service_config).await }, | ||||
|         ) | ||||
|         .map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?; | ||||
|  | ||||
|         self.start_existing(&config.name) | ||||
|     } | ||||
|  | ||||
|     fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         let client = Arc::clone(&self.client); | ||||
|         let service_name_owned = service_name.to_string(); | ||||
|         let service_name_for_error = service_name.to_string(); | ||||
|         self.execute_async(async move { client.start(&service_name_owned).await }) | ||||
|             .map_err(|e| ServiceManagerError::StartFailed(service_name_for_error, e.to_string())) | ||||
|     } | ||||
|  | ||||
|     fn start_and_confirm( | ||||
|         &self, | ||||
|         config: &ServiceConfig, | ||||
|         timeout_secs: u64, | ||||
|     ) -> Result<(), ServiceManagerError> { | ||||
|         // Start the service first | ||||
|         self.start(config)?; | ||||
|  | ||||
|         // Wait for confirmation with timeout using the shared runtime | ||||
|         self.execute_async_with_timeout( | ||||
|             async move { | ||||
|                 let start_time = std::time::Instant::now(); | ||||
|                 let timeout_duration = Duration::from_secs(timeout_secs); | ||||
|  | ||||
|                 while start_time.elapsed() < timeout_duration { | ||||
|                     // We need to call status in a blocking way from within the async context | ||||
|                     // For now, we'll use a simple polling approach | ||||
|                     tokio::time::sleep(Duration::from_millis(100)).await; | ||||
|                 } | ||||
|  | ||||
|                 // Return a timeout error that will be handled by execute_async_with_timeout | ||||
|                 // Use a generic error since we don't know the exact ZinitError variants | ||||
|                 Err(ZinitError::from(std::io::Error::new( | ||||
|                     std::io::ErrorKind::TimedOut, | ||||
|                     "Timeout waiting for service confirmation", | ||||
|                 ))) | ||||
|             }, | ||||
|             timeout_secs, | ||||
|         )?; | ||||
|  | ||||
|         // Check final status | ||||
|         match self.status(&config.name)? { | ||||
|             ServiceStatus::Running => Ok(()), | ||||
|             ServiceStatus::Failed => Err(ServiceManagerError::StartFailed( | ||||
|                 config.name.clone(), | ||||
|                 "Service failed to start".to_string(), | ||||
|             )), | ||||
|             _ => Err(ServiceManagerError::StartFailed( | ||||
|                 config.name.clone(), | ||||
|                 format!("Service did not start within {} seconds", timeout_secs), | ||||
|             )), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn start_existing_and_confirm( | ||||
|         &self, | ||||
|         service_name: &str, | ||||
|         timeout_secs: u64, | ||||
|     ) -> Result<(), ServiceManagerError> { | ||||
|         // Start the existing service first | ||||
|         self.start_existing(service_name)?; | ||||
|  | ||||
|         // Wait for confirmation with timeout using the shared runtime | ||||
|         self.execute_async_with_timeout( | ||||
|             async move { | ||||
|                 let start_time = std::time::Instant::now(); | ||||
|                 let timeout_duration = Duration::from_secs(timeout_secs); | ||||
|  | ||||
|                 while start_time.elapsed() < timeout_duration { | ||||
|                     tokio::time::sleep(Duration::from_millis(100)).await; | ||||
|                 } | ||||
|  | ||||
|                 // Return a timeout error that will be handled by execute_async_with_timeout | ||||
|                 // Use a generic error since we don't know the exact ZinitError variants | ||||
|                 Err(ZinitError::from(std::io::Error::new( | ||||
|                     std::io::ErrorKind::TimedOut, | ||||
|                     "Timeout waiting for service confirmation", | ||||
|                 ))) | ||||
|             }, | ||||
|             timeout_secs, | ||||
|         )?; | ||||
|  | ||||
|         // Check final status | ||||
|         match self.status(service_name)? { | ||||
|             ServiceStatus::Running => Ok(()), | ||||
|             ServiceStatus::Failed => Err(ServiceManagerError::StartFailed( | ||||
|                 service_name.to_string(), | ||||
|                 "Service failed to start".to_string(), | ||||
|             )), | ||||
|             _ => Err(ServiceManagerError::StartFailed( | ||||
|                 service_name.to_string(), | ||||
|                 format!("Service did not start within {} seconds", timeout_secs), | ||||
|             )), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         let client = Arc::clone(&self.client); | ||||
|         let service_name_owned = service_name.to_string(); | ||||
|         let service_name_for_error = service_name.to_string(); | ||||
|         self.execute_async(async move { client.stop(&service_name_owned).await }) | ||||
|             .map_err(|e| ServiceManagerError::StopFailed(service_name_for_error, e.to_string())) | ||||
|     } | ||||
|  | ||||
|     fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         let client = Arc::clone(&self.client); | ||||
|         let service_name_owned = service_name.to_string(); | ||||
|         let service_name_for_error = service_name.to_string(); | ||||
|         self.execute_async(async move { client.restart(&service_name_owned).await }) | ||||
|             .map_err(|e| ServiceManagerError::RestartFailed(service_name_for_error, e.to_string())) | ||||
|     } | ||||
|  | ||||
|     fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> { | ||||
|         let client = Arc::clone(&self.client); | ||||
|         let service_name_owned = service_name.to_string(); | ||||
|         let service_name_for_error = service_name.to_string(); | ||||
|         let status: ZinitServiceStatus = self | ||||
|             .execute_async(async move { client.status(&service_name_owned).await }) | ||||
|             .map_err(|e| { | ||||
|                 // Check if this is a "service not found" error | ||||
|                 if e.to_string().contains("not found") || e.to_string().contains("does not exist") { | ||||
|                     ServiceManagerError::ServiceNotFound(service_name_for_error) | ||||
|                 } else { | ||||
|                     ServiceManagerError::Other(e.to_string()) | ||||
|                 } | ||||
|             })?; | ||||
|  | ||||
|         // ServiceStatus is a struct with fields, not an enum | ||||
|         // We need to check the state field to determine the status | ||||
|         // Convert ServiceState to string and match on that | ||||
|         let state_str = format!("{:?}", status.state).to_lowercase(); | ||||
|         let service_status = match state_str.as_str() { | ||||
|             s if s.contains("running") => crate::ServiceStatus::Running, | ||||
|             s if s.contains("stopped") => crate::ServiceStatus::Stopped, | ||||
|             s if s.contains("failed") => crate::ServiceStatus::Failed, | ||||
|             _ => crate::ServiceStatus::Unknown, | ||||
|         }; | ||||
|         Ok(service_status) | ||||
|     } | ||||
|  | ||||
|     fn logs( | ||||
|         &self, | ||||
|         service_name: &str, | ||||
|         _lines: Option<usize>, | ||||
|     ) -> Result<String, ServiceManagerError> { | ||||
|         // The logs method takes (follow: bool, filter: Option<impl AsRef<str>>) | ||||
|         let client = Arc::clone(&self.client); | ||||
|         let service_name_owned = service_name.to_string(); | ||||
|         let logs = self | ||||
|             .execute_async(async move { | ||||
|                 use futures::StreamExt; | ||||
|                 use tokio::time::{timeout, Duration}; | ||||
|  | ||||
|                 let mut log_stream = client | ||||
|                     .logs(false, Some(service_name_owned.as_str())) | ||||
|                     .await?; | ||||
|                 let mut logs = Vec::new(); | ||||
|  | ||||
|                 // Collect logs from the stream with a reasonable limit | ||||
|                 let mut count = 0; | ||||
|                 const MAX_LOGS: usize = 100; | ||||
|                 const LOG_TIMEOUT: Duration = Duration::from_secs(5); | ||||
|  | ||||
|                 // Use timeout to prevent hanging | ||||
|                 let result = timeout(LOG_TIMEOUT, async { | ||||
|                     while let Some(log_result) = log_stream.next().await { | ||||
|                         match log_result { | ||||
|                             Ok(log_entry) => { | ||||
|                                 logs.push(format!("{:?}", log_entry)); | ||||
|                                 count += 1; | ||||
|                                 if count >= MAX_LOGS { | ||||
|                                     break; | ||||
|                                 } | ||||
|                             } | ||||
|                             Err(_) => break, | ||||
|                         } | ||||
|                     } | ||||
|                 }) | ||||
|                 .await; | ||||
|  | ||||
|                 // Handle timeout - this is not an error, just means no more logs available | ||||
|                 if result.is_err() { | ||||
|                     log::debug!( | ||||
|                         "Log reading timed out after {} seconds, returning {} logs", | ||||
|                         LOG_TIMEOUT.as_secs(), | ||||
|                         logs.len() | ||||
|                     ); | ||||
|                 } | ||||
|  | ||||
|                 Ok::<Vec<String>, ZinitError>(logs) | ||||
|             }) | ||||
|             .map_err(|e| { | ||||
|                 ServiceManagerError::LogsFailed(service_name.to_string(), e.to_string()) | ||||
|             })?; | ||||
|         Ok(logs.join("\n")) | ||||
|     } | ||||
|  | ||||
|     fn list(&self) -> Result<Vec<String>, ServiceManagerError> { | ||||
|         let client = Arc::clone(&self.client); | ||||
|         let services = self | ||||
|             .execute_async(async move { client.list().await }) | ||||
|             .map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||
|         Ok(services.keys().cloned().collect()) | ||||
|     } | ||||
|  | ||||
|     fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         // Try to stop the service first, but don't fail if it's already stopped or doesn't exist | ||||
|         if let Err(e) = self.stop(service_name) { | ||||
|             // Log the error but continue with removal | ||||
|             log::warn!( | ||||
|                 "Failed to stop service '{}' before removal: {}", | ||||
|                 service_name, | ||||
|                 e | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         let client = Arc::clone(&self.client); | ||||
|         let service_name = service_name.to_string(); | ||||
|         self.execute_async(async move { client.delete_service(&service_name).await }) | ||||
|             .map_err(|e| ServiceManagerError::Other(e.to_string())) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										243
									
								
								_archive/service_manager/tests/factory_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										243
									
								
								_archive/service_manager/tests/factory_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,243 @@ | ||||
| use sal_service_manager::{create_service_manager, ServiceConfig, ServiceManager}; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| #[test] | ||||
| fn test_create_service_manager() { | ||||
|     // Test that the factory function creates the appropriate service manager for the platform | ||||
|     let manager = create_service_manager().expect("Failed to create service manager"); | ||||
|  | ||||
|     // Test basic functionality - should be able to call methods without panicking | ||||
|     let list_result = manager.list(); | ||||
|  | ||||
|     // The result might be an error (if no service system is available), but it shouldn't panic | ||||
|     match list_result { | ||||
|         Ok(services) => { | ||||
|             println!( | ||||
|                 "✓ Service manager created successfully, found {} services", | ||||
|                 services.len() | ||||
|             ); | ||||
|         } | ||||
|         Err(e) => { | ||||
|             println!("✓ Service manager created, but got expected error: {}", e); | ||||
|             // This is expected on systems without the appropriate service manager | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_service_config_creation() { | ||||
|     // Test creating various service configurations | ||||
|     let basic_config = ServiceConfig { | ||||
|         name: "test-service".to_string(), | ||||
|         binary_path: "/usr/bin/echo".to_string(), | ||||
|         args: vec!["hello".to_string(), "world".to_string()], | ||||
|         working_directory: None, | ||||
|         environment: HashMap::new(), | ||||
|         auto_restart: false, | ||||
|     }; | ||||
|  | ||||
|     assert_eq!(basic_config.name, "test-service"); | ||||
|     assert_eq!(basic_config.binary_path, "/usr/bin/echo"); | ||||
|     assert_eq!(basic_config.args.len(), 2); | ||||
|     assert_eq!(basic_config.args[0], "hello"); | ||||
|     assert_eq!(basic_config.args[1], "world"); | ||||
|     assert!(basic_config.working_directory.is_none()); | ||||
|     assert!(basic_config.environment.is_empty()); | ||||
|     assert!(!basic_config.auto_restart); | ||||
|  | ||||
|     println!("✓ Basic service config created successfully"); | ||||
|  | ||||
|     // Test config with environment variables | ||||
|     let mut env = HashMap::new(); | ||||
|     env.insert("PATH".to_string(), "/usr/bin:/bin".to_string()); | ||||
|     env.insert("HOME".to_string(), "/tmp".to_string()); | ||||
|  | ||||
|     let env_config = ServiceConfig { | ||||
|         name: "env-service".to_string(), | ||||
|         binary_path: "/usr/bin/env".to_string(), | ||||
|         args: vec![], | ||||
|         working_directory: Some("/tmp".to_string()), | ||||
|         environment: env.clone(), | ||||
|         auto_restart: true, | ||||
|     }; | ||||
|  | ||||
|     assert_eq!(env_config.name, "env-service"); | ||||
|     assert_eq!(env_config.binary_path, "/usr/bin/env"); | ||||
|     assert!(env_config.args.is_empty()); | ||||
|     assert_eq!(env_config.working_directory, Some("/tmp".to_string())); | ||||
|     assert_eq!(env_config.environment.len(), 2); | ||||
|     assert_eq!( | ||||
|         env_config.environment.get("PATH"), | ||||
|         Some(&"/usr/bin:/bin".to_string()) | ||||
|     ); | ||||
|     assert_eq!( | ||||
|         env_config.environment.get("HOME"), | ||||
|         Some(&"/tmp".to_string()) | ||||
|     ); | ||||
|     assert!(env_config.auto_restart); | ||||
|  | ||||
|     println!("✓ Environment service config created successfully"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_service_config_clone() { | ||||
|     // Test that ServiceConfig can be cloned | ||||
|     let original_config = ServiceConfig { | ||||
|         name: "original".to_string(), | ||||
|         binary_path: "/bin/sh".to_string(), | ||||
|         args: vec!["-c".to_string(), "echo test".to_string()], | ||||
|         working_directory: Some("/home".to_string()), | ||||
|         environment: { | ||||
|             let mut env = HashMap::new(); | ||||
|             env.insert("TEST".to_string(), "value".to_string()); | ||||
|             env | ||||
|         }, | ||||
|         auto_restart: true, | ||||
|     }; | ||||
|  | ||||
|     let cloned_config = original_config.clone(); | ||||
|  | ||||
|     assert_eq!(original_config.name, cloned_config.name); | ||||
|     assert_eq!(original_config.binary_path, cloned_config.binary_path); | ||||
|     assert_eq!(original_config.args, cloned_config.args); | ||||
|     assert_eq!( | ||||
|         original_config.working_directory, | ||||
|         cloned_config.working_directory | ||||
|     ); | ||||
|     assert_eq!(original_config.environment, cloned_config.environment); | ||||
|     assert_eq!(original_config.auto_restart, cloned_config.auto_restart); | ||||
|  | ||||
|     println!("✓ Service config cloning works correctly"); | ||||
| } | ||||
|  | ||||
| #[cfg(target_os = "macos")] | ||||
| #[test] | ||||
| fn test_macos_service_manager() { | ||||
|     use sal_service_manager::LaunchctlServiceManager; | ||||
|  | ||||
|     // Test creating macOS-specific service manager | ||||
|     let manager = LaunchctlServiceManager::new(); | ||||
|  | ||||
|     // Test basic functionality | ||||
|     let list_result = manager.list(); | ||||
|     match list_result { | ||||
|         Ok(services) => { | ||||
|             println!( | ||||
|                 "✓ macOS LaunchctlServiceManager created successfully, found {} services", | ||||
|                 services.len() | ||||
|             ); | ||||
|         } | ||||
|         Err(e) => { | ||||
|             println!( | ||||
|                 "✓ macOS LaunchctlServiceManager created, but got expected error: {}", | ||||
|                 e | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(target_os = "linux")] | ||||
| #[test] | ||||
| fn test_linux_service_manager() { | ||||
|     use sal_service_manager::SystemdServiceManager; | ||||
|  | ||||
|     // Test creating Linux-specific service manager | ||||
|     let manager = SystemdServiceManager::new(); | ||||
|  | ||||
|     // Test basic functionality | ||||
|     let list_result = manager.list(); | ||||
|     match list_result { | ||||
|         Ok(services) => { | ||||
|             println!( | ||||
|                 "✓ Linux SystemdServiceManager created successfully, found {} services", | ||||
|                 services.len() | ||||
|             ); | ||||
|         } | ||||
|         Err(e) => { | ||||
|             println!( | ||||
|                 "✓ Linux SystemdServiceManager created, but got expected error: {}", | ||||
|                 e | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_service_status_debug() { | ||||
|     use sal_service_manager::ServiceStatus; | ||||
|  | ||||
|     // Test that ServiceStatus can be debugged and cloned | ||||
|     let statuses = vec![ | ||||
|         ServiceStatus::Running, | ||||
|         ServiceStatus::Stopped, | ||||
|         ServiceStatus::Failed, | ||||
|         ServiceStatus::Unknown, | ||||
|     ]; | ||||
|  | ||||
|     for status in &statuses { | ||||
|         let cloned = status.clone(); | ||||
|         let debug_str = format!("{:?}", status); | ||||
|  | ||||
|         assert!(!debug_str.is_empty()); | ||||
|         assert_eq!(status, &cloned); | ||||
|  | ||||
|         println!( | ||||
|             "✓ ServiceStatus::{:?} debug and clone work correctly", | ||||
|             status | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_service_manager_error_debug() { | ||||
|     use sal_service_manager::ServiceManagerError; | ||||
|  | ||||
|     // Test that ServiceManagerError can be debugged and displayed | ||||
|     let errors = vec![ | ||||
|         ServiceManagerError::ServiceNotFound("test".to_string()), | ||||
|         ServiceManagerError::ServiceAlreadyExists("test".to_string()), | ||||
|         ServiceManagerError::StartFailed("test".to_string(), "reason".to_string()), | ||||
|         ServiceManagerError::StopFailed("test".to_string(), "reason".to_string()), | ||||
|         ServiceManagerError::RestartFailed("test".to_string(), "reason".to_string()), | ||||
|         ServiceManagerError::LogsFailed("test".to_string(), "reason".to_string()), | ||||
|         ServiceManagerError::Other("generic error".to_string()), | ||||
|     ]; | ||||
|  | ||||
|     for error in &errors { | ||||
|         let debug_str = format!("{:?}", error); | ||||
|         let display_str = format!("{}", error); | ||||
|  | ||||
|         assert!(!debug_str.is_empty()); | ||||
|         assert!(!display_str.is_empty()); | ||||
|  | ||||
|         println!("✓ Error debug: {:?}", error); | ||||
|         println!("✓ Error display: {}", error); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_service_manager_trait_object() { | ||||
|     // Test that we can use ServiceManager as a trait object | ||||
|     let manager: Box<dyn ServiceManager> = | ||||
|         create_service_manager().expect("Failed to create service manager"); | ||||
|  | ||||
|     // Test that we can call methods through the trait object | ||||
|     let list_result = manager.list(); | ||||
|  | ||||
|     match list_result { | ||||
|         Ok(services) => { | ||||
|             println!("✓ Trait object works, found {} services", services.len()); | ||||
|         } | ||||
|         Err(e) => { | ||||
|             println!("✓ Trait object works, got expected error: {}", e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Test exists method | ||||
|     let exists_result = manager.exists("non-existent-service"); | ||||
|     match exists_result { | ||||
|         Ok(false) => println!("✓ Trait object exists method works correctly"), | ||||
|         Ok(true) => println!("⚠ Unexpectedly found non-existent service"), | ||||
|         Err(_) => println!("✓ Trait object exists method works (with error)"), | ||||
|     } | ||||
| } | ||||
							
								
								
									
										177
									
								
								_archive/service_manager/tests/rhai/service_lifecycle.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								_archive/service_manager/tests/rhai/service_lifecycle.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,177 @@ | ||||
| // Service lifecycle management test script | ||||
| // This script tests REAL complete service lifecycle scenarios | ||||
|  | ||||
| print("=== Service Lifecycle Management Test ==="); | ||||
|  | ||||
| // Create service manager | ||||
| let manager = create_service_manager(); | ||||
| print("✓ Service manager created"); | ||||
|  | ||||
| // Test configuration - real services for testing | ||||
| let test_services = [ | ||||
|     #{ | ||||
|         name: "lifecycle-test-1", | ||||
|         binary_path: "/bin/echo", | ||||
|         args: ["Lifecycle test 1"], | ||||
|         working_directory: "/tmp", | ||||
|         environment: #{}, | ||||
|         auto_restart: false | ||||
|     }, | ||||
|     #{ | ||||
|         name: "lifecycle-test-2", | ||||
|         binary_path: "/bin/echo", | ||||
|         args: ["Lifecycle test 2"], | ||||
|         working_directory: "/tmp", | ||||
|         environment: #{ "TEST_VAR": "test_value" }, | ||||
|         auto_restart: false | ||||
|     } | ||||
| ]; | ||||
|  | ||||
| let total_tests = 0; | ||||
| let passed_tests = 0; | ||||
|  | ||||
| // Test 1: Service Creation and Start | ||||
| print("\n1. Testing service creation and start..."); | ||||
| for service_config in test_services { | ||||
|     print(`\nStarting service: ${service_config.name}`); | ||||
|     try { | ||||
|         start(manager, service_config); | ||||
|         print(`  ✓ Service ${service_config.name} started successfully`); | ||||
|         passed_tests += 1; | ||||
|     } catch(e) { | ||||
|         print(`  ✗ Service ${service_config.name} start failed: ${e}`); | ||||
|     } | ||||
|     total_tests += 1; | ||||
| } | ||||
|  | ||||
| // Test 2: Service Existence Check | ||||
| print("\n2. Testing service existence checks..."); | ||||
| for service_config in test_services { | ||||
|     print(`\nChecking existence of: ${service_config.name}`); | ||||
|     try { | ||||
|         let service_exists = exists(manager, service_config.name); | ||||
|         if service_exists { | ||||
|             print(`  ✓ Service ${service_config.name} exists: ${service_exists}`); | ||||
|             passed_tests += 1; | ||||
|         } else { | ||||
|             print(`  ✗ Service ${service_config.name} doesn't exist after start`); | ||||
|         } | ||||
|     } catch(e) { | ||||
|         print(`  ✗ Existence check failed for ${service_config.name}: ${e}`); | ||||
|     } | ||||
|     total_tests += 1; | ||||
| } | ||||
|  | ||||
| // Test 3: Status Check | ||||
| print("\n3. Testing status checks..."); | ||||
| for service_config in test_services { | ||||
|     print(`\nChecking status of: ${service_config.name}`); | ||||
|     try { | ||||
|         let service_status = status(manager, service_config.name); | ||||
|         print(`  ✓ Service ${service_config.name} status: ${service_status}`); | ||||
|         passed_tests += 1; | ||||
|     } catch(e) { | ||||
|         print(`  ✗ Status check failed for ${service_config.name}: ${e}`); | ||||
|     } | ||||
|     total_tests += 1; | ||||
| } | ||||
|  | ||||
| // Test 4: Service List Check | ||||
| print("\n4. Testing service list..."); | ||||
| try { | ||||
|     let services = list(manager); | ||||
|     print(`  ✓ Service list retrieved (${services.len()} services)`); | ||||
|  | ||||
|     // Check if our test services are in the list | ||||
|     for service_config in test_services { | ||||
|         let found = false; | ||||
|         for service in services { | ||||
|             if service.contains(service_config.name) { | ||||
|                 found = true; | ||||
|                 print(`    ✓ Found ${service_config.name} in list`); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         if !found { | ||||
|             print(`    ⚠ ${service_config.name} not found in service list`); | ||||
|         } | ||||
|     } | ||||
|     passed_tests += 1; | ||||
| } catch(e) { | ||||
|     print(`  ✗ Service list failed: ${e}`); | ||||
| } | ||||
| total_tests += 1; | ||||
|  | ||||
| // Test 5: Service Stop | ||||
| print("\n5. Testing service stop..."); | ||||
| for service_config in test_services { | ||||
|     print(`\nStopping service: ${service_config.name}`); | ||||
|     try { | ||||
|         stop(manager, service_config.name); | ||||
|         print(`  ✓ Service ${service_config.name} stopped successfully`); | ||||
|         passed_tests += 1; | ||||
|     } catch(e) { | ||||
|         print(`  ✗ Service ${service_config.name} stop failed: ${e}`); | ||||
|     } | ||||
|     total_tests += 1; | ||||
| } | ||||
|  | ||||
| // Test 6: Service Removal | ||||
| print("\n6. Testing service removal..."); | ||||
| for service_config in test_services { | ||||
|     print(`\nRemoving service: ${service_config.name}`); | ||||
|     try { | ||||
|         remove(manager, service_config.name); | ||||
|         print(`  ✓ Service ${service_config.name} removed successfully`); | ||||
|         passed_tests += 1; | ||||
|     } catch(e) { | ||||
|         print(`  ✗ Service ${service_config.name} removal failed: ${e}`); | ||||
|     } | ||||
|     total_tests += 1; | ||||
| } | ||||
|  | ||||
| // Test 7: Cleanup Verification | ||||
| print("\n7. Testing cleanup verification..."); | ||||
| for service_config in test_services { | ||||
|     print(`\nVerifying removal of: ${service_config.name}`); | ||||
|     try { | ||||
|         let exists_after_remove = exists(manager, service_config.name); | ||||
|         if !exists_after_remove { | ||||
|             print(`  ✓ Service ${service_config.name} correctly doesn't exist after removal`); | ||||
|             passed_tests += 1; | ||||
|         } else { | ||||
|             print(`  ✗ Service ${service_config.name} still exists after removal`); | ||||
|         } | ||||
|     } catch(e) { | ||||
|         print(`  ✗ Cleanup verification failed for ${service_config.name}: ${e}`); | ||||
|     } | ||||
|     total_tests += 1; | ||||
| } | ||||
|  | ||||
| // Test Summary | ||||
| print("\n=== Lifecycle Test Summary ==="); | ||||
| print(`Services tested: ${test_services.len()}`); | ||||
| print(`Total operations: ${total_tests}`); | ||||
| print(`Successful operations: ${passed_tests}`); | ||||
| print(`Failed operations: ${total_tests - passed_tests}`); | ||||
| print(`Success rate: ${(passed_tests * 100) / total_tests}%`); | ||||
|  | ||||
| if passed_tests == total_tests { | ||||
|     print("\n🎉 All lifecycle tests passed!"); | ||||
|     print("Service manager is working correctly across all scenarios."); | ||||
| } else { | ||||
|     print(`\n⚠ ${total_tests - passed_tests} test(s) failed`); | ||||
|     print("Some service manager operations need attention."); | ||||
| } | ||||
|  | ||||
| print("\n=== Service Lifecycle Test Complete ==="); | ||||
|  | ||||
| // Return test results | ||||
| #{ | ||||
|     summary: #{ | ||||
|         total_tests: total_tests, | ||||
|         passed_tests: passed_tests, | ||||
|         success_rate: (passed_tests * 100) / total_tests, | ||||
|         services_tested: test_services.len() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										218
									
								
								_archive/service_manager/tests/rhai/service_manager_basic.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								_archive/service_manager/tests/rhai/service_manager_basic.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,218 @@ | ||||
| // Basic service manager functionality test script | ||||
| // This script tests the REAL service manager through Rhai integration | ||||
|  | ||||
| print("=== Service Manager Basic Functionality Test ==="); | ||||
|  | ||||
| // Test configuration | ||||
| let test_service_name = "rhai-test-service"; | ||||
| let test_binary = "/bin/echo"; | ||||
| let test_args = ["Hello from Rhai service manager test"]; | ||||
|  | ||||
| print(`Testing service: ${test_service_name}`); | ||||
| print(`Binary: ${test_binary}`); | ||||
| print(`Args: ${test_args}`); | ||||
|  | ||||
| // Test results tracking | ||||
| let test_results = #{ | ||||
|     creation: "NOT_RUN", | ||||
|     exists_before: "NOT_RUN", | ||||
|     start: "NOT_RUN", | ||||
|     exists_after: "NOT_RUN", | ||||
|     status: "NOT_RUN", | ||||
|     list: "NOT_RUN", | ||||
|     stop: "NOT_RUN", | ||||
|     remove: "NOT_RUN", | ||||
|     cleanup: "NOT_RUN" | ||||
| }; | ||||
|  | ||||
| let passed_tests = 0; | ||||
| let total_tests = 0; | ||||
|  | ||||
| // Test 1: Service Manager Creation | ||||
| print("\n1. Testing service manager creation..."); | ||||
| try { | ||||
|     let manager = create_service_manager(); | ||||
|     print("✓ Service manager created successfully"); | ||||
|     test_results["creation"] = "PASS"; | ||||
|     passed_tests += 1; | ||||
|     total_tests += 1; | ||||
| } catch(e) { | ||||
|     print(`✗ Service manager creation failed: ${e}`); | ||||
|     test_results["creation"] = "FAIL"; | ||||
|     total_tests += 1; | ||||
|     // Return early if we can't create the manager | ||||
|     return test_results; | ||||
| } | ||||
|  | ||||
| // Create the service manager for all subsequent tests | ||||
| let manager = create_service_manager(); | ||||
|  | ||||
| // Test 2: Check if service exists before creation | ||||
| print("\n2. Testing service existence check (before creation)..."); | ||||
| try { | ||||
|     let exists_before = exists(manager, test_service_name); | ||||
|     print(`✓ Service existence check: ${exists_before}`); | ||||
|  | ||||
|     if !exists_before { | ||||
|         print("✓ Service correctly doesn't exist before creation"); | ||||
|         test_results["exists_before"] = "PASS"; | ||||
|         passed_tests += 1; | ||||
|     } else { | ||||
|         print("⚠ Service unexpectedly exists before creation"); | ||||
|         test_results["exists_before"] = "WARN"; | ||||
|     } | ||||
|     total_tests += 1; | ||||
| } catch(e) { | ||||
|     print(`✗ Service existence check failed: ${e}`); | ||||
|     test_results["exists_before"] = "FAIL"; | ||||
|     total_tests += 1; | ||||
| } | ||||
|  | ||||
| // Test 3: Start the service | ||||
| print("\n3. Testing service start..."); | ||||
| try { | ||||
|     // Create a service configuration object | ||||
|     let service_config = #{ | ||||
|         name: test_service_name, | ||||
|         binary_path: test_binary, | ||||
|         args: test_args, | ||||
|         working_directory: "/tmp", | ||||
|         environment: #{}, | ||||
|         auto_restart: false | ||||
|     }; | ||||
|  | ||||
|     start(manager, service_config); | ||||
|     print("✓ Service started successfully"); | ||||
|     test_results["start"] = "PASS"; | ||||
|     passed_tests += 1; | ||||
|     total_tests += 1; | ||||
| } catch(e) { | ||||
|     print(`✗ Service start failed: ${e}`); | ||||
|     test_results["start"] = "FAIL"; | ||||
|     total_tests += 1; | ||||
| } | ||||
|  | ||||
| // Test 4: Check if service exists after creation | ||||
| print("\n4. Testing service existence check (after creation)..."); | ||||
| try { | ||||
|     let exists_after = exists(manager, test_service_name); | ||||
|     print(`✓ Service existence check: ${exists_after}`); | ||||
|  | ||||
|     if exists_after { | ||||
|         print("✓ Service correctly exists after creation"); | ||||
|         test_results["exists_after"] = "PASS"; | ||||
|         passed_tests += 1; | ||||
|     } else { | ||||
|         print("✗ Service doesn't exist after creation"); | ||||
|         test_results["exists_after"] = "FAIL"; | ||||
|     } | ||||
|     total_tests += 1; | ||||
| } catch(e) { | ||||
|     print(`✗ Service existence check failed: ${e}`); | ||||
|     test_results["exists_after"] = "FAIL"; | ||||
|     total_tests += 1; | ||||
| } | ||||
|  | ||||
| // Test 5: Check service status | ||||
| print("\n5. Testing service status..."); | ||||
| try { | ||||
|     let service_status = status(manager, test_service_name); | ||||
|     print(`✓ Service status: ${service_status}`); | ||||
|     test_results["status"] = "PASS"; | ||||
|     passed_tests += 1; | ||||
|     total_tests += 1; | ||||
| } catch(e) { | ||||
|     print(`✗ Service status check failed: ${e}`); | ||||
|     test_results["status"] = "FAIL"; | ||||
|     total_tests += 1; | ||||
| } | ||||
|  | ||||
| // Test 6: List services | ||||
| print("\n6. Testing service list..."); | ||||
| try { | ||||
|     let services = list(manager); | ||||
|     print("✓ Service list retrieved"); | ||||
|  | ||||
|     // Skip service search due to Rhai type constraints with Vec iteration | ||||
|     print("  ⚠️  Skipping service search due to Rhai type constraints"); | ||||
|  | ||||
|     test_results["list"] = "PASS"; | ||||
|     passed_tests += 1; | ||||
|     total_tests += 1; | ||||
| } catch(e) { | ||||
|     print(`✗ Service list failed: ${e}`); | ||||
|     test_results["list"] = "FAIL"; | ||||
|     total_tests += 1; | ||||
| } | ||||
|  | ||||
| // Test 7: Stop the service | ||||
| print("\n7. Testing service stop..."); | ||||
| try { | ||||
|     stop(manager, test_service_name); | ||||
|     print(`✓ Service stopped: ${test_service_name}`); | ||||
|     test_results["stop"] = "PASS"; | ||||
|     passed_tests += 1; | ||||
|     total_tests += 1; | ||||
| } catch(e) { | ||||
|     print(`✗ Service stop failed: ${e}`); | ||||
|     test_results["stop"] = "FAIL"; | ||||
|     total_tests += 1; | ||||
| } | ||||
|  | ||||
| // Test 8: Remove the service | ||||
| print("\n8. Testing service remove..."); | ||||
| try { | ||||
|     remove(manager, test_service_name); | ||||
|     print(`✓ Service removed: ${test_service_name}`); | ||||
|     test_results["remove"] = "PASS"; | ||||
|     passed_tests += 1; | ||||
|     total_tests += 1; | ||||
| } catch(e) { | ||||
|     print(`✗ Service remove failed: ${e}`); | ||||
|     test_results["remove"] = "FAIL"; | ||||
|     total_tests += 1; | ||||
| } | ||||
|  | ||||
| // Test 9: Verify cleanup | ||||
| print("\n9. Testing cleanup verification..."); | ||||
| try { | ||||
|     let exists_after_remove = exists(manager, test_service_name); | ||||
|     if !exists_after_remove { | ||||
|         print("✓ Service correctly doesn't exist after removal"); | ||||
|         test_results["cleanup"] = "PASS"; | ||||
|         passed_tests += 1; | ||||
|     } else { | ||||
|         print("✗ Service still exists after removal"); | ||||
|         test_results["cleanup"] = "FAIL"; | ||||
|     } | ||||
|     total_tests += 1; | ||||
| } catch(e) { | ||||
|     print(`✗ Cleanup verification failed: ${e}`); | ||||
|     test_results["cleanup"] = "FAIL"; | ||||
|     total_tests += 1; | ||||
| } | ||||
|  | ||||
| // Test Summary | ||||
| print("\n=== Test Summary ==="); | ||||
| print(`Total tests: ${total_tests}`); | ||||
| print(`Passed: ${passed_tests}`); | ||||
| print(`Failed: ${total_tests - passed_tests}`); | ||||
| print(`Success rate: ${(passed_tests * 100) / total_tests}%`); | ||||
|  | ||||
| print("\nDetailed Results:"); | ||||
| for test_name in test_results.keys() { | ||||
|     let result = test_results[test_name]; | ||||
|     let status_icon = if result == "PASS" { "✓" } else if result == "FAIL" { "✗" } else { "⚠" }; | ||||
|     print(`  ${status_icon} ${test_name}: ${result}`); | ||||
| } | ||||
|  | ||||
| if passed_tests == total_tests { | ||||
|     print("\n🎉 All tests passed!"); | ||||
| } else { | ||||
|     print(`\n⚠ ${total_tests - passed_tests} test(s) failed`); | ||||
| } | ||||
|  | ||||
| print("\n=== Service Manager Basic Test Complete ==="); | ||||
|  | ||||
| // Return test results for potential use by calling code | ||||
| test_results | ||||
							
								
								
									
										252
									
								
								_archive/service_manager/tests/rhai_integration_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								_archive/service_manager/tests/rhai_integration_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,252 @@ | ||||
| use rhai::{Engine, EvalAltResult}; | ||||
| use std::fs; | ||||
| use std::path::Path; | ||||
|  | ||||
| /// Helper function to create a Rhai engine for service manager testing | ||||
| fn create_service_manager_engine() -> Result<Engine, Box<EvalAltResult>> { | ||||
|     #[cfg(feature = "rhai")] | ||||
|     { | ||||
|         let mut engine = Engine::new(); | ||||
|         // Register the service manager module for real testing | ||||
|         sal_service_manager::rhai::register_service_manager_module(&mut engine)?; | ||||
|         Ok(engine) | ||||
|     } | ||||
|     #[cfg(not(feature = "rhai"))] | ||||
|     { | ||||
|         Ok(Engine::new()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Helper function to run a Rhai script file | ||||
| fn run_rhai_script(script_path: &str) -> Result<rhai::Dynamic, Box<EvalAltResult>> { | ||||
|     let engine = create_service_manager_engine()?; | ||||
|  | ||||
|     // Read the script file | ||||
|     let script_content = fs::read_to_string(script_path) | ||||
|         .map_err(|e| format!("Failed to read script file {}: {}", script_path, e))?; | ||||
|  | ||||
|     // Execute the script | ||||
|     engine.eval::<rhai::Dynamic>(&script_content) | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_service_manager_basic() { | ||||
|     let script_path = "tests/rhai/service_manager_basic.rhai"; | ||||
|  | ||||
|     if !Path::new(script_path).exists() { | ||||
|         println!("⚠ Skipping test: Rhai script not found at {}", script_path); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     println!("Running Rhai service manager basic test..."); | ||||
|  | ||||
|     match run_rhai_script(script_path) { | ||||
|         Ok(result) => { | ||||
|             println!("✓ Rhai basic test completed successfully"); | ||||
|  | ||||
|             // Try to extract test results if the script returns them | ||||
|             if let Some(map) = result.try_cast::<rhai::Map>() { | ||||
|                 println!("Test results received from Rhai script:"); | ||||
|                 for (key, value) in map.iter() { | ||||
|                     println!("  {}: {:?}", key, value); | ||||
|                 } | ||||
|  | ||||
|                 // Check if all tests passed | ||||
|                 let all_passed = map.values().all(|v| { | ||||
|                     if let Some(s) = v.clone().try_cast::<String>() { | ||||
|                         s == "PASS" | ||||
|                     } else { | ||||
|                         false | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 if all_passed { | ||||
|                     println!("✓ All Rhai tests reported as PASS"); | ||||
|                 } else { | ||||
|                     println!("⚠ Some Rhai tests did not pass"); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Err(e) => { | ||||
|             println!("✗ Rhai basic test failed: {}", e); | ||||
|             assert!(false, "Rhai script execution failed: {}", e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_service_lifecycle() { | ||||
|     let script_path = "tests/rhai/service_lifecycle.rhai"; | ||||
|  | ||||
|     if !Path::new(script_path).exists() { | ||||
|         println!("⚠ Skipping test: Rhai script not found at {}", script_path); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     println!("Running Rhai service lifecycle test..."); | ||||
|  | ||||
|     match run_rhai_script(script_path) { | ||||
|         Ok(result) => { | ||||
|             println!("✓ Rhai lifecycle test completed successfully"); | ||||
|  | ||||
|             // Try to extract test results if the script returns them | ||||
|             if let Some(map) = result.try_cast::<rhai::Map>() { | ||||
|                 println!("Lifecycle test results received from Rhai script:"); | ||||
|  | ||||
|                 // Extract summary if available | ||||
|                 if let Some(summary) = map.get("summary") { | ||||
|                     if let Some(summary_map) = summary.clone().try_cast::<rhai::Map>() { | ||||
|                         println!("Summary:"); | ||||
|                         for (key, value) in summary_map.iter() { | ||||
|                             println!("  {}: {:?}", key, value); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Extract performance metrics if available | ||||
|                 if let Some(performance) = map.get("performance") { | ||||
|                     if let Some(perf_map) = performance.clone().try_cast::<rhai::Map>() { | ||||
|                         println!("Performance:"); | ||||
|                         for (key, value) in perf_map.iter() { | ||||
|                             println!("  {}: {:?}", key, value); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Err(e) => { | ||||
|             println!("✗ Rhai lifecycle test failed: {}", e); | ||||
|             assert!(false, "Rhai script execution failed: {}", e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_engine_functionality() { | ||||
|     println!("Testing basic Rhai engine functionality..."); | ||||
|  | ||||
|     let engine = create_service_manager_engine().expect("Failed to create Rhai engine"); | ||||
|  | ||||
|     // Test basic Rhai functionality | ||||
|     let test_script = r#" | ||||
|         let test_results = #{ | ||||
|             basic_math: 2 + 2 == 4, | ||||
|             string_ops: "hello".len() == 5, | ||||
|             array_ops: [1, 2, 3].len() == 3, | ||||
|             map_ops: #{ a: 1, b: 2 }.len() == 2 | ||||
|         }; | ||||
|          | ||||
|         let all_passed = true; | ||||
|         for result in test_results.values() { | ||||
|             if !result { | ||||
|                 all_passed = false; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         #{ | ||||
|             results: test_results, | ||||
|             all_passed: all_passed | ||||
|         } | ||||
|     "#; | ||||
|  | ||||
|     match engine.eval::<rhai::Dynamic>(test_script) { | ||||
|         Ok(result) => { | ||||
|             if let Some(map) = result.try_cast::<rhai::Map>() { | ||||
|                 if let Some(all_passed) = map.get("all_passed") { | ||||
|                     if let Some(passed) = all_passed.clone().try_cast::<bool>() { | ||||
|                         if passed { | ||||
|                             println!("✓ All basic Rhai functionality tests passed"); | ||||
|                         } else { | ||||
|                             println!("✗ Some basic Rhai functionality tests failed"); | ||||
|                             assert!(false, "Basic Rhai tests failed"); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if let Some(results) = map.get("results") { | ||||
|                     if let Some(results_map) = results.clone().try_cast::<rhai::Map>() { | ||||
|                         println!("Detailed results:"); | ||||
|                         for (test_name, result) in results_map.iter() { | ||||
|                             let status = if let Some(passed) = result.clone().try_cast::<bool>() { | ||||
|                                 if passed { | ||||
|                                     "✓" | ||||
|                                 } else { | ||||
|                                     "✗" | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 "?" | ||||
|                             }; | ||||
|                             println!("  {} {}: {:?}", status, test_name, result); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Err(e) => { | ||||
|             println!("✗ Basic Rhai functionality test failed: {}", e); | ||||
|             assert!(false, "Basic Rhai test failed: {}", e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_script_error_handling() { | ||||
|     println!("Testing Rhai error handling..."); | ||||
|  | ||||
|     let engine = create_service_manager_engine().expect("Failed to create Rhai engine"); | ||||
|  | ||||
|     // Test script with intentional error | ||||
|     let error_script = r#" | ||||
|         let result = "test"; | ||||
|         result.non_existent_method(); // This should cause an error | ||||
|     "#; | ||||
|  | ||||
|     match engine.eval::<rhai::Dynamic>(error_script) { | ||||
|         Ok(_) => { | ||||
|             println!("⚠ Expected error but script succeeded"); | ||||
|             assert!( | ||||
|                 false, | ||||
|                 "Error handling test failed - expected error but got success" | ||||
|             ); | ||||
|         } | ||||
|         Err(e) => { | ||||
|             println!("✓ Error correctly caught: {}", e); | ||||
|             // Verify it's the expected type of error | ||||
|             assert!(e.to_string().contains("method") || e.to_string().contains("function")); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_script_files_exist() { | ||||
|     println!("Checking that Rhai test scripts exist..."); | ||||
|  | ||||
|     let script_files = [ | ||||
|         "tests/rhai/service_manager_basic.rhai", | ||||
|         "tests/rhai/service_lifecycle.rhai", | ||||
|     ]; | ||||
|  | ||||
|     for script_file in &script_files { | ||||
|         if Path::new(script_file).exists() { | ||||
|             println!("✓ Found script: {}", script_file); | ||||
|  | ||||
|             // Verify the file is readable and not empty | ||||
|             match fs::read_to_string(script_file) { | ||||
|                 Ok(content) => { | ||||
|                     if content.trim().is_empty() { | ||||
|                         assert!(false, "Script file {} is empty", script_file); | ||||
|                     } | ||||
|                     println!("  Content length: {} characters", content.len()); | ||||
|                 } | ||||
|                 Err(e) => { | ||||
|                     assert!(false, "Failed to read script file {}: {}", script_file, e); | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             assert!(false, "Required script file not found: {}", script_file); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     println!("✓ All required Rhai script files exist and are readable"); | ||||
| } | ||||
							
								
								
									
										317
									
								
								_archive/service_manager/tests/zinit_integration_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								_archive/service_manager/tests/zinit_integration_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,317 @@ | ||||
| use sal_service_manager::{ | ||||
|     ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus, ZinitServiceManager, | ||||
| }; | ||||
| use std::collections::HashMap; | ||||
| use std::time::Duration; | ||||
| use tokio::time::sleep; | ||||
|  | ||||
| /// Helper function to find an available Zinit socket path | ||||
| async fn get_available_socket_path() -> Option<String> { | ||||
|     let socket_paths = [ | ||||
|         "/var/run/zinit.sock", | ||||
|         "/tmp/zinit.sock", | ||||
|         "/run/zinit.sock", | ||||
|         "./zinit.sock", | ||||
|     ]; | ||||
|  | ||||
|     for path in &socket_paths { | ||||
|         // Try to create a ZinitServiceManager to test connectivity | ||||
|         if let Ok(manager) = ZinitServiceManager::new(path) { | ||||
|             // Test if we can list services (basic connectivity test) | ||||
|             if manager.list().is_ok() { | ||||
|                 println!("✓ Found working Zinit socket at: {}", path); | ||||
|                 return Some(path.to_string()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     None | ||||
| } | ||||
|  | ||||
| /// Helper function to clean up test services | ||||
| async fn cleanup_test_service(manager: &dyn ServiceManager, service_name: &str) { | ||||
|     let _ = manager.stop(service_name); | ||||
|     let _ = manager.remove(service_name); | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_zinit_service_manager_creation() { | ||||
|     if let Some(socket_path) = get_available_socket_path().await { | ||||
|         let manager = ZinitServiceManager::new(&socket_path); | ||||
|         assert!( | ||||
|             manager.is_ok(), | ||||
|             "Should be able to create ZinitServiceManager" | ||||
|         ); | ||||
|  | ||||
|         let manager = manager.unwrap(); | ||||
|  | ||||
|         // Test basic connectivity by listing services | ||||
|         let list_result = manager.list(); | ||||
|         assert!(list_result.is_ok(), "Should be able to list services"); | ||||
|  | ||||
|         println!("✓ ZinitServiceManager created successfully"); | ||||
|     } else { | ||||
|         println!("⚠ Skipping test_zinit_service_manager_creation: No Zinit socket available"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_service_lifecycle() { | ||||
|     if let Some(socket_path) = get_available_socket_path().await { | ||||
|         let manager = ZinitServiceManager::new(&socket_path).expect("Failed to create manager"); | ||||
|         let service_name = "test-lifecycle-service"; | ||||
|  | ||||
|         // Clean up any existing service first | ||||
|         cleanup_test_service(&manager, service_name).await; | ||||
|  | ||||
|         let config = ServiceConfig { | ||||
|             name: service_name.to_string(), | ||||
|             binary_path: "echo".to_string(), | ||||
|             args: vec!["Hello from lifecycle test".to_string()], | ||||
|             working_directory: Some("/tmp".to_string()), | ||||
|             environment: HashMap::new(), | ||||
|             auto_restart: false, | ||||
|         }; | ||||
|  | ||||
|         // Test service creation and start | ||||
|         println!("Testing service creation and start..."); | ||||
|         let start_result = manager.start(&config); | ||||
|         match start_result { | ||||
|             Ok(_) => { | ||||
|                 println!("✓ Service started successfully"); | ||||
|  | ||||
|                 // Wait a bit for the service to run | ||||
|                 sleep(Duration::from_millis(500)).await; | ||||
|  | ||||
|                 // Test service exists | ||||
|                 let exists = manager.exists(service_name); | ||||
|                 assert!(exists.is_ok(), "Should be able to check if service exists"); | ||||
|  | ||||
|                 if let Ok(true) = exists { | ||||
|                     println!("✓ Service exists check passed"); | ||||
|  | ||||
|                     // Test service status | ||||
|                     let status_result = manager.status(service_name); | ||||
|                     match status_result { | ||||
|                         Ok(status) => { | ||||
|                             println!("✓ Service status: {:?}", status); | ||||
|                             assert!( | ||||
|                                 matches!(status, ServiceStatus::Running | ServiceStatus::Stopped), | ||||
|                                 "Service should be running or stopped (for oneshot)" | ||||
|                             ); | ||||
|                         } | ||||
|                         Err(e) => println!("⚠ Status check failed: {}", e), | ||||
|                     } | ||||
|  | ||||
|                     // Test service logs | ||||
|                     let logs_result = manager.logs(service_name, None); | ||||
|                     match logs_result { | ||||
|                         Ok(logs) => { | ||||
|                             println!("✓ Retrieved logs: {}", logs.len()); | ||||
|                             // For echo command, we should have some output | ||||
|                             assert!( | ||||
|                                 !logs.is_empty() || logs.contains("Hello"), | ||||
|                                 "Should have log output" | ||||
|                             ); | ||||
|                         } | ||||
|                         Err(e) => println!("⚠ Logs retrieval failed: {}", e), | ||||
|                     } | ||||
|  | ||||
|                     // Test service list | ||||
|                     let list_result = manager.list(); | ||||
|                     match list_result { | ||||
|                         Ok(services) => { | ||||
|                             println!("✓ Listed {} services", services.len()); | ||||
|                             assert!( | ||||
|                                 services.contains(&service_name.to_string()), | ||||
|                                 "Service should appear in list" | ||||
|                             ); | ||||
|                         } | ||||
|                         Err(e) => println!("⚠ List services failed: {}", e), | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Test service stop | ||||
|                 println!("Testing service stop..."); | ||||
|                 let stop_result = manager.stop(service_name); | ||||
|                 match stop_result { | ||||
|                     Ok(_) => println!("✓ Service stopped successfully"), | ||||
|                     Err(e) => println!("⚠ Stop failed: {}", e), | ||||
|                 } | ||||
|  | ||||
|                 // Test service removal | ||||
|                 println!("Testing service removal..."); | ||||
|                 let remove_result = manager.remove(service_name); | ||||
|                 match remove_result { | ||||
|                     Ok(_) => println!("✓ Service removed successfully"), | ||||
|                     Err(e) => println!("⚠ Remove failed: {}", e), | ||||
|                 } | ||||
|             } | ||||
|             Err(e) => { | ||||
|                 println!("⚠ Service creation/start failed: {}", e); | ||||
|                 // This might be expected if zinit doesn't allow service creation | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Final cleanup | ||||
|         cleanup_test_service(&manager, service_name).await; | ||||
|     } else { | ||||
|         println!("⚠ Skipping test_service_lifecycle: No Zinit socket available"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_service_start_and_confirm() { | ||||
|     if let Some(socket_path) = get_available_socket_path().await { | ||||
|         let manager = ZinitServiceManager::new(&socket_path).expect("Failed to create manager"); | ||||
|         let service_name = "test-start-confirm-service"; | ||||
|  | ||||
|         // Clean up any existing service first | ||||
|         cleanup_test_service(&manager, service_name).await; | ||||
|  | ||||
|         let config = ServiceConfig { | ||||
|             name: service_name.to_string(), | ||||
|             binary_path: "sleep".to_string(), | ||||
|             args: vec!["5".to_string()], // Sleep for 5 seconds | ||||
|             working_directory: Some("/tmp".to_string()), | ||||
|             environment: HashMap::new(), | ||||
|             auto_restart: false, | ||||
|         }; | ||||
|  | ||||
|         // Test start_and_confirm with timeout | ||||
|         println!("Testing start_and_confirm with timeout..."); | ||||
|         let start_result = manager.start_and_confirm(&config, 10); | ||||
|         match start_result { | ||||
|             Ok(_) => { | ||||
|                 println!("✓ Service started and confirmed successfully"); | ||||
|  | ||||
|                 // Verify it's actually running | ||||
|                 let status = manager.status(service_name); | ||||
|                 match status { | ||||
|                     Ok(ServiceStatus::Running) => println!("✓ Service confirmed running"), | ||||
|                     Ok(other_status) => { | ||||
|                         println!("⚠ Service in unexpected state: {:?}", other_status) | ||||
|                     } | ||||
|                     Err(e) => println!("⚠ Status check failed: {}", e), | ||||
|                 } | ||||
|             } | ||||
|             Err(e) => { | ||||
|                 println!("⚠ start_and_confirm failed: {}", e); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Test start_existing_and_confirm | ||||
|         println!("Testing start_existing_and_confirm..."); | ||||
|         let start_existing_result = manager.start_existing_and_confirm(service_name, 5); | ||||
|         match start_existing_result { | ||||
|             Ok(_) => println!("✓ start_existing_and_confirm succeeded"), | ||||
|             Err(e) => println!("⚠ start_existing_and_confirm failed: {}", e), | ||||
|         } | ||||
|  | ||||
|         // Cleanup | ||||
|         cleanup_test_service(&manager, service_name).await; | ||||
|     } else { | ||||
|         println!("⚠ Skipping test_service_start_and_confirm: No Zinit socket available"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_service_restart() { | ||||
|     if let Some(socket_path) = get_available_socket_path().await { | ||||
|         let manager = ZinitServiceManager::new(&socket_path).expect("Failed to create manager"); | ||||
|         let service_name = "test-restart-service"; | ||||
|  | ||||
|         // Clean up any existing service first | ||||
|         cleanup_test_service(&manager, service_name).await; | ||||
|  | ||||
|         let config = ServiceConfig { | ||||
|             name: service_name.to_string(), | ||||
|             binary_path: "echo".to_string(), | ||||
|             args: vec!["Restart test".to_string()], | ||||
|             working_directory: Some("/tmp".to_string()), | ||||
|             environment: HashMap::new(), | ||||
|             auto_restart: true, // Enable auto-restart for this test | ||||
|         }; | ||||
|  | ||||
|         // Start the service first | ||||
|         let start_result = manager.start(&config); | ||||
|         if start_result.is_ok() { | ||||
|             // Wait for service to be established | ||||
|             sleep(Duration::from_millis(1000)).await; | ||||
|  | ||||
|             // Test restart | ||||
|             println!("Testing service restart..."); | ||||
|             let restart_result = manager.restart(service_name); | ||||
|             match restart_result { | ||||
|                 Ok(_) => { | ||||
|                     println!("✓ Service restarted successfully"); | ||||
|  | ||||
|                     // Wait and check status | ||||
|                     sleep(Duration::from_millis(500)).await; | ||||
|  | ||||
|                     let status_result = manager.status(service_name); | ||||
|                     match status_result { | ||||
|                         Ok(status) => { | ||||
|                             println!("✓ Service state after restart: {:?}", status); | ||||
|                         } | ||||
|                         Err(e) => println!("⚠ Status check after restart failed: {}", e), | ||||
|                     } | ||||
|                 } | ||||
|                 Err(e) => { | ||||
|                     println!("⚠ Restart failed: {}", e); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Cleanup | ||||
|         cleanup_test_service(&manager, service_name).await; | ||||
|     } else { | ||||
|         println!("⚠ Skipping test_service_restart: No Zinit socket available"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_error_handling() { | ||||
|     if let Some(socket_path) = get_available_socket_path().await { | ||||
|         let manager = ZinitServiceManager::new(&socket_path).expect("Failed to create manager"); | ||||
|  | ||||
|         // Test operations on non-existent service | ||||
|         let non_existent_service = "non-existent-service-12345"; | ||||
|  | ||||
|         // Test status of non-existent service | ||||
|         let status_result = manager.status(non_existent_service); | ||||
|         match status_result { | ||||
|             Err(ServiceManagerError::ServiceNotFound(_)) => { | ||||
|                 println!("✓ Correctly returned ServiceNotFound for non-existent service"); | ||||
|             } | ||||
|             Err(other_error) => { | ||||
|                 println!( | ||||
|                     "⚠ Got different error for non-existent service: {}", | ||||
|                     other_error | ||||
|                 ); | ||||
|             } | ||||
|             Ok(_) => { | ||||
|                 println!("⚠ Unexpectedly found non-existent service"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Test exists for non-existent service | ||||
|         let exists_result = manager.exists(non_existent_service); | ||||
|         match exists_result { | ||||
|             Ok(false) => println!("✓ Correctly reported non-existent service as not existing"), | ||||
|             Ok(true) => println!("⚠ Incorrectly reported non-existent service as existing"), | ||||
|             Err(e) => println!("⚠ Error checking existence: {}", e), | ||||
|         } | ||||
|  | ||||
|         // Test stop of non-existent service | ||||
|         let stop_result = manager.stop(non_existent_service); | ||||
|         match stop_result { | ||||
|             Err(_) => println!("✓ Correctly failed to stop non-existent service"), | ||||
|             Ok(_) => println!("⚠ Unexpectedly succeeded in stopping non-existent service"), | ||||
|         } | ||||
|  | ||||
|         println!("✓ Error handling tests completed"); | ||||
|     } else { | ||||
|         println!("⚠ Skipping test_error_handling: No Zinit socket available"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										0
									
								
								cargo_instructions.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								cargo_instructions.md
									
									
									
									
									
										Normal file
									
								
							| @@ -1,64 +1,76 @@ | ||||
| # Hero Vault Cryptography Examples | ||||
| # SAL Vault Examples | ||||
|  | ||||
| This directory contains examples demonstrating the Hero Vault cryptography functionality integrated into the SAL project. | ||||
| This directory contains examples demonstrating the SAL Vault functionality. | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| Hero Vault provides cryptographic operations including: | ||||
| SAL Vault provides secure key management and cryptographic operations including: | ||||
|  | ||||
| - Key space management (creation, loading, encryption, decryption) | ||||
| - Keypair management (creation, selection, listing) | ||||
| - Digital signatures (signing and verification) | ||||
| - Symmetric encryption (key generation, encryption, decryption) | ||||
| - Ethereum wallet functionality | ||||
| - Smart contract interactions | ||||
| - Key-value store with encryption | ||||
| - Vault creation and management | ||||
| - KeySpace operations (encrypted key-value stores) | ||||
| - Symmetric key generation and operations | ||||
| - Asymmetric key operations (signing and verification) | ||||
| - Secure key derivation from passwords | ||||
|  | ||||
| ## Example Files | ||||
| ## Current Status | ||||
|  | ||||
| - `example.rhai` - Basic example demonstrating key management, signing, and encryption | ||||
| - `advanced_example.rhai` - Advanced example with error handling, conditional logic, and more complex operations | ||||
| - `key_persistence_example.rhai` - Demonstrates creating and saving a key space to disk | ||||
| - `load_existing_space.rhai` - Shows how to load a previously created key space and use its keypairs | ||||
| - `contract_example.rhai` - Demonstrates loading a contract ABI and interacting with smart contracts | ||||
| - `agung_send_transaction.rhai` - Demonstrates sending native tokens on the Agung network | ||||
| - `agung_contract_with_args.rhai` - Shows how to interact with contracts with arguments on Agung | ||||
| ⚠️ **Note**: The vault module is currently being updated to use Lee's implementation. | ||||
| The Rhai scripting integration is temporarily disabled while we adapt the examples | ||||
| to work with the new vault API. | ||||
|  | ||||
| ## Running the Examples | ||||
| ## Available Operations | ||||
|  | ||||
| You can run the examples using the `herodo` tool that comes with the SAL project: | ||||
| - **Vault Management**: Create and manage vault instances | ||||
| - **KeySpace Operations**: Open encrypted key-value stores within vaults | ||||
| - **Symmetric Encryption**: Generate keys and encrypt/decrypt data | ||||
| - **Asymmetric Operations**: Create keypairs, sign messages, verify signatures | ||||
|  | ||||
| ```bash | ||||
| # Run a single example | ||||
| herodo --path example.rhai | ||||
| ## Example Files (Legacy - Sameh's Implementation) | ||||
|  | ||||
| # Run all examples using the provided script | ||||
| ./run_examples.sh | ||||
| ⚠️ **These examples are currently archived and use the previous vault implementation**: | ||||
|  | ||||
| - `_archive/example.rhai` - Basic example demonstrating key management, signing, and encryption | ||||
| - `_archive/advanced_example.rhai` - Advanced example with error handling and complex operations | ||||
| - `_archive/key_persistence_example.rhai` - Demonstrates creating and saving a key space to disk | ||||
| - `_archive/load_existing_space.rhai` - Shows how to load a previously created key space | ||||
| - `_archive/contract_example.rhai` - Demonstrates smart contract interactions (Ethereum) | ||||
| - `_archive/agung_send_transaction.rhai` - Demonstrates Ethereum transactions on Agung network | ||||
| - `_archive/agung_contract_with_args.rhai` - Shows contract interactions with arguments | ||||
|  | ||||
| ## Current Implementation (Lee's Vault) | ||||
|  | ||||
| The current vault implementation provides: | ||||
|  | ||||
| ```rust | ||||
| // Create a new vault | ||||
| let vault = Vault::new(&path).await?; | ||||
|  | ||||
| // Open an encrypted keyspace | ||||
| let keyspace = vault.open_keyspace("my_space", "password").await?; | ||||
|  | ||||
| // Perform cryptographic operations | ||||
| // (API documentation coming soon) | ||||
| ``` | ||||
|  | ||||
| ## Key Space Storage | ||||
| ## Migration Status | ||||
|  | ||||
| Key spaces are stored in the `~/.hero-vault/key-spaces/` directory by default. Each key space is stored in a separate JSON file named after the key space (e.g., `my_space.json`). | ||||
|  | ||||
| ## Ethereum Functionality | ||||
|  | ||||
| The Hero Vault module provides comprehensive Ethereum wallet functionality: | ||||
|  | ||||
| - Creating and managing wallets for different networks | ||||
| - Sending ETH transactions | ||||
| - Checking balances | ||||
| - Interacting with smart contracts (read and write functions) | ||||
| - Support for multiple networks (Ethereum, Gnosis, Peaq, Agung, etc.) | ||||
| - ✅ **Vault Core**: Lee's implementation is active | ||||
| - ✅ **Archive**: Sameh's implementation preserved in `vault/_archive/` | ||||
| - ⏳ **Rhai Integration**: Being developed for Lee's implementation | ||||
| - ⏳ **Examples**: Will be updated to use Lee's API | ||||
| - ❌ **Ethereum Features**: Not available in Lee's implementation | ||||
|  | ||||
| ## Security | ||||
|  | ||||
| Key spaces are encrypted with ChaCha20Poly1305 using a key derived from the provided password. The encryption ensures that the key material is secure at rest. | ||||
| The vault uses: | ||||
|  | ||||
| ## Best Practices | ||||
| - **ChaCha20Poly1305** for symmetric encryption | ||||
| - **Password-based key derivation** for keyspace encryption | ||||
| - **Secure key storage** with proper isolation | ||||
|  | ||||
| 1. **Use Strong Passwords**: Since the security of your key spaces depends on the strength of your passwords, use strong, unique passwords. | ||||
| 2. **Backup Key Spaces**: Regularly backup your key spaces directory to prevent data loss. | ||||
| 3. **Script Organization**: Split your scripts into logical units, with separate scripts for key creation and key usage. | ||||
| 4. **Error Handling**: Always check the return values of functions to ensure operations succeeded before proceeding. | ||||
| 5. **Network Selection**: When working with Ethereum functionality, be explicit about which network you're targeting to avoid confusion. | ||||
| 6. **Gas Management**: For Ethereum transactions, consider gas costs and set appropriate gas limits. | ||||
| ## Next Steps | ||||
|  | ||||
| 1. **Rhai Integration**: Implement Rhai bindings for Lee's vault | ||||
| 2. **New Examples**: Create examples using Lee's simpler API | ||||
| 3. **Documentation**: Complete API documentation for Lee's implementation | ||||
| 4. **Migration Guide**: Provide guidance for users migrating from Sameh's implementation | ||||
|   | ||||
							
								
								
									
										134
									
								
								examples/kubernetes/clusters/generic.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								examples/kubernetes/clusters/generic.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | ||||
| //! Generic Application Deployment Example | ||||
| //! | ||||
| //! This example shows how to deploy any containerized application using the | ||||
| //! KubernetesManager convenience methods. This works for any Docker image. | ||||
|  | ||||
| use sal_kubernetes::KubernetesManager; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| #[tokio::main] | ||||
| async fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     // Create Kubernetes manager | ||||
|     let km = KubernetesManager::new("default").await?; | ||||
|  | ||||
|     // Clean up any existing resources first | ||||
|     println!("=== Cleaning up existing resources ==="); | ||||
|     let apps_to_clean = ["web-server", "node-app", "mongodb"]; | ||||
|  | ||||
|     for app in &apps_to_clean { | ||||
|         match km.deployment_delete(app).await { | ||||
|             Ok(_) => println!("✓ Deleted existing deployment: {}", app), | ||||
|             Err(_) => println!("✓ No existing deployment to delete: {}", app), | ||||
|         } | ||||
|  | ||||
|         match km.service_delete(app).await { | ||||
|             Ok(_) => println!("✓ Deleted existing service: {}", app), | ||||
|             Err(_) => println!("✓ No existing service to delete: {}", app), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Example 1: Simple web server deployment | ||||
|     println!("\n=== Example 1: Simple Nginx Web Server ==="); | ||||
|  | ||||
|     km.deploy_application("web-server", "nginx:latest", 2, 80, None, None) | ||||
|         .await?; | ||||
|     println!("✅ Nginx web server deployed!"); | ||||
|  | ||||
|     // Example 2: Node.js application with labels | ||||
|     println!("\n=== Example 2: Node.js Application ==="); | ||||
|  | ||||
|     let mut node_labels = HashMap::new(); | ||||
|     node_labels.insert("app".to_string(), "node-app".to_string()); | ||||
|     node_labels.insert("tier".to_string(), "backend".to_string()); | ||||
|     node_labels.insert("environment".to_string(), "production".to_string()); | ||||
|  | ||||
|     // Configure Node.js environment variables | ||||
|     let mut node_env_vars = HashMap::new(); | ||||
|     node_env_vars.insert("NODE_ENV".to_string(), "production".to_string()); | ||||
|     node_env_vars.insert("PORT".to_string(), "3000".to_string()); | ||||
|     node_env_vars.insert("LOG_LEVEL".to_string(), "info".to_string()); | ||||
|     node_env_vars.insert("MAX_CONNECTIONS".to_string(), "1000".to_string()); | ||||
|  | ||||
|     km.deploy_application( | ||||
|         "node-app",          // name | ||||
|         "node:18-alpine",    // image | ||||
|         3,                   // replicas - scale to 3 instances | ||||
|         3000,                // port | ||||
|         Some(node_labels),   // labels | ||||
|         Some(node_env_vars), // environment variables | ||||
|     ) | ||||
|     .await?; | ||||
|  | ||||
|     println!("✅ Node.js application deployed!"); | ||||
|  | ||||
|     // Example 3: Database deployment (any database) | ||||
|     println!("\n=== Example 3: MongoDB Database ==="); | ||||
|  | ||||
|     let mut mongo_labels = HashMap::new(); | ||||
|     mongo_labels.insert("app".to_string(), "mongodb".to_string()); | ||||
|     mongo_labels.insert("type".to_string(), "database".to_string()); | ||||
|     mongo_labels.insert("engine".to_string(), "mongodb".to_string()); | ||||
|  | ||||
|     // Configure MongoDB environment variables | ||||
|     let mut mongo_env_vars = HashMap::new(); | ||||
|     mongo_env_vars.insert( | ||||
|         "MONGO_INITDB_ROOT_USERNAME".to_string(), | ||||
|         "admin".to_string(), | ||||
|     ); | ||||
|     mongo_env_vars.insert( | ||||
|         "MONGO_INITDB_ROOT_PASSWORD".to_string(), | ||||
|         "mongopassword".to_string(), | ||||
|     ); | ||||
|     mongo_env_vars.insert("MONGO_INITDB_DATABASE".to_string(), "myapp".to_string()); | ||||
|  | ||||
|     km.deploy_application( | ||||
|         "mongodb",            // name | ||||
|         "mongo:6.0",          // image | ||||
|         1,                    // replicas - single instance for simplicity | ||||
|         27017,                // port | ||||
|         Some(mongo_labels),   // labels | ||||
|         Some(mongo_env_vars), // environment variables | ||||
|     ) | ||||
|     .await?; | ||||
|  | ||||
|     println!("✅ MongoDB deployed!"); | ||||
|  | ||||
|     // Check status of all deployments | ||||
|     println!("\n=== Checking Deployment Status ==="); | ||||
|  | ||||
|     let deployments = km.deployments_list().await?; | ||||
|  | ||||
|     for deployment in &deployments { | ||||
|         if let Some(name) = &deployment.metadata.name { | ||||
|             let total_replicas = deployment | ||||
|                 .spec | ||||
|                 .as_ref() | ||||
|                 .and_then(|s| s.replicas) | ||||
|                 .unwrap_or(0); | ||||
|             let ready_replicas = deployment | ||||
|                 .status | ||||
|                 .as_ref() | ||||
|                 .and_then(|s| s.ready_replicas) | ||||
|                 .unwrap_or(0); | ||||
|  | ||||
|             println!( | ||||
|                 "{}: {}/{} replicas ready", | ||||
|                 name, ready_replicas, total_replicas | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     println!("\n🎉 All deployments completed!"); | ||||
|     println!("\n💡 Key Points:"); | ||||
|     println!("  • Any Docker image can be deployed using this simple interface"); | ||||
|     println!("  • Use labels to organize and identify your applications"); | ||||
|     println!( | ||||
|         "  • The same method works for databases, web servers, APIs, and any containerized app" | ||||
|     ); | ||||
|     println!("  • For advanced configuration, use the individual KubernetesManager methods"); | ||||
|     println!( | ||||
|         "  • Environment variables and resource limits can be added via direct Kubernetes API" | ||||
|     ); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										79
									
								
								examples/kubernetes/clusters/postgres.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								examples/kubernetes/clusters/postgres.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| //! PostgreSQL Cluster Deployment Example (Rhai) | ||||
| //! | ||||
| //! This script shows how to deploy a PostgreSQL cluster using Rhai scripting | ||||
| //! with the KubernetesManager convenience methods. | ||||
|  | ||||
| print("=== PostgreSQL Cluster Deployment ==="); | ||||
|  | ||||
| // Create Kubernetes manager for the database namespace | ||||
| print("Creating Kubernetes manager for 'database' namespace..."); | ||||
| let km = kubernetes_manager_new("database"); | ||||
| print("✓ Kubernetes manager created"); | ||||
|  | ||||
| // Create the namespace if it doesn't exist | ||||
| print("Creating namespace 'database' if it doesn't exist..."); | ||||
| try { | ||||
|     create_namespace(km, "database"); | ||||
|     print("✓ Namespace 'database' created"); | ||||
| } catch(e) { | ||||
|     if e.to_string().contains("already exists") { | ||||
|         print("✓ Namespace 'database' already exists"); | ||||
|     } else { | ||||
|         print("⚠️ Warning: " + e); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Clean up any existing resources first | ||||
| print("\nCleaning up any existing PostgreSQL resources..."); | ||||
| try { | ||||
|     delete_deployment(km, "postgres-cluster"); | ||||
|     print("✓ Deleted existing deployment"); | ||||
| } catch(e) { | ||||
|     print("✓ No existing deployment to delete"); | ||||
| } | ||||
|  | ||||
| try { | ||||
|     delete_service(km, "postgres-cluster"); | ||||
|     print("✓ Deleted existing service"); | ||||
| } catch(e) { | ||||
|     print("✓ No existing service to delete"); | ||||
| } | ||||
|  | ||||
| // Create PostgreSQL cluster using the convenience method | ||||
| print("\nDeploying PostgreSQL cluster..."); | ||||
|  | ||||
| try { | ||||
|     // Deploy PostgreSQL using the convenience method | ||||
|     let result = deploy_application(km, "postgres-cluster", "postgres:15", 2, 5432, #{ | ||||
|         "app": "postgres-cluster", | ||||
|         "type": "database", | ||||
|         "engine": "postgresql" | ||||
|     }, #{ | ||||
|         "POSTGRES_DB": "myapp", | ||||
|         "POSTGRES_USER": "postgres", | ||||
|         "POSTGRES_PASSWORD": "secretpassword", | ||||
|         "PGDATA": "/var/lib/postgresql/data/pgdata" | ||||
|     }); | ||||
|     print("✓ " + result); | ||||
|  | ||||
|     print("\n✅ PostgreSQL cluster deployed successfully!"); | ||||
|  | ||||
|     print("\n📋 Connection Information:"); | ||||
|     print("  Host: postgres-cluster.database.svc.cluster.local"); | ||||
|     print("  Port: 5432"); | ||||
|     print("  Database: postgres (default)"); | ||||
|     print("  Username: postgres (default)"); | ||||
|  | ||||
|     print("\n🔧 To connect from another pod:"); | ||||
|     print("  psql -h postgres-cluster.database.svc.cluster.local -U postgres"); | ||||
|  | ||||
|     print("\n💡 Next steps:"); | ||||
|     print("  • Set POSTGRES_PASSWORD environment variable"); | ||||
|     print("  • Configure persistent storage"); | ||||
|     print("  • Set up backup and monitoring"); | ||||
|  | ||||
| } catch(e) { | ||||
|     print("❌ Failed to deploy PostgreSQL cluster: " + e); | ||||
| } | ||||
|  | ||||
| print("\n=== Deployment Complete ==="); | ||||
							
								
								
									
										112
									
								
								examples/kubernetes/clusters/postgres.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								examples/kubernetes/clusters/postgres.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| //! PostgreSQL Cluster Deployment Example | ||||
| //! | ||||
| //! This example shows how to deploy a PostgreSQL cluster using the | ||||
| //! KubernetesManager convenience methods. | ||||
|  | ||||
| use sal_kubernetes::KubernetesManager; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| #[tokio::main] | ||||
| async fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     // Create Kubernetes manager for the database namespace | ||||
|     let km = KubernetesManager::new("database").await?; | ||||
|  | ||||
|     // Create the namespace if it doesn't exist | ||||
|     println!("Creating namespace 'database' if it doesn't exist..."); | ||||
|     match km.namespace_create("database").await { | ||||
|         Ok(_) => println!("✓ Namespace 'database' created"), | ||||
|         Err(e) => { | ||||
|             if e.to_string().contains("already exists") { | ||||
|                 println!("✓ Namespace 'database' already exists"); | ||||
|             } else { | ||||
|                 return Err(e.into()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Clean up any existing resources first | ||||
|     println!("Cleaning up any existing PostgreSQL resources..."); | ||||
|     match km.deployment_delete("postgres-cluster").await { | ||||
|         Ok(_) => println!("✓ Deleted existing deployment"), | ||||
|         Err(_) => println!("✓ No existing deployment to delete"), | ||||
|     } | ||||
|  | ||||
|     match km.service_delete("postgres-cluster").await { | ||||
|         Ok(_) => println!("✓ Deleted existing service"), | ||||
|         Err(_) => println!("✓ No existing service to delete"), | ||||
|     } | ||||
|  | ||||
|     // Configure PostgreSQL-specific labels | ||||
|     let mut labels = HashMap::new(); | ||||
|     labels.insert("app".to_string(), "postgres-cluster".to_string()); | ||||
|     labels.insert("type".to_string(), "database".to_string()); | ||||
|     labels.insert("engine".to_string(), "postgresql".to_string()); | ||||
|  | ||||
|     // Configure PostgreSQL environment variables | ||||
|     let mut env_vars = HashMap::new(); | ||||
|     env_vars.insert("POSTGRES_DB".to_string(), "myapp".to_string()); | ||||
|     env_vars.insert("POSTGRES_USER".to_string(), "postgres".to_string()); | ||||
|     env_vars.insert( | ||||
|         "POSTGRES_PASSWORD".to_string(), | ||||
|         "secretpassword".to_string(), | ||||
|     ); | ||||
|     env_vars.insert( | ||||
|         "PGDATA".to_string(), | ||||
|         "/var/lib/postgresql/data/pgdata".to_string(), | ||||
|     ); | ||||
|  | ||||
|     // Deploy the PostgreSQL cluster using the convenience method | ||||
|     println!("Deploying PostgreSQL cluster..."); | ||||
|     km.deploy_application( | ||||
|         "postgres-cluster", // name | ||||
|         "postgres:15",      // image | ||||
|         2,                  // replicas (1 master + 1 replica) | ||||
|         5432,               // port | ||||
|         Some(labels),       // labels | ||||
|         Some(env_vars),     // environment variables | ||||
|     ) | ||||
|     .await?; | ||||
|  | ||||
|     println!("✅ PostgreSQL cluster deployed successfully!"); | ||||
|  | ||||
|     // Check deployment status | ||||
|     let deployments = km.deployments_list().await?; | ||||
|     let postgres_deployment = deployments | ||||
|         .iter() | ||||
|         .find(|d| d.metadata.name.as_ref() == Some(&"postgres-cluster".to_string())); | ||||
|  | ||||
|     if let Some(deployment) = postgres_deployment { | ||||
|         let total_replicas = deployment | ||||
|             .spec | ||||
|             .as_ref() | ||||
|             .and_then(|s| s.replicas) | ||||
|             .unwrap_or(0); | ||||
|         let ready_replicas = deployment | ||||
|             .status | ||||
|             .as_ref() | ||||
|             .and_then(|s| s.ready_replicas) | ||||
|             .unwrap_or(0); | ||||
|  | ||||
|         println!( | ||||
|             "Deployment status: {}/{} replicas ready", | ||||
|             ready_replicas, total_replicas | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     println!("\n📋 Connection Information:"); | ||||
|     println!("  Host: postgres-cluster.database.svc.cluster.local"); | ||||
|     println!("  Port: 5432"); | ||||
|     println!("  Database: postgres (default)"); | ||||
|     println!("  Username: postgres (default)"); | ||||
|     println!("  Password: Set POSTGRES_PASSWORD environment variable"); | ||||
|  | ||||
|     println!("\n🔧 To connect from another pod:"); | ||||
|     println!("  psql -h postgres-cluster.database.svc.cluster.local -U postgres"); | ||||
|  | ||||
|     println!("\n💡 Next steps:"); | ||||
|     println!("  • Set environment variables for database credentials"); | ||||
|     println!("  • Add persistent volume claims for data storage"); | ||||
|     println!("  • Configure backup and monitoring"); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										79
									
								
								examples/kubernetes/clusters/redis.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								examples/kubernetes/clusters/redis.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| //! Redis Cluster Deployment Example (Rhai) | ||||
| //! | ||||
| //! This script shows how to deploy a Redis cluster using Rhai scripting | ||||
| //! with the KubernetesManager convenience methods. | ||||
|  | ||||
| print("=== Redis Cluster Deployment ==="); | ||||
|  | ||||
| // Create Kubernetes manager for the cache namespace | ||||
| print("Creating Kubernetes manager for 'cache' namespace..."); | ||||
| let km = kubernetes_manager_new("cache"); | ||||
| print("✓ Kubernetes manager created"); | ||||
|  | ||||
| // Create the namespace if it doesn't exist | ||||
| print("Creating namespace 'cache' if it doesn't exist..."); | ||||
| try { | ||||
|     create_namespace(km, "cache"); | ||||
|     print("✓ Namespace 'cache' created"); | ||||
| } catch(e) { | ||||
|     if e.to_string().contains("already exists") { | ||||
|         print("✓ Namespace 'cache' already exists"); | ||||
|     } else { | ||||
|         print("⚠️ Warning: " + e); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Clean up any existing resources first | ||||
| print("\nCleaning up any existing Redis resources..."); | ||||
| try { | ||||
|     delete_deployment(km, "redis-cluster"); | ||||
|     print("✓ Deleted existing deployment"); | ||||
| } catch(e) { | ||||
|     print("✓ No existing deployment to delete"); | ||||
| } | ||||
|  | ||||
| try { | ||||
|     delete_service(km, "redis-cluster"); | ||||
|     print("✓ Deleted existing service"); | ||||
| } catch(e) { | ||||
|     print("✓ No existing service to delete"); | ||||
| } | ||||
|  | ||||
| // Create Redis cluster using the convenience method | ||||
| print("\nDeploying Redis cluster..."); | ||||
|  | ||||
| try { | ||||
|     // Deploy Redis using the convenience method | ||||
|     let result = deploy_application(km, "redis-cluster", "redis:7-alpine", 3, 6379, #{ | ||||
|         "app": "redis-cluster", | ||||
|         "type": "cache", | ||||
|         "engine": "redis" | ||||
|     }, #{ | ||||
|         "REDIS_PASSWORD": "redispassword", | ||||
|         "REDIS_PORT": "6379", | ||||
|         "REDIS_DATABASES": "16", | ||||
|         "REDIS_MAXMEMORY": "256mb", | ||||
|         "REDIS_MAXMEMORY_POLICY": "allkeys-lru" | ||||
|     }); | ||||
|     print("✓ " + result); | ||||
|  | ||||
|     print("\n✅ Redis cluster deployed successfully!"); | ||||
|  | ||||
|     print("\n📋 Connection Information:"); | ||||
|     print("  Host: redis-cluster.cache.svc.cluster.local"); | ||||
|     print("  Port: 6379"); | ||||
|  | ||||
|     print("\n🔧 To connect from another pod:"); | ||||
|     print("  redis-cli -h redis-cluster.cache.svc.cluster.local"); | ||||
|  | ||||
|     print("\n💡 Next steps:"); | ||||
|     print("  • Configure Redis authentication"); | ||||
|     print("  • Set up Redis clustering configuration"); | ||||
|     print("  • Add persistent storage"); | ||||
|     print("  • Configure memory policies"); | ||||
|  | ||||
| } catch(e) { | ||||
|     print("❌ Failed to deploy Redis cluster: " + e); | ||||
| } | ||||
|  | ||||
| print("\n=== Deployment Complete ==="); | ||||
							
								
								
									
										109
									
								
								examples/kubernetes/clusters/redis.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								examples/kubernetes/clusters/redis.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| //! Redis Cluster Deployment Example | ||||
| //! | ||||
| //! This example shows how to deploy a Redis cluster using the | ||||
| //! KubernetesManager convenience methods. | ||||
|  | ||||
| use sal_kubernetes::KubernetesManager; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| #[tokio::main] | ||||
| async fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     // Create Kubernetes manager for the cache namespace | ||||
|     let km = KubernetesManager::new("cache").await?; | ||||
|  | ||||
|     // Create the namespace if it doesn't exist | ||||
|     println!("Creating namespace 'cache' if it doesn't exist..."); | ||||
|     match km.namespace_create("cache").await { | ||||
|         Ok(_) => println!("✓ Namespace 'cache' created"), | ||||
|         Err(e) => { | ||||
|             if e.to_string().contains("already exists") { | ||||
|                 println!("✓ Namespace 'cache' already exists"); | ||||
|             } else { | ||||
|                 return Err(e.into()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Clean up any existing resources first | ||||
|     println!("Cleaning up any existing Redis resources..."); | ||||
|     match km.deployment_delete("redis-cluster").await { | ||||
|         Ok(_) => println!("✓ Deleted existing deployment"), | ||||
|         Err(_) => println!("✓ No existing deployment to delete"), | ||||
|     } | ||||
|  | ||||
|     match km.service_delete("redis-cluster").await { | ||||
|         Ok(_) => println!("✓ Deleted existing service"), | ||||
|         Err(_) => println!("✓ No existing service to delete"), | ||||
|     } | ||||
|  | ||||
|     // Configure Redis-specific labels | ||||
|     let mut labels = HashMap::new(); | ||||
|     labels.insert("app".to_string(), "redis-cluster".to_string()); | ||||
|     labels.insert("type".to_string(), "cache".to_string()); | ||||
|     labels.insert("engine".to_string(), "redis".to_string()); | ||||
|  | ||||
|     // Configure Redis environment variables | ||||
|     let mut env_vars = HashMap::new(); | ||||
|     env_vars.insert("REDIS_PASSWORD".to_string(), "redispassword".to_string()); | ||||
|     env_vars.insert("REDIS_PORT".to_string(), "6379".to_string()); | ||||
|     env_vars.insert("REDIS_DATABASES".to_string(), "16".to_string()); | ||||
|     env_vars.insert("REDIS_MAXMEMORY".to_string(), "256mb".to_string()); | ||||
|     env_vars.insert( | ||||
|         "REDIS_MAXMEMORY_POLICY".to_string(), | ||||
|         "allkeys-lru".to_string(), | ||||
|     ); | ||||
|  | ||||
|     // Deploy the Redis cluster using the convenience method | ||||
|     println!("Deploying Redis cluster..."); | ||||
|     km.deploy_application( | ||||
|         "redis-cluster",  // name | ||||
|         "redis:7-alpine", // image | ||||
|         3,                // replicas (Redis cluster nodes) | ||||
|         6379,             // port | ||||
|         Some(labels),     // labels | ||||
|         Some(env_vars),   // environment variables | ||||
|     ) | ||||
|     .await?; | ||||
|  | ||||
|     println!("✅ Redis cluster deployed successfully!"); | ||||
|  | ||||
|     // Check deployment status | ||||
|     let deployments = km.deployments_list().await?; | ||||
|     let redis_deployment = deployments | ||||
|         .iter() | ||||
|         .find(|d| d.metadata.name.as_ref() == Some(&"redis-cluster".to_string())); | ||||
|  | ||||
|     if let Some(deployment) = redis_deployment { | ||||
|         let total_replicas = deployment | ||||
|             .spec | ||||
|             .as_ref() | ||||
|             .and_then(|s| s.replicas) | ||||
|             .unwrap_or(0); | ||||
|         let ready_replicas = deployment | ||||
|             .status | ||||
|             .as_ref() | ||||
|             .and_then(|s| s.ready_replicas) | ||||
|             .unwrap_or(0); | ||||
|  | ||||
|         println!( | ||||
|             "Deployment status: {}/{} replicas ready", | ||||
|             ready_replicas, total_replicas | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     println!("\n📋 Connection Information:"); | ||||
|     println!("  Host: redis-cluster.cache.svc.cluster.local"); | ||||
|     println!("  Port: 6379"); | ||||
|     println!("  Password: Configure REDIS_PASSWORD environment variable"); | ||||
|  | ||||
|     println!("\n🔧 To connect from another pod:"); | ||||
|     println!("  redis-cli -h redis-cluster.cache.svc.cluster.local"); | ||||
|  | ||||
|     println!("\n💡 Next steps:"); | ||||
|     println!("  • Configure Redis authentication with environment variables"); | ||||
|     println!("  • Set up Redis clustering configuration"); | ||||
|     println!("  • Add persistent volume claims for data persistence"); | ||||
|     println!("  • Configure memory limits and eviction policies"); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| // Example of using the network modules in SAL through Rhai | ||||
| // Shows TCP port checking, HTTP URL validation, and SSH command execution | ||||
|  | ||||
|  | ||||
| // Function to print section header | ||||
| fn section(title) { | ||||
|     print("\n"); | ||||
| @@ -19,14 +20,14 @@ let host = "localhost"; | ||||
| let port = 22; | ||||
| print(`Checking if port ${port} is open on ${host}...`); | ||||
| let is_open = tcp.check_port(host, port); | ||||
| print(`Port ${port} is ${is_open ? "open" : "closed"}`); | ||||
| print(`Port ${port} is ${if is_open { "open" } else { "closed" }}`); | ||||
|  | ||||
| // Check multiple ports | ||||
| let ports = [22, 80, 443]; | ||||
| print(`Checking multiple ports on ${host}...`); | ||||
| let port_results = tcp.check_ports(host, ports); | ||||
| for result in port_results { | ||||
|     print(`Port ${result.port} is ${result.is_open ? "open" : "closed"}`); | ||||
|     print(`Port ${result.port} is ${if result.is_open { "open" } else { "closed" }}`); | ||||
| } | ||||
|  | ||||
| // HTTP connectivity checks | ||||
| @@ -39,7 +40,7 @@ let http = net::new_http_connector(); | ||||
| let url = "https://www.example.com"; | ||||
| print(`Checking if ${url} is reachable...`); | ||||
| let is_reachable = http.check_url(url); | ||||
| print(`${url} is ${is_reachable ? "reachable" : "unreachable"}`); | ||||
| print(`${url} is ${if is_reachable { "reachable" } else { "unreachable" }}`); | ||||
|  | ||||
| // Check the status code of a URL | ||||
| print(`Checking status code of ${url}...`); | ||||
| @@ -68,7 +69,7 @@ if is_open { | ||||
|     let ssh = net::new_ssh_builder() | ||||
|         .host("localhost") | ||||
|         .port(22) | ||||
|         .user(os::get_env("USER") || "root") | ||||
|         .user(if os::get_env("USER") != () { os::get_env("USER") } else { "root" }) | ||||
|         .timeout(10) | ||||
|         .build(); | ||||
|      | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| print("Running a basic command using run().do()..."); | ||||
| print("Running a basic command using run().execute()..."); | ||||
|  | ||||
| // Execute a simple command | ||||
| let result = run("echo Hello from run_basic!").do(); | ||||
| let result = run("echo Hello from run_basic!").execute(); | ||||
|  | ||||
| // Print the command result | ||||
| print(`Command: echo Hello from run_basic!`); | ||||
| @@ -13,6 +13,6 @@ print(`Stderr:\n${result.stderr}`); | ||||
| // Example of a command that might fail (if 'nonexistent_command' doesn't exist) | ||||
| // This will halt execution by default because ignore_error() is not used. | ||||
| // print("Running a command that will fail (and should halt)..."); | ||||
| // let fail_result = run("nonexistent_command").do(); // This line will cause the script to halt if the command doesn't exist | ||||
| // let fail_result = run("nonexistent_command").execute(); // This line will cause the script to halt if the command doesn't exist | ||||
|  | ||||
| print("Basic run() example finished."); | ||||
| @@ -2,7 +2,7 @@ print("Running a command that will fail, but ignoring the error..."); | ||||
|  | ||||
| // Run a command that exits with a non-zero code (will fail) | ||||
| // Using .ignore_error() prevents the script from halting | ||||
| let result = run("exit 1").ignore_error().do(); | ||||
| let result = run("exit 1").ignore_error().execute(); | ||||
|  | ||||
| print(`Command finished.`); | ||||
| print(`Success: ${result.success}`); // This should be false | ||||
| @@ -22,7 +22,7 @@ print("\nScript continued execution after the potentially failing command."); | ||||
| // Example of a command that might fail due to OS error (e.g., command not found) | ||||
| // This *might* still halt depending on how the underlying Rust function handles it, | ||||
| // as ignore_error() primarily prevents halting on *command* non-zero exit codes. | ||||
| // let os_error_result = run("nonexistent_command_123").ignore_error().do(); | ||||
| // let os_error_result = run("nonexistent_command_123").ignore_error().execute(); | ||||
| // print(`OS Error Command Success: ${os_error_result.success}`); | ||||
| // print(`OS Error Command Exit Code: ${os_error_result.code}`); | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| print("Running a command using run().log().do()..."); | ||||
| print("Running a command using run().log().execute()..."); | ||||
|  | ||||
| // The .log() method will print the command string to the console before execution. | ||||
| // This is useful for debugging or tracing which commands are being run. | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| print("Running a command using run().silent().do()...\n"); | ||||
| print("Running a command using run().silent().execute()...\n"); | ||||
|  | ||||
| // This command will print to standard output and standard error | ||||
| // However, because .silent() is used, the output will not appear in the console directly | ||||
| let result = run("echo 'This should be silent stdout.'; echo 'This should be silent stderr.' >&2; exit 0").silent().do(); | ||||
| let result = run("echo 'This should be silent stdout.'; echo 'This should be silent stderr.' >&2; exit 0").silent().execute(); | ||||
|  | ||||
| // The output is still captured in the CommandResult | ||||
| print(`Command finished.`); | ||||
| @@ -12,7 +12,7 @@ print(`Captured Stdout:\\n${result.stdout}`); | ||||
| print(`Captured Stderr:\\n${result.stderr}`); | ||||
|  | ||||
| // Example of a silent command that fails (but won't halt because we only suppress output) | ||||
| // let fail_result = run("echo 'This is silent failure stderr.' >&2; exit 1").silent().do(); | ||||
| // let fail_result = run("echo 'This is silent failure stderr.' >&2; exit 1").silent().execute(); | ||||
| // print(`Failed command finished (silent):`); | ||||
| // print(`Success: ${fail_result.success}`); | ||||
| // print(`Exit Code: ${fail_result.code}`); | ||||
|   | ||||
							
								
								
									
										116
									
								
								examples/service_manager/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								examples/service_manager/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| # Service Manager Examples | ||||
|  | ||||
| This directory contains examples demonstrating the SAL service manager functionality for dynamically launching and managing services across platforms. | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| The service manager provides a unified interface for managing system services: | ||||
| - **macOS**: Uses `launchctl` for service management | ||||
| - **Linux**: Uses `zinit` for service management (systemd also available as alternative) | ||||
|  | ||||
| ## Examples | ||||
|  | ||||
| ### 1. Circle Worker Manager (`circle_worker_manager.rhai`) | ||||
|  | ||||
| **Primary Use Case**: Demonstrates dynamic circle worker management for freezone residents. | ||||
|  | ||||
| This example shows: | ||||
| - Creating service configurations for circle workers | ||||
| - Complete service lifecycle management (start, stop, restart, remove) | ||||
| - Status monitoring and log retrieval | ||||
| - Error handling and cleanup | ||||
|  | ||||
| ```bash | ||||
| # Run the circle worker management example | ||||
| herodo examples/service_manager/circle_worker_manager.rhai | ||||
| ``` | ||||
|  | ||||
| ### 2. Basic Usage (`basic_usage.rhai`) | ||||
|  | ||||
| **Learning Example**: Simple demonstration of the core service manager API. | ||||
|  | ||||
| This example covers: | ||||
| - Creating and configuring services | ||||
| - Starting and stopping services | ||||
| - Checking service status | ||||
| - Listing managed services | ||||
| - Retrieving service logs | ||||
|  | ||||
| ```bash | ||||
| # Run the basic usage example | ||||
| herodo examples/service_manager/basic_usage.rhai | ||||
| ``` | ||||
|  | ||||
| ## Prerequisites | ||||
|  | ||||
| ### Linux (zinit) | ||||
|  | ||||
| Make sure zinit is installed and running: | ||||
|  | ||||
| ```bash | ||||
| # Start zinit with default socket | ||||
| zinit -s /tmp/zinit.sock init | ||||
| ``` | ||||
|  | ||||
| ### macOS (launchctl) | ||||
|  | ||||
| No additional setup required - uses the built-in launchctl system. | ||||
|  | ||||
| ## Service Manager API | ||||
|  | ||||
| The service manager provides these key functions: | ||||
|  | ||||
| - `create_service_manager()` - Create platform-appropriate service manager | ||||
| - `start(manager, config)` - Start a new service | ||||
| - `stop(manager, service_name)` - Stop a running service | ||||
| - `restart(manager, service_name)` - Restart a service | ||||
| - `status(manager, service_name)` - Get service status | ||||
| - `logs(manager, service_name, lines)` - Retrieve service logs | ||||
| - `list(manager)` - List all managed services | ||||
| - `remove(manager, service_name)` - Remove a service | ||||
| - `exists(manager, service_name)` - Check if service exists | ||||
| - `start_and_confirm(manager, config, timeout)` - Start with confirmation | ||||
|  | ||||
| ## Service Configuration | ||||
|  | ||||
| Services are configured using a map with these fields: | ||||
|  | ||||
| ```rhai | ||||
| let config = #{ | ||||
|     name: "my-service",                    // Service name | ||||
|     binary_path: "/usr/bin/my-app",        // Executable path | ||||
|     args: ["--config", "/etc/my-app.conf"], // Command arguments | ||||
|     working_directory: "/var/lib/my-app",   // Working directory (optional) | ||||
|     environment: #{                         // Environment variables | ||||
|         "VAR1": "value1", | ||||
|         "VAR2": "value2" | ||||
|     }, | ||||
|     auto_restart: true                      // Auto-restart on failure | ||||
| }; | ||||
| ``` | ||||
|  | ||||
| ## Real-World Usage | ||||
|  | ||||
| The circle worker example demonstrates the exact use case requested by the team: | ||||
|  | ||||
| > "We want to be able to launch circle workers dynamically. For instance when someone registers to the freezone, we need to be able to launch a circle worker for the new resident." | ||||
|  | ||||
| The service manager enables: | ||||
| 1. **Dynamic service creation** - Create services on-demand for new residents | ||||
| 2. **Cross-platform support** - Works on both macOS and Linux | ||||
| 3. **Lifecycle management** - Full control over service lifecycle | ||||
| 4. **Monitoring and logging** - Track service status and retrieve logs | ||||
| 5. **Cleanup** - Proper service removal when no longer needed | ||||
|  | ||||
| ## Error Handling | ||||
|  | ||||
| All service manager functions can throw errors. Use try-catch blocks for robust error handling: | ||||
|  | ||||
| ```rhai | ||||
| try { | ||||
|     sm::start(manager, config); | ||||
|     print("✅ Service started successfully"); | ||||
| } catch (error) { | ||||
|     print(`❌ Failed to start service: ${error}`); | ||||
| } | ||||
| ``` | ||||
							
								
								
									
										85
									
								
								examples/service_manager/basic_usage.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								examples/service_manager/basic_usage.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| // Basic Service Manager Usage Example | ||||
| // | ||||
| // This example demonstrates the basic API of the service manager. | ||||
| // It works on both macOS (launchctl) and Linux (zinit/systemd). | ||||
| // | ||||
| // Prerequisites: | ||||
| // | ||||
| // Linux: The service manager will automatically discover running zinit servers | ||||
| //        or fall back to systemd. To use zinit, start it with: | ||||
| //   zinit -s /tmp/zinit.sock init | ||||
| // | ||||
| //   You can also specify a custom socket path: | ||||
| //   export ZINIT_SOCKET_PATH=/your/custom/path/zinit.sock | ||||
| // | ||||
| // macOS: No additional setup required (uses launchctl). | ||||
| // | ||||
| // Usage: | ||||
| //   herodo examples/service_manager/basic_usage.rhai | ||||
|  | ||||
| // Service Manager Basic Usage Example | ||||
| // This example uses the SAL service manager through Rhai integration | ||||
|  | ||||
| print("🚀 Basic Service Manager Usage Example"); | ||||
| print("======================================"); | ||||
|  | ||||
| // Create a service manager for the current platform | ||||
| let manager = create_service_manager(); | ||||
|  | ||||
| print("🍎 Using service manager for current platform"); | ||||
|  | ||||
| // Create a simple service configuration | ||||
| let config = #{ | ||||
|     name: "example-service", | ||||
|     binary_path: "/bin/echo", | ||||
|     args: ["Hello from service manager!"], | ||||
|     working_directory: "/tmp", | ||||
|     environment: #{ | ||||
|         "EXAMPLE_VAR": "hello_world" | ||||
|     }, | ||||
|     auto_restart: false | ||||
| }; | ||||
|  | ||||
| print("\n📝 Service Configuration:"); | ||||
| print(`   Name: ${config.name}`); | ||||
| print(`   Binary: ${config.binary_path}`); | ||||
| print(`   Args: ${config.args}`); | ||||
|  | ||||
| // Start the service | ||||
| print("\n🚀 Starting service..."); | ||||
| start(manager, config); | ||||
| print("✅ Service started successfully"); | ||||
|  | ||||
| // Check service status | ||||
| print("\n📊 Checking service status..."); | ||||
| let status = status(manager, "example-service"); | ||||
| print(`Status: ${status}`); | ||||
|  | ||||
| // List all services | ||||
| print("\n📋 Listing all managed services..."); | ||||
| let services = list(manager); | ||||
| print(`Found ${services.len()} services:`); | ||||
| for service in services { | ||||
|     print(`  - ${service}`); | ||||
| } | ||||
|  | ||||
| // Get service logs | ||||
| print("\n📄 Getting service logs..."); | ||||
| let logs = logs(manager, "example-service", 5); | ||||
| if logs.trim() == "" { | ||||
|     print("No logs available"); | ||||
| } else { | ||||
|     print(`Logs:\n${logs}`); | ||||
| } | ||||
|  | ||||
| // Stop the service | ||||
| print("\n🛑 Stopping service..."); | ||||
| stop(manager, "example-service"); | ||||
| print("✅ Service stopped"); | ||||
|  | ||||
| // Remove the service | ||||
| print("\n🗑️  Removing service..."); | ||||
| remove(manager, "example-service"); | ||||
| print("✅ Service removed"); | ||||
|  | ||||
| print("\n🎉 Example completed successfully!"); | ||||
							
								
								
									
										141
									
								
								examples/service_manager/circle_worker_manager.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								examples/service_manager/circle_worker_manager.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| // Circle Worker Manager Example | ||||
| // | ||||
| // This example demonstrates how to use the service manager to dynamically launch | ||||
| // circle workers for new freezone residents. This is the primary use case requested | ||||
| // by the team. | ||||
| // | ||||
| // Usage: | ||||
| // | ||||
| // On macOS (uses launchctl): | ||||
| //   herodo examples/service_manager/circle_worker_manager.rhai | ||||
| // | ||||
| // On Linux (uses zinit - requires zinit to be running): | ||||
| //   First start zinit: zinit -s /tmp/zinit.sock init | ||||
| //   herodo examples/service_manager/circle_worker_manager.rhai | ||||
|  | ||||
| // Circle Worker Manager Example | ||||
| // This example uses the SAL service manager through Rhai integration | ||||
|  | ||||
| print("🚀 Circle Worker Manager Example"); | ||||
| print("================================="); | ||||
|  | ||||
| // Create the appropriate service manager for the current platform | ||||
| let service_manager = create_service_manager(); | ||||
| print("✅ Created service manager for current platform"); | ||||
|  | ||||
| // Simulate a new freezone resident registration | ||||
| let resident_id = "resident_12345"; | ||||
| let worker_name = `circle-worker-${resident_id}`; | ||||
|  | ||||
| print(`\n📝 New freezone resident registered: ${resident_id}`); | ||||
| print(`🔧 Creating circle worker service: ${worker_name}`); | ||||
|  | ||||
| // Create service configuration for the circle worker | ||||
| let config = #{ | ||||
|     name: worker_name, | ||||
|     binary_path: "/bin/sh", | ||||
|     args: [ | ||||
|         "-c", | ||||
|         `echo 'Circle worker for ${resident_id} starting...'; sleep 30; echo 'Circle worker for ${resident_id} completed'` | ||||
|     ], | ||||
|     working_directory: "/tmp", | ||||
|     environment: #{ | ||||
|         "RESIDENT_ID": resident_id, | ||||
|         "WORKER_TYPE": "circle", | ||||
|         "LOG_LEVEL": "info" | ||||
|     }, | ||||
|     auto_restart: true | ||||
| }; | ||||
|  | ||||
| print("📋 Service configuration created:"); | ||||
| print(`   Name: ${config.name}`); | ||||
| print(`   Binary: ${config.binary_path}`); | ||||
| print(`   Args: ${config.args}`); | ||||
| print(`   Auto-restart: ${config.auto_restart}`); | ||||
|  | ||||
| print(`\n🔄 Demonstrating service lifecycle for: ${worker_name}`); | ||||
|  | ||||
| // 1. Check if service already exists | ||||
| print("\n1️⃣ Checking if service exists..."); | ||||
| if exists(service_manager, worker_name) { | ||||
|     print("⚠️  Service already exists, removing it first..."); | ||||
|     remove(service_manager, worker_name); | ||||
|     print("🗑️  Existing service removed"); | ||||
| } else { | ||||
|     print("✅ Service doesn't exist, ready to create"); | ||||
| } | ||||
|  | ||||
| // 2. Start the service | ||||
| print("\n2️⃣ Starting the circle worker service..."); | ||||
| start(service_manager, config); | ||||
| print("✅ Service started successfully"); | ||||
|  | ||||
| // 3. Check service status | ||||
| print("\n3️⃣ Checking service status..."); | ||||
| let status = status(service_manager, worker_name); | ||||
| print(`📊 Service status: ${status}`); | ||||
|  | ||||
| // 4. List all services to show our service is there | ||||
| print("\n4️⃣ Listing all managed services..."); | ||||
| let services = list(service_manager); | ||||
| print(`📋 Managed services (${services.len()}):`); | ||||
| for service in services { | ||||
|     let marker = if service == worker_name { "👉" } else { "  " }; | ||||
|     print(`   ${marker} ${service}`); | ||||
| } | ||||
|  | ||||
| // 5. Wait a moment and check status again | ||||
| print("\n5️⃣ Waiting 3 seconds and checking status again..."); | ||||
| sleep(3000); // 3 seconds in milliseconds | ||||
| let status = status(service_manager, worker_name); | ||||
| print(`📊 Service status after 3s: ${status}`); | ||||
|  | ||||
| // 6. Get service logs | ||||
| print("\n6️⃣ Retrieving service logs..."); | ||||
| let logs = logs(service_manager, worker_name, 10); | ||||
| if logs.trim() == "" { | ||||
|     print("📄 No logs available yet (this is normal for new services)"); | ||||
| } else { | ||||
|     print("📄 Recent logs:"); | ||||
|     let log_lines = logs.split('\n'); | ||||
|     for i in 0..5 { | ||||
|         if i < log_lines.len() { | ||||
|             print(`   ${log_lines[i]}`); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // 7. Demonstrate start_and_confirm with timeout | ||||
| print("\n7️⃣ Testing start_and_confirm (should succeed quickly since already running)..."); | ||||
| start_and_confirm(service_manager, config, 5); | ||||
| print("✅ Service confirmed running within timeout"); | ||||
|  | ||||
| // 8. Stop the service | ||||
| print("\n8️⃣ Stopping the service..."); | ||||
| stop(service_manager, worker_name); | ||||
| print("🛑 Service stopped"); | ||||
|  | ||||
| // 9. Check status after stopping | ||||
| print("\n9️⃣ Checking status after stop..."); | ||||
| let status = status(service_manager, worker_name); | ||||
| print(`📊 Service status after stop: ${status}`); | ||||
|  | ||||
| // 10. Restart the service | ||||
| print("\n🔟 Restarting the service..."); | ||||
| restart(service_manager, worker_name); | ||||
| print("🔄 Service restarted successfully"); | ||||
|  | ||||
| // 11. Final cleanup | ||||
| print("\n🧹 Cleaning up - removing the service..."); | ||||
| remove(service_manager, worker_name); | ||||
| print("🗑️  Service removed successfully"); | ||||
|  | ||||
| // 12. Verify removal | ||||
| print("\n✅ Verifying service removal..."); | ||||
| if !exists(service_manager, worker_name) { | ||||
|     print("✅ Service successfully removed"); | ||||
| } else { | ||||
|     print("⚠️  Service still exists after removal"); | ||||
| } | ||||
|  | ||||
| print("\n🎉 Circle worker management demonstration complete!"); | ||||
| @@ -3,7 +3,7 @@ | ||||
| //! This library loads the Rhai engine, registers all SAL modules, | ||||
| //! and executes Rhai scripts from a specified directory in sorted order. | ||||
|  | ||||
| use rhai::Engine; | ||||
| use rhai::{Engine, Scope}; | ||||
| use std::error::Error; | ||||
| use std::fs; | ||||
| use std::path::{Path, PathBuf}; | ||||
| @@ -29,6 +29,19 @@ pub fn run(script_path: &str) -> Result<(), Box<dyn Error>> { | ||||
|  | ||||
|     // Create a new Rhai engine | ||||
|     let mut engine = Engine::new(); | ||||
|      | ||||
|     // TODO: if we create a scope here we could clean up all the different functionsand types regsitered wit the engine | ||||
|     // We should generalize the way we add things to the scope for each module sepeartely | ||||
|     let mut scope = Scope::new(); | ||||
|     // Conditionally add Hetzner client only when env config is present | ||||
|     if let Ok(cfg) = sal::hetzner::config::Config::from_env() { | ||||
|         let hetzner_client = sal::hetzner::api::Client::new(cfg); | ||||
|         scope.push("hetzner", hetzner_client); | ||||
|     } | ||||
|     // This makes it easy to call e.g. `hetzner.get_server()` or `mycelium.get_connected_peers()` | ||||
|     // --> without the need of manually created a client for each one first | ||||
|     // --> could be conditionally compiled to only use those who we need (we only push the things to the scope that we actually need to run the script) | ||||
|  | ||||
|  | ||||
|     // Register println function for output | ||||
|     engine.register_fn("println", |s: &str| println!("{}", s)); | ||||
| @@ -78,19 +91,20 @@ pub fn run(script_path: &str) -> Result<(), Box<dyn Error>> { | ||||
|         let script = fs::read_to_string(&script_file)?; | ||||
|  | ||||
|         // Execute the script | ||||
|         match engine.eval::<rhai::Dynamic>(&script) { | ||||
|             Ok(result) => { | ||||
|                 println!("Script executed successfully"); | ||||
|                 if !result.is_unit() { | ||||
|                     println!("Result: {}", result); | ||||
|                 } | ||||
|             } | ||||
|             Err(err) => { | ||||
|                 eprintln!("Error executing script: {}", err); | ||||
|                 // Exit with error code when a script fails | ||||
|                 process::exit(1); | ||||
|             } | ||||
|         } | ||||
|         // match engine.eval::<rhai::Dynamic>(&script) { | ||||
|         //     Ok(result) => { | ||||
|         //         println!("Script executed successfully"); | ||||
|         //         if !result.is_unit() { | ||||
|         //             println!("Result: {}", result); | ||||
|         //         } | ||||
|         //     } | ||||
|         //     Err(err) => { | ||||
|         //         eprintln!("Error executing script: {}", err); | ||||
|         //         // Exit with error code when a script fails | ||||
|         //         process::exit(1); | ||||
|         //     } | ||||
|         // } | ||||
|         engine.run_with_scope(&mut scope, &script)?; | ||||
|     } | ||||
|  | ||||
|     println!("\nAll scripts executed successfully!"); | ||||
|   | ||||
| @@ -1,227 +0,0 @@ | ||||
| # SAL Kubernetes (`sal-kubernetes`) | ||||
|  | ||||
| Kubernetes cluster management and operations for the System Abstraction Layer (SAL). | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| Add this to your `Cargo.toml`: | ||||
|  | ||||
| ```toml | ||||
| [dependencies] | ||||
| sal-kubernetes = "0.1.0" | ||||
| ``` | ||||
|  | ||||
| ## ⚠️ **IMPORTANT SECURITY NOTICE** | ||||
|  | ||||
| **This package includes destructive operations that can permanently delete Kubernetes resources!** | ||||
|  | ||||
| - The `delete(pattern)` function uses PCRE regex patterns to bulk delete resources | ||||
| - **Always test patterns in a safe environment first** | ||||
| - Use specific patterns to avoid accidental deletion of critical resources | ||||
| - Consider the impact on dependent resources before deletion | ||||
| - **No confirmation prompts** - deletions are immediate and irreversible | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| This package provides a high-level interface for managing Kubernetes clusters using the `kube-rs` SDK. It focuses on namespace-scoped operations through the `KubernetesManager` factory pattern. | ||||
|  | ||||
| ### Production Safety Features | ||||
|  | ||||
| - **Configurable Timeouts**: All operations have configurable timeouts to prevent hanging | ||||
| - **Exponential Backoff Retry**: Automatic retry logic for transient failures | ||||
| - **Rate Limiting**: Built-in rate limiting to prevent API overload | ||||
| - **Comprehensive Error Handling**: Detailed error types and proper error propagation | ||||
| - **Structured Logging**: Production-ready logging for monitoring and debugging | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - **Namespace-scoped Management**: Each `KubernetesManager` instance operates on a single namespace | ||||
| - **Pod Management**: List, create, and manage pods | ||||
| - **Pattern-based Deletion**: Delete resources using PCRE pattern matching | ||||
| - **Namespace Operations**: Create and manage namespaces (idempotent operations) | ||||
| - **Resource Management**: Support for pods, services, deployments, configmaps, secrets, and more | ||||
| - **Rhai Integration**: Full scripting support through Rhai wrappers | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ### Basic Operations | ||||
|  | ||||
| ```rust | ||||
| use sal_kubernetes::KubernetesManager; | ||||
|  | ||||
| #[tokio::main] | ||||
| async fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     // Create a manager for the "default" namespace | ||||
|     let km = KubernetesManager::new("default").await?; | ||||
|      | ||||
|     // List all pods in the namespace | ||||
|     let pods = km.pods_list().await?; | ||||
|     println!("Found {} pods", pods.len()); | ||||
|      | ||||
|     // Create a namespace (no error if it already exists) | ||||
|     km.namespace_create("my-namespace").await?; | ||||
|      | ||||
|     // Delete resources matching a pattern | ||||
|     km.delete("test-.*").await?; | ||||
|      | ||||
|     Ok(()) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Rhai Scripting | ||||
|  | ||||
| ```javascript | ||||
| // Create Kubernetes manager for namespace | ||||
| let km = kubernetes_manager_new("default"); | ||||
|  | ||||
| // List pods | ||||
| let pods = pods_list(km); | ||||
| print("Found " + pods.len() + " pods"); | ||||
|  | ||||
| // Create namespace | ||||
| namespace_create(km, "my-app"); | ||||
|  | ||||
| // Delete test resources | ||||
| delete(km, "test-.*"); | ||||
| ``` | ||||
|  | ||||
| ## Dependencies | ||||
|  | ||||
| - `kube`: Kubernetes client library | ||||
| - `k8s-openapi`: Kubernetes API types | ||||
| - `tokio`: Async runtime | ||||
| - `regex`: Pattern matching for resource deletion | ||||
| - `rhai`: Scripting integration (optional) | ||||
|  | ||||
| ## Configuration | ||||
|  | ||||
| ### Kubernetes Authentication | ||||
|  | ||||
| The package uses the standard Kubernetes configuration methods: | ||||
| - In-cluster configuration (when running in a pod) | ||||
| - Kubeconfig file (`~/.kube/config` or `KUBECONFIG` environment variable) | ||||
| - Service account tokens | ||||
|  | ||||
| ### Production Safety Configuration | ||||
|  | ||||
| ```rust | ||||
| use sal_kubernetes::{KubernetesManager, KubernetesConfig}; | ||||
| use std::time::Duration; | ||||
|  | ||||
| // Create with custom configuration | ||||
| let config = KubernetesConfig::new() | ||||
|     .with_timeout(Duration::from_secs(60)) | ||||
|     .with_retries(5, Duration::from_secs(1), Duration::from_secs(30)) | ||||
|     .with_rate_limit(20, 50); | ||||
|  | ||||
| let km = KubernetesManager::with_config("my-namespace", config).await?; | ||||
| ``` | ||||
|  | ||||
| ### Pre-configured Profiles | ||||
|  | ||||
| ```rust | ||||
| // High-throughput environment | ||||
| let config = KubernetesConfig::high_throughput(); | ||||
|  | ||||
| // Low-latency environment | ||||
| let config = KubernetesConfig::low_latency(); | ||||
|  | ||||
| // Development/testing | ||||
| let config = KubernetesConfig::development(); | ||||
| ``` | ||||
|  | ||||
| ## Error Handling | ||||
|  | ||||
| All operations return `Result<T, KubernetesError>` with comprehensive error types for different failure scenarios including API errors, configuration issues, and permission problems. | ||||
|  | ||||
| ## API Reference | ||||
|  | ||||
| ### KubernetesManager | ||||
|  | ||||
| The main interface for Kubernetes operations. Each instance is scoped to a single namespace. | ||||
|  | ||||
| #### Constructor | ||||
|  | ||||
| - `KubernetesManager::new(namespace)` - Create a manager for the specified namespace | ||||
|  | ||||
| #### Resource Listing | ||||
|  | ||||
| - `pods_list()` - List all pods in the namespace | ||||
| - `services_list()` - List all services in the namespace | ||||
| - `deployments_list()` - List all deployments in the namespace | ||||
| - `configmaps_list()` - List all configmaps in the namespace | ||||
| - `secrets_list()` - List all secrets in the namespace | ||||
|  | ||||
| #### Resource Management | ||||
|  | ||||
| - `pod_get(name)` - Get a specific pod by name | ||||
| - `service_get(name)` - Get a specific service by name | ||||
| - `deployment_get(name)` - Get a specific deployment by name | ||||
| - `pod_delete(name)` - Delete a specific pod by name | ||||
| - `service_delete(name)` - Delete a specific service by name | ||||
| - `deployment_delete(name)` - Delete a specific deployment by name | ||||
|  | ||||
| #### Pattern-based Operations | ||||
|  | ||||
| - `delete(pattern)` - Delete all resources matching a PCRE pattern | ||||
|  | ||||
| #### Namespace Operations | ||||
|  | ||||
| - `namespace_create(name)` - Create a namespace (idempotent) | ||||
| - `namespace_exists(name)` - Check if a namespace exists | ||||
| - `namespaces_list()` - List all namespaces (cluster-wide) | ||||
|  | ||||
| #### Utility Functions | ||||
|  | ||||
| - `resource_counts()` - Get counts of all resource types in the namespace | ||||
| - `namespace()` - Get the namespace this manager operates on | ||||
|  | ||||
| ### Rhai Functions | ||||
|  | ||||
| When using the Rhai integration, the following functions are available: | ||||
|  | ||||
| - `kubernetes_manager_new(namespace)` - Create a KubernetesManager | ||||
| - `pods_list(km)` - List pods | ||||
| - `services_list(km)` - List services | ||||
| - `deployments_list(km)` - List deployments | ||||
| - `namespaces_list(km)` - List all namespaces | ||||
| - `delete(km, pattern)` - Delete resources matching pattern | ||||
| - `namespace_create(km, name)` - Create namespace | ||||
| - `namespace_exists(km, name)` - Check namespace existence | ||||
| - `resource_counts(km)` - Get resource counts | ||||
| - `pod_delete(km, name)` - Delete specific pod | ||||
| - `service_delete(km, name)` - Delete specific service | ||||
| - `deployment_delete(km, name)` - Delete specific deployment | ||||
| - `namespace(km)` - Get manager's namespace | ||||
|  | ||||
| ## Examples | ||||
|  | ||||
| The `examples/kubernetes/` directory contains comprehensive examples: | ||||
|  | ||||
| - `basic_operations.rhai` - Basic listing and counting operations | ||||
| - `namespace_management.rhai` - Creating and managing namespaces | ||||
| - `pattern_deletion.rhai` - Using PCRE patterns for bulk deletion | ||||
| - `multi_namespace_operations.rhai` - Working across multiple namespaces | ||||
|  | ||||
| ## Testing | ||||
|  | ||||
| Run tests with: | ||||
|  | ||||
| ```bash | ||||
| # Unit tests (no cluster required) | ||||
| cargo test --package sal-kubernetes | ||||
|  | ||||
| # Integration tests (requires cluster) | ||||
| KUBERNETES_TEST_ENABLED=1 cargo test --package sal-kubernetes | ||||
|  | ||||
| # Rhai integration tests | ||||
| KUBERNETES_TEST_ENABLED=1 cargo test --package sal-kubernetes --features rhai | ||||
| ``` | ||||
|  | ||||
| ## Security Considerations | ||||
|  | ||||
| - Always use specific PCRE patterns to avoid accidental deletion of important resources | ||||
| - Test deletion patterns in a safe environment first | ||||
| - Ensure proper RBAC permissions are configured | ||||
| - Be cautious with cluster-wide operations like namespace listing | ||||
| - Consider using dry-run approaches when possible | ||||
							
								
								
									
										12
									
								
								packages/clients/hetznerclient/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								packages/clients/hetznerclient/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| [package] | ||||
| name = "sal-hetzner" | ||||
| version = "0.1.0" | ||||
| edition = "2024" | ||||
|  | ||||
| [dependencies] | ||||
| prettytable = "0.10.0" | ||||
| reqwest.workspace = true | ||||
| rhai = { workspace = true, features = ["serde"] } | ||||
| serde = { workspace = true, features = ["derive"] } | ||||
| serde_json.workspace = true | ||||
| thiserror.workspace = true | ||||
							
								
								
									
										54
									
								
								packages/clients/hetznerclient/src/api/error.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								packages/clients/hetznerclient/src/api/error.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| use std::fmt; | ||||
|  | ||||
| use serde::Deserialize; | ||||
| use thiserror::Error; | ||||
|  | ||||
| #[derive(Debug, Error)] | ||||
| pub enum AppError { | ||||
|     #[error("Request failed: {0}")] | ||||
|     RequestError(#[from] reqwest::Error), | ||||
|     #[error("API error: {0}")] | ||||
|     ApiError(ApiError), | ||||
|     #[error("Deserialization Error: {0:?}")] | ||||
|     SerdeJsonError(#[from] serde_json::Error), | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| pub struct ApiError { | ||||
|     pub status: u16, | ||||
|     pub message: String, | ||||
| } | ||||
|  | ||||
| impl From<reqwest::blocking::Response> for ApiError { | ||||
|     fn from(value: reqwest::blocking::Response) -> Self { | ||||
|         ApiError { | ||||
|             status: value.status().into(), | ||||
|             message: value.text().unwrap_or("The API call returned an error.".to_string()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl fmt::Display for ApiError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         #[derive(Deserialize)] | ||||
|         struct HetznerApiError { | ||||
|             code: String, | ||||
|             message: String, | ||||
|         } | ||||
|  | ||||
|         #[derive(Deserialize)] | ||||
|         struct HetznerApiErrorWrapper { | ||||
|             error: HetznerApiError, | ||||
|         } | ||||
|  | ||||
|         if let Ok(wrapper) = serde_json::from_str::<HetznerApiErrorWrapper>(&self.message) { | ||||
|             write!( | ||||
|                 f, | ||||
|                 "Status: {}, Code: {}, Message: {}", | ||||
|                 self.status, wrapper.error.code, wrapper.error.message | ||||
|             ) | ||||
|         } else { | ||||
|             write!(f, "Status: {}: {}", self.status, self.message) | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										513
									
								
								packages/clients/hetznerclient/src/api/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										513
									
								
								packages/clients/hetznerclient/src/api/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,513 @@ | ||||
| pub mod error; | ||||
| pub mod models; | ||||
|  | ||||
| use self::models::{ | ||||
|     Boot, Rescue, Server, SshKey, ServerAddonProduct, ServerAddonProductWrapper, | ||||
|     AuctionServerProduct, AuctionServerProductWrapper, AuctionTransaction, | ||||
|     AuctionTransactionWrapper, BootWrapper, Cancellation, CancellationWrapper, | ||||
|     OrderServerBuilder, OrderServerProduct, OrderServerProductWrapper, RescueWrapped, | ||||
|     ServerWrapper, SshKeyWrapper, Transaction, TransactionWrapper, | ||||
|     ServerAddonTransaction, ServerAddonTransactionWrapper, | ||||
|     OrderServerAddonBuilder, | ||||
| }; | ||||
| use crate::api::error::ApiError; | ||||
| use crate::config::Config; | ||||
| use error::AppError; | ||||
| use reqwest::blocking::Client as HttpClient; | ||||
| use serde_json::json; | ||||
|  | ||||
| #[derive(Clone)] | ||||
| pub struct Client { | ||||
|     http_client: HttpClient, | ||||
|     config: Config, | ||||
| } | ||||
|  | ||||
| impl Client { | ||||
|     pub fn new(config: Config) -> Self { | ||||
|         Self { | ||||
|             http_client: HttpClient::new(), | ||||
|             config, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn handle_response<T>(&self, response: reqwest::blocking::Response) -> Result<T, AppError> | ||||
|     where | ||||
|         T: serde::de::DeserializeOwned, | ||||
|     { | ||||
|         let status = response.status(); | ||||
|         let body = response.text()?; | ||||
|  | ||||
|         if status.is_success() { | ||||
|             serde_json::from_str::<T>(&body).map_err(Into::into) | ||||
|         } else { | ||||
|             Err(AppError::ApiError(ApiError { | ||||
|                 status: status.as_u16(), | ||||
|                 message: body, | ||||
|             })) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn get_server(&self, server_number: i32) -> Result<Server, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!("{}/server/{}", self.config.api_url, server_number)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: ServerWrapper = self.handle_response(response)?; | ||||
|         Ok(wrapped.server) | ||||
|     } | ||||
|  | ||||
|     pub fn get_servers(&self) -> Result<Vec<Server>, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!("{}/server", self.config.api_url)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: Vec<ServerWrapper> = self.handle_response(response)?; | ||||
|         let servers = wrapped.into_iter().map(|sw| sw.server).collect(); | ||||
|         Ok(servers) | ||||
|     } | ||||
|  | ||||
|     pub fn update_server_name(&self, server_number: i32, name: &str) -> Result<Server, AppError> { | ||||
|         let params = [("server_name", name)]; | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .post(format!("{}/server/{}", self.config.api_url, server_number)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .form(¶ms) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: ServerWrapper = self.handle_response(response)?; | ||||
|         Ok(wrapped.server) | ||||
|     } | ||||
|  | ||||
|     pub fn get_cancellation_data(&self, server_number: i32) -> Result<Cancellation, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!( | ||||
|                 "{}/server/{}/cancellation", | ||||
|                 self.config.api_url, server_number | ||||
|             )) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: CancellationWrapper = self.handle_response(response)?; | ||||
|         Ok(wrapped.cancellation) | ||||
|     } | ||||
|  | ||||
|     pub fn cancel_server( | ||||
|         &self, | ||||
|         server_number: i32, | ||||
|         cancellation_date: &str, | ||||
|     ) -> Result<Cancellation, AppError> { | ||||
|         let params = [("cancellation_date", cancellation_date)]; | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .post(format!( | ||||
|                 "{}/server/{}/cancellation", | ||||
|                 self.config.api_url, server_number | ||||
|             )) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .form(¶ms) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: CancellationWrapper = self.handle_response(response)?; | ||||
|         Ok(wrapped.cancellation) | ||||
|     } | ||||
|  | ||||
|     pub fn withdraw_cancellation(&self, server_number: i32) -> Result<(), AppError> { | ||||
|         self.http_client | ||||
|             .delete(format!( | ||||
|                 "{}/server/{}/cancellation", | ||||
|                 self.config.api_url, server_number | ||||
|             )) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn get_ssh_keys(&self) -> Result<Vec<SshKey>, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!("{}/key", self.config.api_url)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: Vec<SshKeyWrapper> = self.handle_response(response)?; | ||||
|         let keys = wrapped.into_iter().map(|sk| sk.key).collect(); | ||||
|         Ok(keys) | ||||
|     } | ||||
|  | ||||
|     pub fn get_ssh_key(&self, fingerprint: &str) -> Result<SshKey, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!("{}/key/{}", self.config.api_url, fingerprint)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: SshKeyWrapper = self.handle_response(response)?; | ||||
|         Ok(wrapped.key) | ||||
|     } | ||||
|  | ||||
|     pub fn add_ssh_key(&self, name: &str, data: &str) -> Result<SshKey, AppError> { | ||||
|         let params = [("name", name), ("data", data)]; | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .post(format!("{}/key", self.config.api_url)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .form(¶ms) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: SshKeyWrapper = self.handle_response(response)?; | ||||
|         Ok(wrapped.key) | ||||
|     } | ||||
|  | ||||
|     pub fn update_ssh_key_name(&self, fingerprint: &str, name: &str) -> Result<SshKey, AppError> { | ||||
|         let params = [("name", name)]; | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .post(format!("{}/key/{}", self.config.api_url, fingerprint)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .form(¶ms) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: SshKeyWrapper = self.handle_response(response)?; | ||||
|         Ok(wrapped.key) | ||||
|     } | ||||
|  | ||||
|     pub fn delete_ssh_key(&self, fingerprint: &str) -> Result<(), AppError> { | ||||
|         self.http_client | ||||
|             .delete(format!("{}/key/{}", self.config.api_url, fingerprint)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|     pub fn get_boot_configuration(&self, server_number: i32) -> Result<Boot, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!("{}/boot/{}", self.config.api_url, server_number)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: BootWrapper = self.handle_response(response)?; | ||||
|         Ok(wrapped.boot) | ||||
|     } | ||||
|  | ||||
|     pub fn get_rescue_boot_configuration(&self, server_number: i32) -> Result<Rescue, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!( | ||||
|                 "{}/boot/{}/rescue", | ||||
|                 self.config.api_url, server_number | ||||
|             )) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: RescueWrapped = self.handle_response(response)?; | ||||
|         Ok(wrapped.rescue) | ||||
|     } | ||||
|  | ||||
|     pub fn enable_rescue_mode( | ||||
|         &self, | ||||
|         server_number: i32, | ||||
|         os: &str, | ||||
|         authorized_keys: Option<&[String]>, | ||||
|     ) -> Result<Rescue, AppError> { | ||||
|         let mut params = vec![("os", os)]; | ||||
|         if let Some(keys) = authorized_keys { | ||||
|             for key in keys { | ||||
|                 params.push(("authorized_key[]", key)); | ||||
|             } | ||||
|         } | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .post(format!( | ||||
|                 "{}/boot/{}/rescue", | ||||
|                 self.config.api_url, server_number | ||||
|             )) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .form(¶ms) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: RescueWrapped = self.handle_response(response)?; | ||||
|         Ok(wrapped.rescue) | ||||
|     } | ||||
|  | ||||
|     pub fn disable_rescue_mode(&self, server_number: i32) -> Result<Rescue, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .delete(format!( | ||||
|                 "{}/boot/{}/rescue", | ||||
|                 self.config.api_url, server_number | ||||
|             )) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: RescueWrapped = self.handle_response(response)?; | ||||
|         Ok(wrapped.rescue) | ||||
|     } | ||||
|  | ||||
|     pub fn get_server_products( | ||||
|         &self, | ||||
|     ) -> Result<Vec<OrderServerProduct>, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!("{}/order/server/product", &self.config.api_url)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: Vec<OrderServerProductWrapper> = self.handle_response(response)?; | ||||
|         let products = wrapped.into_iter().map(|sop| sop.product).collect(); | ||||
|         Ok(products) | ||||
|     } | ||||
|  | ||||
|     pub fn get_server_product_by_id( | ||||
|         &self, | ||||
|         product_id: &str, | ||||
|     ) -> Result<OrderServerProduct, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!( | ||||
|                 "{}/order/server/product/{}", | ||||
|                 &self.config.api_url, product_id | ||||
|             )) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: OrderServerProductWrapper = self.handle_response(response)?; | ||||
|         Ok(wrapped.product) | ||||
|     } | ||||
|     pub fn order_server(&self, order: OrderServerBuilder) -> Result<Transaction, AppError> { | ||||
|         let mut params = json!({ | ||||
|             "product_id": order.product_id, | ||||
|             "dist": order.dist, | ||||
|             "location": order.location, | ||||
|             "authorized_key": order.authorized_keys.unwrap_or_default(), | ||||
|         }); | ||||
|  | ||||
|         if let Some(addons) = order.addons { | ||||
|             params["addon"] = json!(addons); | ||||
|         } | ||||
|  | ||||
|         if let Some(test) = order.test { | ||||
|             if test { | ||||
|                 params["test"] = json!(test); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .post(format!("{}/order/server/transaction", &self.config.api_url)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .json(¶ms) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: TransactionWrapper = self.handle_response(response)?; | ||||
|         Ok(wrapped.transaction) | ||||
|     } | ||||
|  | ||||
|     pub fn get_transaction_by_id(&self, transaction_id: &str) -> Result<Transaction, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!( | ||||
|                 "{}/order/server/transaction/{}", | ||||
|                 &self.config.api_url, transaction_id | ||||
|             )) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: TransactionWrapper = self.handle_response(response)?; | ||||
|         Ok(wrapped.transaction) | ||||
|     } | ||||
|     pub fn get_transactions(&self) -> Result<Vec<Transaction>, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!("{}/order/server/transaction", &self.config.api_url)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: Vec<TransactionWrapper> = self.handle_response(response)?; | ||||
|         let transactions = wrapped.into_iter().map(|t| t.transaction).collect(); | ||||
|         Ok(transactions) | ||||
|     } | ||||
|     pub fn get_auction_server_products(&self) -> Result<Vec<AuctionServerProduct>, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!( | ||||
|                 "{}/order/server_market/product", | ||||
|                 &self.config.api_url | ||||
|             )) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: Vec<AuctionServerProductWrapper> = self.handle_response(response)?; | ||||
|         let products = wrapped.into_iter().map(|asp| asp.product).collect(); | ||||
|         Ok(products) | ||||
|     } | ||||
|     pub fn get_auction_server_product_by_id(&self, product_id: &str) -> Result<AuctionServerProduct, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!("{}/order/server_market/product/{}", &self.config.api_url, product_id)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: AuctionServerProductWrapper = self.handle_response(response)?; | ||||
|         Ok(wrapped.product) | ||||
|     } | ||||
|     pub fn get_auction_transactions(&self) -> Result<Vec<AuctionTransaction>, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!("{}/order/server_market/transaction", &self.config.api_url)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: Vec<AuctionTransactionWrapper> = self.handle_response(response)?; | ||||
|         let transactions = wrapped.into_iter().map(|t| t.transaction).collect(); | ||||
|         Ok(transactions) | ||||
|     } | ||||
|  | ||||
|     pub fn get_auction_transaction_by_id(&self, transaction_id: &str) -> Result<AuctionTransaction, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!("{}/order/server_market/transaction/{}", &self.config.api_url, transaction_id)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: AuctionTransactionWrapper = self.handle_response(response)?; | ||||
|         Ok(wrapped.transaction) | ||||
|     } | ||||
|  | ||||
|     pub fn get_server_addon_products( | ||||
|         &self, | ||||
|         server_number: i64, | ||||
|     ) -> Result<Vec<ServerAddonProduct>, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!( | ||||
|                 "{}/order/server_addon/{}/product", | ||||
|                 &self.config.api_url, server_number | ||||
|             )) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: Vec<ServerAddonProductWrapper> = self.handle_response(response)?; | ||||
|         let products = wrapped.into_iter().map(|sap| sap.product).collect(); | ||||
|         Ok(products) | ||||
|     } | ||||
|  | ||||
|     pub fn order_auction_server( | ||||
|         &self, | ||||
|         product_id: i64, | ||||
|         authorized_keys: Vec<String>, | ||||
|         dist: Option<String>, | ||||
|         arch: Option<String>, | ||||
|         lang: Option<String>, | ||||
|         comment: Option<String>, | ||||
|         addons: Option<Vec<String>>, | ||||
|         test: Option<bool>, | ||||
|     ) -> Result<AuctionTransaction, AppError> { | ||||
|         let mut params: Vec<(&str, String)> = Vec::new(); | ||||
|  | ||||
|         params.push(("product_id", product_id.to_string())); | ||||
|  | ||||
|         for key in &authorized_keys { | ||||
|             params.push(("authorized_key[]", key.clone())); | ||||
|         } | ||||
|  | ||||
|         if let Some(dist) = dist { | ||||
|             params.push(("dist", dist)); | ||||
|         } | ||||
|         if let Some(arch) = arch { | ||||
|             params.push(("@deprecated arch", arch)); | ||||
|         } | ||||
|         if let Some(lang) = lang { | ||||
|             params.push(("lang", lang)); | ||||
|         } | ||||
|         if let Some(comment) = comment { | ||||
|             params.push(("comment", comment)); | ||||
|         } | ||||
|         if let Some(addons) = addons { | ||||
|             for addon in addons { | ||||
|                 params.push(("addon[]", addon)); | ||||
|             } | ||||
|         } | ||||
|         if let Some(test) = test { | ||||
|             params.push(("test", test.to_string())); | ||||
|         } | ||||
|  | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .post(format!("{}/order/server_market/transaction", &self.config.api_url)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .form(¶ms) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: AuctionTransactionWrapper = self.handle_response(response)?; | ||||
|         Ok(wrapped.transaction) | ||||
|     } | ||||
|  | ||||
|     pub fn get_server_addon_transactions(&self) -> Result<Vec<ServerAddonTransaction>, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!("{}/order/server_addon/transaction", &self.config.api_url)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: Vec<ServerAddonTransactionWrapper> = self.handle_response(response)?; | ||||
|         let transactions = wrapped.into_iter().map(|satw| satw.transaction).collect(); | ||||
|         Ok(transactions) | ||||
|     } | ||||
|  | ||||
|     pub fn get_server_addon_transaction_by_id( | ||||
|         &self, | ||||
|         transaction_id: &str, | ||||
|     ) -> Result<ServerAddonTransaction, AppError> { | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .get(format!( | ||||
|                 "{}/order/server_addon/transaction/{}", | ||||
|                 &self.config.api_url, transaction_id | ||||
|             )) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: ServerAddonTransactionWrapper = self.handle_response(response)?; | ||||
|         Ok(wrapped.transaction) | ||||
|     } | ||||
|  | ||||
|     pub fn order_server_addon( | ||||
|         &self, | ||||
|         order: OrderServerAddonBuilder, | ||||
|     ) -> Result<ServerAddonTransaction, AppError> { | ||||
|         let mut params = json!({ | ||||
|             "server_number": order.server_number, | ||||
|             "product_id": order.product_id, | ||||
|         }); | ||||
|  | ||||
|         if let Some(reason) = order.reason { | ||||
|             params["reason"] = json!(reason); | ||||
|         } | ||||
|         if let Some(gateway) = order.gateway { | ||||
|             params["gateway"] = json!(gateway); | ||||
|         } | ||||
|         if let Some(test) = order.test { | ||||
|             if test { | ||||
|                 params["test"] = json!(test); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let response = self | ||||
|             .http_client | ||||
|             .post(format!("{}/order/server_addon/transaction", &self.config.api_url)) | ||||
|             .basic_auth(&self.config.username, Some(&self.config.password)) | ||||
|             .form(¶ms) | ||||
|             .send()?; | ||||
|  | ||||
|         let wrapped: ServerAddonTransactionWrapper = self.handle_response(response)?; | ||||
|         Ok(wrapped.transaction) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										1894
									
								
								packages/clients/hetznerclient/src/api/models.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1894
									
								
								packages/clients/hetznerclient/src/api/models.rs
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										25
									
								
								packages/clients/hetznerclient/src/config.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								packages/clients/hetznerclient/src/config.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| use std::env; | ||||
|  | ||||
| #[derive(Clone)] | ||||
| pub struct Config { | ||||
|     pub username: String, | ||||
|     pub password: String, | ||||
|     pub api_url: String, | ||||
| } | ||||
|  | ||||
| impl Config { | ||||
|     pub fn from_env() -> Result<Self, String> { | ||||
|         let username = env::var("HETZNER_USERNAME") | ||||
|             .map_err(|_| "HETZNER_USERNAME environment variable not set".to_string())?; | ||||
|         let password = env::var("HETZNER_PASSWORD") | ||||
|             .map_err(|_| "HETZNER_PASSWORD environment variable not set".to_string())?; | ||||
|         let api_url = env::var("HETZNER_API_URL") | ||||
|             .unwrap_or_else(|_| "https://robot-ws.your-server.de".to_string()); | ||||
|  | ||||
|         Ok(Config { | ||||
|             username, | ||||
|             password, | ||||
|             api_url, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3
									
								
								packages/clients/hetznerclient/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								packages/clients/hetznerclient/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| pub mod api; | ||||
| pub mod config; | ||||
| pub mod rhai; | ||||
							
								
								
									
										63
									
								
								packages/clients/hetznerclient/src/rhai/boot.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								packages/clients/hetznerclient/src/rhai/boot.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| use crate::api::{ | ||||
|     models::{Boot, Rescue}, | ||||
|     Client, | ||||
| }; | ||||
| use rhai::{plugin::*, Engine}; | ||||
|  | ||||
| pub fn register(engine: &mut Engine) { | ||||
|     let boot_module = exported_module!(boot_api); | ||||
|     engine.register_global_module(boot_module.into()); | ||||
| } | ||||
|  | ||||
| #[export_module] | ||||
| pub mod boot_api { | ||||
|     use super::*; | ||||
|     use rhai::EvalAltResult; | ||||
|  | ||||
|     #[rhai_fn(name = "get_boot_configuration", return_raw)] | ||||
|     pub fn get_boot_configuration( | ||||
|         client: &mut Client, | ||||
|         server_number: i64, | ||||
|     ) -> Result<Boot, Box<EvalAltResult>> { | ||||
|         client | ||||
|             .get_boot_configuration(server_number as i32) | ||||
|             .map_err(|e| e.to_string().into()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "get_rescue_boot_configuration", return_raw)] | ||||
|     pub fn get_rescue_boot_configuration( | ||||
|         client: &mut Client, | ||||
|         server_number: i64, | ||||
|     ) -> Result<Rescue, Box<EvalAltResult>> { | ||||
|         client | ||||
|             .get_rescue_boot_configuration(server_number as i32) | ||||
|             .map_err(|e| e.to_string().into()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "enable_rescue_mode", return_raw)] | ||||
|     pub fn enable_rescue_mode( | ||||
|         client: &mut Client, | ||||
|         server_number: i64, | ||||
|         os: &str, | ||||
|         authorized_keys: rhai::Array, | ||||
|     ) -> Result<Rescue, Box<EvalAltResult>> { | ||||
|         let keys: Vec<String> = authorized_keys | ||||
|             .into_iter() | ||||
|             .map(|k| k.into_string().unwrap()) | ||||
|             .collect(); | ||||
|  | ||||
|         client | ||||
|             .enable_rescue_mode(server_number as i32, os, Some(&keys)) | ||||
|             .map_err(|e| e.to_string().into()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "disable_rescue_mode", return_raw)] | ||||
|     pub fn disable_rescue_mode( | ||||
|         client: &mut Client, | ||||
|         server_number: i64, | ||||
|     ) -> Result<Rescue, Box<EvalAltResult>> { | ||||
|         client | ||||
|             .disable_rescue_mode(server_number as i32) | ||||
|             .map_err(|e| e.to_string().into()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										54
									
								
								packages/clients/hetznerclient/src/rhai/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								packages/clients/hetznerclient/src/rhai/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| use rhai::{Engine, EvalAltResult}; | ||||
|  | ||||
| use crate::api::models::{ | ||||
|     AuctionServerProduct, AuctionTransaction, AuctionTransactionProduct, AuthorizedKey, Boot, | ||||
|     Cancellation, Cpanel, HostKey, Linux, OrderAuctionServerBuilder, OrderServerAddonBuilder, | ||||
|     OrderServerBuilder, OrderServerProduct, Plesk, Rescue, Server, ServerAddonProduct, | ||||
|     ServerAddonResource, ServerAddonTransaction, SshKey, Transaction, TransactionProduct, Vnc, | ||||
|     Windows, | ||||
| }; | ||||
|  | ||||
| pub mod boot; | ||||
| pub mod printing; | ||||
| pub mod server; | ||||
| pub mod server_ordering; | ||||
| pub mod ssh_keys; | ||||
|  | ||||
| // here just register the hetzner module | ||||
| pub fn register_hetzner_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { | ||||
|     // TODO:register types | ||||
|     engine.build_type::<Server>(); | ||||
|     engine.build_type::<SshKey>(); | ||||
|     engine.build_type::<Boot>(); | ||||
|     engine.build_type::<Rescue>(); | ||||
|     engine.build_type::<Linux>(); | ||||
|     engine.build_type::<Vnc>(); | ||||
|     engine.build_type::<Windows>(); | ||||
|     engine.build_type::<Plesk>(); | ||||
|     engine.build_type::<Cpanel>(); | ||||
|     engine.build_type::<Cancellation>(); | ||||
|     engine.build_type::<OrderServerProduct>(); | ||||
|     engine.build_type::<Transaction>(); | ||||
|     engine.build_type::<AuthorizedKey>(); | ||||
|     engine.build_type::<TransactionProduct>(); | ||||
|     engine.build_type::<HostKey>(); | ||||
|     engine.build_type::<AuctionServerProduct>(); | ||||
|     engine.build_type::<AuctionTransaction>(); | ||||
|     engine.build_type::<AuctionTransactionProduct>(); | ||||
|     engine.build_type::<OrderAuctionServerBuilder>(); | ||||
|     engine.build_type::<OrderServerBuilder>(); | ||||
|     engine.build_type::<ServerAddonProduct>(); | ||||
|     engine.build_type::<ServerAddonTransaction>(); | ||||
|     engine.build_type::<ServerAddonResource>(); | ||||
|     engine.build_type::<OrderServerAddonBuilder>(); | ||||
|  | ||||
|     server::register(engine); | ||||
|     ssh_keys::register(engine); | ||||
|     boot::register(engine); | ||||
|     server_ordering::register(engine); | ||||
|  | ||||
|     // TODO: push hetzner to scope as value client: | ||||
|     // scope.push("hetzner", client); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										43
									
								
								packages/clients/hetznerclient/src/rhai/printing/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								packages/clients/hetznerclient/src/rhai/printing/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| use rhai::{Array, Engine}; | ||||
| use crate::{api::models::{OrderServerProduct, AuctionServerProduct, AuctionTransaction, ServerAddonProduct, ServerAddonTransaction, Server, SshKey}}; | ||||
|  | ||||
| mod servers_table; | ||||
| mod ssh_keys_table; | ||||
| mod server_ordering_table; | ||||
|  | ||||
| // This will be called when we print(...) or pretty_print() an Array (with Dynamic values) | ||||
| pub fn pretty_print_dispatch(array: Array) { | ||||
|     if array.is_empty() { | ||||
|         println!("<empty table>"); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     let first = &array[0]; | ||||
|  | ||||
|     if first.is::<Server>() { | ||||
|         println!("Yeah first is server!"); | ||||
|         servers_table::pretty_print_servers(array); | ||||
|     } else if first.is::<SshKey>() { | ||||
|         ssh_keys_table::pretty_print_ssh_keys(array); | ||||
|     } | ||||
|     else if first.is::<OrderServerProduct>() { | ||||
|         server_ordering_table::pretty_print_server_products(array); | ||||
|     } else if first.is::<AuctionServerProduct>() { | ||||
|         server_ordering_table::pretty_print_auction_server_products(array); | ||||
|     } else if first.is::<AuctionTransaction>() { | ||||
|         server_ordering_table::pretty_print_auction_transactions(array); | ||||
|     } else if first.is::<ServerAddonProduct>() { | ||||
|         server_ordering_table::pretty_print_server_addon_products(array); | ||||
|     } else if first.is::<ServerAddonTransaction>() { | ||||
|         server_ordering_table::pretty_print_server_addon_transactions(array); | ||||
|     } else { | ||||
|         // Generic fallback for other types | ||||
|         for item in array { | ||||
|             println!("{}", item.to_string()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn register(engine: &mut Engine) { | ||||
|     engine.register_fn("pretty_print", pretty_print_dispatch); | ||||
| } | ||||
| @@ -0,0 +1,293 @@ | ||||
| use prettytable::{row, Table}; | ||||
| use crate::api::models::{OrderServerProduct, ServerAddonProduct, ServerAddonTransaction, ServerAddonResource}; | ||||
|  | ||||
| pub fn pretty_print_server_products(products: rhai::Array) { | ||||
|     let mut table = Table::new(); | ||||
|     table.add_row(row![b => | ||||
|         "ID", | ||||
|         "Name", | ||||
|         "Description", | ||||
|         "Traffic", | ||||
|         "Location", | ||||
|         "Price (Net)", | ||||
|         "Price (Gross)", | ||||
|     ]); | ||||
|  | ||||
|     for product_dyn in products { | ||||
|         if let Some(product) = product_dyn.try_cast::<OrderServerProduct>() { | ||||
|             let mut price_net = "N/A".to_string(); | ||||
|             let mut price_gross = "N/A".to_string(); | ||||
|  | ||||
|             if let Some(first_price) = product.prices.first() { | ||||
|                 price_net = first_price.price.net.clone(); | ||||
|                 price_gross = first_price.price.gross.clone(); | ||||
|             } | ||||
|  | ||||
|             table.add_row(row![ | ||||
|                 product.id, | ||||
|                 product.name, | ||||
|                 product.description.join(", "), | ||||
|                 product.traffic, | ||||
|                 product.location.join(", "), | ||||
|                 price_net, | ||||
|                 price_gross, | ||||
|             ]); | ||||
|         } | ||||
|     } | ||||
|     table.printstd(); | ||||
| } | ||||
|  | ||||
| pub fn pretty_print_auction_server_products(products: rhai::Array) { | ||||
|     let mut table = Table::new(); | ||||
|     table.add_row(row![b => | ||||
|         "ID", | ||||
|         "Name", | ||||
|         "Description", | ||||
|         "Traffic", | ||||
|         "Distributions", | ||||
|         "Architectures", | ||||
|         "Languages", | ||||
|         "CPU", | ||||
|         "CPU Benchmark", | ||||
|         "Memory Size (GB)", | ||||
|         "HDD Size (GB)", | ||||
|         "HDD Text", | ||||
|         "HDD Count", | ||||
|         "Datacenter", | ||||
|         "Network Speed", | ||||
|         "Price (Net)", | ||||
|         "Price (Hourly Net)", | ||||
|         "Price (Setup Net)", | ||||
|         "Price (VAT)", | ||||
|         "Price (Hourly VAT)", | ||||
|         "Price (Setup VAT)", | ||||
|         "Fixed Price", | ||||
|         "Next Reduce (seconds)", | ||||
|         "Next Reduce Date", | ||||
|         "Orderable Addons", | ||||
|     ]); | ||||
|  | ||||
|     for product_dyn in products { | ||||
|         if let Some(product) = product_dyn.try_cast::<crate::api::models::AuctionServerProduct>() { | ||||
|             let mut addons_table = Table::new(); | ||||
|             addons_table.add_row(row![b => "ID", "Name", "Min", "Max", "Prices"]); | ||||
|             for addon in &product.orderable_addons { | ||||
|                 let mut addon_prices_table = Table::new(); | ||||
|                 addon_prices_table.add_row(row![b => "Location", "Net", "Gross", "Hourly Net", "Hourly Gross", "Setup Net", "Setup Gross"]); | ||||
|                 for price in &addon.prices { | ||||
|                     addon_prices_table.add_row(row![ | ||||
|                         price.location, | ||||
|                         price.price.net, | ||||
|                         price.price.gross, | ||||
|                         price.price.hourly_net, | ||||
|                         price.price.hourly_gross, | ||||
|                         price.price_setup.net, | ||||
|                         price.price_setup.gross | ||||
|                     ]); | ||||
|                 } | ||||
|                 addons_table.add_row(row![ | ||||
|                     addon.id, | ||||
|                     addon.name, | ||||
|                     addon.min, | ||||
|                     addon.max, | ||||
|                     addon_prices_table | ||||
|                 ]); | ||||
|             } | ||||
|  | ||||
|             table.add_row(row![ | ||||
|                 product.id, | ||||
|                 product.name, | ||||
|                 product.description.join(", "), | ||||
|                 product.traffic, | ||||
|                 product.dist.join(", "), | ||||
|                 product.arch.as_deref().unwrap_or_default().join(", "), | ||||
|                 product.lang.join(", "), | ||||
|                 product.cpu, | ||||
|                 product.cpu_benchmark, | ||||
|                 product.memory_size, | ||||
|                 product.hdd_size, | ||||
|                 product.hdd_text, | ||||
|                 product.hdd_count, | ||||
|                 product.datacenter, | ||||
|                 product.network_speed, | ||||
|                 product.price, | ||||
|                 product.price_hourly.as_deref().unwrap_or("N/A"), | ||||
|                 product.price_setup, | ||||
|                 product.price_with_vat, | ||||
|                 product.price_hourly_with_vat.as_deref().unwrap_or("N/A"), | ||||
|                 product.price_setup_with_vat, | ||||
|                 product.fixed_price, | ||||
|                 product.next_reduce, | ||||
|                 product.next_reduce_date, | ||||
|                 addons_table, | ||||
|             ]); | ||||
|         } | ||||
|     } | ||||
|     table.printstd(); | ||||
| } | ||||
|  | ||||
| pub fn pretty_print_server_addon_products(products: rhai::Array) { | ||||
|     let mut table = Table::new(); | ||||
|     table.add_row(row![b => | ||||
|         "ID", | ||||
|         "Name", | ||||
|         "Type", | ||||
|         "Location", | ||||
|         "Price (Net)", | ||||
|         "Price (Gross)", | ||||
|         "Hourly Net", | ||||
|         "Hourly Gross", | ||||
|         "Setup Net", | ||||
|         "Setup Gross", | ||||
|     ]); | ||||
|  | ||||
|     for product_dyn in products { | ||||
|         if let Some(product) = product_dyn.try_cast::<ServerAddonProduct>() { | ||||
|             table.add_row(row![ | ||||
|                 product.id, | ||||
|                 product.name, | ||||
|                 product.product_type, | ||||
|                 product.price.location, | ||||
|                 product.price.price.net, | ||||
|                 product.price.price.gross, | ||||
|                 product.price.price.hourly_net, | ||||
|                 product.price.price.hourly_gross, | ||||
|                 product.price.price_setup.net, | ||||
|                 product.price.price_setup.gross, | ||||
|             ]); | ||||
|         } | ||||
|     } | ||||
|     table.printstd(); | ||||
| } | ||||
|  | ||||
| pub fn pretty_print_auction_transactions(transactions: rhai::Array) { | ||||
|     let mut table = Table::new(); | ||||
|     table.add_row(row![b => | ||||
|         "ID", | ||||
|         "Date", | ||||
|         "Status", | ||||
|         "Server Number", | ||||
|         "Server IP", | ||||
|         "Comment", | ||||
|         "Product ID", | ||||
|         "Product Name", | ||||
|         "Product Traffic", | ||||
|         "Product Distributions", | ||||
|         "Product Architectures", | ||||
|         "Product Languages", | ||||
|         "Product CPU", | ||||
|         "Product CPU Benchmark", | ||||
|         "Product Memory Size (GB)", | ||||
|         "Product HDD Size (GB)", | ||||
|         "Product HDD Text", | ||||
|         "Product HDD Count", | ||||
|         "Product Datacenter", | ||||
|         "Product Network Speed", | ||||
|         "Product Fixed Price", | ||||
|         "Product Next Reduce (seconds)", | ||||
|         "Product Next Reduce Date", | ||||
|         "Addons", | ||||
|     ]); | ||||
|  | ||||
|     for transaction_dyn in transactions { | ||||
|         if let Some(transaction) = transaction_dyn.try_cast::<crate::api::models::AuctionTransaction>() { | ||||
|             let _authorized_keys_table = { | ||||
|                 let mut table = Table::new(); | ||||
|                 table.add_row(row![b => "Name", "Fingerprint", "Type", "Size"]); | ||||
|                 for key in &transaction.authorized_key { | ||||
|                     table.add_row(row![ | ||||
|                         key.key.name.as_deref().unwrap_or("N/A"), | ||||
|                         key.key.fingerprint.as_deref().unwrap_or("N/A"), | ||||
|                         key.key.key_type.as_deref().unwrap_or("N/A"), | ||||
|                         key.key.size.map_or("N/A".to_string(), |s| s.to_string()) | ||||
|                     ]); | ||||
|                 } | ||||
|                 table | ||||
|             }; | ||||
|  | ||||
|             let _host_keys_table = { | ||||
|                 let mut table = Table::new(); | ||||
|                 table.add_row(row![b => "Fingerprint", "Type", "Size"]); | ||||
|                 for key in &transaction.host_key { | ||||
|                     table.add_row(row![ | ||||
|                         key.key.fingerprint.as_deref().unwrap_or("N/A"), | ||||
|                         key.key.key_type.as_deref().unwrap_or("N/A"), | ||||
|                         key.key.size.map_or("N/A".to_string(), |s| s.to_string()) | ||||
|                     ]); | ||||
|                 } | ||||
|                 table | ||||
|             }; | ||||
|  | ||||
|             table.add_row(row![ | ||||
|                 transaction.id, | ||||
|                 transaction.date, | ||||
|                 transaction.status, | ||||
|                 transaction.server_number.map_or("N/A".to_string(), |id| id.to_string()), | ||||
|                 transaction.server_ip.as_deref().unwrap_or("N/A"), | ||||
|                 transaction.comment.as_deref().unwrap_or("N/A"), | ||||
|                 transaction.product.id, | ||||
|                 transaction.product.name, | ||||
|                 transaction.product.traffic, | ||||
|                 transaction.product.dist, | ||||
|                 transaction.product.arch.as_deref().unwrap_or("N/A"), | ||||
|                 transaction.product.lang, | ||||
|                 transaction.product.cpu, | ||||
|                 transaction.product.cpu_benchmark, | ||||
|                 transaction.product.memory_size, | ||||
|                 transaction.product.hdd_size, | ||||
|                 transaction.product.hdd_text, | ||||
|                 transaction.product.hdd_count, | ||||
|                 transaction.product.datacenter, | ||||
|                 transaction.product.network_speed, | ||||
|                 transaction.product.fixed_price.unwrap_or_default().to_string(), | ||||
|                 transaction | ||||
|                     .product | ||||
|                     .next_reduce | ||||
|                     .map_or("N/A".to_string(), |r| r.to_string()), | ||||
|                 transaction | ||||
|                     .product | ||||
|                     .next_reduce_date | ||||
|                     .as_deref() | ||||
|                     .unwrap_or("N/A"), | ||||
|                 transaction.addons.join(", "), | ||||
|             ]); | ||||
|         } | ||||
|     } | ||||
|     table.printstd(); | ||||
| } | ||||
|  | ||||
| pub fn pretty_print_server_addon_transactions(transactions: rhai::Array) { | ||||
|     let mut table = Table::new(); | ||||
|     table.add_row(row![b => | ||||
|         "ID", | ||||
|         "Date", | ||||
|         "Status", | ||||
|         "Server Number", | ||||
|         "Product ID", | ||||
|         "Product Name", | ||||
|         "Product Price", | ||||
|         "Resources", | ||||
|     ]); | ||||
|  | ||||
|     for transaction_dyn in transactions { | ||||
|         if let Some(transaction) = transaction_dyn.try_cast::<ServerAddonTransaction>() { | ||||
|             let mut resources_table = Table::new(); | ||||
|             resources_table.add_row(row![b => "Type", "ID"]); | ||||
|             for resource in &transaction.resources { | ||||
|                 resources_table.add_row(row![resource.resource_type, resource.id]); | ||||
|             } | ||||
|  | ||||
|             table.add_row(row![ | ||||
|                 transaction.id, | ||||
|                 transaction.date, | ||||
|                 transaction.status, | ||||
|                 transaction.server_number, | ||||
|                 transaction.product.id, | ||||
|                 transaction.product.name, | ||||
|                 transaction.product.price.to_string(), | ||||
|                 resources_table, | ||||
|             ]); | ||||
|         } | ||||
|     } | ||||
|     table.printstd(); | ||||
| } | ||||
| @@ -0,0 +1,30 @@ | ||||
| use prettytable::{row, Table}; | ||||
| use rhai::Array; | ||||
|  | ||||
| use super::Server; | ||||
|  | ||||
| pub fn pretty_print_servers(servers: Array) { | ||||
|     let mut table = Table::new(); | ||||
|     table.add_row(row![b => | ||||
|         "Number", | ||||
|         "Name", | ||||
|         "IP", | ||||
|         "Product", | ||||
|         "DC", | ||||
|         "Status" | ||||
|     ]); | ||||
|  | ||||
|     for server_dyn in servers { | ||||
|         if let Some(server) = server_dyn.try_cast::<Server>() { | ||||
|             table.add_row(row![ | ||||
|                 server.server_number.to_string(), | ||||
|                 server.server_name, | ||||
|                 server.server_ip.unwrap_or("N/A".to_string()), | ||||
|                 server.product, | ||||
|                 server.dc, | ||||
|                 server.status | ||||
|             ]); | ||||
|         } | ||||
|     } | ||||
|     table.printstd(); | ||||
| } | ||||
| @@ -0,0 +1,26 @@ | ||||
| use prettytable::{row, Table}; | ||||
| use super::SshKey; | ||||
|  | ||||
| pub fn pretty_print_ssh_keys(keys: rhai::Array) { | ||||
|     let mut table = Table::new(); | ||||
|     table.add_row(row![b => | ||||
|         "Name", | ||||
|         "Fingerprint", | ||||
|         "Type", | ||||
|         "Size", | ||||
|         "Created At" | ||||
|     ]); | ||||
|  | ||||
|     for key_dyn in keys { | ||||
|         if let Some(key) = key_dyn.try_cast::<SshKey>() { | ||||
|             table.add_row(row![ | ||||
|                 key.name, | ||||
|                 key.fingerprint, | ||||
|                 key.key_type, | ||||
|                 key.size.to_string(), | ||||
|                 key.created_at | ||||
|             ]); | ||||
|         } | ||||
|     } | ||||
|     table.printstd(); | ||||
| } | ||||
							
								
								
									
										76
									
								
								packages/clients/hetznerclient/src/rhai/server.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								packages/clients/hetznerclient/src/rhai/server.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| use crate::api::{Client, models::Server}; | ||||
| use rhai::{Array, Dynamic, plugin::*}; | ||||
|  | ||||
| pub fn register(engine: &mut Engine) { | ||||
|     let server_module = exported_module!(server_api); | ||||
|     engine.register_global_module(server_module.into()); | ||||
| } | ||||
|  | ||||
| #[export_module] | ||||
| pub mod server_api { | ||||
|     use crate::api::models::Cancellation; | ||||
|  | ||||
|     use super::*; | ||||
|     use rhai::EvalAltResult; | ||||
|  | ||||
|     #[rhai_fn(name = "get_server", return_raw)] | ||||
|     pub fn get_server( | ||||
|         client: &mut Client, | ||||
|         server_number: i64, | ||||
|     ) -> Result<Server, Box<EvalAltResult>> { | ||||
|         client | ||||
|             .get_server(server_number as i32) | ||||
|             .map_err(|e| e.to_string().into()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "get_servers", return_raw)] | ||||
|     pub fn get_servers(client: &mut Client) -> Result<Array, Box<EvalAltResult>> { | ||||
|         let servers = client | ||||
|             .get_servers() | ||||
|             .map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; | ||||
|         println!("number of SERVERS we got: {:#?}", servers.len()); | ||||
|         Ok(servers.into_iter().map(Dynamic::from).collect()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "update_server_name", return_raw)] | ||||
|     pub fn update_server_name( | ||||
|         client: &mut Client, | ||||
|         server_number: i64, | ||||
|         name: &str, | ||||
|     ) -> Result<Server, Box<EvalAltResult>> { | ||||
|         client | ||||
|             .update_server_name(server_number as i32, name) | ||||
|             .map_err(|e| e.to_string().into()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "get_cancellation_data", return_raw)] | ||||
|     pub fn get_cancellation_data( | ||||
|         client: &mut Client, | ||||
|         server_number: i64, | ||||
|     ) -> Result<Cancellation, Box<EvalAltResult>> { | ||||
|         client | ||||
|             .get_cancellation_data(server_number as i32) | ||||
|             .map_err(|e| e.to_string().into()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "cancel_server", return_raw)] | ||||
|     pub fn cancel_server( | ||||
|         client: &mut Client, | ||||
|         server_number: i64, | ||||
|         cancellation_date: &str, | ||||
|     ) -> Result<Cancellation, Box<EvalAltResult>> { | ||||
|         client | ||||
|             .cancel_server(server_number as i32, cancellation_date) | ||||
|             .map_err(|e| e.to_string().into()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "withdraw_cancellation", return_raw)] | ||||
|     pub fn withdraw_cancellation( | ||||
|         client: &mut Client, | ||||
|         server_number: i64, | ||||
|     ) -> Result<(), Box<EvalAltResult>> { | ||||
|         client | ||||
|             .withdraw_cancellation(server_number as i32) | ||||
|             .map_err(|e| e.to_string().into()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										170
									
								
								packages/clients/hetznerclient/src/rhai/server_ordering.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								packages/clients/hetznerclient/src/rhai/server_ordering.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,170 @@ | ||||
| use crate::api::{ | ||||
|     Client, | ||||
|     models::{ | ||||
|         AuctionServerProduct, AuctionTransaction, OrderAuctionServerBuilder, OrderServerBuilder, | ||||
|         OrderServerProduct, ServerAddonProduct, ServerAddonTransaction, Transaction, | ||||
|     }, | ||||
| }; | ||||
| use rhai::{Array, Dynamic, plugin::*}; | ||||
|  | ||||
| pub fn register(engine: &mut Engine) { | ||||
|     let server_order_module = exported_module!(server_order_api); | ||||
|     engine.register_global_module(server_order_module.into()); | ||||
| } | ||||
|  | ||||
| #[export_module] | ||||
| pub mod server_order_api { | ||||
|     use crate::api::models::OrderServerAddonBuilder; | ||||
|  | ||||
|     #[rhai_fn(name = "get_server_products", return_raw)] | ||||
|     pub fn get_server_ordering_product_overview( | ||||
|         client: &mut Client, | ||||
|     ) -> Result<Array, Box<EvalAltResult>> { | ||||
|         let overview_servers = client | ||||
|             .get_server_products() | ||||
|             .map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; | ||||
|         Ok(overview_servers.into_iter().map(Dynamic::from).collect()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "get_server_product_by_id", return_raw)] | ||||
|     pub fn get_server_ordering_product_by_id( | ||||
|         client: &mut Client, | ||||
|         product_id: &str, | ||||
|     ) -> Result<OrderServerProduct, Box<EvalAltResult>> { | ||||
|         let product = client | ||||
|             .get_server_product_by_id(product_id) | ||||
|             .map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; | ||||
|         Ok(product) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "order_server", return_raw)] | ||||
|     pub fn order_server( | ||||
|         client: &mut Client, | ||||
|         order: OrderServerBuilder, | ||||
|     ) -> Result<Transaction, Box<EvalAltResult>> { | ||||
|         let transaction = client | ||||
|             .order_server(order) | ||||
|             .map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; | ||||
|         Ok(transaction) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "get_transaction_by_id", return_raw)] | ||||
|     pub fn get_transaction_by_id( | ||||
|         client: &mut Client, | ||||
|         transaction_id: &str, | ||||
|     ) -> Result<Transaction, Box<EvalAltResult>> { | ||||
|         let transaction = client | ||||
|             .get_transaction_by_id(transaction_id) | ||||
|             .map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; | ||||
|         Ok(transaction) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "get_transactions", return_raw)] | ||||
|     pub fn get_transactions(client: &mut Client) -> Result<Array, Box<EvalAltResult>> { | ||||
|         let transactions = client | ||||
|             .get_transactions() | ||||
|             .map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; | ||||
|         Ok(transactions.into_iter().map(Dynamic::from).collect()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "get_auction_server_products", return_raw)] | ||||
|     pub fn get_auction_server_products(client: &mut Client) -> Result<Array, Box<EvalAltResult>> { | ||||
|         let products = client | ||||
|             .get_auction_server_products() | ||||
|             .map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; | ||||
|         Ok(products.into_iter().map(Dynamic::from).collect()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "get_auction_server_product_by_id", return_raw)] | ||||
|     pub fn get_auction_server_product_by_id( | ||||
|         client: &mut Client, | ||||
|         product_id: &str, | ||||
|     ) -> Result<AuctionServerProduct, Box<EvalAltResult>> { | ||||
|         let product = client | ||||
|             .get_auction_server_product_by_id(product_id) | ||||
|             .map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; | ||||
|         Ok(product) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "get_auction_transactions", return_raw)] | ||||
|     pub fn get_auction_transactions(client: &mut Client) -> Result<Array, Box<EvalAltResult>> { | ||||
|         let transactions = client | ||||
|             .get_auction_transactions() | ||||
|             .map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; | ||||
|         Ok(transactions.into_iter().map(Dynamic::from).collect()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "get_auction_transaction_by_id", return_raw)] | ||||
|     pub fn get_auction_transaction_by_id( | ||||
|         client: &mut Client, | ||||
|         transaction_id: &str, | ||||
|     ) -> Result<AuctionTransaction, Box<EvalAltResult>> { | ||||
|         let transaction = client | ||||
|             .get_auction_transaction_by_id(transaction_id) | ||||
|             .map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; | ||||
|         Ok(transaction) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "get_server_addon_products", return_raw)] | ||||
|     pub fn get_server_addon_products( | ||||
|         client: &mut Client, | ||||
|         server_number: i64, | ||||
|     ) -> Result<Array, Box<EvalAltResult>> { | ||||
|         let products = client | ||||
|             .get_server_addon_products(server_number) | ||||
|             .map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; | ||||
|         Ok(products.into_iter().map(Dynamic::from).collect()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "get_server_addon_transactions", return_raw)] | ||||
|     pub fn get_server_addon_transactions( | ||||
|         client: &mut Client, | ||||
|     ) -> Result<Array, Box<EvalAltResult>> { | ||||
|         let transactions = client | ||||
|             .get_server_addon_transactions() | ||||
|             .map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; | ||||
|         Ok(transactions.into_iter().map(Dynamic::from).collect()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "get_server_addon_transaction_by_id", return_raw)] | ||||
|     pub fn get_server_addon_transaction_by_id( | ||||
|         client: &mut Client, | ||||
|         transaction_id: &str, | ||||
|     ) -> Result<ServerAddonTransaction, Box<EvalAltResult>> { | ||||
|         let transaction = client | ||||
|             .get_server_addon_transaction_by_id(transaction_id) | ||||
|             .map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; | ||||
|         Ok(transaction) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "order_auction_server", return_raw)] | ||||
|     pub fn order_auction_server( | ||||
|         client: &mut Client, | ||||
|         order: OrderAuctionServerBuilder, | ||||
|     ) -> Result<AuctionTransaction, Box<EvalAltResult>> { | ||||
|         println!("Builder struct being used to order server: {:#?}", order); | ||||
|         let transaction = client.order_auction_server( | ||||
|             order.product_id, | ||||
|             order.authorized_keys.unwrap_or(vec![]), | ||||
|             order.dist, | ||||
|             None, | ||||
|             order.lang, | ||||
|             order.comment, | ||||
|             order.addon, | ||||
|             order.test, | ||||
|         ).map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; | ||||
|         Ok(transaction) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "order_server_addon", return_raw)] | ||||
|     pub fn order_server_addon( | ||||
|         client: &mut Client, | ||||
|         order: OrderServerAddonBuilder, | ||||
|     ) -> Result<ServerAddonTransaction, Box<EvalAltResult>> { | ||||
|         println!("Builder struct being used to order server addon: {:#?}", order); | ||||
|         let transaction = client | ||||
|             .order_server_addon(order) | ||||
|             .map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; | ||||
|         Ok(transaction) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										89
									
								
								packages/clients/hetznerclient/src/rhai/ssh_keys.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								packages/clients/hetznerclient/src/rhai/ssh_keys.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| use crate::api::{Client, models::SshKey}; | ||||
| use prettytable::{Table, row}; | ||||
| use rhai::{Array, Dynamic, Engine, plugin::*}; | ||||
|  | ||||
| pub fn register(engine: &mut Engine) { | ||||
|     let ssh_keys_module = exported_module!(ssh_keys_api); | ||||
|     engine.register_global_module(ssh_keys_module.into()); | ||||
| } | ||||
|  | ||||
| #[export_module] | ||||
| pub mod ssh_keys_api { | ||||
|     use super::*; | ||||
|     use rhai::EvalAltResult; | ||||
|  | ||||
|     #[rhai_fn(name = "get_ssh_keys", return_raw)] | ||||
|     pub fn get_ssh_keys(client: &mut Client) -> Result<Array, Box<EvalAltResult>> { | ||||
|         let ssh_keys = client | ||||
|             .get_ssh_keys() | ||||
|             .map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; | ||||
|         Ok(ssh_keys.into_iter().map(Dynamic::from).collect()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "get_ssh_key", return_raw)] | ||||
|     pub fn get_ssh_key( | ||||
|         client: &mut Client, | ||||
|         fingerprint: &str, | ||||
|     ) -> Result<SshKey, Box<EvalAltResult>> { | ||||
|         client | ||||
|             .get_ssh_key(fingerprint) | ||||
|             .map_err(|e| e.to_string().into()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "add_ssh_key", return_raw)] | ||||
|     pub fn add_ssh_key( | ||||
|         client: &mut Client, | ||||
|         name: &str, | ||||
|         data: &str, | ||||
|     ) -> Result<SshKey, Box<EvalAltResult>> { | ||||
|         client | ||||
|             .add_ssh_key(name, data) | ||||
|             .map_err(|e| e.to_string().into()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "update_ssh_key_name", return_raw)] | ||||
|     pub fn update_ssh_key_name( | ||||
|         client: &mut Client, | ||||
|         fingerprint: &str, | ||||
|         name: &str, | ||||
|     ) -> Result<SshKey, Box<EvalAltResult>> { | ||||
|         client | ||||
|             .update_ssh_key_name(fingerprint, name) | ||||
|             .map_err(|e| e.to_string().into()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "delete_ssh_key", return_raw)] | ||||
|     pub fn delete_ssh_key( | ||||
|         client: &mut Client, | ||||
|         fingerprint: &str, | ||||
|     ) -> Result<(), Box<EvalAltResult>> { | ||||
|         client | ||||
|             .delete_ssh_key(fingerprint) | ||||
|             .map_err(|e| e.to_string().into()) | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "pretty_print")] | ||||
|     pub fn pretty_print_ssh_keys(keys: Array) { | ||||
|         let mut table = Table::new(); | ||||
|         table.add_row(row![b => | ||||
|             "Name", | ||||
|             "Fingerprint", | ||||
|             "Type", | ||||
|             "Size", | ||||
|             "Created At" | ||||
|         ]); | ||||
|  | ||||
|         for key_dyn in keys { | ||||
|             if let Some(key) = key_dyn.try_cast::<SshKey>() { | ||||
|                 table.add_row(row![ | ||||
|                     key.name, | ||||
|                     key.fingerprint, | ||||
|                     key.key_type, | ||||
|                     key.size.to_string(), | ||||
|                     key.created_at | ||||
|                 ]); | ||||
|             } | ||||
|         } | ||||
|         table.printstd(); | ||||
|     } | ||||
| } | ||||
| @@ -9,22 +9,22 @@ license = "Apache-2.0" | ||||
| 
 | ||||
| [dependencies] | ||||
| # HTTP client for async requests | ||||
| reqwest = { version = "0.12.15", features = ["json"] } | ||||
| reqwest = { workspace = true } | ||||
| # JSON handling | ||||
| serde_json = "1.0" | ||||
| serde_json = { workspace = true } | ||||
| # Base64 encoding/decoding for message payloads | ||||
| base64 = "0.22.1" | ||||
| base64 = { workspace = true } | ||||
| # Async runtime | ||||
| tokio = { version = "1.45.0", features = ["full"] } | ||||
| tokio = { workspace = true } | ||||
| # Rhai scripting support | ||||
| rhai = { version = "1.12.0", features = ["sync"] } | ||||
| rhai = { workspace = true } | ||||
| # Logging | ||||
| log = "0.4" | ||||
| log = { workspace = true } | ||||
| # URL encoding for API parameters | ||||
| urlencoding = "2.1.3" | ||||
| urlencoding = { workspace = true } | ||||
| 
 | ||||
| [dev-dependencies] | ||||
| # For async testing | ||||
| tokio-test = "0.4.4" | ||||
| tokio-test = { workspace = true } | ||||
| # For temporary files in tests | ||||
| tempfile = "3.5" | ||||
| tempfile = { workspace = true } | ||||
| @@ -11,24 +11,24 @@ categories = ["database", "api-bindings"] | ||||
| 
 | ||||
| [dependencies] | ||||
| # PostgreSQL client dependencies | ||||
| postgres = "0.19.4" | ||||
| postgres-types = "0.2.5" | ||||
| tokio-postgres = "0.7.8" | ||||
| postgres = { workspace = true } | ||||
| postgres-types = { workspace = true } | ||||
| tokio-postgres = { workspace = true } | ||||
| 
 | ||||
| # Connection pooling | ||||
| r2d2 = "0.8.10" | ||||
| r2d2_postgres = "0.18.2" | ||||
| r2d2 = { workspace = true } | ||||
| r2d2_postgres = { workspace = true } | ||||
| 
 | ||||
| # Utility dependencies | ||||
| lazy_static = "1.4.0" | ||||
| thiserror = "2.0.12" | ||||
| lazy_static = { workspace = true } | ||||
| thiserror = { workspace = true } | ||||
| 
 | ||||
| # Rhai scripting support | ||||
| rhai = { version = "1.12.0", features = ["sync"] } | ||||
| rhai = { workspace = true } | ||||
| 
 | ||||
| # SAL dependencies | ||||
| sal-virt = { path = "../virt" } | ||||
| sal-virt = { workspace = true } | ||||
| 
 | ||||
| [dev-dependencies] | ||||
| tempfile = "3.5" | ||||
| tokio-test = "0.4.4" | ||||
| tempfile = { workspace = true } | ||||
| tokio-test = { workspace = true } | ||||
| @@ -11,11 +11,11 @@ categories = ["database", "caching", "api-bindings"] | ||||
| 
 | ||||
| [dependencies] | ||||
| # Core Redis functionality | ||||
| redis = "0.31.0" | ||||
| lazy_static = "1.4.0" | ||||
| redis = { workspace = true } | ||||
| lazy_static = { workspace = true } | ||||
| 
 | ||||
| # Rhai integration (optional) | ||||
| rhai = { version = "1.12.0", features = ["sync"], optional = true } | ||||
| rhai = { workspace = true, optional = true } | ||||
| 
 | ||||
| [features] | ||||
| default = ["rhai"] | ||||
| @@ -23,4 +23,4 @@ rhai = ["dep:rhai"] | ||||
| 
 | ||||
| [dev-dependencies] | ||||
| # For testing | ||||
| tempfile = "3.5" | ||||
| tempfile = { workspace = true } | ||||
| @@ -9,20 +9,20 @@ license = "Apache-2.0" | ||||
| 
 | ||||
| [dependencies] | ||||
| # Core dependencies | ||||
| anyhow = "1.0.98" | ||||
| futures = "0.3.30" | ||||
| lazy_static = "1.4.0" | ||||
| log = "0.4" | ||||
| serde_json = "1.0" | ||||
| thiserror = "2.0.12" | ||||
| tokio = { version = "1.45.0", features = ["full"] } | ||||
| anyhow = { workspace = true } | ||||
| futures = { workspace = true } | ||||
| lazy_static = { workspace = true } | ||||
| log = { workspace = true } | ||||
| serde_json = { workspace = true } | ||||
| thiserror = { workspace = true } | ||||
| tokio = { workspace = true } | ||||
| 
 | ||||
| # Zinit client | ||||
| zinit-client = "0.3.0" | ||||
| zinit-client = { workspace = true } | ||||
| 
 | ||||
| # Rhai integration | ||||
| rhai = { version = "1.12.0", features = ["sync"] } | ||||
| rhai = { workspace = true } | ||||
| 
 | ||||
| [dev-dependencies] | ||||
| tokio-test = "0.4.4" | ||||
| tempfile = "3.5" | ||||
| tokio-test = { workspace = true } | ||||
| tempfile = { workspace = true } | ||||
| @@ -149,34 +149,51 @@ impl ZinitClientWrapper { | ||||
|     // Get logs with real implementation
 | ||||
|     pub async fn logs(&self, filter: Option<String>) -> Result<Vec<String>, ZinitError> { | ||||
|         use futures::StreamExt; | ||||
|         use tokio::time::{timeout, Duration}; | ||||
| 
 | ||||
|         // The logs method requires a follow parameter and filter
 | ||||
|         let follow = false; // Don't follow logs, just get existing ones
 | ||||
|         let mut log_stream = self.client.logs(follow, filter).await?; | ||||
|         let mut logs = Vec::new(); | ||||
| 
 | ||||
|         // Collect logs from the stream with a reasonable limit
 | ||||
|         // Collect logs from the stream with a reasonable limit and timeout
 | ||||
|         let mut count = 0; | ||||
|         const MAX_LOGS: usize = 1000; | ||||
|         const LOG_TIMEOUT: Duration = Duration::from_secs(5); | ||||
| 
 | ||||
|         while let Some(log_result) = log_stream.next().await { | ||||
|             match log_result { | ||||
|                 Ok(log_entry) => { | ||||
|                     // Convert LogEntry to String using Debug formatting
 | ||||
|                     logs.push(format!("{:?}", log_entry)); | ||||
|                     count += 1; | ||||
|                     if count >= MAX_LOGS { | ||||
|         // Use timeout to prevent hanging
 | ||||
|         let result = timeout(LOG_TIMEOUT, async { | ||||
|             while let Some(log_result) = log_stream.next().await { | ||||
|                 match log_result { | ||||
|                     Ok(log_entry) => { | ||||
|                         // Convert LogEntry to String using Debug formatting
 | ||||
|                         logs.push(format!("{:?}", log_entry)); | ||||
|                         count += 1; | ||||
|                         if count >= MAX_LOGS { | ||||
|                             break; | ||||
|                         } | ||||
|                     } | ||||
|                     Err(e) => { | ||||
|                         log::warn!("Error reading log entry: {}", e); | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|                 Err(e) => { | ||||
|                     log::warn!("Error reading log entry: {}", e); | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|         .await; | ||||
| 
 | ||||
|         // Handle timeout - this is not an error, just means no more logs available
 | ||||
|         match result { | ||||
|             Ok(_) => Ok(logs), | ||||
|             Err(_) => { | ||||
|                 log::debug!( | ||||
|                     "Log reading timed out after {} seconds, returning {} logs", | ||||
|                     LOG_TIMEOUT.as_secs(), | ||||
|                     logs.len() | ||||
|                 ); | ||||
|                 Ok(logs) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         Ok(logs) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user