From 6b12001ca24b3cf5d566f6f685704fcd96bf4539 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Thu, 10 Jul 2025 00:40:11 +0300 Subject: [PATCH] feat: Add Kubernetes examples and update dependencies - Add Kubernetes examples demonstrating deployment of various applications (PostgreSQL, Redis, generic). This improves the documentation and provides practical usage examples. - Add `tokio` dependency for async examples. This enables the use of asynchronous operations in the examples. - Add `once_cell` dependency for improved resource management in Kubernetes module. This allows efficient management of singletons and other resources. --- Cargo.toml | 19 +- examples/kubernetes/clusters/generic.rs | 63 ++- examples/kubernetes/clusters/postgres.rhai | 34 ++ examples/kubernetes/clusters/postgres.rs | 39 ++ examples/kubernetes/clusters/redis.rhai | 35 ++ examples/kubernetes/clusters/redis.rs | 37 ++ kubernetes/Cargo.toml | 1 + kubernetes/README.md | 258 +++++++++++- kubernetes/examples/generic.rs | 97 ----- kubernetes/examples/postgres.rs | 73 ---- kubernetes/examples/redis.rs | 72 ---- kubernetes/src/kubernetes_manager.rs | 186 +++++---- kubernetes/src/rhai.rs | 175 +++++++- kubernetes/tests/crud_operations_test.rs | 149 +++++-- kubernetes/tests/deployment_env_vars_test.rs | 384 ++++++++++++++++++ kubernetes/tests/edge_cases_test.rs | 293 +++++++++++++ kubernetes/tests/rhai/crud_operations.rhai | 2 +- kubernetes/tests/rhai/env_vars_test.rhai | 199 +++++++++ kubernetes/tests/rhai/new_functions_test.rhai | 51 +++ kubernetes/tests/rhai/pod_env_vars_test.rhai | 142 +++++++ kubernetes/tests/rhai/run_all_tests.rhai | 14 +- kubernetes/tests/rhai_tests.rs | 69 +++- .../kubernetes/01_namespace_operations.rhai | 2 - rhai_tests/kubernetes/02_pod_management.rhai | 2 - .../kubernetes/03_pcre_pattern_matching.rhai | 2 - rhai_tests/kubernetes/04_error_handling.rhai | 2 - .../kubernetes/05_production_safety.rhai | 2 - rhai_tests/kubernetes/run_all_tests.rhai | 2 - test_service_manager.rhai | 29 -- 29 files changed, 1951 insertions(+), 482 deletions(-) delete mode 100644 kubernetes/examples/generic.rs delete mode 100644 kubernetes/examples/postgres.rs delete mode 100644 kubernetes/examples/redis.rs create mode 100644 kubernetes/tests/deployment_env_vars_test.rs create mode 100644 kubernetes/tests/edge_cases_test.rs create mode 100644 kubernetes/tests/rhai/env_vars_test.rhai create mode 100644 kubernetes/tests/rhai/new_functions_test.rhai create mode 100644 kubernetes/tests/rhai/pod_env_vars_test.rhai delete mode 100644 test_service_manager.rhai diff --git a/Cargo.toml b/Cargo.toml index 3b3b3b0..be745ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,7 +88,8 @@ urlencoding = "2.1.3" tokio-test = "0.4.4" [dependencies] -thiserror = "2.0.12" # For error handling in the main Error enum +thiserror = "2.0.12" # For error handling in the main Error enum +tokio = { workspace = true } # For async examples # Optional dependencies - users can choose which modules to include sal-git = { path = "git", optional = true } @@ -146,3 +147,19 @@ all = [ "rhai", "service_manager", ] + +# 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"] diff --git a/examples/kubernetes/clusters/generic.rs b/examples/kubernetes/clusters/generic.rs index 94b7c66..9bb341c 100644 --- a/examples/kubernetes/clusters/generic.rs +++ b/examples/kubernetes/clusters/generic.rs @@ -11,10 +11,26 @@ async fn main() -> Result<(), Box> { // Create Kubernetes manager let km = KubernetesManager::new("default").await?; - // Example 1: Simple web server deployment - println!("=== Example 1: Simple Nginx Web Server ==="); + // Clean up any existing resources first + println!("=== Cleaning up existing resources ==="); + let apps_to_clean = ["web-server", "node-app", "mongodb"]; - km.deploy_application("web-server", "nginx:latest", 2, 80, None) + 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!"); @@ -26,12 +42,20 @@ async fn main() -> Result<(), Box> { 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 + "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?; @@ -45,12 +69,25 @@ async fn main() -> Result<(), Box> { 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 + "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?; diff --git a/examples/kubernetes/clusters/postgres.rhai b/examples/kubernetes/clusters/postgres.rhai index 75cd9bf..6d302d4 100644 --- a/examples/kubernetes/clusters/postgres.rhai +++ b/examples/kubernetes/clusters/postgres.rhai @@ -10,6 +10,35 @@ 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..."); @@ -19,6 +48,11 @@ try { "app": "postgres-cluster", "type": "database", "engine": "postgresql" + }, #{ + "POSTGRES_DB": "myapp", + "POSTGRES_USER": "postgres", + "POSTGRES_PASSWORD": "secretpassword", + "PGDATA": "/var/lib/postgresql/data/pgdata" }); print("✓ " + result); diff --git a/examples/kubernetes/clusters/postgres.rs b/examples/kubernetes/clusters/postgres.rs index dbbd389..b495940 100644 --- a/examples/kubernetes/clusters/postgres.rs +++ b/examples/kubernetes/clusters/postgres.rs @@ -11,12 +11,50 @@ async fn main() -> Result<(), Box> { // 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( @@ -25,6 +63,7 @@ async fn main() -> Result<(), Box> { 2, // replicas (1 master + 1 replica) 5432, // port Some(labels), // labels + Some(env_vars), // environment variables ) .await?; diff --git a/examples/kubernetes/clusters/redis.rhai b/examples/kubernetes/clusters/redis.rhai index 939f87e..6e75f2d 100644 --- a/examples/kubernetes/clusters/redis.rhai +++ b/examples/kubernetes/clusters/redis.rhai @@ -10,6 +10,35 @@ 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..."); @@ -19,6 +48,12 @@ try { "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); diff --git a/examples/kubernetes/clusters/redis.rs b/examples/kubernetes/clusters/redis.rs index fb27832..16b9fb7 100644 --- a/examples/kubernetes/clusters/redis.rs +++ b/examples/kubernetes/clusters/redis.rs @@ -11,12 +11,48 @@ async fn main() -> Result<(), Box> { // 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( @@ -25,6 +61,7 @@ async fn main() -> Result<(), Box> { 3, // replicas (Redis cluster nodes) 6379, // port Some(labels), // labels + Some(env_vars), // environment variables ) .await?; diff --git a/kubernetes/Cargo.toml b/kubernetes/Cargo.toml index e2ce593..b07c347 100644 --- a/kubernetes/Cargo.toml +++ b/kubernetes/Cargo.toml @@ -39,6 +39,7 @@ log = "0.4" # Rhai scripting support (optional) rhai = { version = "1.12.0", features = ["sync"], optional = true } +once_cell = "1.20.2" # UUID for resource identification uuid = { version = "1.16.0", features = ["v4"] } diff --git a/kubernetes/README.md b/kubernetes/README.md index 8a6c135..d78b704 100644 --- a/kubernetes/README.md +++ b/kubernetes/README.md @@ -35,15 +35,96 @@ This package provides a high-level interface for managing Kubernetes clusters us ## Features +- **Application Deployment**: Deploy complete applications with a single method call +- **Environment Variables & Labels**: Configure containers with environment variables and Kubernetes labels +- **Resource Lifecycle Management**: Automatic cleanup and replacement of existing resources - **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 +- **Rhai Integration**: Full scripting support through Rhai wrappers with environment variables + +## Core Concepts + +### Labels vs Environment Variables + +Understanding the difference between labels and environment variables is crucial for effective Kubernetes deployments: + +#### **Labels** (Kubernetes Metadata) + +- **Purpose**: Organize, select, and manage Kubernetes resources +- **Scope**: Kubernetes cluster management and resource organization +- **Visibility**: Used by Kubernetes controllers, selectors, and monitoring systems +- **Examples**: `app=my-app`, `tier=backend`, `environment=production`, `version=v1.2.3` +- **Use Cases**: Resource grouping, service discovery, monitoring labels, deployment strategies + +#### **Environment Variables** (Container Configuration) + +- **Purpose**: Configure application runtime behavior and settings +- **Scope**: Inside container processes - available to your application code +- **Visibility**: Accessible via `process.env`, `os.environ`, etc. in your application +- **Examples**: `NODE_ENV=production`, `DATABASE_URL=postgres://...`, `API_KEY=secret` +- **Use Cases**: Database connections, API keys, feature flags, runtime configuration + +#### **Example: Complete Application Configuration** + +```rust +// Labels: For Kubernetes resource management +let mut labels = HashMap::new(); +labels.insert("app".to_string(), "web-api".to_string()); // Service discovery +labels.insert("tier".to_string(), "backend".to_string()); // Architecture layer +labels.insert("environment".to_string(), "production".to_string()); // Deployment stage +labels.insert("version".to_string(), "v2.1.0".to_string()); // Release version + +// Environment Variables: For application configuration +let mut env_vars = HashMap::new(); +env_vars.insert("NODE_ENV".to_string(), "production".to_string()); // Runtime mode +env_vars.insert("DATABASE_URL".to_string(), "postgres://db:5432/app".to_string()); // DB connection +env_vars.insert("REDIS_URL".to_string(), "redis://cache:6379".to_string()); // Cache connection +env_vars.insert("LOG_LEVEL".to_string(), "info".to_string()); // Logging config +``` ## Usage +### Application Deployment (Recommended) + +Deploy complete applications with labels and environment variables: + +```rust +use sal_kubernetes::KubernetesManager; +use std::collections::HashMap; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let km = KubernetesManager::new("default").await?; + + // Configure labels for Kubernetes resource organization + let mut labels = HashMap::new(); + labels.insert("app".to_string(), "my-app".to_string()); + labels.insert("tier".to_string(), "backend".to_string()); + + // Configure environment variables for the container + let mut env_vars = HashMap::new(); + env_vars.insert("NODE_ENV".to_string(), "production".to_string()); + env_vars.insert("DATABASE_URL".to_string(), "postgres://db:5432/myapp".to_string()); + env_vars.insert("API_KEY".to_string(), "secret-api-key".to_string()); + + // Deploy application with deployment + service + km.deploy_application( + "my-app", // name + "node:18-alpine", // image + 3, // replicas + 3000, // port + Some(labels), // Kubernetes labels + Some(env_vars), // container environment variables + ).await?; + + println!("✅ Application deployed successfully!"); + Ok(()) +} +``` + ### Basic Operations ```rust @@ -53,17 +134,17 @@ use sal_kubernetes::KubernetesManager; async fn main() -> Result<(), Box> { // 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(()) } ``` @@ -74,14 +155,24 @@ async fn main() -> Result<(), Box> { // Create Kubernetes manager for namespace let km = kubernetes_manager_new("default"); -// List pods +// Deploy application with labels and environment variables +deploy_application(km, "my-app", "node:18-alpine", 3, 3000, #{ + "app": "my-app", + "tier": "backend", + "environment": "production" +}, #{ + "NODE_ENV": "production", + "DATABASE_URL": "postgres://db:5432/myapp", + "API_KEY": "secret-api-key" +}); + +print("✅ Application deployed!"); + +// Basic operations let pods = pods_list(km); print("Found " + pods.len() + " pods"); -// Create namespace -namespace_create(km, "my-app"); - -// Delete test resources +namespace_create(km, "my-namespace"); delete(km, "test-.*"); ``` @@ -98,6 +189,7 @@ delete(km, "test-.*"); ### 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 @@ -144,6 +236,18 @@ The main interface for Kubernetes operations. Each instance is scoped to a singl - `KubernetesManager::new(namespace)` - Create a manager for the specified namespace +#### Application Deployment + +- `deploy_application(name, image, replicas, port, labels, env_vars)` - Deploy complete application with deployment and service +- `deployment_create(name, image, replicas, labels, env_vars)` - Create deployment with environment variables and labels + +#### Resource Creation + +- `pod_create(name, image, labels, env_vars)` - Create pod with environment variables and labels +- `service_create(name, selector, port, target_port)` - Create service with port mapping +- `configmap_create(name, data)` - Create configmap with data +- `secret_create(name, data, secret_type)` - Create secret with data and optional type + #### Resource Listing - `pods_list()` - List all pods in the namespace @@ -160,6 +264,8 @@ The main interface for Kubernetes operations. Each instance is scoped to a singl - `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 +- `configmap_delete(name)` - Delete a specific configmap by name +- `secret_delete(name)` - Delete a specific secret by name #### Pattern-based Operations @@ -180,32 +286,93 @@ The main interface for Kubernetes operations. Each instance is scoped to a singl When using the Rhai integration, the following functions are available: +**Manager Creation & Application Deployment:** + - `kubernetes_manager_new(namespace)` - Create a KubernetesManager +- `deploy_application(km, name, image, replicas, port, labels, env_vars)` - Deploy application with environment variables + +**Resource Listing:** + - `pods_list(km)` - List pods - `services_list(km)` - List services - `deployments_list(km)` - List deployments +- `configmaps_list(km)` - List configmaps +- `secrets_list(km)` - List secrets - `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 + +**Resource Operations:** + +- `delete(km, pattern)` - Delete resources matching pattern - `pod_delete(km, name)` - Delete specific pod - `service_delete(km, name)` - Delete specific service - `deployment_delete(km, name)` - Delete specific deployment +- `configmap_delete(km, name)` - Delete specific configmap +- `secret_delete(km, name)` - Delete specific secret + +**Namespace Functions:** + +- `namespace_create(km, name)` - Create namespace +- `namespace_exists(km, name)` - Check namespace existence +- `namespace_delete(km, name)` - Delete namespace - `namespace(km)` - Get manager's namespace ## Examples -The `examples/kubernetes/` directory contains comprehensive examples: +The `examples/kubernetes/clusters/` 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 +### Rust Examples + +Run with: `cargo run --example --features kubernetes` + +- `postgres` - PostgreSQL database deployment with environment variables +- `redis` - Redis cache deployment with configuration +- `generic` - Multiple application deployments (nginx, node.js, mongodb) + +### Rhai Examples + +Run with: `./target/debug/herodo examples/kubernetes/clusters/