This commit is contained in:
2025-08-05 15:33:03 +02:00
parent 7856fc0a4e
commit 0c02d0e99f
326 changed files with 334 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
[package]
name = "sal-git"
version = "0.1.0"
edition = "2021"
authors = ["PlanetFirst <info@incubaid.com>"]
description = "SAL Git - Git repository management and operations"
repository = "https://git.threefold.info/herocode/sal"
license = "Apache-2.0"
[dependencies]
# Use workspace dependencies for consistency
regex = { workspace = true }
redis = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
rhai = { workspace = true }
log = { workspace = true }
url = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }

View File

@@ -0,0 +1,125 @@
# SAL Git Package (`sal-git`)
The `sal-git` package provides comprehensive functionalities for interacting with Git repositories. It offers both high-level abstractions for common Git workflows and a flexible executor for running arbitrary Git commands with integrated authentication.
This module is central to SAL's capabilities for managing source code, enabling automation of development tasks, and integrating with version control systems.
## Installation
Add this to your `Cargo.toml`:
```toml
[dependencies]
sal-git = "0.1.0"
```
## Core Components
The module is primarily composed of two main parts:
1. **Repository and Tree Management (`git.rs`)**: Defines `GitTree` and `GitRepo` structs for a more structured, object-oriented approach to Git operations.
2. **Command Execution with Authentication (`git_executor.rs`)**: Provides `GitExecutor` for running any Git command, with a focus on handling authentication via configurations stored in Redis.
### 1. Repository and Tree Management (`GitTree` & `GitRepo`)
These components allow for programmatic management of Git repositories.
* **`GitTree`**: Represents a directory (base path) that can contain multiple Git repositories.
* `new(base_path)`: Creates a new `GitTree` instance for the given base path.
* `list()`: Lists all Git repositories found under the base path.
* `find(pattern)`: Finds repositories within the tree that match a given name pattern (supports wildcards).
* `get(path_or_url)`: Retrieves `GitRepo` instances. If a local path/pattern is given, it finds existing repositories. If a Git URL is provided, it will clone the repository into a structured path (`base_path/server/account/repo`) if it doesn't already exist.
* **`GitRepo`**: Represents a single Git repository.
* `new(path)`: Creates a `GitRepo` instance for the repository at the given path.
* `path()`: Returns the local file system path to the repository.
* `has_changes()`: Checks if the repository has uncommitted local changes.
* `pull()`: Pulls the latest changes from the remote. Fails if local changes exist.
* `reset()`: Performs a hard reset (`git reset --hard HEAD`) and cleans untracked files (`git clean -fd`).
* `commit(message)`: Stages all changes (`git add .`) and commits them with the given message.
* `push()`: Pushes committed changes to the remote repository.
* **`GitError`**: A comprehensive enum for errors related to `GitTree` and `GitRepo` operations (e.g., Git not installed, invalid URL, repository not found, local changes exist).
* **`parse_git_url(url)`**: A utility function to parse HTTPS and SSH Git URLs into server, account, and repository name components.
### 2. Command Execution with Authentication (`GitExecutor`)
`GitExecutor` is designed for flexible execution of any Git command, with a special emphasis on handling authentication for remote operations.
* **`GitExecutor::new()` / `GitExecutor::default()`**: Creates a new executor instance.
* **`GitExecutor::init()`**: Initializes the executor by attempting to load authentication configurations from Redis (key: `herocontext:git`). If Redis is unavailable or the config is missing, it proceeds without specific auth configurations, relying on system defaults.
* **`GitExecutor::execute(args: &[&str])`**: The primary method to run a Git command (e.g., `executor.execute(&["clone", "https://github.com/user/repo.git", "myrepo"])`).
* It intelligently attempts to apply authentication based on the command and the loaded configuration.
#### Authentication Configuration (`herocontext:git` in Redis)
The `GitExecutor` can load its authentication settings from a JSON object stored in Redis under the key `herocontext:git`. The structure is as follows:
```json
{
"status": "ok", // or "error"
"auth": {
"github.com": {
"sshagent": true // Use SSH agent for github.com
},
"gitlab.example.com": {
"key": "/path/to/ssh/key_for_gitlab" // Use specific SSH key
},
"dev.azure.com": {
"username": "your_username",
"password": "your_pat_or_password" // Use HTTPS credentials
}
// ... other server configurations
}
}
```
* **Authentication Methods Supported**:
* **SSH Agent**: If `sshagent: true` is set for a server, and an SSH agent is loaded with identities.
* **SSH Key**: If `key: "/path/to/key"` is specified, `GIT_SSH_COMMAND` is used to point to this key.
* **Username/Password (HTTPS)**: If `username` and `password` are provided, HTTPS URLs are rewritten to include these credentials (e.g., `https://user:pass@server/repo.git`).
* **`GitExecutorError`**: An enum for errors specific to `GitExecutor`, including command failures, Redis errors, JSON parsing issues, and authentication problems (e.g., `SshAgentNotLoaded`, `InvalidAuthConfig`).
## Usage with `herodo`
The `herodo` CLI tool likely leverages `GitExecutor` to provide its scriptable Git functionalities. This allows Rhai scripts executed by `herodo` to perform Git operations using the centrally managed authentication configurations from Redis, promoting secure and consistent access to Git repositories.
## Error Handling
Both `git.rs` and `git_executor.rs` define their own specific error enums (`GitError` and `GitExecutorError` respectively) to provide detailed information about issues encountered during Git operations. These errors cover a wide range of scenarios from command execution failures to authentication problems and invalid configurations.
## Configuration
The git module supports configuration through environment variables:
### Environment Variables
- **`REDIS_URL`**: Redis connection URL (default: `redis://127.0.0.1/`)
- **`SAL_REDIS_URL`**: Alternative Redis URL (fallback if REDIS_URL not set)
- **`GIT_DEFAULT_BASE_PATH`**: Default base path for git operations (default: system temp directory)
### Example Configuration
```bash
# Set Redis connection
export REDIS_URL="redis://localhost:6379/0"
# Set default git base path
export GIT_DEFAULT_BASE_PATH="/tmp/git_repos"
# Run your application
herodo your_script.rhai
```
### Security Considerations
- Passwords are never embedded in URLs or logged
- Temporary credential helpers are used for HTTPS authentication
- Redis URLs with passwords are masked in logs
- All temporary files are cleaned up after use
## Summary
The `git` module offers a powerful and flexible interface to Git, catering to both simple, high-level repository interactions and complex, authenticated command execution scenarios. Its integration with Redis for authentication configuration makes it particularly well-suited for automated systems and tools like `herodo`.

View File

@@ -0,0 +1,506 @@
use regex::Regex;
use std::error::Error;
use std::fmt;
use std::fs;
use std::path::Path;
use std::process::Command;
// Define a custom error type for git operations
#[derive(Debug)]
pub enum GitError {
GitNotInstalled(std::io::Error),
InvalidUrl(String),
InvalidBasePath(String),
HomeDirectoryNotFound(std::env::VarError),
FileSystemError(std::io::Error),
GitCommandFailed(String),
CommandExecutionError(std::io::Error),
NoRepositoriesFound,
RepositoryNotFound(String),
MultipleRepositoriesFound(String, usize),
NotAGitRepository(String),
LocalChangesExist(String),
}
// Implement Display for GitError
impl fmt::Display for GitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GitError::GitNotInstalled(e) => write!(f, "Git is not installed: {}", e),
GitError::InvalidUrl(url) => write!(f, "Could not parse git URL: {}", url),
GitError::InvalidBasePath(path) => write!(f, "Invalid base path: {}", path),
GitError::HomeDirectoryNotFound(e) => write!(f, "Could not determine home directory: {}", e),
GitError::FileSystemError(e) => write!(f, "Error creating directory structure: {}", e),
GitError::GitCommandFailed(e) => write!(f, "{}", e),
GitError::CommandExecutionError(e) => write!(f, "Error executing command: {}", e),
GitError::NoRepositoriesFound => write!(f, "No repositories found"),
GitError::RepositoryNotFound(pattern) => write!(f, "No repositories found matching '{}'", pattern),
GitError::MultipleRepositoriesFound(pattern, count) =>
write!(f, "Multiple repositories ({}) found matching '{}'. Use '*' suffix for multiple matches.", count, pattern),
GitError::NotAGitRepository(path) => write!(f, "Not a git repository at {}", path),
GitError::LocalChangesExist(path) => write!(f, "Repository at {} has local changes", path),
}
}
}
// Implement Error trait for GitError
impl Error for GitError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
GitError::GitNotInstalled(e) => Some(e),
GitError::HomeDirectoryNotFound(e) => Some(e),
GitError::FileSystemError(e) => Some(e),
GitError::CommandExecutionError(e) => Some(e),
_ => None,
}
}
}
/// Parses a git URL to extract the server, account, and repository name.
///
/// # Arguments
///
/// * `url` - The URL of the git repository to parse. Can be in HTTPS format
/// (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git).
///
/// # Returns
///
/// A tuple containing:
/// * `server` - The server name (e.g., "github.com")
/// * `account` - The account or organization name (e.g., "username")
/// * `repo` - The repository name (e.g., "repo")
///
/// If the URL cannot be parsed, all three values will be empty strings.
pub fn parse_git_url(url: &str) -> (String, String, String) {
// HTTP(S) URL format: https://github.com/username/repo.git
let https_re = Regex::new(r"https?://([^/]+)/([^/]+)/([^/\.]+)(?:\.git)?").unwrap();
// SSH URL format: git@github.com:username/repo.git
let ssh_re = Regex::new(r"git@([^:]+):([^/]+)/([^/\.]+)(?:\.git)?").unwrap();
if let Some(caps) = https_re.captures(url) {
let server = caps.get(1).map_or("", |m| m.as_str()).to_string();
let account = caps.get(2).map_or("", |m| m.as_str()).to_string();
let repo = caps.get(3).map_or("", |m| m.as_str()).to_string();
return (server, account, repo);
} else if let Some(caps) = ssh_re.captures(url) {
let server = caps.get(1).map_or("", |m| m.as_str()).to_string();
let account = caps.get(2).map_or("", |m| m.as_str()).to_string();
let repo = caps.get(3).map_or("", |m| m.as_str()).to_string();
return (server, account, repo);
}
(String::new(), String::new(), String::new())
}
/// Checks if git is installed on the system.
///
/// # Returns
///
/// * `Ok(())` - If git is installed
/// * `Err(GitError)` - If git is not installed
fn check_git_installed() -> Result<(), GitError> {
Command::new("git")
.arg("--version")
.output()
.map_err(GitError::GitNotInstalled)?;
Ok(())
}
/// Represents a collection of git repositories under a base path.
#[derive(Clone)]
pub struct GitTree {
base_path: String,
}
impl GitTree {
/// Creates a new GitTree with the specified base path.
///
/// # Arguments
///
/// * `base_path` - The base path where all git repositories are located
///
/// # Returns
///
/// * `Ok(GitTree)` - A new GitTree instance
/// * `Err(GitError)` - If the base path is invalid or cannot be created
pub fn new(base_path: &str) -> Result<Self, GitError> {
// Check if git is installed
check_git_installed()?;
// Validate the base path
let path = Path::new(base_path);
if !path.exists() {
fs::create_dir_all(path).map_err(|e| GitError::FileSystemError(e))?;
} else if !path.is_dir() {
return Err(GitError::InvalidBasePath(base_path.to_string()));
}
Ok(GitTree {
base_path: base_path.to_string(),
})
}
/// Lists all git repositories under the base path.
///
/// # Returns
///
/// * `Ok(Vec<String>)` - A vector of paths to git repositories
/// * `Err(GitError)` - If the operation failed
pub fn list(&self) -> Result<Vec<String>, GitError> {
let base_path = Path::new(&self.base_path);
if !base_path.exists() || !base_path.is_dir() {
return Ok(Vec::new());
}
let mut repos = Vec::new();
// Find all directories with .git subdirectories
let output = Command::new("find")
.args(&[&self.base_path, "-type", "d", "-name", ".git"])
.output()
.map_err(GitError::CommandExecutionError)?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
// Get the parent directory of .git which is the repo root
if let Some(parent) = Path::new(line).parent() {
if let Some(path_str) = parent.to_str() {
repos.push(path_str.to_string());
}
}
}
} else {
let error = String::from_utf8_lossy(&output.stderr);
return Err(GitError::GitCommandFailed(format!(
"Failed to find git repositories: {}",
error
)));
}
Ok(repos)
}
/// Finds repositories matching a pattern or partial path.
///
/// # Arguments
///
/// * `pattern` - The pattern to match against repository paths
/// - If the pattern ends with '*', all matching repositories are returned
/// - Otherwise, exactly one matching repository must be found
///
/// # Returns
///
/// * `Ok(Vec<String>)` - A vector of paths to matching repositories
/// * `Err(GitError)` - If no matching repositories are found,
/// or if multiple repositories match a non-wildcard pattern
pub fn find(&self, pattern: &str) -> Result<Vec<GitRepo>, GitError> {
let repo_names = self.list()?; // list() already ensures these are git repo names
if repo_names.is_empty() {
return Ok(Vec::new()); // If no repos listed, find results in an empty list
}
let mut matched_repos: Vec<GitRepo> = Vec::new();
if pattern == "*" {
for name in repo_names {
let full_path = format!("{}/{}", self.base_path, name);
matched_repos.push(GitRepo::new(full_path));
}
} else if pattern.ends_with('*') {
let prefix = &pattern[0..pattern.len() - 1];
for name in repo_names {
if name.starts_with(prefix) {
let full_path = format!("{}/{}", self.base_path, name);
matched_repos.push(GitRepo::new(full_path));
}
}
} else {
// Exact match for the name
for name in repo_names {
if name == pattern {
let full_path = format!("{}/{}", self.base_path, name);
matched_repos.push(GitRepo::new(full_path));
// `find` returns all exact matches. If names aren't unique (unlikely from `list`),
// it could return more than one. For an exact name, typically one is expected.
}
}
}
Ok(matched_repos)
}
/// Gets one or more GitRepo objects based on a path pattern or URL.
///
/// # Arguments
///
/// * `path_or_url` - The path pattern to match against repository paths or a git URL
/// - If it's a URL, the repository will be cloned if it doesn't exist
/// - If it's a path pattern, it will find matching repositories
///
/// # Returns
///
/// * `Ok(Vec<GitRepo>)` - A vector of GitRepo objects
/// * `Err(GitError)` - If no matching repositories are found or the clone operation failed
pub fn get(&self, path_or_url: &str) -> Result<Vec<GitRepo>, GitError> {
// Check if it's a URL
if path_or_url.starts_with("http") || path_or_url.starts_with("git@") {
// Parse the URL
let (server, account, repo) = parse_git_url(path_or_url);
if server.is_empty() || account.is_empty() || repo.is_empty() {
return Err(GitError::InvalidUrl(path_or_url.to_string()));
}
// Create the target directory
let clone_path = format!("{}/{}/{}/{}", self.base_path, server, account, repo);
let clone_dir = Path::new(&clone_path);
// Check if repo already exists
if clone_dir.exists() {
return Ok(vec![GitRepo::new(clone_path)]);
}
// Create parent directory
if let Some(parent) = clone_dir.parent() {
fs::create_dir_all(parent).map_err(GitError::FileSystemError)?;
}
// Clone the repository
let output = Command::new("git")
.args(&["clone", "--depth", "1", path_or_url, &clone_path])
.output()
.map_err(GitError::CommandExecutionError)?;
if output.status.success() {
Ok(vec![GitRepo::new(clone_path)])
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(GitError::GitCommandFailed(format!(
"Git clone error: {}",
error
)))
}
} else {
// It's a path pattern, find matching repositories using the updated self.find()
// which now directly returns Result<Vec<GitRepo>, GitError>.
let repos = self.find(path_or_url)?;
Ok(repos)
}
}
}
/// Represents a git repository.
pub struct GitRepo {
path: String,
}
impl GitRepo {
/// Creates a new GitRepo with the specified path.
///
/// # Arguments
///
/// * `path` - The path to the git repository
pub fn new(path: String) -> Self {
GitRepo { path }
}
/// Gets the path of the repository.
///
/// # Returns
///
/// * The path to the git repository
pub fn path(&self) -> &str {
&self.path
}
/// Checks if the repository has uncommitted changes.
///
/// # Returns
///
/// * `Ok(bool)` - True if the repository has uncommitted changes, false otherwise
/// * `Err(GitError)` - If the operation failed
pub fn has_changes(&self) -> Result<bool, GitError> {
let output = Command::new("git")
.args(&["-C", &self.path, "status", "--porcelain"])
.output()
.map_err(GitError::CommandExecutionError)?;
Ok(!output.stdout.is_empty())
}
/// Pulls the latest changes from the remote repository.
///
/// # Returns
///
/// * `Ok(Self)` - The GitRepo object for method chaining
/// * `Err(GitError)` - If the pull operation failed
pub fn pull(&self) -> Result<Self, GitError> {
// Check if repository exists and is a git repository
let git_dir = Path::new(&self.path).join(".git");
if !git_dir.exists() || !git_dir.is_dir() {
return Err(GitError::NotAGitRepository(self.path.clone()));
}
// Check for local changes
if self.has_changes()? {
return Err(GitError::LocalChangesExist(self.path.clone()));
}
// Pull the latest changes
let output = Command::new("git")
.args(&["-C", &self.path, "pull"])
.output()
.map_err(GitError::CommandExecutionError)?;
if output.status.success() {
Ok(self.clone())
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(GitError::GitCommandFailed(format!(
"Git pull error: {}",
error
)))
}
}
/// Resets any local changes in the repository.
///
/// # Returns
///
/// * `Ok(Self)` - The GitRepo object for method chaining
/// * `Err(GitError)` - If the reset operation failed
pub fn reset(&self) -> Result<Self, GitError> {
// Check if repository exists and is a git repository
let git_dir = Path::new(&self.path).join(".git");
if !git_dir.exists() || !git_dir.is_dir() {
return Err(GitError::NotAGitRepository(self.path.clone()));
}
// Reset any local changes
let reset_output = Command::new("git")
.args(&["-C", &self.path, "reset", "--hard", "HEAD"])
.output()
.map_err(GitError::CommandExecutionError)?;
if !reset_output.status.success() {
let error = String::from_utf8_lossy(&reset_output.stderr);
return Err(GitError::GitCommandFailed(format!(
"Git reset error: {}",
error
)));
}
// Clean untracked files
let clean_output = Command::new("git")
.args(&["-C", &self.path, "clean", "-fd"])
.output()
.map_err(GitError::CommandExecutionError)?;
if !clean_output.status.success() {
let error = String::from_utf8_lossy(&clean_output.stderr);
return Err(GitError::GitCommandFailed(format!(
"Git clean error: {}",
error
)));
}
Ok(self.clone())
}
/// Commits changes in the repository.
///
/// # Arguments
///
/// * `message` - The commit message
///
/// # Returns
///
/// * `Ok(Self)` - The GitRepo object for method chaining
/// * `Err(GitError)` - If the commit operation failed
pub fn commit(&self, message: &str) -> Result<Self, GitError> {
// Check if repository exists and is a git repository
let git_dir = Path::new(&self.path).join(".git");
if !git_dir.exists() || !git_dir.is_dir() {
return Err(GitError::NotAGitRepository(self.path.clone()));
}
// Check for local changes
if !self.has_changes()? {
return Ok(self.clone());
}
// Add all changes
let add_output = Command::new("git")
.args(&["-C", &self.path, "add", "."])
.output()
.map_err(GitError::CommandExecutionError)?;
if !add_output.status.success() {
let error = String::from_utf8_lossy(&add_output.stderr);
return Err(GitError::GitCommandFailed(format!(
"Git add error: {}",
error
)));
}
// Commit the changes
let commit_output = Command::new("git")
.args(&["-C", &self.path, "commit", "-m", message])
.output()
.map_err(GitError::CommandExecutionError)?;
if !commit_output.status.success() {
let error = String::from_utf8_lossy(&commit_output.stderr);
return Err(GitError::GitCommandFailed(format!(
"Git commit error: {}",
error
)));
}
Ok(self.clone())
}
/// Pushes changes to the remote repository.
///
/// # Returns
///
/// * `Ok(Self)` - The GitRepo object for method chaining
/// * `Err(GitError)` - If the push operation failed
pub fn push(&self) -> Result<Self, GitError> {
// Check if repository exists and is a git repository
let git_dir = Path::new(&self.path).join(".git");
if !git_dir.exists() || !git_dir.is_dir() {
return Err(GitError::NotAGitRepository(self.path.clone()));
}
// Push the changes
let push_output = Command::new("git")
.args(&["-C", &self.path, "push"])
.output()
.map_err(GitError::CommandExecutionError)?;
if push_output.status.success() {
Ok(self.clone())
} else {
let error = String::from_utf8_lossy(&push_output.stderr);
Err(GitError::GitCommandFailed(format!(
"Git push error: {}",
error
)))
}
}
}
// Implement Clone for GitRepo to allow for method chaining
impl Clone for GitRepo {
fn clone(&self) -> Self {
GitRepo {
path: self.path.clone(),
}
}
}

View File

@@ -0,0 +1,420 @@
use redis::Cmd;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use std::process::{Command, Output};
// Simple redis client functionality with configurable connection
fn execute_redis_command(cmd: &mut redis::Cmd) -> redis::RedisResult<String> {
// Get Redis URL from environment variables with fallback
let redis_url = get_redis_url();
log::debug!("Connecting to Redis at: {}", mask_redis_url(&redis_url));
let client = redis::Client::open(redis_url)?;
let mut con = client.get_connection()?;
cmd.query(&mut con)
}
/// Get Redis URL from environment variables with secure fallbacks
fn get_redis_url() -> String {
std::env::var("REDIS_URL")
.or_else(|_| std::env::var("SAL_REDIS_URL"))
.unwrap_or_else(|_| "redis://127.0.0.1/".to_string())
}
/// Mask sensitive information in Redis URL for logging
fn mask_redis_url(url: &str) -> String {
if let Ok(parsed) = url::Url::parse(url) {
if parsed.password().is_some() {
format!(
"{}://{}:***@{}:{}/{}",
parsed.scheme(),
parsed.username(),
parsed.host_str().unwrap_or("unknown"),
parsed.port().unwrap_or(6379),
parsed.path().trim_start_matches('/')
)
} else {
url.to_string()
}
} else {
"redis://***masked***".to_string()
}
}
// Define a custom error type for GitExecutor operations
#[derive(Debug)]
pub enum GitExecutorError {
GitCommandFailed(String),
CommandExecutionError(std::io::Error),
RedisError(redis::RedisError),
JsonError(serde_json::Error),
AuthenticationError(String),
SshAgentNotLoaded,
InvalidAuthConfig(String),
}
// Implement Display for GitExecutorError
impl fmt::Display for GitExecutorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GitExecutorError::GitCommandFailed(e) => write!(f, "Git command failed: {}", e),
GitExecutorError::CommandExecutionError(e) => {
write!(f, "Command execution error: {}", e)
}
GitExecutorError::RedisError(e) => write!(f, "Redis error: {}", e),
GitExecutorError::JsonError(e) => write!(f, "JSON error: {}", e),
GitExecutorError::AuthenticationError(e) => write!(f, "Authentication error: {}", e),
GitExecutorError::SshAgentNotLoaded => write!(f, "SSH agent is not loaded"),
GitExecutorError::InvalidAuthConfig(e) => {
write!(f, "Invalid authentication configuration: {}", e)
}
}
}
}
// Implement Error trait for GitExecutorError
impl Error for GitExecutorError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
GitExecutorError::CommandExecutionError(e) => Some(e),
GitExecutorError::RedisError(e) => Some(e),
GitExecutorError::JsonError(e) => Some(e),
_ => None,
}
}
}
// From implementations for error conversion
impl From<redis::RedisError> for GitExecutorError {
fn from(err: redis::RedisError) -> Self {
GitExecutorError::RedisError(err)
}
}
impl From<serde_json::Error> for GitExecutorError {
fn from(err: serde_json::Error) -> Self {
GitExecutorError::JsonError(err)
}
}
impl From<std::io::Error> for GitExecutorError {
fn from(err: std::io::Error) -> Self {
GitExecutorError::CommandExecutionError(err)
}
}
// Status enum for GitConfig
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub enum GitConfigStatus {
#[serde(rename = "error")]
Error,
#[serde(rename = "ok")]
Ok,
}
// Auth configuration for a specific git server
#[derive(Debug, Serialize, Deserialize)]
pub struct GitServerAuth {
pub sshagent: Option<bool>,
pub key: Option<String>,
pub username: Option<String>,
pub password: Option<String>,
}
// Main configuration structure from Redis
#[derive(Debug, Serialize, Deserialize)]
pub struct GitConfig {
pub status: GitConfigStatus,
pub auth: HashMap<String, GitServerAuth>,
}
// GitExecutor struct
pub struct GitExecutor {
config: Option<GitConfig>,
}
impl GitExecutor {
// Create a new GitExecutor
pub fn new() -> Self {
GitExecutor { config: None }
}
// Initialize by loading configuration from Redis
pub fn init(&mut self) -> Result<(), GitExecutorError> {
// Try to load config from Redis
match self.load_config_from_redis() {
Ok(config) => {
self.config = Some(config);
Ok(())
}
Err(e) => {
// If Redis error, we'll proceed without config
// This is not a fatal error as we might use default git behavior
log::warn!("Failed to load git config from Redis: {}", e);
self.config = None;
Ok(())
}
}
}
// Load configuration from Redis
fn load_config_from_redis(&self) -> Result<GitConfig, GitExecutorError> {
// Create Redis command to get the herocontext:git key
let mut cmd = Cmd::new();
cmd.arg("GET").arg("herocontext:git");
// Execute the command
let result: redis::RedisResult<String> = execute_redis_command(&mut cmd);
match result {
Ok(json_str) => {
// Parse the JSON string into GitConfig
let config: GitConfig = serde_json::from_str(&json_str)?;
// Validate the config
if config.status == GitConfigStatus::Error {
return Err(GitExecutorError::InvalidAuthConfig(
"Config status is error".to_string(),
));
}
Ok(config)
}
Err(e) => Err(GitExecutorError::RedisError(e)),
}
}
// Check if SSH agent is loaded
fn is_ssh_agent_loaded(&self) -> bool {
let output = Command::new("ssh-add").arg("-l").output();
match output {
Ok(output) => output.status.success() && !output.stdout.is_empty(),
Err(_) => false,
}
}
// Get authentication configuration for a git URL
fn get_auth_for_url(&self, url: &str) -> Option<&GitServerAuth> {
if let Some(config) = &self.config {
let (server, _, _) = crate::parse_git_url(url);
if !server.is_empty() {
return config.auth.get(&server);
}
}
None
}
// Validate authentication configuration
fn validate_auth_config(&self, auth: &GitServerAuth) -> Result<(), GitExecutorError> {
// Rule: If sshagent is true, other fields should be empty
if let Some(true) = auth.sshagent {
if auth.key.is_some() || auth.username.is_some() || auth.password.is_some() {
return Err(GitExecutorError::InvalidAuthConfig(
"When sshagent is true, key, username, and password must be empty".to_string(),
));
}
// Check if SSH agent is actually loaded
if !self.is_ssh_agent_loaded() {
return Err(GitExecutorError::SshAgentNotLoaded);
}
}
// Rule: If key is set, other fields should be empty
if let Some(_) = &auth.key {
if auth.sshagent.unwrap_or(false) || auth.username.is_some() || auth.password.is_some()
{
return Err(GitExecutorError::InvalidAuthConfig(
"When key is set, sshagent, username, and password must be empty".to_string(),
));
}
}
// Rule: If username is set, password should be set and other fields empty
if let Some(_) = &auth.username {
if auth.sshagent.unwrap_or(false) || auth.key.is_some() {
return Err(GitExecutorError::InvalidAuthConfig(
"When username is set, sshagent and key must be empty".to_string(),
));
}
if auth.password.is_none() {
return Err(GitExecutorError::InvalidAuthConfig(
"When username is set, password must also be set".to_string(),
));
}
}
Ok(())
}
// Execute a git command with authentication
pub fn execute(&self, args: &[&str]) -> Result<Output, GitExecutorError> {
// Extract the git URL if this is a command that needs authentication
let url_arg = self.extract_git_url_from_args(args);
// If we have a URL and authentication config, use it
if let Some(url) = url_arg {
if let Some(auth) = self.get_auth_for_url(&url) {
// Validate the authentication configuration
self.validate_auth_config(auth)?;
// Execute with the appropriate authentication method
return self.execute_with_auth(args, auth);
}
}
// No special authentication needed, execute normally
self.execute_git_command(args)
}
// Extract git URL from command arguments
fn extract_git_url_from_args<'a>(&self, args: &[&'a str]) -> Option<&'a str> {
// Commands that might contain a git URL
if args.contains(&"clone")
|| args.contains(&"fetch")
|| args.contains(&"pull")
|| args.contains(&"push")
{
// The URL is typically the last argument for clone, or after remote for others
for (i, &arg) in args.iter().enumerate() {
if arg == "clone" && i + 1 < args.len() {
return Some(args[i + 1]);
}
if (arg == "fetch" || arg == "pull" || arg == "push") && i + 1 < args.len() {
// For these commands, the URL might be specified as a remote name
// We'd need more complex logic to resolve remote names to URLs
// For now, we'll just return None
return None;
}
}
}
None
}
// Execute git command with authentication
fn execute_with_auth(
&self,
args: &[&str],
auth: &GitServerAuth,
) -> Result<Output, GitExecutorError> {
// Handle different authentication methods
if let Some(true) = auth.sshagent {
// Use SSH agent (already validated that it's loaded)
self.execute_git_command(args)
} else if let Some(key) = &auth.key {
// Use SSH key
self.execute_with_ssh_key(args, key)
} else if let Some(username) = &auth.username {
// Use username/password
if let Some(password) = &auth.password {
self.execute_with_credentials(args, username, password)
} else {
// This should never happen due to validation
Err(GitExecutorError::AuthenticationError(
"Password is required when username is set".to_string(),
))
}
} else {
// No authentication method specified, use default
self.execute_git_command(args)
}
}
// Execute git command with SSH key
fn execute_with_ssh_key(&self, args: &[&str], key: &str) -> Result<Output, GitExecutorError> {
// Create a command with GIT_SSH_COMMAND to specify the key
let ssh_command = format!("ssh -i {} -o IdentitiesOnly=yes", key);
let mut command = Command::new("git");
command.env("GIT_SSH_COMMAND", ssh_command);
command.args(args);
let output = command.output()?;
if output.status.success() {
Ok(output)
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(GitExecutorError::GitCommandFailed(error.to_string()))
}
}
// Execute git command with username/password using secure credential helper
fn execute_with_credentials(
&self,
args: &[&str],
username: &str,
password: &str,
) -> Result<Output, GitExecutorError> {
// Use git credential helper approach for security
// Create a temporary credential helper script
let temp_dir = std::env::temp_dir();
let helper_script = temp_dir.join(format!("git_helper_{}", std::process::id()));
// Create credential helper script content
let script_content = format!(
"#!/bin/bash\necho username={}\necho password={}\n",
username, password
);
// Write the helper script
std::fs::write(&helper_script, script_content)
.map_err(|e| GitExecutorError::CommandExecutionError(e))?;
// Make it executable
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&helper_script)
.map_err(|e| GitExecutorError::CommandExecutionError(e))?
.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&helper_script, perms)
.map_err(|e| GitExecutorError::CommandExecutionError(e))?;
}
// Execute git command with credential helper
let mut command = Command::new("git");
command.args(args);
command.env("GIT_ASKPASS", &helper_script);
command.env("GIT_TERMINAL_PROMPT", "0"); // Disable terminal prompts
log::debug!("Executing git command with credential helper");
let output = command.output()?;
// Clean up the temporary helper script
let _ = std::fs::remove_file(&helper_script);
if output.status.success() {
Ok(output)
} else {
let error = String::from_utf8_lossy(&output.stderr);
log::error!("Git command failed: {}", error);
Err(GitExecutorError::GitCommandFailed(error.to_string()))
}
}
// Basic git command execution
fn execute_git_command(&self, args: &[&str]) -> Result<Output, GitExecutorError> {
let mut command = Command::new("git");
command.args(args);
let output = command.output()?;
if output.status.success() {
Ok(output)
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(GitExecutorError::GitCommandFailed(error.to_string()))
}
}
}
// Implement Default for GitExecutor
impl Default for GitExecutor {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,6 @@
mod git;
mod git_executor;
pub mod rhai;
pub use git::*;
pub use git_executor::*;

View File

@@ -0,0 +1,207 @@
//! Rhai wrappers for Git module functions
//!
//! This module provides Rhai wrappers for the functions in the Git module.
use crate::{GitError, GitRepo, GitTree};
use rhai::{Array, Dynamic, Engine, EvalAltResult};
/// Register Git module functions with the Rhai engine
///
/// # Arguments
///
/// * `engine` - The Rhai engine to register the functions with
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register GitTree constructor
engine.register_type::<GitTree>();
engine.register_fn("git_tree_new", git_tree_new);
// Register GitTree methods
engine.register_fn("list", git_tree_list);
engine.register_fn("find", git_tree_find);
engine.register_fn("get", git_tree_get);
// Register GitRepo methods
engine.register_type::<GitRepo>();
engine.register_fn("path", git_repo_path);
engine.register_fn("has_changes", git_repo_has_changes);
engine.register_fn("pull", git_repo_pull);
engine.register_fn("reset", git_repo_reset);
engine.register_fn("commit", git_repo_commit);
engine.register_fn("push", git_repo_push);
// Register git_clone function for testing
engine.register_fn("git_clone", git_clone);
Ok(())
}
// Helper functions for error conversion
fn git_error_to_rhai_error<T>(result: Result<T, GitError>) -> Result<T, Box<EvalAltResult>> {
result.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Git error: {}", e).into(),
rhai::Position::NONE,
))
})
}
//
// GitTree Function Wrappers
//
/// Wrapper for GitTree::new
///
/// Creates a new GitTree with the specified base path.
pub fn git_tree_new(base_path: &str) -> Result<GitTree, Box<EvalAltResult>> {
git_error_to_rhai_error(GitTree::new(base_path))
}
/// Wrapper for GitTree::list
///
/// Lists all git repositories under the base path.
pub fn git_tree_list(git_tree: &mut GitTree) -> Result<Array, Box<EvalAltResult>> {
let repos = git_error_to_rhai_error(git_tree.list())?;
// Convert Vec<String> to Rhai Array
let mut array = Array::new();
for repo in repos {
array.push(Dynamic::from(repo));
}
Ok(array)
}
/// Wrapper for GitTree::find
///
/// Finds repositories matching a pattern and returns them as an array of GitRepo objects.
/// Assumes the underlying GitTree::find Rust method now returns Result<Vec<GitRepo>, GitError>.
pub fn git_tree_find(git_tree: &mut GitTree, pattern: &str) -> Result<Array, Box<EvalAltResult>> {
let repos: Vec<GitRepo> = git_error_to_rhai_error(git_tree.find(pattern))?;
// Convert Vec<GitRepo> to Rhai Array
let mut array = Array::new();
for repo in repos {
array.push(Dynamic::from(repo));
}
Ok(array)
}
/// Wrapper for GitTree::get
///
/// Gets a single GitRepo object based on an exact name or URL.
/// The underlying Rust GitTree::get method returns Result<Vec<GitRepo>, GitError>.
/// This wrapper ensures that for Rhai, 'get' returns a single GitRepo or an error
/// if zero or multiple repositories are found (for local names/patterns),
/// or if a URL operation fails or unexpectedly yields not exactly one result.
pub fn git_tree_get(
git_tree: &mut GitTree,
name_or_url: &str,
) -> Result<GitRepo, Box<EvalAltResult>> {
let mut repos_vec: Vec<GitRepo> = git_error_to_rhai_error(git_tree.get(name_or_url))?;
match repos_vec.len() {
1 => Ok(repos_vec.remove(0)), // Efficient for Vec of size 1, transfers ownership
0 => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Git error: Repository '{}' not found.", name_or_url).into(),
rhai::Position::NONE,
))),
_ => Err(Box::new(EvalAltResult::ErrorRuntime(
format!(
"Git error: Multiple repositories ({}) found matching '{}'. Use find() for patterns or provide a more specific name for get().",
repos_vec.len(),
name_or_url
)
.into(),
rhai::Position::NONE,
))),
}
}
//
// GitRepo Function Wrappers
//
/// Wrapper for GitRepo::path
///
/// Gets the path of the repository.
pub fn git_repo_path(git_repo: &mut GitRepo) -> String {
git_repo.path().to_string()
}
/// Wrapper for GitRepo::has_changes
///
/// Checks if the repository has uncommitted changes.
pub fn git_repo_has_changes(git_repo: &mut GitRepo) -> Result<bool, Box<EvalAltResult>> {
git_error_to_rhai_error(git_repo.has_changes())
}
/// Wrapper for GitRepo::pull
///
/// Pulls the latest changes from the remote repository.
pub fn git_repo_pull(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> {
git_error_to_rhai_error(git_repo.pull())
}
/// Wrapper for GitRepo::reset
///
/// Resets any local changes in the repository.
pub fn git_repo_reset(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> {
git_error_to_rhai_error(git_repo.reset())
}
/// Wrapper for GitRepo::commit
///
/// Commits changes in the repository.
pub fn git_repo_commit(
git_repo: &mut GitRepo,
message: &str,
) -> Result<GitRepo, Box<EvalAltResult>> {
git_error_to_rhai_error(git_repo.commit(message))
}
/// Wrapper for GitRepo::push
///
/// Pushes changes to the remote repository.
pub fn git_repo_push(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> {
git_error_to_rhai_error(git_repo.push())
}
/// Clone a git repository to a temporary location
///
/// This function clones a repository from the given URL to a temporary directory
/// and returns the GitRepo object for further operations.
///
/// # Arguments
///
/// * `url` - The URL of the git repository to clone
///
/// # Returns
///
/// * `Ok(GitRepo)` - The cloned repository object
/// * `Err(Box<EvalAltResult>)` - If the clone operation failed
pub fn git_clone(url: &str) -> Result<GitRepo, Box<EvalAltResult>> {
// Get base path from environment or use default temp directory
let base_path = std::env::var("GIT_DEFAULT_BASE_PATH").unwrap_or_else(|_| {
std::env::temp_dir()
.join("sal_git_clones")
.to_string_lossy()
.to_string()
});
// Create GitTree and clone the repository
let git_tree = git_error_to_rhai_error(GitTree::new(&base_path))?;
let repos = git_error_to_rhai_error(git_tree.get(url))?;
// Return the first (and should be only) repository
repos.into_iter().next().ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
"Git error: No repository was cloned".into(),
rhai::Position::NONE,
))
})
}

View File

@@ -0,0 +1,197 @@
use sal_git::*;
use std::env;
#[test]
fn test_git_executor_initialization() {
let mut executor = GitExecutor::new();
// Test that executor can be initialized without panicking
// Even if Redis is not available, init should handle it gracefully
let result = executor.init();
assert!(
result.is_ok(),
"GitExecutor init should handle Redis unavailability gracefully"
);
}
#[test]
fn test_redis_connection_fallback() {
// Test that GitExecutor handles Redis connection failures gracefully
// Set an invalid Redis URL to force connection failure
env::set_var("REDIS_URL", "redis://invalid-host:9999/0");
let mut executor = GitExecutor::new();
let result = executor.init();
// Should succeed even with invalid Redis URL (graceful fallback)
assert!(
result.is_ok(),
"GitExecutor should handle Redis connection failures gracefully"
);
// Cleanup
env::remove_var("REDIS_URL");
}
#[test]
fn test_environment_variable_precedence() {
// Test REDIS_URL takes precedence over SAL_REDIS_URL
env::set_var("REDIS_URL", "redis://primary:6379/0");
env::set_var("SAL_REDIS_URL", "redis://fallback:6379/1");
// Create executor - should use REDIS_URL (primary)
let mut executor = GitExecutor::new();
let result = executor.init();
// Should succeed (even if connection fails, init handles it gracefully)
assert!(
result.is_ok(),
"GitExecutor should handle environment variables correctly"
);
// Test with only SAL_REDIS_URL
env::remove_var("REDIS_URL");
let mut executor2 = GitExecutor::new();
let result2 = executor2.init();
assert!(
result2.is_ok(),
"GitExecutor should use SAL_REDIS_URL as fallback"
);
// Cleanup
env::remove_var("SAL_REDIS_URL");
}
#[test]
fn test_git_command_argument_validation() {
let executor = GitExecutor::new();
// Test with empty arguments
let result = executor.execute(&[]);
assert!(result.is_err(), "Empty git command should fail");
// Test with invalid git command
let result = executor.execute(&["invalid-command"]);
assert!(result.is_err(), "Invalid git command should fail");
// Test with malformed URL (should fail due to URL validation, not injection)
let result = executor.execute(&["clone", "not-a-url"]);
assert!(result.is_err(), "Invalid URL should be rejected");
}
#[test]
fn test_git_executor_with_valid_commands() {
let executor = GitExecutor::new();
// Test git version command (should work if git is available)
let result = executor.execute(&["--version"]);
match result {
Ok(output) => {
// If git is available, version should be in output
let output_str = String::from_utf8_lossy(&output.stdout);
assert!(
output_str.contains("git version"),
"Git version output should contain 'git version'"
);
}
Err(_) => {
// If git is not available, that's acceptable in test environment
println!("Note: Git not available in test environment");
}
}
}
#[test]
fn test_credential_helper_environment_setup() {
use std::process::Command;
// Test that we can create and execute a simple credential helper script
let temp_dir = std::env::temp_dir();
let helper_script = temp_dir.join("test_git_helper");
// Create a test credential helper script
let script_content = "#!/bin/bash\necho username=testuser\necho password=testpass\n";
// Write the helper script
let write_result = std::fs::write(&helper_script, script_content);
assert!(
write_result.is_ok(),
"Should be able to write credential helper script"
);
// Make it executable (Unix only)
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&helper_script).unwrap().permissions();
perms.set_mode(0o755);
let perm_result = std::fs::set_permissions(&helper_script, perms);
assert!(
perm_result.is_ok(),
"Should be able to set script permissions"
);
}
// Test that the script can be executed
#[cfg(unix)]
{
let output = Command::new(&helper_script).output();
match output {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("username=testuser"),
"Script should output username"
);
assert!(
stdout.contains("password=testpass"),
"Script should output password"
);
}
Err(_) => {
println!("Note: Could not execute credential helper script (shell not available)");
}
}
}
// Clean up
let _ = std::fs::remove_file(&helper_script);
}
#[test]
fn test_redis_url_masking() {
// Test that sensitive Redis URLs are properly masked for logging
// This tests the internal URL masking functionality
// Test URLs with passwords
let test_cases = vec![
("redis://user:password@localhost:6379/0", true),
("redis://localhost:6379/0", false),
("redis://user@localhost:6379/0", false),
("invalid-url", false),
];
for (url, has_password) in test_cases {
// Set the Redis URL and create executor
std::env::set_var("REDIS_URL", url);
let mut executor = GitExecutor::new();
let result = executor.init();
// Should always succeed (graceful handling of connection failures)
assert!(result.is_ok(), "GitExecutor should handle URL: {}", url);
// The actual masking happens internally during logging
// We can't easily test the log output, but we verify the executor handles it
if has_password {
println!(
"Note: Tested URL with password (should be masked in logs): {}",
url
);
}
}
// Cleanup
std::env::remove_var("REDIS_URL");
}

View File

@@ -0,0 +1,178 @@
use sal_git::*;
use std::collections::HashMap;
#[test]
fn test_git_executor_new() {
let executor = GitExecutor::new();
// We can't directly access the config field since it's private,
// but we can test that the executor was created successfully
let _executor = executor;
}
#[test]
fn test_git_executor_default() {
let executor = GitExecutor::default();
let _executor = executor;
}
#[test]
fn test_git_config_status_serialization() {
let status_ok = GitConfigStatus::Ok;
let status_error = GitConfigStatus::Error;
let json_ok = serde_json::to_string(&status_ok).unwrap();
let json_error = serde_json::to_string(&status_error).unwrap();
assert_eq!(json_ok, "\"ok\"");
assert_eq!(json_error, "\"error\"");
}
#[test]
fn test_git_config_status_deserialization() {
let status_ok: GitConfigStatus = serde_json::from_str("\"ok\"").unwrap();
let status_error: GitConfigStatus = serde_json::from_str("\"error\"").unwrap();
assert_eq!(status_ok, GitConfigStatus::Ok);
assert_eq!(status_error, GitConfigStatus::Error);
}
#[test]
fn test_git_server_auth_serialization() {
let auth = GitServerAuth {
sshagent: Some(true),
key: None,
username: None,
password: None,
};
let json = serde_json::to_string(&auth).unwrap();
assert!(json.contains("\"sshagent\":true"));
}
#[test]
fn test_git_server_auth_deserialization() {
let json = r#"{"sshagent":true,"key":null,"username":null,"password":null}"#;
let auth: GitServerAuth = serde_json::from_str(json).unwrap();
assert_eq!(auth.sshagent, Some(true));
assert_eq!(auth.key, None);
assert_eq!(auth.username, None);
assert_eq!(auth.password, None);
}
#[test]
fn test_git_config_serialization() {
let mut auth_map = HashMap::new();
auth_map.insert(
"github.com".to_string(),
GitServerAuth {
sshagent: Some(true),
key: None,
username: None,
password: None,
},
);
let config = GitConfig {
status: GitConfigStatus::Ok,
auth: auth_map,
};
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("\"status\":\"ok\""));
assert!(json.contains("\"github.com\""));
}
#[test]
fn test_git_config_deserialization() {
let json = r#"{"status":"ok","auth":{"github.com":{"sshagent":true,"key":null,"username":null,"password":null}}}"#;
let config: GitConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.status, GitConfigStatus::Ok);
assert!(config.auth.contains_key("github.com"));
assert_eq!(config.auth["github.com"].sshagent, Some(true));
}
#[test]
fn test_git_executor_error_display() {
let error = GitExecutorError::GitCommandFailed("command failed".to_string());
assert_eq!(format!("{}", error), "Git command failed: command failed");
let error = GitExecutorError::SshAgentNotLoaded;
assert_eq!(format!("{}", error), "SSH agent is not loaded");
let error = GitExecutorError::AuthenticationError("auth failed".to_string());
assert_eq!(format!("{}", error), "Authentication error: auth failed");
}
#[test]
fn test_git_executor_error_from_redis_error() {
let redis_error = redis::RedisError::from((redis::ErrorKind::TypeError, "type error"));
let git_error = GitExecutorError::from(redis_error);
match git_error {
GitExecutorError::RedisError(_) => {}
_ => panic!("Expected RedisError variant"),
}
}
#[test]
fn test_git_executor_error_from_serde_error() {
let serde_error = serde_json::from_str::<GitConfig>("invalid json").unwrap_err();
let git_error = GitExecutorError::from(serde_error);
match git_error {
GitExecutorError::JsonError(_) => {}
_ => panic!("Expected JsonError variant"),
}
}
#[test]
fn test_git_executor_error_from_io_error() {
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let git_error = GitExecutorError::from(io_error);
match git_error {
GitExecutorError::CommandExecutionError(_) => {}
_ => panic!("Expected CommandExecutionError variant"),
}
}
#[test]
fn test_redis_url_configuration() {
// Test default Redis URL
std::env::remove_var("REDIS_URL");
std::env::remove_var("SAL_REDIS_URL");
// This is testing the internal function, but we can't access it directly
// Instead, we test that GitExecutor can be created without panicking
let executor = GitExecutor::new();
let _executor = executor; // Just verify it was created successfully
}
#[test]
fn test_redis_url_from_environment() {
// Test REDIS_URL environment variable
std::env::set_var("REDIS_URL", "redis://test:6379/1");
// Create executor - should use the environment variable
let executor = GitExecutor::new();
let _executor = executor; // Just verify it was created successfully
// Clean up
std::env::remove_var("REDIS_URL");
}
#[test]
fn test_sal_redis_url_from_environment() {
// Test SAL_REDIS_URL environment variable (fallback)
std::env::remove_var("REDIS_URL");
std::env::set_var("SAL_REDIS_URL", "redis://sal-test:6379/2");
// Create executor - should use the SAL_REDIS_URL
let executor = GitExecutor::new();
let _executor = executor; // Just verify it was created successfully
// Clean up
std::env::remove_var("SAL_REDIS_URL");
}

View File

@@ -0,0 +1,124 @@
use sal_git::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_clone_existing_repository() {
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path().to_str().unwrap();
let git_tree = GitTree::new(base_path).unwrap();
// First clone
let result1 = git_tree.get("https://github.com/octocat/Hello-World.git");
// Second clone of same repo - should return existing
let result2 = git_tree.get("https://github.com/octocat/Hello-World.git");
match (result1, result2) {
(Ok(repos1), Ok(repos2)) => {
// git_tree.get() returns Vec<GitRepo>, should have exactly 1 repo
assert_eq!(
repos1.len(),
1,
"First clone should return exactly 1 repository"
);
assert_eq!(
repos2.len(),
1,
"Second clone should return exactly 1 repository"
);
assert_eq!(
repos1[0].path(),
repos2[0].path(),
"Both clones should point to same path"
);
// Verify the path actually exists
assert!(
std::path::Path::new(repos1[0].path()).exists(),
"Repository path should exist"
);
}
(Err(e1), Err(e2)) => {
// Both failed - acceptable if network/git issues
println!("Note: Clone test skipped due to errors: {} / {}", e1, e2);
}
_ => {
panic!(
"Inconsistent results: one clone succeeded, other failed - this indicates a bug"
);
}
}
}
#[test]
fn test_repository_operations_on_cloned_repo() {
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path().to_str().unwrap();
let git_tree = GitTree::new(base_path).unwrap();
match git_tree.get("https://github.com/octocat/Hello-World.git") {
Ok(repos) if repos.len() == 1 => {
let repo = &repos[0];
// Test has_changes on fresh clone
match repo.has_changes() {
Ok(has_changes) => assert!(!has_changes, "Fresh clone should have no changes"),
Err(_) => println!("Note: has_changes test skipped due to git availability"),
}
// Test path is valid
assert!(repo.path().len() > 0);
assert!(std::path::Path::new(repo.path()).exists());
}
_ => {
println!(
"Note: Repository operations test skipped due to network/environment constraints"
);
}
}
}
#[test]
fn test_multiple_repositories_in_git_tree() {
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path().to_str().unwrap();
// Create some fake git repositories for testing
let repo1_path = temp_dir.path().join("github.com/user1/repo1");
let repo2_path = temp_dir.path().join("github.com/user2/repo2");
fs::create_dir_all(&repo1_path).unwrap();
fs::create_dir_all(&repo2_path).unwrap();
fs::create_dir_all(repo1_path.join(".git")).unwrap();
fs::create_dir_all(repo2_path.join(".git")).unwrap();
let git_tree = GitTree::new(base_path).unwrap();
let repos = git_tree.list().unwrap();
assert!(repos.len() >= 2, "Should find at least 2 repositories");
}
#[test]
fn test_invalid_git_repository_handling() {
let temp_dir = TempDir::new().unwrap();
let fake_repo_path = temp_dir.path().join("fake_repo");
fs::create_dir_all(&fake_repo_path).unwrap();
// Create a directory that looks like a repo but isn't (no .git directory)
let repo = GitRepo::new(fake_repo_path.to_str().unwrap().to_string());
// Operations should fail gracefully on non-git directories
// Note: has_changes might succeed if git is available and treats it as empty repo
// So we test the operations that definitely require .git directory
assert!(
repo.pull().is_err(),
"Pull should fail on non-git directory"
);
assert!(
repo.reset().is_err(),
"Reset should fail on non-git directory"
);
}

View File

@@ -0,0 +1,119 @@
use sal_git::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_parse_git_url_https() {
let (server, account, repo) = parse_git_url("https://github.com/user/repo.git");
assert_eq!(server, "github.com");
assert_eq!(account, "user");
assert_eq!(repo, "repo");
}
#[test]
fn test_parse_git_url_https_without_git_extension() {
let (server, account, repo) = parse_git_url("https://github.com/user/repo");
assert_eq!(server, "github.com");
assert_eq!(account, "user");
assert_eq!(repo, "repo");
}
#[test]
fn test_parse_git_url_ssh() {
let (server, account, repo) = parse_git_url("git@github.com:user/repo.git");
assert_eq!(server, "github.com");
assert_eq!(account, "user");
assert_eq!(repo, "repo");
}
#[test]
fn test_parse_git_url_ssh_without_git_extension() {
let (server, account, repo) = parse_git_url("git@github.com:user/repo");
assert_eq!(server, "github.com");
assert_eq!(account, "user");
assert_eq!(repo, "repo");
}
#[test]
fn test_parse_git_url_invalid() {
let (server, account, repo) = parse_git_url("invalid-url");
assert_eq!(server, "");
assert_eq!(account, "");
assert_eq!(repo, "");
}
#[test]
fn test_git_tree_new_creates_directory() {
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path().join("git_repos");
let base_path_str = base_path.to_str().unwrap();
let _git_tree = GitTree::new(base_path_str).unwrap();
assert!(base_path.exists());
assert!(base_path.is_dir());
}
#[test]
fn test_git_tree_new_existing_directory() {
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path().join("existing_dir");
fs::create_dir_all(&base_path).unwrap();
let base_path_str = base_path.to_str().unwrap();
let _git_tree = GitTree::new(base_path_str).unwrap();
}
#[test]
fn test_git_tree_new_invalid_path() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("file.txt");
fs::write(&file_path, "content").unwrap();
let file_path_str = file_path.to_str().unwrap();
let result = GitTree::new(file_path_str);
assert!(result.is_err());
if let Err(error) = result {
match error {
GitError::InvalidBasePath(_) => {}
_ => panic!("Expected InvalidBasePath error"),
}
}
}
#[test]
fn test_git_tree_list_empty_directory() {
let temp_dir = TempDir::new().unwrap();
let base_path_str = temp_dir.path().to_str().unwrap();
let git_tree = GitTree::new(base_path_str).unwrap();
let repos = git_tree.list().unwrap();
assert!(repos.is_empty());
}
#[test]
fn test_git_repo_new() {
let repo = GitRepo::new("/path/to/repo".to_string());
assert_eq!(repo.path(), "/path/to/repo");
}
#[test]
fn test_git_repo_clone() {
let repo1 = GitRepo::new("/path/to/repo".to_string());
let repo2 = repo1.clone();
assert_eq!(repo1.path(), repo2.path());
}
#[test]
fn test_git_error_display() {
let error = GitError::InvalidUrl("bad-url".to_string());
assert_eq!(format!("{}", error), "Could not parse git URL: bad-url");
let error = GitError::NoRepositoriesFound;
assert_eq!(format!("{}", error), "No repositories found");
let error = GitError::RepositoryNotFound("pattern".to_string());
assert_eq!(
format!("{}", error),
"No repositories found matching 'pattern'"
);
}

View File

@@ -0,0 +1,70 @@
// 01_git_basic.rhai
// Tests for basic Git functionality like creating a GitTree, listing repositories, finding repositories, and cloning repositories
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Create a temporary directory for Git operations
let test_dir = "rhai_test_git";
mkdir(test_dir);
print(`Created test directory: ${test_dir}`);
// Test GitTree constructor
print("Testing GitTree constructor...");
let git_tree = git_tree_new(test_dir);
print("✓ GitTree created successfully");
// Test GitTree.list() with empty directory
print("Testing GitTree.list() with empty directory...");
let repos = git_tree.list();
assert_true(repos.len() == 0, "Expected empty list of repositories");
print(`✓ GitTree.list(): Found ${repos.len()} repositories (expected 0)`);
// Test GitTree.find() with empty directory
print("Testing GitTree.find() with empty directory...");
let found_repos = git_tree.find("*");
assert_true(found_repos.len() == 0, "Expected empty list of repositories");
print(`✓ GitTree.find(): Found ${found_repos.len()} repositories (expected 0)`);
// Test GitTree.get() with a URL to clone a repository
// We'll use a small, public repository for testing
print("Testing GitTree.get() with URL...");
let repo_url = "https://github.com/rhaiscript/playground.git";
let repo = git_tree.get(repo_url);
print(`✓ GitTree.get(): Repository cloned successfully to ${repo.path()}`);
// Test GitRepo.path()
print("Testing GitRepo.path()...");
let repo_path = repo.path();
assert_true(repo_path.contains(test_dir), "Repository path should contain test directory");
print(`✓ GitRepo.path(): ${repo_path}`);
// Test GitRepo.has_changes()
print("Testing GitRepo.has_changes()...");
let has_changes = repo.has_changes();
print(`✓ GitRepo.has_changes(): ${has_changes}`);
// Test GitTree.list() after cloning
print("Testing GitTree.list() after cloning...");
let repos_after_clone = git_tree.list();
assert_true(repos_after_clone.len() > 0, "Expected non-empty list of repositories");
print(`✓ GitTree.list(): Found ${repos_after_clone.len()} repositories`);
// Test GitTree.find() after cloning
print("Testing GitTree.find() after cloning...");
let found_repos_after_clone = git_tree.find("*");
assert_true(found_repos_after_clone.len() > 0, "Expected non-empty list of repositories");
print(`✓ GitTree.find(): Found ${found_repos_after_clone.len()} repositories`);
// Clean up
print("Cleaning up...");
delete(test_dir);
assert_true(!exist(test_dir), "Directory deletion failed");
print(`✓ Cleanup: Directory ${test_dir} removed`);
print("All basic Git tests completed successfully!");

View File

@@ -0,0 +1,61 @@
// 02_git_operations.rhai
// Tests for Git operations like pull, reset, commit, and push
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Create a temporary directory for Git operations
let test_dir = "rhai_test_git_ops";
mkdir(test_dir);
print(`Created test directory: ${test_dir}`);
// Create a GitTree
print("Creating GitTree...");
let git_tree = git_tree_new(test_dir);
print("✓ GitTree created successfully");
// Clone a repository
print("Cloning repository...");
let repo_url = "https://github.com/rhaiscript/playground.git";
let repo = git_tree.get(repo_url);
print(`✓ Repository cloned successfully to ${repo.path()}`);
// Test GitRepo.pull()
print("Testing GitRepo.pull()...");
try {
let pulled_repo = repo.pull();
print("✓ GitRepo.pull(): Pull operation completed successfully");
} catch(err) {
// Pull might fail if there are no changes or network issues
print(`Note: GitRepo.pull() failed (expected): ${err}`);
print("✓ GitRepo.pull(): Method exists and can be called");
}
// Test GitRepo.reset()
print("Testing GitRepo.reset()...");
try {
let reset_repo = repo.reset();
print("✓ GitRepo.reset(): Reset operation completed successfully");
} catch(err) {
print(`Error in GitRepo.reset(): ${err}`);
throw err;
}
// Note: We won't test commit and push as they would modify the remote repository
// Instead, we'll just verify that the methods exist and can be called
print("Note: Not testing commit and push to avoid modifying remote repositories");
print("✓ GitRepo.commit() and GitRepo.push() methods exist");
// Clean up
print("Cleaning up...");
delete(test_dir);
assert_true(!exist(test_dir), "Directory deletion failed");
print(`✓ Cleanup: Directory ${test_dir} removed`);
print("All Git operations tests completed successfully!");

View File

@@ -0,0 +1,151 @@
// run_all_tests.rhai
// Test runner for all Git module tests
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Test counters
let passed = 0;
let failed = 0;
print("=== Git Module Test Suite ===");
print("Running comprehensive tests for Git module functionality...");
// Test 1: Basic Git Operations
print("\n--- Running Basic Git Operations Tests ---");
try {
// Create a temporary directory for Git operations
let test_dir = "rhai_test_git";
mkdir(test_dir);
print(`Created test directory: ${test_dir}`);
// Test GitTree constructor
print("Testing GitTree constructor...");
let git_tree = git_tree_new(test_dir);
print("✓ GitTree created successfully");
// Test GitTree.list() with empty directory
print("Testing GitTree.list() with empty directory...");
let repos = git_tree.list();
assert_true(repos.len() == 0, "Expected empty list of repositories");
print(`✓ GitTree.list(): Found ${repos.len()} repositories (expected 0)`);
// Test GitTree.find() with empty directory
print("Testing GitTree.find() with empty directory...");
let found_repos = git_tree.find("*");
assert_true(found_repos.len() == 0, "Expected empty list of repositories");
print(`✓ GitTree.find(): Found ${found_repos.len()} repositories (expected 0)`);
// Clean up
print("Cleaning up...");
delete(test_dir);
assert_true(!exist(test_dir), "Directory deletion failed");
print(`✓ Cleanup: Directory ${test_dir} removed`);
print("--- Basic Git Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Basic Git Operations Tests: ${err}`);
failed += 1;
}
// Test 2: Git Repository Operations
print("\n--- Running Git Repository Operations Tests ---");
try {
// Create a temporary directory for Git operations
let test_dir = "rhai_test_git_ops";
mkdir(test_dir);
print(`Created test directory: ${test_dir}`);
// Create a GitTree
print("Creating GitTree...");
let git_tree = git_tree_new(test_dir);
print("✓ GitTree created successfully");
// Clean up
print("Cleaning up...");
delete(test_dir);
assert_true(!exist(test_dir), "Directory deletion failed");
print(`✓ Cleanup: Directory ${test_dir} removed`);
print("--- Git Repository Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Git Repository Operations Tests: ${err}`);
failed += 1;
}
// Test 3: Git Error Handling and Real Functionality
print("\n--- Running Git Error Handling and Real Functionality Tests ---");
try {
print("Testing git_clone with invalid URL...");
try {
git_clone("invalid-url-format");
print("!!! Expected error but got success");
failed += 1;
} catch(err) {
assert_true(err.contains("Git error"), "Expected Git error message");
print("✓ git_clone properly handles invalid URLs");
}
print("Testing git_clone with real repository...");
try {
let repo = git_clone("https://github.com/octocat/Hello-World.git");
let path = repo.path();
assert_true(path.len() > 0, "Repository path should not be empty");
print(`✓ git_clone successfully cloned repository to: ${path}`);
// Test repository operations
print("Testing repository operations...");
let has_changes = repo.has_changes();
print(`✓ Repository has_changes check: ${has_changes}`);
} catch(err) {
// Network issues or git not available are acceptable failures
if err.contains("Git error") || err.contains("command") || err.contains("Failed to clone") {
print(`Note: git_clone test skipped due to environment: ${err}`);
} else {
print(`!!! Unexpected error in git_clone: ${err}`);
failed += 1;
}
}
print("Testing GitTree with invalid path...");
try {
let git_tree = git_tree_new("/invalid/nonexistent/path");
print("Note: GitTree creation succeeded (directory was created)");
// Clean up if it was created
try {
delete("/invalid");
} catch(cleanup_err) {
// Ignore cleanup errors
}
} catch(err) {
print(`✓ GitTree properly handles invalid paths: ${err}`);
}
print("--- Git Error Handling Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Git Error Handling Tests: ${err}`);
failed += 1;
}
// Summary
print("\n=== Test Results ===");
print(`Passed: ${passed}`);
print(`Failed: ${failed}`);
print(`Total: ${passed + failed}`);
if failed == 0 {
print("🎉 All tests passed!");
} else {
print("❌ Some tests failed!");
}
print("=== Git Module Test Suite Complete ===");

View File

@@ -0,0 +1,121 @@
use rhai::Engine;
use sal_git::rhai::*;
#[test]
fn test_git_clone_with_various_url_formats() {
let mut engine = Engine::new();
register_git_module(&mut engine).unwrap();
let test_cases = vec![
(
"https://github.com/octocat/Hello-World.git",
"HTTPS with .git",
),
(
"https://github.com/octocat/Hello-World",
"HTTPS without .git",
),
// SSH would require key setup: ("git@github.com:octocat/Hello-World.git", "SSH format"),
];
for (url, description) in test_cases {
let script = format!(
r#"
let result = "";
try {{
let repo = git_clone("{}");
let path = repo.path();
if path.len() > 0 {{
result = "success";
}} else {{
result = "no_path";
}}
}} catch(e) {{
if e.contains("Git error") {{
result = "git_error";
}} else {{
result = "unexpected_error";
}}
}}
result
"#,
url
);
let result = engine.eval::<String>(&script);
assert!(
result.is_ok(),
"Failed to execute script for {}: {:?}",
description,
result
);
let outcome = result.unwrap();
// Accept success or git_error (network issues)
assert!(
outcome == "success" || outcome == "git_error",
"Unexpected outcome for {}: {}",
description,
outcome
);
}
}
#[test]
fn test_git_tree_operations_comprehensive() {
let mut engine = Engine::new();
register_git_module(&mut engine).unwrap();
let script = r#"
let results = [];
try {
// Test GitTree creation
let git_tree = git_tree_new("/tmp/rhai_comprehensive_test");
results.push("git_tree_created");
// Test list on empty directory
let repos = git_tree.list();
results.push("list_executed");
// Test find with pattern
let found = git_tree.find("nonexistent");
results.push("find_executed");
} catch(e) {
results.push("error_occurred");
}
results.len()
"#;
let result = engine.eval::<i64>(&script);
assert!(result.is_ok());
assert!(result.unwrap() >= 3, "Should execute at least 3 operations");
}
#[test]
fn test_error_message_quality() {
let mut engine = Engine::new();
register_git_module(&mut engine).unwrap();
let script = r#"
let error_msg = "";
try {
git_clone("invalid-url-format");
} catch(e) {
error_msg = e;
}
error_msg
"#;
let result = engine.eval::<String>(&script);
assert!(result.is_ok());
let error_msg = result.unwrap();
assert!(
error_msg.contains("Git error"),
"Error should contain 'Git error'"
);
assert!(error_msg.len() > 10, "Error message should be descriptive");
}

View File

@@ -0,0 +1,101 @@
use rhai::Engine;
use sal_git::rhai::*;
#[test]
fn test_register_git_module() {
let mut engine = Engine::new();
let result = register_git_module(&mut engine);
assert!(result.is_ok());
}
#[test]
fn test_git_tree_new_function_registered() {
let mut engine = Engine::new();
register_git_module(&mut engine).unwrap();
// Test that the function is registered by trying to call it
// This will fail because /nonexistent doesn't exist, but it proves the function is registered
let result = engine.eval::<String>(
r#"
let result = "";
try {
let git_tree = git_tree_new("/nonexistent");
result = "success";
} catch(e) {
result = "error_caught";
}
result
"#,
);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "error_caught");
}
#[test]
fn test_git_clone_function_registered() {
let mut engine = Engine::new();
register_git_module(&mut engine).unwrap();
// Test that git_clone function is registered by testing with invalid URL
let result = engine.eval::<String>(
r#"
let result = "";
try {
git_clone("invalid-url-format");
result = "unexpected_success";
} catch(e) {
// Should catch error for invalid URL
if e.contains("Git error") {
result = "error_caught_correctly";
} else {
result = "wrong_error_type";
}
}
result
"#,
);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "error_caught_correctly");
}
#[test]
fn test_git_clone_with_valid_public_repo() {
let mut engine = Engine::new();
register_git_module(&mut engine).unwrap();
// Test with a real public repository (small one for testing)
let result = engine.eval::<String>(
r#"
let result = "";
try {
let repo = git_clone("https://github.com/octocat/Hello-World.git");
// If successful, repo should have a valid path
let path = repo.path();
if path.len() > 0 {
result = "clone_successful";
} else {
result = "clone_failed_no_path";
}
} catch(e) {
// Network issues or git not available are acceptable failures
if e.contains("Git error") || e.contains("command") {
result = "acceptable_failure";
} else {
result = "unexpected_error";
}
}
result
"#,
);
assert!(result.is_ok());
let outcome = result.unwrap();
// Accept either successful clone or acceptable failure (network/git issues)
assert!(
outcome == "clone_successful" || outcome == "acceptable_failure",
"Unexpected outcome: {}",
outcome
);
}

View File

@@ -0,0 +1,57 @@
[package]
name = "sal-kubernetes"
version = "0.1.0"
edition = "2021"
authors = ["PlanetFirst <info@incubaid.com>"]
description = "SAL Kubernetes - Kubernetes cluster management and operations using kube-rs SDK"
repository = "https://git.threefold.info/herocode/sal"
license = "Apache-2.0"
keywords = ["kubernetes", "k8s", "cluster", "container", "orchestration"]
categories = ["api-bindings", "development-tools"]
[dependencies]
# Kubernetes client library
kube = { version = "0.95.0", features = ["client", "config", "derive"] }
k8s-openapi = { version = "0.23.0", features = ["latest"] }
# Async runtime
tokio = { version = "1.45.0", features = ["full"] }
# Production safety features
tokio-retry = "0.3.0"
governor = "0.6.3"
tower = { version = "0.5.2", features = ["timeout", "limit"] }
# Error handling
thiserror = "2.0.12"
anyhow = "1.0.98"
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"
# Regular expressions for pattern matching
regex = "1.10.2"
# Logging
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"] }
# Base64 encoding for secrets
base64 = "0.22.1"
[dev-dependencies]
tempfile = "3.5"
tokio-test = "0.4.4"
env_logger = "0.11.5"
[features]
default = ["rhai"]
rhai = ["dep:rhai"]

View File

@@ -0,0 +1,443 @@
# 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
- **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 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<dyn std::error::Error>> {
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
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");
// 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");
namespace_create(km, "my-namespace");
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
#### 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
- `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
- `configmap_delete(name)` - Delete a specific configmap by name
- `secret_delete(name)` - Delete a specific secret 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:
**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
- `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/clusters/` directory contains comprehensive examples:
### Rust Examples
Run with: `cargo run --example <name> --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/<script>.rhai`
- `postgres.rhai` - PostgreSQL cluster deployment script
- `redis.rhai` - Redis cluster deployment script
### Real-World Examples
#### PostgreSQL Database
```rust
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());
km.deploy_application("postgres", "postgres:15", 1, 5432, Some(labels), Some(env_vars)).await?;
```
#### Redis Cache
```rust
let mut env_vars = HashMap::new();
env_vars.insert("REDIS_PASSWORD".to_string(), "redispassword".to_string());
env_vars.insert("REDIS_MAXMEMORY".to_string(), "256mb".to_string());
km.deploy_application("redis", "redis:7-alpine", 3, 6379, None, Some(env_vars)).await?;
```
## Testing
### Test Coverage
The module includes comprehensive test coverage:
- **Unit Tests**: Core functionality without cluster dependency
- **Integration Tests**: Real Kubernetes cluster operations
- **Environment Variables Tests**: Complete env var functionality testing
- **Edge Cases Tests**: Error handling and boundary conditions
- **Rhai Integration Tests**: Scripting environment testing
- **Production Readiness Tests**: Concurrent operations and error handling
### Running Tests
```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
# Run specific test suites
cargo test --package sal-kubernetes deployment_env_vars_test
cargo test --package sal-kubernetes edge_cases_test
# Rhai environment variables test
KUBERNETES_TEST_ENABLED=1 ./target/debug/herodo kubernetes/tests/rhai/env_vars_test.rhai
```
### Test Requirements
- **Kubernetes Cluster**: Integration tests require a running Kubernetes cluster
- **Environment Variable**: Set `KUBERNETES_TEST_ENABLED=1` to enable integration tests
- **Permissions**: Tests require permissions to create/delete resources in the `default` namespace
## Production Considerations
### Security
- 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
- Use Kubernetes secrets for sensitive environment variables instead of plain text
### Performance & Scalability
- Consider adding resource limits (CPU/memory) for production deployments
- Use persistent volumes for stateful applications
- Configure readiness and liveness probes for health checks
- Implement proper monitoring and logging labels
### Environment Variables Best Practices
- Use Kubernetes secrets for sensitive data (passwords, API keys)
- Validate environment variable values before deployment
- Use consistent naming conventions (e.g., `DATABASE_URL`, `API_KEY`)
- Document required vs optional environment variables
### Example: Production-Ready Deployment
```rust
// Production labels for monitoring and management
let mut labels = HashMap::new();
labels.insert("app".to_string(), "web-api".to_string());
labels.insert("version".to_string(), "v1.2.3".to_string());
labels.insert("environment".to_string(), "production".to_string());
labels.insert("team".to_string(), "backend".to_string());
// Non-sensitive environment variables
let mut env_vars = HashMap::new();
env_vars.insert("NODE_ENV".to_string(), "production".to_string());
env_vars.insert("LOG_LEVEL".to_string(), "info".to_string());
env_vars.insert("PORT".to_string(), "3000".to_string());
// Note: Use Kubernetes secrets for DATABASE_URL, API_KEY, etc.
km.deploy_application("web-api", "myapp:v1.2.3", 3, 3000, Some(labels), Some(env_vars)).await?;
```

View File

@@ -0,0 +1,113 @@
//! Configuration for production safety features
use std::time::Duration;
/// Configuration for Kubernetes operations with production safety features
#[derive(Debug, Clone)]
pub struct KubernetesConfig {
/// Timeout for individual API operations
pub operation_timeout: Duration,
/// Maximum number of retry attempts for failed operations
pub max_retries: u32,
/// Base delay for exponential backoff retry strategy
pub retry_base_delay: Duration,
/// Maximum delay between retries
pub retry_max_delay: Duration,
/// Rate limiting: maximum requests per second
pub rate_limit_rps: u32,
/// Rate limiting: burst capacity
pub rate_limit_burst: u32,
}
impl Default for KubernetesConfig {
fn default() -> Self {
Self {
// Conservative timeout for production
operation_timeout: Duration::from_secs(30),
// Reasonable retry attempts
max_retries: 3,
// Exponential backoff starting at 1 second
retry_base_delay: Duration::from_secs(1),
// Maximum 30 seconds between retries
retry_max_delay: Duration::from_secs(30),
// Conservative rate limiting: 10 requests per second
rate_limit_rps: 10,
// Allow small bursts
rate_limit_burst: 20,
}
}
}
impl KubernetesConfig {
/// Create a new configuration with custom settings
pub fn new() -> Self {
Self::default()
}
/// Set operation timeout
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.operation_timeout = timeout;
self
}
/// Set retry configuration
pub fn with_retries(mut self, max_retries: u32, base_delay: Duration, max_delay: Duration) -> Self {
self.max_retries = max_retries;
self.retry_base_delay = base_delay;
self.retry_max_delay = max_delay;
self
}
/// Set rate limiting configuration
pub fn with_rate_limit(mut self, rps: u32, burst: u32) -> Self {
self.rate_limit_rps = rps;
self.rate_limit_burst = burst;
self
}
/// Create configuration optimized for high-throughput environments
pub fn high_throughput() -> Self {
Self {
operation_timeout: Duration::from_secs(60),
max_retries: 5,
retry_base_delay: Duration::from_millis(500),
retry_max_delay: Duration::from_secs(60),
rate_limit_rps: 50,
rate_limit_burst: 100,
}
}
/// Create configuration optimized for low-latency environments
pub fn low_latency() -> Self {
Self {
operation_timeout: Duration::from_secs(10),
max_retries: 2,
retry_base_delay: Duration::from_millis(100),
retry_max_delay: Duration::from_secs(5),
rate_limit_rps: 20,
rate_limit_burst: 40,
}
}
/// Create configuration for development/testing
pub fn development() -> Self {
Self {
operation_timeout: Duration::from_secs(120),
max_retries: 1,
retry_base_delay: Duration::from_millis(100),
retry_max_delay: Duration::from_secs(2),
rate_limit_rps: 100,
rate_limit_burst: 200,
}
}
}

View File

@@ -0,0 +1,85 @@
//! Error types for SAL Kubernetes operations
use thiserror::Error;
/// Errors that can occur during Kubernetes operations
#[derive(Error, Debug)]
pub enum KubernetesError {
/// Kubernetes API client error
#[error("Kubernetes API error: {0}")]
ApiError(#[from] kube::Error),
/// Configuration error
#[error("Configuration error: {0}")]
ConfigError(String),
/// Resource not found error
#[error("Resource not found: {0}")]
ResourceNotFound(String),
/// Invalid resource name or pattern
#[error("Invalid resource name or pattern: {0}")]
InvalidResourceName(String),
/// Regular expression error
#[error("Regular expression error: {0}")]
RegexError(#[from] regex::Error),
/// Serialization/deserialization error
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
/// YAML parsing error
#[error("YAML error: {0}")]
YamlError(#[from] serde_yaml::Error),
/// Generic operation error
#[error("Operation failed: {0}")]
OperationError(String),
/// Namespace error
#[error("Namespace error: {0}")]
NamespaceError(String),
/// Permission denied error
#[error("Permission denied: {0}")]
PermissionDenied(String),
/// Timeout error
#[error("Operation timed out: {0}")]
Timeout(String),
/// Generic error wrapper
#[error("Generic error: {0}")]
Generic(#[from] anyhow::Error),
}
impl KubernetesError {
/// Create a new configuration error
pub fn config_error(msg: impl Into<String>) -> Self {
Self::ConfigError(msg.into())
}
/// Create a new operation error
pub fn operation_error(msg: impl Into<String>) -> Self {
Self::OperationError(msg.into())
}
/// Create a new namespace error
pub fn namespace_error(msg: impl Into<String>) -> Self {
Self::NamespaceError(msg.into())
}
/// Create a new permission denied error
pub fn permission_denied(msg: impl Into<String>) -> Self {
Self::PermissionDenied(msg.into())
}
/// Create a new timeout error
pub fn timeout(msg: impl Into<String>) -> Self {
Self::Timeout(msg.into())
}
}
/// Result type for Kubernetes operations
pub type KubernetesResult<T> = Result<T, KubernetesError>;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
//! SAL Kubernetes: Kubernetes cluster management and operations
//!
//! This package provides Kubernetes cluster management functionality including:
//! - Namespace-scoped resource management via KubernetesManager
//! - Pod listing and management
//! - Resource deletion with PCRE pattern matching
//! - Namespace creation and management
//! - Support for various Kubernetes resources (pods, services, deployments, etc.)
//!
//! # Example
//!
//! ```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 (idempotent)
//! km.namespace_create("my-namespace").await?;
//!
//! // Delete resources matching a pattern
//! km.delete("test-.*").await?;
//!
//! Ok(())
//! }
//! ```
pub mod config;
pub mod error;
pub mod kubernetes_manager;
// Rhai integration module
#[cfg(feature = "rhai")]
pub mod rhai;
// Re-export main types for convenience
pub use config::KubernetesConfig;
pub use error::KubernetesError;
pub use kubernetes_manager::KubernetesManager;
// Re-export commonly used Kubernetes types
pub use k8s_openapi::api::apps::v1::{Deployment, ReplicaSet};
pub use k8s_openapi::api::core::v1::{Namespace, Pod, Service};

View File

@@ -0,0 +1,729 @@
//! Rhai wrappers for Kubernetes module functions
//!
//! This module provides Rhai wrappers for the functions in the Kubernetes module,
//! enabling scripting access to Kubernetes operations.
use crate::{KubernetesError, KubernetesManager};
use once_cell::sync::Lazy;
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
use std::sync::Mutex;
use tokio::runtime::Runtime;
// Global Tokio runtime for blocking async operations
static RUNTIME: Lazy<Mutex<Runtime>> =
Lazy::new(|| Mutex::new(Runtime::new().expect("Failed to create Tokio runtime")));
/// Helper function to convert Rhai Map to HashMap for environment variables
///
/// # Arguments
///
/// * `rhai_map` - Rhai Map containing key-value pairs
///
/// # Returns
///
/// * `Option<std::collections::HashMap<String, String>>` - Converted HashMap or None if empty
fn convert_rhai_map_to_env_vars(
rhai_map: Map,
) -> Option<std::collections::HashMap<String, String>> {
if rhai_map.is_empty() {
None
} else {
Some(
rhai_map
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
)
}
}
/// Helper function to execute async operations with proper runtime handling
///
/// This uses a global runtime to ensure consistent async execution
fn execute_async<F, T>(future: F) -> Result<T, Box<EvalAltResult>>
where
F: std::future::Future<Output = Result<T, KubernetesError>>,
{
// Get the global runtime
let rt = match RUNTIME.lock() {
Ok(rt) => rt,
Err(e) => {
return Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to acquire runtime lock: {e}").into(),
rhai::Position::NONE,
)));
}
};
// Execute the future in a blocking manner
rt.block_on(future).map_err(kubernetes_error_to_rhai_error)
}
/// Create a new KubernetesManager for the specified namespace
///
/// # Arguments
///
/// * `namespace` - The Kubernetes namespace to operate on
///
/// # Returns
///
/// * `Result<KubernetesManager, Box<EvalAltResult>>` - The manager instance or an error
fn kubernetes_manager_new(namespace: String) -> Result<KubernetesManager, Box<EvalAltResult>> {
execute_async(KubernetesManager::new(namespace))
}
/// List all pods in the namespace
///
/// # Arguments
///
/// * `km` - The KubernetesManager instance
///
/// # Returns
///
/// * `Result<Array, Box<EvalAltResult>>` - Array of pod names or an error
fn pods_list(km: &mut KubernetesManager) -> Result<Array, Box<EvalAltResult>> {
let pods = execute_async(km.pods_list())?;
let pod_names: Array = pods
.iter()
.filter_map(|pod| pod.metadata.name.as_ref())
.map(|name| Dynamic::from(name.clone()))
.collect();
Ok(pod_names)
}
/// List all services in the namespace
///
/// # Arguments
///
/// * `km` - The KubernetesManager instance
///
/// # Returns
///
/// * `Result<Array, Box<EvalAltResult>>` - Array of service names or an error
fn services_list(km: &mut KubernetesManager) -> Result<Array, Box<EvalAltResult>> {
let services = execute_async(km.services_list())?;
let service_names: Array = services
.iter()
.filter_map(|service| service.metadata.name.as_ref())
.map(|name| Dynamic::from(name.clone()))
.collect();
Ok(service_names)
}
/// List all deployments in the namespace
///
/// # Arguments
///
/// * `km` - The KubernetesManager instance
///
/// # Returns
///
/// * `Result<Array, Box<EvalAltResult>>` - Array of deployment names or an error
fn deployments_list(km: &mut KubernetesManager) -> Result<Array, Box<EvalAltResult>> {
let deployments = execute_async(km.deployments_list())?;
let deployment_names: Array = deployments
.iter()
.filter_map(|deployment| deployment.metadata.name.as_ref())
.map(|name| Dynamic::from(name.clone()))
.collect();
Ok(deployment_names)
}
/// List all configmaps in the namespace
///
/// # Arguments
///
/// * `km` - The KubernetesManager instance
///
/// # Returns
///
/// * `Result<Array, Box<EvalAltResult>>` - Array of configmap names or an error
fn configmaps_list(km: &mut KubernetesManager) -> Result<Array, Box<EvalAltResult>> {
let configmaps = execute_async(km.configmaps_list())?;
let configmap_names: Array = configmaps
.iter()
.filter_map(|configmap| configmap.metadata.name.as_ref())
.map(|name| Dynamic::from(name.clone()))
.collect();
Ok(configmap_names)
}
/// List all secrets in the namespace
///
/// # Arguments
///
/// * `km` - The KubernetesManager instance
///
/// # Returns
///
/// * `Result<Array, Box<EvalAltResult>>` - Array of secret names or an error
fn secrets_list(km: &mut KubernetesManager) -> Result<Array, Box<EvalAltResult>> {
let secrets = execute_async(km.secrets_list())?;
let secret_names: Array = secrets
.iter()
.filter_map(|secret| secret.metadata.name.as_ref())
.map(|name| Dynamic::from(name.clone()))
.collect();
Ok(secret_names)
}
/// Delete resources matching a PCRE pattern
///
/// # Arguments
///
/// * `km` - The KubernetesManager instance
/// * `pattern` - PCRE pattern to match resource names against
///
/// # Returns
///
/// * `Result<i64, Box<EvalAltResult>>` - Number of resources deleted or an error
///
/// Create a pod with a single container (backward compatible version)
///
/// # Arguments
///
/// * `km` - Mutable reference to KubernetesManager
/// * `name` - Name of the pod
/// * `image` - Container image to use
/// * `labels` - Optional labels as a Map
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - Pod name or an error
fn pod_create(
km: &mut KubernetesManager,
name: String,
image: String,
labels: Map,
) -> Result<String, Box<EvalAltResult>> {
let labels_map: Option<std::collections::HashMap<String, String>> = if labels.is_empty() {
None
} else {
Some(
labels
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
)
};
let pod = execute_async(km.pod_create(&name, &image, labels_map, None))?;
Ok(pod.metadata.name.unwrap_or(name))
}
/// Create a pod with a single container and environment variables
///
/// # Arguments
///
/// * `km` - Mutable reference to KubernetesManager
/// * `name` - Name of the pod
/// * `image` - Container image to use
/// * `labels` - Optional labels as a Map
/// * `env_vars` - Optional environment variables as a Map
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - Pod name or an error
fn pod_create_with_env(
km: &mut KubernetesManager,
name: String,
image: String,
labels: Map,
env_vars: Map,
) -> Result<String, Box<EvalAltResult>> {
let labels_map: Option<std::collections::HashMap<String, String>> = if labels.is_empty() {
None
} else {
Some(
labels
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
)
};
let env_vars_map = convert_rhai_map_to_env_vars(env_vars);
let pod = execute_async(km.pod_create(&name, &image, labels_map, env_vars_map))?;
Ok(pod.metadata.name.unwrap_or(name))
}
/// Create a service
///
/// # Arguments
///
/// * `km` - Mutable reference to KubernetesManager
/// * `name` - Name of the service
/// * `selector` - Labels to select pods as a Map
/// * `port` - Port to expose
/// * `target_port` - Target port on pods (optional, defaults to port)
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - Service name or an error
fn service_create(
km: &mut KubernetesManager,
name: String,
selector: Map,
port: i64,
target_port: i64,
) -> Result<String, Box<EvalAltResult>> {
let selector_map: std::collections::HashMap<String, String> = selector
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let target_port_opt = if target_port == 0 {
None
} else {
Some(target_port as i32)
};
let service =
execute_async(km.service_create(&name, selector_map, port as i32, target_port_opt))?;
Ok(service.metadata.name.unwrap_or(name))
}
/// Create a deployment
///
/// # Arguments
///
/// * `km` - Mutable reference to KubernetesManager
/// * `name` - Name of the deployment
/// * `image` - Container image to use
/// * `replicas` - Number of replicas
/// * `labels` - Optional labels as a Map
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - Deployment name or an error
fn deployment_create(
km: &mut KubernetesManager,
name: String,
image: String,
replicas: i64,
labels: Map,
env_vars: Map,
) -> Result<String, Box<EvalAltResult>> {
let labels_map: Option<std::collections::HashMap<String, String>> = if labels.is_empty() {
None
} else {
Some(
labels
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
)
};
let env_vars_map = convert_rhai_map_to_env_vars(env_vars);
let deployment = execute_async(km.deployment_create(
&name,
&image,
replicas as i32,
labels_map,
env_vars_map,
))?;
Ok(deployment.metadata.name.unwrap_or(name))
}
/// Create a ConfigMap
///
/// # Arguments
///
/// * `km` - Mutable reference to KubernetesManager
/// * `name` - Name of the ConfigMap
/// * `data` - Data as a Map
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - ConfigMap name or an error
fn configmap_create(
km: &mut KubernetesManager,
name: String,
data: Map,
) -> Result<String, Box<EvalAltResult>> {
let data_map: std::collections::HashMap<String, String> = data
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let configmap = execute_async(km.configmap_create(&name, data_map))?;
Ok(configmap.metadata.name.unwrap_or(name))
}
/// Create a Secret
///
/// # Arguments
///
/// * `km` - Mutable reference to KubernetesManager
/// * `name` - Name of the Secret
/// * `data` - Data as a Map (will be base64 encoded)
/// * `secret_type` - Type of secret (optional, defaults to "Opaque")
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - Secret name or an error
fn secret_create(
km: &mut KubernetesManager,
name: String,
data: Map,
secret_type: String,
) -> Result<String, Box<EvalAltResult>> {
let data_map: std::collections::HashMap<String, String> = data
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let secret_type_opt = if secret_type.is_empty() {
None
} else {
Some(secret_type.as_str())
};
let secret = execute_async(km.secret_create(&name, data_map, secret_type_opt))?;
Ok(secret.metadata.name.unwrap_or(name))
}
/// Get a pod by name
///
/// # Arguments
///
/// * `km` - Mutable reference to KubernetesManager
/// * `name` - Name of the pod to get
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - Pod name or an error
fn pod_get(km: &mut KubernetesManager, name: String) -> Result<String, Box<EvalAltResult>> {
let pod = execute_async(km.pod_get(&name))?;
Ok(pod.metadata.name.unwrap_or(name))
}
/// Get a service by name
///
/// # Arguments
///
/// * `km` - Mutable reference to KubernetesManager
/// * `name` - Name of the service to get
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - Service name or an error
fn service_get(km: &mut KubernetesManager, name: String) -> Result<String, Box<EvalAltResult>> {
let service = execute_async(km.service_get(&name))?;
Ok(service.metadata.name.unwrap_or(name))
}
/// Get a deployment by name
///
/// # Arguments
///
/// * `km` - Mutable reference to KubernetesManager
/// * `name` - Name of the deployment to get
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - Deployment name or an error
fn deployment_get(km: &mut KubernetesManager, name: String) -> Result<String, Box<EvalAltResult>> {
let deployment = execute_async(km.deployment_get(&name))?;
Ok(deployment.metadata.name.unwrap_or(name))
}
fn delete(km: &mut KubernetesManager, pattern: String) -> Result<i64, Box<EvalAltResult>> {
let deleted_count = execute_async(km.delete(&pattern))?;
Ok(deleted_count as i64)
}
/// Create a namespace (idempotent operation)
///
/// # Arguments
///
/// * `km` - The KubernetesManager instance
/// * `name` - The name of the namespace to create
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Success or an error
fn namespace_create(km: &mut KubernetesManager, name: String) -> Result<(), Box<EvalAltResult>> {
execute_async(km.namespace_create(&name))
}
/// Delete a namespace (destructive operation)
///
/// # Arguments
///
/// * `km` - Mutable reference to KubernetesManager
/// * `name` - Name of the namespace to delete
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Success or an error
fn namespace_delete(km: &mut KubernetesManager, name: String) -> Result<(), Box<EvalAltResult>> {
execute_async(km.namespace_delete(&name))
}
/// Check if a namespace exists
///
/// # Arguments
///
/// * `km` - The KubernetesManager instance
/// * `name` - The name of the namespace to check
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - True if namespace exists, false otherwise
fn namespace_exists(km: &mut KubernetesManager, name: String) -> Result<bool, Box<EvalAltResult>> {
execute_async(km.namespace_exists(&name))
}
/// List all namespaces
///
/// # Arguments
///
/// * `km` - The KubernetesManager instance
///
/// # Returns
///
/// * `Result<Array, Box<EvalAltResult>>` - Array of namespace names or an error
fn namespaces_list(km: &mut KubernetesManager) -> Result<Array, Box<EvalAltResult>> {
let namespaces = execute_async(km.namespaces_list())?;
let namespace_names: Array = namespaces
.iter()
.filter_map(|ns| ns.metadata.name.as_ref())
.map(|name| Dynamic::from(name.clone()))
.collect();
Ok(namespace_names)
}
/// Get resource counts for the namespace
///
/// # Arguments
///
/// * `km` - The KubernetesManager instance
///
/// # Returns
///
/// * `Result<Map, Box<EvalAltResult>>` - Map of resource counts by type or an error
fn resource_counts(km: &mut KubernetesManager) -> Result<Map, Box<EvalAltResult>> {
let counts = execute_async(km.resource_counts())?;
let mut rhai_map = Map::new();
for (key, value) in counts {
rhai_map.insert(key.into(), Dynamic::from(value as i64));
}
Ok(rhai_map)
}
/// Deploy a complete application with deployment and service
///
/// # Arguments
///
/// * `km` - Mutable reference to KubernetesManager
/// * `name` - Name of the application
/// * `image` - Container image to use
/// * `replicas` - Number of replicas
/// * `port` - Port the application listens on
/// * `labels` - Optional labels as a Map
/// * `env_vars` - Optional environment variables as a Map
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - Success message or an error
fn deploy_application(
km: &mut KubernetesManager,
name: String,
image: String,
replicas: i64,
port: i64,
labels: Map,
env_vars: Map,
) -> Result<String, Box<EvalAltResult>> {
let labels_map: Option<std::collections::HashMap<String, String>> = if labels.is_empty() {
None
} else {
Some(
labels
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
)
};
let env_vars_map = convert_rhai_map_to_env_vars(env_vars);
execute_async(km.deploy_application(
&name,
&image,
replicas as i32,
port as i32,
labels_map,
env_vars_map,
))?;
Ok(format!("Successfully deployed application '{name}'"))
}
/// Delete a specific pod by name
///
/// # Arguments
///
/// * `km` - The KubernetesManager instance
/// * `name` - The name of the pod to delete
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Success or an error
fn pod_delete(km: &mut KubernetesManager, name: String) -> Result<(), Box<EvalAltResult>> {
execute_async(km.pod_delete(&name))
}
/// Delete a specific service by name
///
/// # Arguments
///
/// * `km` - The KubernetesManager instance
/// * `name` - The name of the service to delete
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Success or an error
fn service_delete(km: &mut KubernetesManager, name: String) -> Result<(), Box<EvalAltResult>> {
execute_async(km.service_delete(&name))
}
/// Delete a specific deployment by name
///
/// # Arguments
///
/// * `km` - The KubernetesManager instance
/// * `name` - The name of the deployment to delete
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Success or an error
fn deployment_delete(km: &mut KubernetesManager, name: String) -> Result<(), Box<EvalAltResult>> {
execute_async(km.deployment_delete(&name))
}
/// Delete a ConfigMap by name
///
/// # Arguments
///
/// * `km` - Mutable reference to KubernetesManager
/// * `name` - Name of the ConfigMap to delete
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Success or an error
fn configmap_delete(km: &mut KubernetesManager, name: String) -> Result<(), Box<EvalAltResult>> {
execute_async(km.configmap_delete(&name))
}
/// Delete a Secret by name
///
/// # Arguments
///
/// * `km` - Mutable reference to KubernetesManager
/// * `name` - Name of the Secret to delete
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Success or an error
fn secret_delete(km: &mut KubernetesManager, name: String) -> Result<(), Box<EvalAltResult>> {
execute_async(km.secret_delete(&name))
}
/// Get the namespace this manager operates on
///
/// # Arguments
///
/// * `km` - The KubernetesManager instance
///
/// # Returns
///
/// * `String` - The namespace name
fn kubernetes_manager_namespace(km: &mut KubernetesManager) -> String {
km.namespace().to_string()
}
/// Register Kubernetes module functions with the Rhai engine
///
/// # Arguments
///
/// * `engine` - The Rhai engine to register the functions with
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_kubernetes_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register KubernetesManager type
engine.register_type::<KubernetesManager>();
// Register KubernetesManager constructor and methods
engine.register_fn("kubernetes_manager_new", kubernetes_manager_new);
engine.register_fn("namespace", kubernetes_manager_namespace);
// Register resource listing functions
engine.register_fn("pods_list", pods_list);
engine.register_fn("services_list", services_list);
engine.register_fn("deployments_list", deployments_list);
engine.register_fn("configmaps_list", configmaps_list);
engine.register_fn("secrets_list", secrets_list);
engine.register_fn("namespaces_list", namespaces_list);
// Register resource creation methods (object-oriented style)
engine.register_fn("create_pod", pod_create);
engine.register_fn("create_pod_with_env", pod_create_with_env);
engine.register_fn("create_service", service_create);
engine.register_fn("create_deployment", deployment_create);
engine.register_fn("create_configmap", configmap_create);
engine.register_fn("create_secret", secret_create);
// Register resource get methods
engine.register_fn("get_pod", pod_get);
engine.register_fn("get_service", service_get);
engine.register_fn("get_deployment", deployment_get);
// Register resource management methods
engine.register_fn("delete", delete);
engine.register_fn("delete_pod", pod_delete);
engine.register_fn("delete_service", service_delete);
engine.register_fn("delete_deployment", deployment_delete);
engine.register_fn("delete_configmap", configmap_delete);
engine.register_fn("delete_secret", secret_delete);
// Register namespace methods (object-oriented style)
engine.register_fn("create_namespace", namespace_create);
engine.register_fn("delete_namespace", namespace_delete);
engine.register_fn("namespace_exists", namespace_exists);
// Register utility functions
engine.register_fn("resource_counts", resource_counts);
// Register convenience functions
engine.register_fn("deploy_application", deploy_application);
Ok(())
}
// Helper function for error conversion
fn kubernetes_error_to_rhai_error(error: KubernetesError) -> Box<EvalAltResult> {
Box::new(EvalAltResult::ErrorRuntime(
format!("Kubernetes error: {error}").into(),
rhai::Position::NONE,
))
}

View File

@@ -0,0 +1,253 @@
//! CRUD operations tests for SAL Kubernetes
//!
//! These tests verify that all Create, Read, Update, Delete operations work correctly.
#[cfg(test)]
mod crud_tests {
use sal_kubernetes::KubernetesManager;
use std::collections::HashMap;
/// Check if Kubernetes integration tests should run
fn should_run_k8s_tests() -> bool {
std::env::var("KUBERNETES_TEST_ENABLED").unwrap_or_default() == "1"
}
#[tokio::test]
async fn test_complete_crud_operations() {
if !should_run_k8s_tests() {
println!("Skipping CRUD test. Set KUBERNETES_TEST_ENABLED=1 to enable.");
return;
}
println!("🔍 Testing complete CRUD operations...");
// Create a test namespace for our operations
let test_namespace = "sal-crud-test";
let km = KubernetesManager::new("default")
.await
.expect("Should connect to cluster");
// Clean up any existing test namespace
let _ = km.namespace_delete(test_namespace).await;
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
// CREATE operations
println!("\n=== CREATE Operations ===");
// 1. Create namespace
km.namespace_create(test_namespace)
.await
.expect("Should create test namespace");
println!("✅ Created namespace: {}", test_namespace);
// Switch to test namespace
let test_km = KubernetesManager::new(test_namespace)
.await
.expect("Should connect to test namespace");
// 2. Create ConfigMap
let mut config_data = HashMap::new();
config_data.insert(
"app.properties".to_string(),
"debug=true\nport=8080".to_string(),
);
config_data.insert(
"config.yaml".to_string(),
"key: value\nenv: test".to_string(),
);
let configmap = test_km
.configmap_create("test-config", config_data)
.await
.expect("Should create ConfigMap");
println!(
"✅ Created ConfigMap: {}",
configmap.metadata.name.unwrap_or_default()
);
// 3. Create Secret
let mut secret_data = HashMap::new();
secret_data.insert("username".to_string(), "testuser".to_string());
secret_data.insert("password".to_string(), "secret123".to_string());
let secret = test_km
.secret_create("test-secret", secret_data, None)
.await
.expect("Should create Secret");
println!(
"✅ Created Secret: {}",
secret.metadata.name.unwrap_or_default()
);
// 4. Create Pod
let mut pod_labels = HashMap::new();
pod_labels.insert("app".to_string(), "test-app".to_string());
pod_labels.insert("version".to_string(), "v1".to_string());
let pod = test_km
.pod_create("test-pod", "nginx:alpine", Some(pod_labels.clone()), None)
.await
.expect("Should create Pod");
println!("✅ Created Pod: {}", pod.metadata.name.unwrap_or_default());
// 5. Create Service
let service = test_km
.service_create("test-service", pod_labels.clone(), 80, Some(80))
.await
.expect("Should create Service");
println!(
"✅ Created Service: {}",
service.metadata.name.unwrap_or_default()
);
// 6. Create Deployment
let deployment = test_km
.deployment_create("test-deployment", "nginx:alpine", 2, Some(pod_labels), None)
.await
.expect("Should create Deployment");
println!(
"✅ Created Deployment: {}",
deployment.metadata.name.unwrap_or_default()
);
// READ operations
println!("\n=== READ Operations ===");
// List all resources
let pods = test_km.pods_list().await.expect("Should list pods");
println!("✅ Listed {} pods", pods.len());
let services = test_km.services_list().await.expect("Should list services");
println!("✅ Listed {} services", services.len());
let deployments = test_km
.deployments_list()
.await
.expect("Should list deployments");
println!("✅ Listed {} deployments", deployments.len());
let configmaps = test_km
.configmaps_list()
.await
.expect("Should list configmaps");
println!("✅ Listed {} configmaps", configmaps.len());
let secrets = test_km.secrets_list().await.expect("Should list secrets");
println!("✅ Listed {} secrets", secrets.len());
// Get specific resources
let pod = test_km.pod_get("test-pod").await.expect("Should get pod");
println!(
"✅ Retrieved pod: {}",
pod.metadata.name.unwrap_or_default()
);
let service = test_km
.service_get("test-service")
.await
.expect("Should get service");
println!(
"✅ Retrieved service: {}",
service.metadata.name.unwrap_or_default()
);
let deployment = test_km
.deployment_get("test-deployment")
.await
.expect("Should get deployment");
println!(
"✅ Retrieved deployment: {}",
deployment.metadata.name.unwrap_or_default()
);
// Resource counts
let counts = test_km
.resource_counts()
.await
.expect("Should get resource counts");
println!("✅ Resource counts: {:?}", counts);
// DELETE operations
println!("\n=== DELETE Operations ===");
// Delete individual resources
test_km
.pod_delete("test-pod")
.await
.expect("Should delete pod");
println!("✅ Deleted pod");
test_km
.service_delete("test-service")
.await
.expect("Should delete service");
println!("✅ Deleted service");
test_km
.deployment_delete("test-deployment")
.await
.expect("Should delete deployment");
println!("✅ Deleted deployment");
test_km
.configmap_delete("test-config")
.await
.expect("Should delete configmap");
println!("✅ Deleted configmap");
test_km
.secret_delete("test-secret")
.await
.expect("Should delete secret");
println!("✅ Deleted secret");
// Verify resources are deleted
let final_counts = test_km
.resource_counts()
.await
.expect("Should get final resource counts");
println!("✅ Final resource counts: {:?}", final_counts);
// Delete the test namespace
km.namespace_delete(test_namespace)
.await
.expect("Should delete test namespace");
println!("✅ Deleted test namespace");
println!("\n🎉 All CRUD operations completed successfully!");
}
#[tokio::test]
async fn test_error_handling_in_crud() {
if !should_run_k8s_tests() {
println!("Skipping CRUD error handling test. Set KUBERNETES_TEST_ENABLED=1 to enable.");
return;
}
println!("🔍 Testing error handling in CRUD operations...");
let km = KubernetesManager::new("default")
.await
.expect("Should connect to cluster");
// Test creating resources with invalid names
let result = km.pod_create("", "nginx", None, None).await;
assert!(result.is_err(), "Should fail with empty pod name");
println!("✅ Empty pod name properly rejected");
// Test getting non-existent resources
let result = km.pod_get("non-existent-pod").await;
assert!(result.is_err(), "Should fail to get non-existent pod");
println!("✅ Non-existent pod properly handled");
// Test deleting non-existent resources
let result = km.service_delete("non-existent-service").await;
assert!(
result.is_err(),
"Should fail to delete non-existent service"
);
println!("✅ Non-existent service deletion properly handled");
println!("✅ Error handling in CRUD operations is robust");
}
}

View File

@@ -0,0 +1,384 @@
//! Tests for deployment creation with environment variables
//!
//! These tests verify the new environment variable functionality in deployments
//! and the enhanced deploy_application method.
use sal_kubernetes::KubernetesManager;
use std::collections::HashMap;
/// Check if Kubernetes integration tests should run
fn should_run_k8s_tests() -> bool {
std::env::var("KUBERNETES_TEST_ENABLED").unwrap_or_default() == "1"
}
#[tokio::test]
async fn test_deployment_create_with_env_vars() {
if !should_run_k8s_tests() {
println!("Skipping Kubernetes integration tests. Set KUBERNETES_TEST_ENABLED=1 to enable.");
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return, // Skip if can't connect
};
// Clean up any existing test deployment
let _ = km.deployment_delete("test-env-deployment").await;
// Create deployment with environment variables
let mut labels = HashMap::new();
labels.insert("app".to_string(), "test-env-app".to_string());
labels.insert("test".to_string(), "env-vars".to_string());
let mut env_vars = HashMap::new();
env_vars.insert("TEST_VAR_1".to_string(), "value1".to_string());
env_vars.insert("TEST_VAR_2".to_string(), "value2".to_string());
env_vars.insert("NODE_ENV".to_string(), "test".to_string());
let result = km
.deployment_create(
"test-env-deployment",
"nginx:latest",
1,
Some(labels),
Some(env_vars),
)
.await;
assert!(
result.is_ok(),
"Failed to create deployment with env vars: {:?}",
result
);
// Verify the deployment was created
let deployment = km.deployment_get("test-env-deployment").await;
assert!(deployment.is_ok(), "Failed to get created deployment");
let deployment = deployment.unwrap();
// Verify environment variables are set in the container spec
if let Some(spec) = &deployment.spec {
if let Some(template) = &spec.template.spec {
if let Some(container) = template.containers.first() {
if let Some(env) = &container.env {
// Check that our environment variables are present
let env_map: HashMap<String, String> = env
.iter()
.filter_map(|e| e.value.as_ref().map(|v| (e.name.clone(), v.clone())))
.collect();
assert_eq!(env_map.get("TEST_VAR_1"), Some(&"value1".to_string()));
assert_eq!(env_map.get("TEST_VAR_2"), Some(&"value2".to_string()));
assert_eq!(env_map.get("NODE_ENV"), Some(&"test".to_string()));
} else {
panic!("No environment variables found in container spec");
}
}
}
}
// Clean up
let _ = km.deployment_delete("test-env-deployment").await;
}
#[tokio::test]
async fn test_pod_create_with_env_vars() {
if !should_run_k8s_tests() {
println!("Skipping Kubernetes integration tests. Set KUBERNETES_TEST_ENABLED=1 to enable.");
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return, // Skip if can't connect
};
// Clean up any existing test pod
let _ = km.pod_delete("test-env-pod").await;
// Create pod with environment variables
let mut env_vars = HashMap::new();
env_vars.insert("NODE_ENV".to_string(), "test".to_string());
env_vars.insert(
"DATABASE_URL".to_string(),
"postgres://localhost:5432/test".to_string(),
);
env_vars.insert("API_KEY".to_string(), "test-api-key-12345".to_string());
let mut labels = HashMap::new();
labels.insert("app".to_string(), "test-env-pod-app".to_string());
labels.insert("test".to_string(), "environment-variables".to_string());
let result = km
.pod_create("test-env-pod", "nginx:latest", Some(labels), Some(env_vars))
.await;
assert!(
result.is_ok(),
"Failed to create pod with env vars: {:?}",
result
);
if let Ok(pod) = result {
let pod_name = pod
.metadata
.name
.as_ref()
.unwrap_or(&"".to_string())
.clone();
assert_eq!(pod_name, "test-env-pod");
println!("✅ Created pod with environment variables: {}", pod_name);
// Verify the pod has the expected environment variables
if let Some(spec) = &pod.spec {
if let Some(container) = spec.containers.first() {
if let Some(env) = &container.env {
let env_names: Vec<String> = env.iter().map(|e| e.name.clone()).collect();
assert!(env_names.contains(&"NODE_ENV".to_string()));
assert!(env_names.contains(&"DATABASE_URL".to_string()));
assert!(env_names.contains(&"API_KEY".to_string()));
println!("✅ Pod has expected environment variables");
}
}
}
}
// Clean up
let _ = km.pod_delete("test-env-pod").await;
}
#[tokio::test]
async fn test_deployment_create_without_env_vars() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
// Clean up any existing test deployment
let _ = km.deployment_delete("test-no-env-deployment").await;
// Create deployment without environment variables
let mut labels = HashMap::new();
labels.insert("app".to_string(), "test-no-env-app".to_string());
let result = km
.deployment_create(
"test-no-env-deployment",
"nginx:latest",
1,
Some(labels),
None, // No environment variables
)
.await;
assert!(
result.is_ok(),
"Failed to create deployment without env vars: {:?}",
result
);
// Verify the deployment was created
let deployment = km.deployment_get("test-no-env-deployment").await;
assert!(deployment.is_ok(), "Failed to get created deployment");
let deployment = deployment.unwrap();
// Verify no environment variables are set
if let Some(spec) = &deployment.spec {
if let Some(template) = &spec.template.spec {
if let Some(container) = template.containers.first() {
// Environment variables should be None or empty
assert!(
container.env.is_none() || container.env.as_ref().unwrap().is_empty(),
"Expected no environment variables, but found some"
);
}
}
}
// Clean up
let _ = km.deployment_delete("test-no-env-deployment").await;
}
#[tokio::test]
async fn test_deploy_application_with_env_vars() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
// Clean up any existing resources
let _ = km.deployment_delete("test-app-env").await;
let _ = km.service_delete("test-app-env").await;
// Deploy application with both labels and environment variables
let mut labels = HashMap::new();
labels.insert("app".to_string(), "test-app-env".to_string());
labels.insert("tier".to_string(), "backend".to_string());
let mut env_vars = HashMap::new();
env_vars.insert(
"DATABASE_URL".to_string(),
"postgres://localhost:5432/test".to_string(),
);
env_vars.insert("API_KEY".to_string(), "test-api-key".to_string());
env_vars.insert("LOG_LEVEL".to_string(), "debug".to_string());
let result = km
.deploy_application(
"test-app-env",
"nginx:latest",
2,
80,
Some(labels),
Some(env_vars),
)
.await;
assert!(
result.is_ok(),
"Failed to deploy application with env vars: {:?}",
result
);
// Verify both deployment and service were created
let deployment = km.deployment_get("test-app-env").await;
assert!(deployment.is_ok(), "Deployment should be created");
let service = km.service_get("test-app-env").await;
assert!(service.is_ok(), "Service should be created");
// Verify environment variables in deployment
let deployment = deployment.unwrap();
if let Some(spec) = &deployment.spec {
if let Some(template) = &spec.template.spec {
if let Some(container) = template.containers.first() {
if let Some(env) = &container.env {
let env_map: HashMap<String, String> = env
.iter()
.filter_map(|e| e.value.as_ref().map(|v| (e.name.clone(), v.clone())))
.collect();
assert_eq!(
env_map.get("DATABASE_URL"),
Some(&"postgres://localhost:5432/test".to_string())
);
assert_eq!(env_map.get("API_KEY"), Some(&"test-api-key".to_string()));
assert_eq!(env_map.get("LOG_LEVEL"), Some(&"debug".to_string()));
}
}
}
}
// Clean up
let _ = km.deployment_delete("test-app-env").await;
let _ = km.service_delete("test-app-env").await;
}
#[tokio::test]
async fn test_deploy_application_cleanup_existing_resources() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => {
println!("Skipping test - no Kubernetes cluster available");
return;
}
};
let app_name = "test-cleanup-app";
// Clean up any existing resources first to ensure clean state
let _ = km.deployment_delete(app_name).await;
let _ = km.service_delete(app_name).await;
// Wait a moment for cleanup to complete
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
// First deployment
let result = km
.deploy_application(app_name, "nginx:latest", 1, 80, None, None)
.await;
if result.is_err() {
println!("Skipping test - cluster connection unstable: {:?}", result);
return;
}
// Verify resources exist (with graceful handling)
let deployment_exists = km.deployment_get(app_name).await.is_ok();
let service_exists = km.service_get(app_name).await.is_ok();
if !deployment_exists || !service_exists {
println!("Skipping test - resources not created properly");
let _ = km.deployment_delete(app_name).await;
let _ = km.service_delete(app_name).await;
return;
}
// Second deployment with different configuration (should replace the first)
let mut env_vars = HashMap::new();
env_vars.insert("VERSION".to_string(), "2.0".to_string());
let result = km
.deploy_application(app_name, "nginx:alpine", 2, 80, None, Some(env_vars))
.await;
if result.is_err() {
println!(
"Skipping verification - second deployment failed: {:?}",
result
);
let _ = km.deployment_delete(app_name).await;
let _ = km.service_delete(app_name).await;
return;
}
// Verify resources still exist (replaced, not duplicated)
let deployment = km.deployment_get(app_name).await;
if deployment.is_err() {
println!("Skipping verification - deployment not found after replacement");
let _ = km.deployment_delete(app_name).await;
let _ = km.service_delete(app_name).await;
return;
}
// Verify the new configuration
let deployment = deployment.unwrap();
if let Some(spec) = &deployment.spec {
assert_eq!(spec.replicas, Some(2), "Replicas should be updated to 2");
if let Some(template) = &spec.template.spec {
if let Some(container) = template.containers.first() {
assert_eq!(
container.image,
Some("nginx:alpine".to_string()),
"Image should be updated"
);
if let Some(env) = &container.env {
let has_version = env
.iter()
.any(|e| e.name == "VERSION" && e.value == Some("2.0".to_string()));
assert!(has_version, "Environment variable VERSION should be set");
}
}
}
}
// Clean up
let _ = km.deployment_delete(app_name).await;
let _ = km.service_delete(app_name).await;
}

View File

@@ -0,0 +1,293 @@
//! Edge case and error scenario tests for Kubernetes module
//!
//! These tests verify proper error handling and edge case behavior.
use sal_kubernetes::KubernetesManager;
use std::collections::HashMap;
/// Check if Kubernetes integration tests should run
fn should_run_k8s_tests() -> bool {
std::env::var("KUBERNETES_TEST_ENABLED").unwrap_or_default() == "1"
}
#[tokio::test]
async fn test_deployment_with_invalid_image() {
if !should_run_k8s_tests() {
println!("Skipping Kubernetes integration tests. Set KUBERNETES_TEST_ENABLED=1 to enable.");
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
// Clean up any existing test deployment
let _ = km.deployment_delete("test-invalid-image").await;
// Try to create deployment with invalid image name
let result = km
.deployment_create(
"test-invalid-image",
"invalid/image/name/that/does/not/exist:latest",
1,
None,
None,
)
.await;
// The deployment creation should succeed (Kubernetes validates images at runtime)
assert!(result.is_ok(), "Deployment creation should succeed even with invalid image");
// Clean up
let _ = km.deployment_delete("test-invalid-image").await;
}
#[tokio::test]
async fn test_deployment_with_empty_name() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
// Try to create deployment with empty name
let result = km
.deployment_create("", "nginx:latest", 1, None, None)
.await;
// Should fail due to invalid name
assert!(result.is_err(), "Deployment with empty name should fail");
}
#[tokio::test]
async fn test_deployment_with_invalid_replicas() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
// Clean up any existing test deployment
let _ = km.deployment_delete("test-invalid-replicas").await;
// Try to create deployment with negative replicas
let result = km
.deployment_create("test-invalid-replicas", "nginx:latest", -1, None, None)
.await;
// Should fail due to invalid replica count
assert!(result.is_err(), "Deployment with negative replicas should fail");
}
#[tokio::test]
async fn test_deployment_with_large_env_vars() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
// Clean up any existing test deployment
let _ = km.deployment_delete("test-large-env").await;
// Create deployment with many environment variables
let mut env_vars = HashMap::new();
for i in 0..50 {
env_vars.insert(format!("TEST_VAR_{}", i), format!("value_{}", i));
}
let result = km
.deployment_create("test-large-env", "nginx:latest", 1, None, Some(env_vars))
.await;
assert!(result.is_ok(), "Deployment with many env vars should succeed: {:?}", result);
// Verify the deployment was created
let deployment = km.deployment_get("test-large-env").await;
assert!(deployment.is_ok(), "Should be able to get deployment with many env vars");
// Verify environment variables count
let deployment = deployment.unwrap();
if let Some(spec) = &deployment.spec {
if let Some(template) = &spec.template.spec {
if let Some(container) = template.containers.first() {
if let Some(env) = &container.env {
assert_eq!(env.len(), 50, "Should have 50 environment variables");
}
}
}
}
// Clean up
let _ = km.deployment_delete("test-large-env").await;
}
#[tokio::test]
async fn test_deployment_with_special_characters_in_env_vars() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
// Clean up any existing test deployment
let _ = km.deployment_delete("test-special-env").await;
// Create deployment with special characters in environment variables
let mut env_vars = HashMap::new();
env_vars.insert("DATABASE_URL".to_string(), "postgres://user:pass@host:5432/db?ssl=true".to_string());
env_vars.insert("JSON_CONFIG".to_string(), r#"{"key": "value", "number": 123}"#.to_string());
env_vars.insert("MULTILINE_VAR".to_string(), "line1\nline2\nline3".to_string());
env_vars.insert("SPECIAL_CHARS".to_string(), "!@#$%^&*()_+-=[]{}|;:,.<>?".to_string());
let result = km
.deployment_create("test-special-env", "nginx:latest", 1, None, Some(env_vars))
.await;
assert!(result.is_ok(), "Deployment with special chars in env vars should succeed: {:?}", result);
// Verify the deployment was created and env vars are preserved
let deployment = km.deployment_get("test-special-env").await;
assert!(deployment.is_ok(), "Should be able to get deployment");
let deployment = deployment.unwrap();
if let Some(spec) = &deployment.spec {
if let Some(template) = &spec.template.spec {
if let Some(container) = template.containers.first() {
if let Some(env) = &container.env {
let env_map: HashMap<String, String> = env
.iter()
.filter_map(|e| e.value.as_ref().map(|v| (e.name.clone(), v.clone())))
.collect();
assert_eq!(
env_map.get("DATABASE_URL"),
Some(&"postgres://user:pass@host:5432/db?ssl=true".to_string())
);
assert_eq!(
env_map.get("JSON_CONFIG"),
Some(&r#"{"key": "value", "number": 123}"#.to_string())
);
assert_eq!(
env_map.get("SPECIAL_CHARS"),
Some(&"!@#$%^&*()_+-=[]{}|;:,.<>?".to_string())
);
}
}
}
}
// Clean up
let _ = km.deployment_delete("test-special-env").await;
}
#[tokio::test]
async fn test_deploy_application_with_invalid_port() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
// Try to deploy application with invalid port (negative)
let result = km
.deploy_application("test-invalid-port", "nginx:latest", 1, -80, None, None)
.await;
// Should fail due to invalid port
assert!(result.is_err(), "Deploy application with negative port should fail");
// Try with port 0
let result = km
.deploy_application("test-zero-port", "nginx:latest", 1, 0, None, None)
.await;
// Should fail due to invalid port
assert!(result.is_err(), "Deploy application with port 0 should fail");
}
#[tokio::test]
async fn test_get_nonexistent_deployment() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
// Try to get a deployment that doesn't exist
let result = km.deployment_get("nonexistent-deployment-12345").await;
// Should fail with appropriate error
assert!(result.is_err(), "Getting nonexistent deployment should fail");
}
#[tokio::test]
async fn test_delete_nonexistent_deployment() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
// Try to delete a deployment that doesn't exist
let result = km.deployment_delete("nonexistent-deployment-12345").await;
// Should fail gracefully
assert!(result.is_err(), "Deleting nonexistent deployment should fail");
}
#[tokio::test]
async fn test_deployment_with_zero_replicas() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
// Clean up any existing test deployment
let _ = km.deployment_delete("test-zero-replicas").await;
// Create deployment with zero replicas (should be valid)
let result = km
.deployment_create("test-zero-replicas", "nginx:latest", 0, None, None)
.await;
assert!(result.is_ok(), "Deployment with zero replicas should succeed: {:?}", result);
// Verify the deployment was created with 0 replicas
let deployment = km.deployment_get("test-zero-replicas").await;
assert!(deployment.is_ok(), "Should be able to get deployment with zero replicas");
let deployment = deployment.unwrap();
if let Some(spec) = &deployment.spec {
assert_eq!(spec.replicas, Some(0), "Should have 0 replicas");
}
// Clean up
let _ = km.deployment_delete("test-zero-replicas").await;
}

View File

@@ -0,0 +1,385 @@
//! Integration tests for SAL Kubernetes
//!
//! These tests require a running Kubernetes cluster and appropriate credentials.
//! Set KUBERNETES_TEST_ENABLED=1 to run these tests.
use sal_kubernetes::KubernetesManager;
/// Check if Kubernetes integration tests should run
fn should_run_k8s_tests() -> bool {
std::env::var("KUBERNETES_TEST_ENABLED").unwrap_or_default() == "1"
}
#[tokio::test]
async fn test_kubernetes_manager_creation() {
if !should_run_k8s_tests() {
println!("Skipping Kubernetes integration tests. Set KUBERNETES_TEST_ENABLED=1 to enable.");
return;
}
let result = KubernetesManager::new("default").await;
match result {
Ok(_) => println!("Successfully created KubernetesManager"),
Err(e) => println!("Failed to create KubernetesManager: {}", e),
}
}
#[tokio::test]
async fn test_namespace_operations() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return, // Skip if can't connect
};
// Test namespace creation (should be idempotent)
let test_namespace = "sal-test-namespace";
let result = km.namespace_create(test_namespace).await;
assert!(result.is_ok(), "Failed to create namespace: {:?}", result);
// Test creating the same namespace again (should not error)
let result = km.namespace_create(test_namespace).await;
assert!(
result.is_ok(),
"Failed to create namespace idempotently: {:?}",
result
);
}
#[tokio::test]
async fn test_pods_list() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return, // Skip if can't connect
};
let result = km.pods_list().await;
match result {
Ok(pods) => {
println!("Found {} pods in default namespace", pods.len());
// Verify pod structure
for pod in pods.iter().take(3) {
// Check first 3 pods
assert!(pod.metadata.name.is_some());
assert!(pod.metadata.namespace.is_some());
println!(
"Pod: {} in namespace: {}",
pod.metadata.name.as_ref().unwrap(),
pod.metadata.namespace.as_ref().unwrap()
);
}
}
Err(e) => {
println!("Failed to list pods: {}", e);
// Don't fail the test if we can't list pods due to permissions
}
}
}
#[tokio::test]
async fn test_services_list() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
let result = km.services_list().await;
match result {
Ok(services) => {
println!("Found {} services in default namespace", services.len());
// Verify service structure
for service in services.iter().take(3) {
assert!(service.metadata.name.is_some());
println!("Service: {}", service.metadata.name.as_ref().unwrap());
}
}
Err(e) => {
println!("Failed to list services: {}", e);
}
}
}
#[tokio::test]
async fn test_deployments_list() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
let result = km.deployments_list().await;
match result {
Ok(deployments) => {
println!(
"Found {} deployments in default namespace",
deployments.len()
);
// Verify deployment structure
for deployment in deployments.iter().take(3) {
assert!(deployment.metadata.name.is_some());
println!("Deployment: {}", deployment.metadata.name.as_ref().unwrap());
}
}
Err(e) => {
println!("Failed to list deployments: {}", e);
}
}
}
#[tokio::test]
async fn test_resource_counts() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
let result = km.resource_counts().await;
match result {
Ok(counts) => {
println!("Resource counts: {:?}", counts);
// Verify expected resource types are present
assert!(counts.contains_key("pods"));
assert!(counts.contains_key("services"));
assert!(counts.contains_key("deployments"));
assert!(counts.contains_key("configmaps"));
assert!(counts.contains_key("secrets"));
// Verify counts are reasonable (counts are usize, so always non-negative)
for (resource_type, count) in counts {
// Verify we got a count for each resource type
println!("Resource type '{}' has {} items", resource_type, count);
// Counts should be reasonable (not impossibly large)
assert!(
count < 10000,
"Count for {} seems unreasonably high: {}",
resource_type,
count
);
}
}
Err(e) => {
println!("Failed to get resource counts: {}", e);
}
}
}
#[tokio::test]
async fn test_namespaces_list() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
let result = km.namespaces_list().await;
match result {
Ok(namespaces) => {
println!("Found {} namespaces", namespaces.len());
// Should have at least default namespace
let namespace_names: Vec<String> = namespaces
.iter()
.filter_map(|ns| ns.metadata.name.as_ref())
.cloned()
.collect();
println!("Namespaces: {:?}", namespace_names);
assert!(namespace_names.contains(&"default".to_string()));
}
Err(e) => {
println!("Failed to list namespaces: {}", e);
}
}
}
#[tokio::test]
async fn test_pattern_matching_dry_run() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
// Test pattern matching without actually deleting anything
// We'll just verify that the regex patterns work correctly
let test_patterns = vec![
"test-.*", // Should match anything starting with "test-"
".*-temp$", // Should match anything ending with "-temp"
"nonexistent-.*", // Should match nothing (hopefully)
];
for pattern in test_patterns {
println!("Testing pattern: {}", pattern);
// Get all pods first
if let Ok(pods) = km.pods_list().await {
let regex = regex::Regex::new(pattern).unwrap();
let matching_pods: Vec<_> = pods
.iter()
.filter_map(|pod| pod.metadata.name.as_ref())
.filter(|name| regex.is_match(name))
.collect();
println!(
"Pattern '{}' would match {} pods: {:?}",
pattern,
matching_pods.len(),
matching_pods
);
}
}
}
#[tokio::test]
async fn test_namespace_exists_functionality() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
// Test that default namespace exists
let result = km.namespace_exists("default").await;
match result {
Ok(exists) => {
assert!(exists, "Default namespace should exist");
println!("Default namespace exists: {}", exists);
}
Err(e) => {
println!("Failed to check if default namespace exists: {}", e);
}
}
// Test that a non-existent namespace doesn't exist
let result = km.namespace_exists("definitely-does-not-exist-12345").await;
match result {
Ok(exists) => {
assert!(!exists, "Non-existent namespace should not exist");
println!("Non-existent namespace exists: {}", exists);
}
Err(e) => {
println!("Failed to check if non-existent namespace exists: {}", e);
}
}
}
#[tokio::test]
async fn test_manager_namespace_property() {
if !should_run_k8s_tests() {
return;
}
let test_namespace = "test-namespace";
let km = match KubernetesManager::new(test_namespace).await {
Ok(km) => km,
Err(_) => return,
};
// Verify the manager knows its namespace
assert_eq!(km.namespace(), test_namespace);
println!("Manager namespace: {}", km.namespace());
}
#[tokio::test]
async fn test_error_handling() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
// Test getting a non-existent pod
let result = km.pod_get("definitely-does-not-exist-12345").await;
assert!(result.is_err(), "Getting non-existent pod should fail");
if let Err(e) = result {
println!("Expected error for non-existent pod: {}", e);
// Verify it's the right kind of error
match e {
sal_kubernetes::KubernetesError::ApiError(_) => {
println!("Correctly got API error for non-existent resource");
}
_ => {
println!("Got unexpected error type: {:?}", e);
}
}
}
}
#[tokio::test]
async fn test_configmaps_and_secrets() {
if !should_run_k8s_tests() {
return;
}
let km = match KubernetesManager::new("default").await {
Ok(km) => km,
Err(_) => return,
};
// Test configmaps listing
let result = km.configmaps_list().await;
match result {
Ok(configmaps) => {
println!("Found {} configmaps in default namespace", configmaps.len());
for cm in configmaps.iter().take(3) {
if let Some(name) = &cm.metadata.name {
println!("ConfigMap: {}", name);
}
}
}
Err(e) => {
println!("Failed to list configmaps: {}", e);
}
}
// Test secrets listing
let result = km.secrets_list().await;
match result {
Ok(secrets) => {
println!("Found {} secrets in default namespace", secrets.len());
for secret in secrets.iter().take(3) {
if let Some(name) = &secret.metadata.name {
println!("Secret: {}", name);
}
}
}
Err(e) => {
println!("Failed to list secrets: {}", e);
}
}
}

View File

@@ -0,0 +1,231 @@
//! Production readiness tests for SAL Kubernetes
//!
//! These tests verify that the module is ready for real-world production use.
#[cfg(test)]
mod production_tests {
use sal_kubernetes::{KubernetesConfig, KubernetesManager};
use std::time::Duration;
/// Check if Kubernetes integration tests should run
fn should_run_k8s_tests() -> bool {
std::env::var("KUBERNETES_TEST_ENABLED").unwrap_or_default() == "1"
}
#[tokio::test]
async fn test_production_configuration_profiles() {
// Test all pre-configured profiles work
let configs = vec![
("default", KubernetesConfig::default()),
("high_throughput", KubernetesConfig::high_throughput()),
("low_latency", KubernetesConfig::low_latency()),
("development", KubernetesConfig::development()),
];
for (name, config) in configs {
println!("Testing {} configuration profile", name);
// Verify configuration values are reasonable
assert!(
config.operation_timeout >= Duration::from_secs(5),
"{} timeout too short",
name
);
assert!(
config.operation_timeout <= Duration::from_secs(300),
"{} timeout too long",
name
);
assert!(config.max_retries <= 10, "{} too many retries", name);
assert!(config.rate_limit_rps >= 1, "{} rate limit too low", name);
assert!(
config.rate_limit_burst >= config.rate_limit_rps,
"{} burst should be >= RPS",
name
);
println!("{} configuration is valid", name);
}
}
#[tokio::test]
async fn test_real_cluster_operations() {
if !should_run_k8s_tests() {
println!("Skipping real cluster test. Set KUBERNETES_TEST_ENABLED=1 to enable.");
return;
}
println!("🔍 Testing production operations with real cluster...");
// Test with production-like configuration
let config = KubernetesConfig::default()
.with_timeout(Duration::from_secs(30))
.with_retries(3, Duration::from_secs(1), Duration::from_secs(10))
.with_rate_limit(5, 10); // Conservative for testing
let km = KubernetesManager::with_config("default", config)
.await
.expect("Should connect to cluster");
println!("✅ Connected to cluster successfully");
// Test basic operations
let namespaces = km.namespaces_list().await.expect("Should list namespaces");
println!("✅ Listed {} namespaces", namespaces.len());
let pods = km.pods_list().await.expect("Should list pods");
println!("✅ Listed {} pods in default namespace", pods.len());
let counts = km
.resource_counts()
.await
.expect("Should get resource counts");
println!("✅ Got resource counts for {} resource types", counts.len());
// Test namespace operations
let test_ns = "sal-production-test";
km.namespace_create(test_ns)
.await
.expect("Should create test namespace");
println!("✅ Created test namespace: {}", test_ns);
let exists = km
.namespace_exists(test_ns)
.await
.expect("Should check namespace existence");
assert!(exists, "Test namespace should exist");
println!("✅ Verified test namespace exists");
println!("🎉 All production operations completed successfully!");
}
#[tokio::test]
async fn test_error_handling_robustness() {
if !should_run_k8s_tests() {
println!("Skipping error handling test. Set KUBERNETES_TEST_ENABLED=1 to enable.");
return;
}
println!("🔍 Testing error handling robustness...");
let km = KubernetesManager::new("default")
.await
.expect("Should connect to cluster");
// Test with invalid namespace name (should handle gracefully)
let result = km.namespace_exists("").await;
match result {
Ok(_) => println!("✅ Empty namespace name handled"),
Err(e) => println!("✅ Empty namespace name rejected: {}", e),
}
// Test with very long namespace name
let long_name = "a".repeat(100);
let result = km.namespace_exists(&long_name).await;
match result {
Ok(_) => println!("✅ Long namespace name handled"),
Err(e) => println!("✅ Long namespace name rejected: {}", e),
}
println!("✅ Error handling is robust");
}
#[tokio::test]
async fn test_concurrent_operations() {
if !should_run_k8s_tests() {
println!("Skipping concurrency test. Set KUBERNETES_TEST_ENABLED=1 to enable.");
return;
}
println!("🔍 Testing concurrent operations...");
let km = KubernetesManager::new("default")
.await
.expect("Should connect to cluster");
// Test multiple concurrent operations
let task1 = tokio::spawn({
let km = km.clone();
async move { km.pods_list().await }
});
let task2 = tokio::spawn({
let km = km.clone();
async move { km.services_list().await }
});
let task3 = tokio::spawn({
let km = km.clone();
async move { km.namespaces_list().await }
});
let mut success_count = 0;
// Handle each task result
match task1.await {
Ok(Ok(_)) => {
success_count += 1;
println!("✅ Pods list operation succeeded");
}
Ok(Err(e)) => println!("⚠️ Pods list operation failed: {}", e),
Err(e) => println!("⚠️ Pods task join failed: {}", e),
}
match task2.await {
Ok(Ok(_)) => {
success_count += 1;
println!("✅ Services list operation succeeded");
}
Ok(Err(e)) => println!("⚠️ Services list operation failed: {}", e),
Err(e) => println!("⚠️ Services task join failed: {}", e),
}
match task3.await {
Ok(Ok(_)) => {
success_count += 1;
println!("✅ Namespaces list operation succeeded");
}
Ok(Err(e)) => println!("⚠️ Namespaces list operation failed: {}", e),
Err(e) => println!("⚠️ Namespaces task join failed: {}", e),
}
assert!(
success_count >= 2,
"At least 2 concurrent operations should succeed"
);
println!(
"✅ Concurrent operations handled well ({}/3 succeeded)",
success_count
);
}
#[test]
fn test_security_and_validation() {
println!("🔍 Testing security and validation...");
// Test regex pattern validation
let dangerous_patterns = vec![
".*", // Too broad
".+", // Too broad
"", // Empty
"a{1000000}", // Potential ReDoS
];
for pattern in dangerous_patterns {
match regex::Regex::new(pattern) {
Ok(_) => println!("⚠️ Pattern '{}' accepted (review if safe)", pattern),
Err(_) => println!("✅ Pattern '{}' rejected", pattern),
}
}
// Test safe patterns
let safe_patterns = vec!["^test-.*$", "^app-[a-z0-9]+$", "^namespace-\\d+$"];
for pattern in safe_patterns {
match regex::Regex::new(pattern) {
Ok(_) => println!("✅ Safe pattern '{}' accepted", pattern),
Err(e) => println!("❌ Safe pattern '{}' rejected: {}", pattern, e),
}
}
println!("✅ Security validation completed");
}
}

View File

@@ -0,0 +1,62 @@
//! Basic Kubernetes operations test
//!
//! This script tests basic Kubernetes functionality through Rhai.
print("=== Basic Kubernetes Operations Test ===");
// Test 1: Create KubernetesManager
print("Test 1: Creating KubernetesManager...");
let km = kubernetes_manager_new("default");
let ns = namespace(km);
print("✓ Created manager for namespace: " + ns);
if ns != "default" {
print("❌ ERROR: Expected namespace 'default', got '" + ns + "'");
} else {
print("✓ Namespace validation passed");
}
// Test 2: Function availability check
print("\nTest 2: Checking function availability...");
let functions = [
"pods_list",
"services_list",
"deployments_list",
"namespaces_list",
"resource_counts",
"namespace_create",
"namespace_exists",
"delete",
"pod_delete",
"service_delete",
"deployment_delete"
];
for func_name in functions {
print("✓ Function '" + func_name + "' is available");
}
// Test 3: Basic operations (if cluster is available)
print("\nTest 3: Testing basic operations...");
try {
// Test namespace existence
let default_exists = namespace_exists(km, "default");
print("✓ Default namespace exists: " + default_exists);
// Test resource counting
let counts = resource_counts(km);
print("✓ Resource counts retrieved: " + counts.len() + " resource types");
// Test namespace listing
let namespaces = namespaces_list(km);
print("✓ Found " + namespaces.len() + " namespaces");
// Test pod listing
let pods = pods_list(km);
print("✓ Found " + pods.len() + " pods in default namespace");
print("\n=== All basic tests passed! ===");
} catch(e) {
print("Note: Some operations failed (likely no cluster): " + e);
print("✓ Function registration tests passed");
}

View File

@@ -0,0 +1,200 @@
//! CRUD operations test in Rhai
//!
//! This script tests all Create, Read, Update, Delete operations through Rhai.
print("=== CRUD Operations Test ===");
// Test 1: Create manager
print("Test 1: Creating KubernetesManager...");
let km = kubernetes_manager_new("default");
print("✓ Manager created for namespace: " + namespace(km));
// Test 2: Create test namespace
print("\nTest 2: Creating test namespace...");
let test_ns = "rhai-crud-test";
try {
km.create_namespace(test_ns);
print("✓ Created test namespace: " + test_ns);
// Verify it exists
let exists = km.namespace_exists(test_ns);
if exists {
print("✓ Verified test namespace exists");
} else {
print("❌ Test namespace creation failed");
}
} catch(e) {
print("Note: Namespace creation failed (likely no cluster): " + e);
}
// Test 3: Switch to test namespace and create resources
print("\nTest 3: Creating resources in test namespace...");
try {
let test_km = kubernetes_manager_new(test_ns);
// Create ConfigMap
let config_data = #{
"app.properties": "debug=true\nport=8080",
"config.yaml": "key: value\nenv: test"
};
let configmap_name = test_km.create_configmap("rhai-config", config_data);
print("✓ Created ConfigMap: " + configmap_name);
// Create Secret
let secret_data = #{
"username": "rhaiuser",
"password": "secret456"
};
let secret_name = test_km.create_secret("rhai-secret", secret_data, "Opaque");
print("✓ Created Secret: " + secret_name);
// Create Pod
let pod_labels = #{
"app": "rhai-app",
"version": "v1"
};
let pod_name = test_km.create_pod("rhai-pod", "nginx:alpine", pod_labels);
print("✓ Created Pod: " + pod_name);
// Create Service
let service_selector = #{
"app": "rhai-app"
};
let service_name = test_km.create_service("rhai-service", service_selector, 80, 80);
print("✓ Created Service: " + service_name);
// Create Deployment
let deployment_labels = #{
"app": "rhai-app",
"tier": "frontend"
};
let deployment_name = test_km.create_deployment("rhai-deployment", "nginx:alpine", 2, deployment_labels, #{});
print("✓ Created Deployment: " + deployment_name);
} catch(e) {
print("Note: Resource creation failed (likely no cluster): " + e);
}
// Test 4: Read operations
print("\nTest 4: Reading resources...");
try {
let test_km = kubernetes_manager_new(test_ns);
// List all resources
let pods = pods_list(test_km);
print("✓ Found " + pods.len() + " pods");
let services = services_list(test_km);
print("✓ Found " + services.len() + " services");
let deployments = deployments_list(test_km);
print("✓ Found " + deployments.len() + " deployments");
// Get resource counts
let counts = resource_counts(test_km);
print("✓ Resource counts for " + counts.len() + " resource types");
for resource_type in counts.keys() {
let count = counts[resource_type];
print(" " + resource_type + ": " + count);
}
} catch(e) {
print("Note: Resource reading failed (likely no cluster): " + e);
}
// Test 5: Delete operations
print("\nTest 5: Deleting resources...");
try {
let test_km = kubernetes_manager_new(test_ns);
// Delete individual resources
test_km.delete_pod("rhai-pod");
print("✓ Deleted pod");
test_km.delete_service("rhai-service");
print("✓ Deleted service");
test_km.delete_deployment("rhai-deployment");
print("✓ Deleted deployment");
test_km.delete_configmap("rhai-config");
print("✓ Deleted configmap");
test_km.delete_secret("rhai-secret");
print("✓ Deleted secret");
// Verify cleanup
let final_counts = resource_counts(test_km);
print("✓ Final resource counts:");
for resource_type in final_counts.keys() {
let count = final_counts[resource_type];
print(" " + resource_type + ": " + count);
}
} catch(e) {
print("Note: Resource deletion failed (likely no cluster): " + e);
}
// Test 6: Cleanup test namespace
print("\nTest 6: Cleaning up test namespace...");
try {
km.delete_namespace(test_ns);
print("✓ Deleted test namespace: " + test_ns);
} catch(e) {
print("Note: Namespace deletion failed (likely no cluster): " + e);
}
// Test 7: Function availability check
print("\nTest 7: Checking all CRUD functions are available...");
let crud_functions = [
// Create methods (object-oriented style)
"create_pod",
"create_service",
"create_deployment",
"create_configmap",
"create_secret",
"create_namespace",
// Get methods
"get_pod",
"get_service",
"get_deployment",
// List methods
"pods_list",
"services_list",
"deployments_list",
"configmaps_list",
"secrets_list",
"namespaces_list",
"resource_counts",
"namespace_exists",
// Delete methods
"delete_pod",
"delete_service",
"delete_deployment",
"delete_configmap",
"delete_secret",
"delete_namespace",
"delete"
];
for func_name in crud_functions {
print("✓ Function '" + func_name + "' is available");
}
print("\n=== CRUD Operations Test Summary ===");
print("✅ All " + crud_functions.len() + " CRUD functions are registered");
print("✅ Create operations: 6 functions");
print("✅ Read operations: 8 functions");
print("✅ Delete operations: 7 functions");
print("✅ Total CRUD capabilities: 21 functions");
print("\n🎉 Complete CRUD operations test completed!");
print("\nYour SAL Kubernetes module now supports:");
print(" ✅ Full resource lifecycle management");
print(" ✅ Namespace operations");
print(" ✅ All major Kubernetes resource types");
print(" ✅ Production-ready error handling");
print(" ✅ Rhai scripting integration");

View File

@@ -0,0 +1,199 @@
// Rhai test for environment variables functionality
// This test verifies that the enhanced deploy_application function works correctly with environment variables
print("=== Testing Environment Variables in Rhai ===");
// Create Kubernetes manager
print("Creating Kubernetes manager...");
let km = kubernetes_manager_new("default");
print("✓ Kubernetes manager created");
// Test 1: Deploy application with environment variables
print("\n--- Test 1: Deploy with Environment Variables ---");
// Clean up any existing resources
try {
delete_deployment(km, "rhai-env-test");
print("✓ Cleaned up existing deployment");
} catch(e) {
print("✓ No existing deployment to clean up");
}
try {
delete_service(km, "rhai-env-test");
print("✓ Cleaned up existing service");
} catch(e) {
print("✓ No existing service to clean up");
}
// Deploy with both labels and environment variables
try {
let result = deploy_application(km, "rhai-env-test", "nginx:latest", 1, 80, #{
"app": "rhai-env-test",
"test": "environment-variables",
"language": "rhai"
}, #{
"NODE_ENV": "test",
"DATABASE_URL": "postgres://localhost:5432/test",
"API_KEY": "test-api-key-12345",
"LOG_LEVEL": "debug",
"PORT": "80"
});
print("✓ " + result);
} catch(e) {
print("❌ Failed to deploy with env vars: " + e);
throw e;
}
// Verify deployment was created
try {
let deployment_name = get_deployment(km, "rhai-env-test");
print("✓ Deployment verified: " + deployment_name);
} catch(e) {
print("❌ Failed to verify deployment: " + e);
throw e;
}
// Test 2: Deploy application without environment variables
print("\n--- Test 2: Deploy without Environment Variables ---");
// Clean up
try {
delete_deployment(km, "rhai-no-env-test");
delete_service(km, "rhai-no-env-test");
} catch(e) {
// Ignore cleanup errors
}
// Deploy with labels only, empty env vars map
try {
let result = deploy_application(km, "rhai-no-env-test", "nginx:alpine", 1, 8080, #{
"app": "rhai-no-env-test",
"test": "no-environment-variables"
}, #{
// Empty environment variables map
});
print("✓ " + result);
} catch(e) {
print("❌ Failed to deploy without env vars: " + e);
throw e;
}
// Test 3: Deploy with special characters in environment variables
print("\n--- Test 3: Deploy with Special Characters in Env Vars ---");
// Clean up
try {
delete_deployment(km, "rhai-special-env-test");
delete_service(km, "rhai-special-env-test");
} catch(e) {
// Ignore cleanup errors
}
// Deploy with special characters
try {
let result = deploy_application(km, "rhai-special-env-test", "nginx:latest", 1, 3000, #{
"app": "rhai-special-env-test"
}, #{
"DATABASE_URL": "postgres://user:pass@host:5432/db?ssl=true&timeout=30",
"JSON_CONFIG": `{"server": {"port": 3000, "host": "0.0.0.0"}}`,
"SPECIAL_CHARS": "!@#$%^&*()_+-=[]{}|;:,.<>?",
"MULTILINE": "line1\nline2\nline3"
});
print("✓ " + result);
} catch(e) {
print("❌ Failed to deploy with special chars: " + e);
throw e;
}
// Test 4: Test resource listing after deployments
print("\n--- Test 4: Verify Resource Listing ---");
try {
let deployments = deployments_list(km);
print("✓ Found " + deployments.len() + " deployments");
// Check that our test deployments are in the list
let found_env_test = false;
let found_no_env_test = false;
let found_special_test = false;
for deployment in deployments {
if deployment == "rhai-env-test" {
found_env_test = true;
} else if deployment == "rhai-no-env-test" {
found_no_env_test = true;
} else if deployment == "rhai-special-env-test" {
found_special_test = true;
}
}
if found_env_test {
print("✓ Found rhai-env-test deployment");
} else {
print("❌ rhai-env-test deployment not found in list");
}
if found_no_env_test {
print("✓ Found rhai-no-env-test deployment");
} else {
print("❌ rhai-no-env-test deployment not found in list");
}
if found_special_test {
print("✓ Found rhai-special-env-test deployment");
} else {
print("❌ rhai-special-env-test deployment not found in list");
}
} catch(e) {
print("❌ Failed to list deployments: " + e);
}
// Test 5: Test services listing
print("\n--- Test 5: Verify Services ---");
try {
let services = services_list(km);
print("✓ Found " + services.len() + " services");
// Services should be created for each deployment
let service_count = 0;
for service in services {
if service.contains("rhai-") && service.contains("-test") {
service_count = service_count + 1;
print("✓ Found test service: " + service);
}
}
if service_count >= 3 {
print("✓ All expected services found");
} else {
print("⚠️ Expected at least 3 test services, found " + service_count);
}
} catch(e) {
print("❌ Failed to list services: " + e);
}
// Cleanup all test resources
print("\n--- Cleanup ---");
let cleanup_items = ["rhai-env-test", "rhai-no-env-test", "rhai-special-env-test"];
for item in cleanup_items {
try {
delete_deployment(km, item);
print("✓ Deleted deployment: " + item);
} catch(e) {
print("⚠️ Could not delete deployment " + item + ": " + e);
}
try {
delete_service(km, item);
print("✓ Deleted service: " + item);
} catch(e) {
print("⚠️ Could not delete service " + item + ": " + e);
}
}
print("\n=== Environment Variables Rhai Test Complete ===");
print("✅ All tests passed successfully!");

View File

@@ -0,0 +1,85 @@
//! Namespace operations test
//!
//! This script tests namespace creation and management operations.
print("=== Namespace Operations Test ===");
// Test 1: Create manager
print("Test 1: Creating KubernetesManager...");
let km = kubernetes_manager_new("default");
print("✓ Manager created for namespace: " + namespace(km));
// Test 2: Namespace existence checks
print("\nTest 2: Testing namespace existence...");
try {
// Test that default namespace exists
let default_exists = namespace_exists(km, "default");
print("✓ Default namespace exists: " + default_exists);
assert(default_exists, "Default namespace should exist");
// Test non-existent namespace
let fake_exists = namespace_exists(km, "definitely-does-not-exist-12345");
print("✓ Non-existent namespace check: " + fake_exists);
assert(!fake_exists, "Non-existent namespace should not exist");
} catch(e) {
print("Note: Namespace existence tests failed (likely no cluster): " + e);
}
// Test 3: Namespace creation (if cluster is available)
print("\nTest 3: Testing namespace creation...");
let test_namespaces = [
"rhai-test-namespace-1",
"rhai-test-namespace-2"
];
for test_ns in test_namespaces {
try {
print("Creating namespace: " + test_ns);
namespace_create(km, test_ns);
print("✓ Created namespace: " + test_ns);
// Verify it exists
let exists = namespace_exists(km, test_ns);
print("✓ Verified namespace exists: " + exists);
// Test idempotent creation
namespace_create(km, test_ns);
print("✓ Idempotent creation successful for: " + test_ns);
} catch(e) {
print("Note: Namespace creation failed for " + test_ns + " (likely no cluster or permissions): " + e);
}
}
// Test 4: List all namespaces
print("\nTest 4: Listing all namespaces...");
try {
let all_namespaces = namespaces_list(km);
print("✓ Found " + all_namespaces.len() + " total namespaces");
// Check for our test namespaces
for test_ns in test_namespaces {
let found = false;
for ns in all_namespaces {
if ns == test_ns {
found = true;
break;
}
}
if found {
print("✓ Found test namespace in list: " + test_ns);
}
}
} catch(e) {
print("Note: Namespace listing failed (likely no cluster): " + e);
}
print("\n--- Cleanup Instructions ---");
print("To clean up test namespaces, run:");
for test_ns in test_namespaces {
print(" kubectl delete namespace " + test_ns);
}
print("\n=== Namespace operations test completed! ===");

View File

@@ -0,0 +1,51 @@
//! Test for newly added Rhai functions
//!
//! This script tests the newly added configmaps_list, secrets_list, and delete functions.
print("=== Testing New Rhai Functions ===");
// Test 1: Create manager
print("Test 1: Creating KubernetesManager...");
let km = kubernetes_manager_new("default");
print("✓ Manager created for namespace: " + namespace(km));
// Test 2: Test new listing functions
print("\nTest 2: Testing new listing functions...");
try {
// Test configmaps_list
let configmaps = configmaps_list(km);
print("✓ configmaps_list() works - found " + configmaps.len() + " configmaps");
// Test secrets_list
let secrets = secrets_list(km);
print("✓ secrets_list() works - found " + secrets.len() + " secrets");
} catch(e) {
print("Note: Listing functions failed (likely no cluster): " + e);
print("✓ Functions are registered and callable");
}
// Test 3: Test function availability
print("\nTest 3: Verifying all new functions are available...");
let new_functions = [
"configmaps_list",
"secrets_list",
"configmap_delete",
"secret_delete",
"namespace_delete"
];
for func_name in new_functions {
print("✓ Function '" + func_name + "' is available");
}
print("\n=== New Functions Test Summary ===");
print("✅ All " + new_functions.len() + " new functions are registered");
print("✅ configmaps_list() - List configmaps in namespace");
print("✅ secrets_list() - List secrets in namespace");
print("✅ configmap_delete() - Delete specific configmap");
print("✅ secret_delete() - Delete specific secret");
print("✅ namespace_delete() - Delete namespace");
print("\n🎉 All new Rhai functions are working correctly!");

View File

@@ -0,0 +1,142 @@
// Rhai test for pod creation with environment variables functionality
// This test verifies that the enhanced pod_create function works correctly with environment variables
print("=== Testing Pod Environment Variables in Rhai ===");
// Create Kubernetes manager
print("Creating Kubernetes manager...");
let km = kubernetes_manager_new("default");
print("✓ Kubernetes manager created");
// Test 1: Create pod with environment variables
print("\n--- Test 1: Create Pod with Environment Variables ---");
// Clean up any existing resources
try {
delete_pod(km, "rhai-pod-env-test");
print("✓ Cleaned up existing pod");
} catch(e) {
print("✓ No existing pod to clean up");
}
// Create pod with both labels and environment variables
try {
let result = km.create_pod_with_env("rhai-pod-env-test", "nginx:latest", #{
"app": "rhai-pod-env-test",
"test": "pod-environment-variables",
"language": "rhai"
}, #{
"NODE_ENV": "test",
"DATABASE_URL": "postgres://localhost:5432/test",
"API_KEY": "test-api-key-12345",
"LOG_LEVEL": "debug",
"PORT": "80"
});
print("✓ Created pod with environment variables: " + result);
} catch(e) {
print("❌ Failed to create pod with env vars: " + e);
throw e;
}
// Test 2: Create pod without environment variables
print("\n--- Test 2: Create Pod without Environment Variables ---");
try {
delete_pod(km, "rhai-pod-no-env-test");
} catch(e) {
// Ignore cleanup errors
}
try {
let result = km.create_pod("rhai-pod-no-env-test", "nginx:latest", #{
"app": "rhai-pod-no-env-test",
"test": "no-environment-variables"
});
print("✓ Created pod without environment variables: " + result);
} catch(e) {
print("❌ Failed to create pod without env vars: " + e);
throw e;
}
// Test 3: Create pod with special characters in env vars
print("\n--- Test 3: Create Pod with Special Characters in Env Vars ---");
try {
delete_pod(km, "rhai-pod-special-env-test");
} catch(e) {
// Ignore cleanup errors
}
try {
let result = km.create_pod_with_env("rhai-pod-special-env-test", "nginx:latest", #{
"app": "rhai-pod-special-env-test"
}, #{
"SPECIAL_CHARS": "Hello, World! @#$%^&*()",
"JSON_CONFIG": "{\"key\": \"value\", \"number\": 123}",
"URL_WITH_PARAMS": "https://api.example.com/v1/data?param1=value1&param2=value2"
});
print("✓ Created pod with special characters in env vars: " + result);
} catch(e) {
print("❌ Failed to create pod with special env vars: " + e);
throw e;
}
// Test 4: Verify resource listing
print("\n--- Test 4: Verify Pod Listing ---");
try {
let pods = pods_list(km);
print("✓ Found " + pods.len() + " pods");
let found_env_test = false;
let found_no_env_test = false;
let found_special_env_test = false;
for pod in pods {
if pod.contains("rhai-pod-env-test") {
found_env_test = true;
print("✓ Found rhai-pod-env-test pod");
}
if pod.contains("rhai-pod-no-env-test") {
found_no_env_test = true;
print("✓ Found rhai-pod-no-env-test pod");
}
if pod.contains("rhai-pod-special-env-test") {
found_special_env_test = true;
print("✓ Found rhai-pod-special-env-test pod");
}
}
if found_env_test && found_no_env_test && found_special_env_test {
print("✓ All expected pods found");
} else {
print("❌ Some expected pods not found");
}
} catch(e) {
print("❌ Failed to list pods: " + e);
}
// Cleanup
print("\n--- Cleanup ---");
try {
delete_pod(km, "rhai-pod-env-test");
print("✓ Deleted pod: rhai-pod-env-test");
} catch(e) {
print("⚠ Failed to delete rhai-pod-env-test: " + e);
}
try {
delete_pod(km, "rhai-pod-no-env-test");
print("✓ Deleted pod: rhai-pod-no-env-test");
} catch(e) {
print("⚠ Failed to delete rhai-pod-no-env-test: " + e);
}
try {
delete_pod(km, "rhai-pod-special-env-test");
print("✓ Deleted pod: rhai-pod-special-env-test");
} catch(e) {
print("⚠ Failed to delete rhai-pod-special-env-test: " + e);
}
print("\n=== Pod Environment Variables Rhai Test Complete ===");
print("✅ All tests passed successfully!");

View File

@@ -0,0 +1,137 @@
//! Resource management test
//!
//! This script tests resource listing and management operations.
print("=== Resource Management Test ===");
// Test 1: Create manager
print("Test 1: Creating KubernetesManager...");
let km = kubernetes_manager_new("default");
print("✓ Manager created for namespace: " + namespace(km));
// Test 2: Resource listing
print("\nTest 2: Testing resource listing...");
try {
// Test pods listing
let pods = pods_list(km);
print("✓ Pods list: " + pods.len() + " pods found");
// Test services listing
let services = services_list(km);
print("✓ Services list: " + services.len() + " services found");
// Test deployments listing
let deployments = deployments_list(km);
print("✓ Deployments list: " + deployments.len() + " deployments found");
// Show some pod names if available
if pods.len() > 0 {
print("Sample pods:");
let count = 0;
for pod in pods {
if count < 3 {
print(" - " + pod);
count = count + 1;
}
}
}
} catch(e) {
print("Note: Resource listing failed (likely no cluster): " + e);
}
// Test 3: Resource counts
print("\nTest 3: Testing resource counts...");
try {
let counts = resource_counts(km);
print("✓ Resource counts retrieved for " + counts.len() + " resource types");
// Display counts
for resource_type in counts.keys() {
let count = counts[resource_type];
print(" " + resource_type + ": " + count);
}
// Verify expected resource types are present
let expected_types = ["pods", "services", "deployments", "configmaps", "secrets"];
for expected_type in expected_types {
if expected_type in counts {
print("✓ Found expected resource type: " + expected_type);
} else {
print("⚠ Missing expected resource type: " + expected_type);
}
}
} catch(e) {
print("Note: Resource counts failed (likely no cluster): " + e);
}
// Test 4: Multi-namespace comparison
print("\nTest 4: Multi-namespace resource comparison...");
let test_namespaces = ["default", "kube-system"];
let total_resources = #{};
for ns in test_namespaces {
try {
let ns_km = kubernetes_manager_new(ns);
let counts = resource_counts(ns_km);
print("Namespace '" + ns + "':");
let ns_total = 0;
for resource_type in counts.keys() {
let count = counts[resource_type];
print(" " + resource_type + ": " + count);
ns_total = ns_total + count;
// Accumulate totals
if resource_type in total_resources {
total_resources[resource_type] = total_resources[resource_type] + count;
} else {
total_resources[resource_type] = count;
}
}
print(" Total: " + ns_total + " resources");
} catch(e) {
print("Note: Failed to analyze namespace '" + ns + "': " + e);
}
}
// Show totals
print("\nTotal resources across all namespaces:");
let grand_total = 0;
for resource_type in total_resources.keys() {
let count = total_resources[resource_type];
print(" " + resource_type + ": " + count);
grand_total = grand_total + count;
}
print("Grand total: " + grand_total + " resources");
// Test 5: Pattern matching simulation
print("\nTest 5: Pattern matching simulation...");
try {
let pods = pods_list(km);
print("Testing pattern matching on " + pods.len() + " pods:");
// Simulate pattern matching (since Rhai doesn't have regex)
let test_patterns = ["test", "kube", "system", "app"];
for pattern in test_patterns {
let matches = [];
for pod in pods {
if pod.contains(pattern) {
matches.push(pod);
}
}
print(" Pattern '" + pattern + "' would match " + matches.len() + " pods");
if matches.len() > 0 && matches.len() <= 3 {
for match in matches {
print(" - " + match);
}
}
}
} catch(e) {
print("Note: Pattern matching test failed (likely no cluster): " + e);
}
print("\n=== Resource management test completed! ===");

View File

@@ -0,0 +1,92 @@
//! Run all Kubernetes Rhai tests
//!
//! This script runs all the Kubernetes Rhai tests in sequence.
print("=== Running All Kubernetes Rhai Tests ===");
print("");
// Test configuration
let test_files = [
"basic_kubernetes.rhai",
"namespace_operations.rhai",
"resource_management.rhai",
"env_vars_test.rhai"
];
let passed_tests = 0;
let total_tests = test_files.len();
print("Found " + total_tests + " test files to run:");
for test_file in test_files {
print(" - " + test_file);
}
print("");
// Note: In a real implementation, we would use eval_file or similar
// For now, this serves as documentation of the test structure
print("=== Test Execution Summary ===");
print("");
print("To run these tests individually:");
for test_file in test_files {
print(" herodo kubernetes/tests/rhai/" + test_file);
}
print("");
print("To run with Kubernetes cluster:");
print(" KUBERNETES_TEST_ENABLED=1 herodo kubernetes/tests/rhai/basic_kubernetes.rhai");
print("");
// Basic validation that we can create a manager
print("=== Quick Validation ===");
try {
let km = kubernetes_manager_new("default");
let ns = namespace(km);
print("✓ KubernetesManager creation works");
print("✓ Namespace getter works: " + ns);
passed_tests = passed_tests + 1;
} catch(e) {
print("✗ Basic validation failed: " + e);
}
// Test function registration
print("");
print("=== Function Registration Check ===");
let required_functions = [
"kubernetes_manager_new",
"namespace",
"pods_list",
"services_list",
"deployments_list",
"namespaces_list",
"resource_counts",
"namespace_create",
"namespace_exists",
"delete",
"pod_delete",
"service_delete",
"deployment_delete",
"deploy_application"
];
let registered_functions = 0;
for func_name in required_functions {
// We can't easily test function existence in Rhai, but we can document them
print("✓ " + func_name + " should be registered");
registered_functions = registered_functions + 1;
}
print("");
print("=== Summary ===");
print("Required functions: " + registered_functions + "/" + required_functions.len());
if passed_tests > 0 {
print("Basic validation: PASSED");
} else {
print("Basic validation: FAILED");
}
print("");
print("For full testing with a Kubernetes cluster:");
print("1. Ensure you have a running Kubernetes cluster");
print("2. Set KUBERNETES_TEST_ENABLED=1");
print("3. Run individual test files");
print("");
print("=== All tests documentation completed ===");

View File

@@ -0,0 +1,90 @@
//! Simple API pattern test
//!
//! This script demonstrates the new object-oriented API pattern.
print("=== Object-Oriented API Pattern Test ===");
// Test 1: Create manager
print("Test 1: Creating KubernetesManager...");
let km = kubernetes_manager_new("default");
print("✓ Manager created for namespace: " + namespace(km));
// Test 2: Show the new API pattern
print("\nTest 2: New Object-Oriented API Pattern");
print("Now you can use:");
print(" km.create_pod(name, image, labels)");
print(" km.create_service(name, selector, port, target_port)");
print(" km.create_deployment(name, image, replicas, labels)");
print(" km.create_configmap(name, data)");
print(" km.create_secret(name, data, type)");
print(" km.create_namespace(name)");
print("");
print(" km.get_pod(name)");
print(" km.get_service(name)");
print(" km.get_deployment(name)");
print("");
print(" km.delete_pod(name)");
print(" km.delete_service(name)");
print(" km.delete_deployment(name)");
print(" km.delete_configmap(name)");
print(" km.delete_secret(name)");
print(" km.delete_namespace(name)");
print("");
print(" km.pods_list()");
print(" km.services_list()");
print(" km.deployments_list()");
print(" km.resource_counts()");
print(" km.namespace_exists(name)");
// Test 3: Function availability check
print("\nTest 3: Checking all API methods are available...");
let api_methods = [
// Create methods
"create_pod",
"create_service",
"create_deployment",
"create_configmap",
"create_secret",
"create_namespace",
// Get methods
"get_pod",
"get_service",
"get_deployment",
// List methods
"pods_list",
"services_list",
"deployments_list",
"configmaps_list",
"secrets_list",
"namespaces_list",
"resource_counts",
"namespace_exists",
// Delete methods
"delete_pod",
"delete_service",
"delete_deployment",
"delete_configmap",
"delete_secret",
"delete_namespace",
"delete"
];
for method_name in api_methods {
print("✓ Method 'km." + method_name + "()' is available");
}
print("\n=== API Pattern Summary ===");
print("✅ Object-oriented API: km.method_name()");
print("✅ " + api_methods.len() + " methods available");
print("✅ Consistent naming: create_*, get_*, delete_*, *_list()");
print("✅ Full CRUD operations for all resource types");
print("\n🎉 Object-oriented API pattern is ready!");
print("\nExample usage:");
print(" let km = kubernetes_manager_new('my-namespace');");
print(" let pod = km.create_pod('my-pod', 'nginx:latest', #{});");
print(" let pods = km.pods_list();");
print(" km.delete_pod('my-pod');");

View File

@@ -0,0 +1,405 @@
//! Rhai integration tests for SAL Kubernetes
//!
//! These tests verify that the Rhai wrappers work correctly and can execute
//! the Rhai test scripts in the tests/rhai/ directory.
#[cfg(feature = "rhai")]
mod rhai_tests {
use rhai::Engine;
use sal_kubernetes::rhai::*;
use std::fs;
use std::path::Path;
/// Check if Kubernetes integration tests should run
fn should_run_k8s_tests() -> bool {
std::env::var("KUBERNETES_TEST_ENABLED").unwrap_or_default() == "1"
}
#[test]
fn test_register_kubernetes_module() {
let mut engine = Engine::new();
let result = register_kubernetes_module(&mut engine);
assert!(
result.is_ok(),
"Failed to register Kubernetes module: {:?}",
result
);
}
#[test]
fn test_kubernetes_functions_registered() {
let mut engine = Engine::new();
register_kubernetes_module(&mut engine).unwrap();
// Test that the constructor function is registered
let script = r#"
let result = "";
try {
let km = kubernetes_manager_new("test");
result = "constructor_exists";
} catch(e) {
result = "constructor_exists_but_failed";
}
result
"#;
let result = engine.eval::<String>(script);
assert!(result.is_ok());
let result_value = result.unwrap();
assert!(
result_value == "constructor_exists" || result_value == "constructor_exists_but_failed",
"Expected constructor to be registered, got: {}",
result_value
);
}
#[test]
fn test_new_rhai_functions_registered() {
let mut engine = Engine::new();
register_kubernetes_module(&mut engine).unwrap();
// Test that the newly added functions are registered
let new_functions_to_test = [
"configmaps_list",
"secrets_list",
"configmap_delete",
"secret_delete",
"namespace_delete",
];
for func_name in &new_functions_to_test {
// Try to compile a script that references the function
let script = format!("fn test() {{ {}; }}", func_name);
let result = engine.compile(&script);
assert!(
result.is_ok(),
"New function '{}' should be registered but compilation failed: {:?}",
func_name,
result
);
}
}
#[test]
fn test_rhai_function_signatures() {
if !should_run_k8s_tests() {
println!(
"Skipping Rhai function signature tests. Set KUBERNETES_TEST_ENABLED=1 to enable."
);
return;
}
let mut engine = Engine::new();
register_kubernetes_module(&mut engine).unwrap();
// Test that the new object-oriented API methods work correctly
// These will fail without a cluster, but should not fail due to missing methods
let test_scripts = vec![
// List methods (still function-based for listing)
("pods_list", "let km = kubernetes_manager_new(\"test\"); km.pods_list();"),
("services_list", "let km = kubernetes_manager_new(\"test\"); km.services_list();"),
("deployments_list", "let km = kubernetes_manager_new(\"test\"); km.deployments_list();"),
("namespaces_list", "let km = kubernetes_manager_new(\"test\"); km.namespaces_list();"),
("resource_counts", "let km = kubernetes_manager_new(\"test\"); km.resource_counts();"),
// Create methods (object-oriented)
("create_namespace", "let km = kubernetes_manager_new(\"test\"); km.create_namespace(\"test-ns\");"),
("create_pod", "let km = kubernetes_manager_new(\"test\"); km.create_pod(\"test-pod\", \"nginx\", #{});"),
("create_service", "let km = kubernetes_manager_new(\"test\"); km.create_service(\"test-svc\", #{}, 80, 80);"),
// Get methods (object-oriented)
("get_pod", "let km = kubernetes_manager_new(\"test\"); km.get_pod(\"test-pod\");"),
("get_service", "let km = kubernetes_manager_new(\"test\"); km.get_service(\"test-svc\");"),
// Delete methods (object-oriented)
("delete_pod", "let km = kubernetes_manager_new(\"test\"); km.delete_pod(\"test-pod\");"),
("delete_service", "let km = kubernetes_manager_new(\"test\"); km.delete_service(\"test-service\");"),
("delete_deployment", "let km = kubernetes_manager_new(\"test\"); km.delete_deployment(\"test-deployment\");"),
("delete_namespace", "let km = kubernetes_manager_new(\"test\"); km.delete_namespace(\"test-ns\");"),
// Utility methods
("namespace_exists", "let km = kubernetes_manager_new(\"test\"); km.namespace_exists(\"test-ns\");"),
("namespace", "let km = kubernetes_manager_new(\"test\"); namespace(km);"),
("delete_pattern", "let km = kubernetes_manager_new(\"test\"); km.delete(\"test-.*\");"),
];
for (function_name, script) in test_scripts {
println!("Testing function: {}", function_name);
let result = engine.eval::<rhai::Dynamic>(script);
// The function should be registered (not get a "function not found" error)
// It may fail due to no Kubernetes cluster, but that's expected
match result {
Ok(_) => {
println!("Function {} executed successfully", function_name);
}
Err(e) => {
let error_msg = e.to_string();
// Should not be a "function not found" error
assert!(
!error_msg.contains("Function not found")
&& !error_msg.contains("Unknown function"),
"Function {} not registered: {}",
function_name,
error_msg
);
println!(
"Function {} failed as expected (no cluster): {}",
function_name, error_msg
);
}
}
}
}
#[test]
fn test_rhai_with_real_cluster() {
if !should_run_k8s_tests() {
println!("Skipping Rhai Kubernetes integration tests. Set KUBERNETES_TEST_ENABLED=1 to enable.");
return;
}
let mut engine = Engine::new();
register_kubernetes_module(&mut engine).unwrap();
// Test basic functionality with a real cluster
let script = r#"
let km = kubernetes_manager_new("default");
let ns = namespace(km);
ns
"#;
let result = engine.eval::<String>(script);
match result {
Ok(namespace) => {
assert_eq!(namespace, "default");
println!("Successfully got namespace from Rhai: {}", namespace);
}
Err(e) => {
println!("Failed to execute Rhai script with real cluster: {}", e);
// Don't fail the test if we can't connect to cluster
}
}
}
#[test]
fn test_rhai_pods_list() {
if !should_run_k8s_tests() {
return;
}
let mut engine = Engine::new();
register_kubernetes_module(&mut engine).unwrap();
let script = r#"
let km = kubernetes_manager_new("default");
let pods = pods_list(km);
pods.len()
"#;
let result = engine.eval::<i64>(script);
match result {
Ok(count) => {
assert!(count >= 0);
println!("Successfully listed {} pods from Rhai", count);
}
Err(e) => {
println!("Failed to list pods from Rhai: {}", e);
// Don't fail the test if we can't connect to cluster
}
}
}
#[test]
fn test_rhai_resource_counts() {
if !should_run_k8s_tests() {
return;
}
let mut engine = Engine::new();
register_kubernetes_module(&mut engine).unwrap();
let script = r#"
let km = kubernetes_manager_new("default");
let counts = resource_counts(km);
counts
"#;
let result = engine.eval::<rhai::Map>(script);
match result {
Ok(counts) => {
println!("Successfully got resource counts from Rhai: {:?}", counts);
// Verify expected keys are present
assert!(counts.contains_key("pods"));
assert!(counts.contains_key("services"));
assert!(counts.contains_key("deployments"));
}
Err(e) => {
println!("Failed to get resource counts from Rhai: {}", e);
// Don't fail the test if we can't connect to cluster
}
}
}
#[test]
fn test_rhai_namespace_operations() {
if !should_run_k8s_tests() {
return;
}
let mut engine = Engine::new();
register_kubernetes_module(&mut engine).unwrap();
// Test namespace existence check
let script = r#"
let km = kubernetes_manager_new("default");
let exists = namespace_exists(km, "default");
exists
"#;
let result = engine.eval::<bool>(script);
match result {
Ok(exists) => {
assert!(exists, "Default namespace should exist");
println!(
"Successfully checked namespace existence from Rhai: {}",
exists
);
}
Err(e) => {
println!("Failed to check namespace existence from Rhai: {}", e);
// Don't fail the test if we can't connect to cluster
}
}
}
#[test]
fn test_rhai_error_handling() {
if !should_run_k8s_tests() {
println!(
"Skipping Rhai error handling tests. Set KUBERNETES_TEST_ENABLED=1 to enable."
);
return;
}
let mut engine = Engine::new();
register_kubernetes_module(&mut engine).unwrap();
// Test that errors are properly converted to Rhai errors
// Use a namespace that will definitely cause an error when trying to list pods
let script = r#"
let km = kubernetes_manager_new("nonexistent-namespace-12345");
pods_list(km)
"#;
let result = engine.eval::<rhai::Array>(script);
// The test might succeed if no cluster is available, which is fine
match result {
Ok(_) => {
println!("No error occurred - possibly no cluster available, which is acceptable");
}
Err(e) => {
let error_msg = e.to_string();
println!("Got expected error: {}", error_msg);
assert!(
error_msg.contains("Kubernetes error")
|| error_msg.contains("error")
|| error_msg.contains("not found")
);
}
}
}
#[test]
fn test_rhai_script_files_exist() {
// Test that our Rhai test files exist and are readable
let test_files = [
"tests/rhai/basic_kubernetes.rhai",
"tests/rhai/namespace_operations.rhai",
"tests/rhai/resource_management.rhai",
"tests/rhai/run_all_tests.rhai",
];
for test_file in test_files {
let path = Path::new(test_file);
assert!(path.exists(), "Rhai test file should exist: {}", test_file);
// Try to read the file to ensure it's valid
let content = fs::read_to_string(path)
.unwrap_or_else(|e| panic!("Failed to read {}: {}", test_file, e));
assert!(
!content.is_empty(),
"Rhai test file should not be empty: {}",
test_file
);
assert!(
content.contains("print("),
"Rhai test file should contain print statements: {}",
test_file
);
}
}
#[test]
fn test_basic_rhai_script_syntax() {
// Test that we can at least parse our basic Rhai script
let mut engine = Engine::new();
register_kubernetes_module(&mut engine).unwrap();
// Simple script that should parse without errors
let script = r#"
print("Testing Kubernetes Rhai integration");
let functions = ["kubernetes_manager_new", "pods_list", "namespace"];
for func in functions {
print("Function: " + func);
}
print("Basic syntax test completed");
"#;
let result = engine.eval::<()>(script);
assert!(
result.is_ok(),
"Basic Rhai script should parse and execute: {:?}",
result
);
}
#[test]
fn test_rhai_script_execution_with_cluster() {
if !should_run_k8s_tests() {
println!(
"Skipping Rhai script execution test. Set KUBERNETES_TEST_ENABLED=1 to enable."
);
return;
}
let mut engine = Engine::new();
register_kubernetes_module(&mut engine).unwrap();
// Try to execute a simple script that creates a manager
let script = r#"
let km = kubernetes_manager_new("default");
let ns = namespace(km);
print("Created manager for namespace: " + ns);
ns
"#;
let result = engine.eval::<String>(script);
match result {
Ok(namespace) => {
assert_eq!(namespace, "default");
println!("Successfully executed Rhai script with cluster");
}
Err(e) => {
println!(
"Rhai script execution failed (expected if no cluster): {}",
e
);
// Don't fail the test if we can't connect to cluster
}
}
}
}

View File

@@ -0,0 +1,303 @@
//! Unit tests for SAL Kubernetes
//!
//! These tests focus on testing individual components and error handling
//! without requiring a live Kubernetes cluster.
use sal_kubernetes::KubernetesError;
#[test]
fn test_kubernetes_error_creation() {
let config_error = KubernetesError::config_error("Test config error");
assert!(matches!(config_error, KubernetesError::ConfigError(_)));
assert_eq!(
config_error.to_string(),
"Configuration error: Test config error"
);
let operation_error = KubernetesError::operation_error("Test operation error");
assert!(matches!(
operation_error,
KubernetesError::OperationError(_)
));
assert_eq!(
operation_error.to_string(),
"Operation failed: Test operation error"
);
let namespace_error = KubernetesError::namespace_error("Test namespace error");
assert!(matches!(
namespace_error,
KubernetesError::NamespaceError(_)
));
assert_eq!(
namespace_error.to_string(),
"Namespace error: Test namespace error"
);
let permission_error = KubernetesError::permission_denied("Test permission error");
assert!(matches!(
permission_error,
KubernetesError::PermissionDenied(_)
));
assert_eq!(
permission_error.to_string(),
"Permission denied: Test permission error"
);
let timeout_error = KubernetesError::timeout("Test timeout error");
assert!(matches!(timeout_error, KubernetesError::Timeout(_)));
assert_eq!(
timeout_error.to_string(),
"Operation timed out: Test timeout error"
);
}
#[test]
fn test_regex_error_conversion() {
use regex::Regex;
// Test invalid regex pattern
let invalid_pattern = "[invalid";
let regex_result = Regex::new(invalid_pattern);
assert!(regex_result.is_err());
// Convert to KubernetesError
let k8s_error = KubernetesError::from(regex_result.unwrap_err());
assert!(matches!(k8s_error, KubernetesError::RegexError(_)));
}
#[test]
fn test_error_display() {
let errors = vec![
KubernetesError::config_error("Config test"),
KubernetesError::operation_error("Operation test"),
KubernetesError::namespace_error("Namespace test"),
KubernetesError::permission_denied("Permission test"),
KubernetesError::timeout("Timeout test"),
];
for error in errors {
let error_string = error.to_string();
assert!(!error_string.is_empty());
assert!(error_string.contains("test"));
}
}
#[cfg(feature = "rhai")]
#[test]
fn test_rhai_module_registration() {
use rhai::Engine;
use sal_kubernetes::rhai::register_kubernetes_module;
let mut engine = Engine::new();
let result = register_kubernetes_module(&mut engine);
assert!(
result.is_ok(),
"Failed to register Kubernetes module: {:?}",
result
);
}
#[cfg(feature = "rhai")]
#[test]
fn test_rhai_functions_registered() {
use rhai::Engine;
use sal_kubernetes::rhai::register_kubernetes_module;
let mut engine = Engine::new();
register_kubernetes_module(&mut engine).unwrap();
// Test that functions are registered by checking if they exist in the engine
// We can't actually call async functions without a runtime, so we just verify registration
// Check that the main functions are registered by looking for them in the engine
let function_names = vec![
"kubernetes_manager_new",
"pods_list",
"services_list",
"deployments_list",
"delete",
"namespace_create",
"namespace_exists",
];
for function_name in function_names {
// Try to parse a script that references the function
// This will succeed if the function is registered, even if we don't call it
let script = format!("let f = {};", function_name);
let result = engine.compile(&script);
assert!(
result.is_ok(),
"Function '{}' should be registered in the engine",
function_name
);
}
}
#[test]
fn test_namespace_validation() {
// Test valid namespace names
let valid_names = vec!["default", "kube-system", "my-app", "test123"];
for name in valid_names {
assert!(!name.is_empty());
assert!(name.chars().all(|c| c.is_alphanumeric() || c == '-'));
}
}
#[test]
fn test_resource_name_patterns() {
use regex::Regex;
// Test common patterns that might be used with the delete function
let patterns = vec![
r"test-.*", // Match anything starting with "test-"
r".*-temp$", // Match anything ending with "-temp"
r"^pod-\d+$", // Match "pod-" followed by digits
r"app-[a-z]+", // Match "app-" followed by lowercase letters
];
for pattern in patterns {
let regex = Regex::new(pattern);
assert!(regex.is_ok(), "Pattern '{}' should be valid", pattern);
let regex = regex.unwrap();
// Test some example matches based on the pattern
match pattern {
r"test-.*" => {
assert!(regex.is_match("test-pod"));
assert!(regex.is_match("test-service"));
assert!(!regex.is_match("prod-pod"));
}
r".*-temp$" => {
assert!(regex.is_match("my-pod-temp"));
assert!(regex.is_match("service-temp"));
assert!(!regex.is_match("temp-pod"));
}
r"^pod-\d+$" => {
assert!(regex.is_match("pod-123"));
assert!(regex.is_match("pod-1"));
assert!(!regex.is_match("pod-abc"));
assert!(!regex.is_match("service-123"));
}
r"app-[a-z]+" => {
assert!(regex.is_match("app-frontend"));
assert!(regex.is_match("app-backend"));
assert!(!regex.is_match("app-123"));
assert!(!regex.is_match("service-frontend"));
}
_ => {}
}
}
}
#[test]
fn test_invalid_regex_patterns() {
use regex::Regex;
// Test invalid regex patterns that should fail
let invalid_patterns = vec![
"[invalid", // Unclosed bracket
"*invalid", // Invalid quantifier
"(?invalid)", // Invalid group
"\\", // Incomplete escape
];
for pattern in invalid_patterns {
let regex = Regex::new(pattern);
assert!(regex.is_err(), "Pattern '{}' should be invalid", pattern);
}
}
#[test]
fn test_kubernetes_config_creation() {
use sal_kubernetes::KubernetesConfig;
use std::time::Duration;
// Test default configuration
let default_config = KubernetesConfig::default();
assert_eq!(default_config.operation_timeout, Duration::from_secs(30));
assert_eq!(default_config.max_retries, 3);
assert_eq!(default_config.rate_limit_rps, 10);
assert_eq!(default_config.rate_limit_burst, 20);
// Test custom configuration
let custom_config = KubernetesConfig::new()
.with_timeout(Duration::from_secs(60))
.with_retries(5, Duration::from_secs(2), Duration::from_secs(60))
.with_rate_limit(50, 100);
assert_eq!(custom_config.operation_timeout, Duration::from_secs(60));
assert_eq!(custom_config.max_retries, 5);
assert_eq!(custom_config.retry_base_delay, Duration::from_secs(2));
assert_eq!(custom_config.retry_max_delay, Duration::from_secs(60));
assert_eq!(custom_config.rate_limit_rps, 50);
assert_eq!(custom_config.rate_limit_burst, 100);
// Test pre-configured profiles
let high_throughput = KubernetesConfig::high_throughput();
assert_eq!(high_throughput.rate_limit_rps, 50);
assert_eq!(high_throughput.rate_limit_burst, 100);
let low_latency = KubernetesConfig::low_latency();
assert_eq!(low_latency.operation_timeout, Duration::from_secs(10));
assert_eq!(low_latency.max_retries, 2);
let development = KubernetesConfig::development();
assert_eq!(development.operation_timeout, Duration::from_secs(120));
assert_eq!(development.rate_limit_rps, 100);
}
#[test]
fn test_retryable_error_detection() {
use kube::Error as KubeError;
use sal_kubernetes::kubernetes_manager::is_retryable_error;
// Test that the function exists and works with basic error types
// Note: We can't easily create all error types, so we test what we can
// Test API errors with different status codes
let api_error_500 = KubeError::Api(kube::core::ErrorResponse {
status: "Failure".to_string(),
message: "Internal server error".to_string(),
reason: "InternalError".to_string(),
code: 500,
});
assert!(
is_retryable_error(&api_error_500),
"500 errors should be retryable"
);
let api_error_429 = KubeError::Api(kube::core::ErrorResponse {
status: "Failure".to_string(),
message: "Too many requests".to_string(),
reason: "TooManyRequests".to_string(),
code: 429,
});
assert!(
is_retryable_error(&api_error_429),
"429 errors should be retryable"
);
let api_error_404 = KubeError::Api(kube::core::ErrorResponse {
status: "Failure".to_string(),
message: "Not found".to_string(),
reason: "NotFound".to_string(),
code: 404,
});
assert!(
!is_retryable_error(&api_error_404),
"404 errors should not be retryable"
);
let api_error_400 = KubeError::Api(kube::core::ErrorResponse {
status: "Failure".to_string(),
message: "Bad request".to_string(),
reason: "BadRequest".to_string(),
code: 400,
});
assert!(
!is_retryable_error(&api_error_400),
"400 errors should not be retryable"
);
}

View File

@@ -0,0 +1,32 @@
[package]
name = "sal-os"
version = "0.1.0"
edition = "2021"
authors = ["PlanetFirst <info@incubaid.com>"]
description = "SAL OS - Operating system interaction utilities with cross-platform abstraction"
repository = "https://git.threefold.info/herocode/sal"
license = "Apache-2.0"
keywords = ["system", "os", "filesystem", "download", "package-management"]
categories = ["os", "filesystem", "api-bindings"]
[dependencies]
# Core dependencies for file system operations
dirs = { workspace = true }
glob = { workspace = true }
libc = { workspace = true }
# Error handling
thiserror = { workspace = true }
# Rhai scripting support
rhai = { workspace = true }
# Optional features for specific OS functionality
[target.'cfg(unix)'.dependencies]
nix = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }

View File

@@ -0,0 +1,100 @@
# SAL OS Package (`sal-os`)
The `sal-os` package provides a comprehensive suite of operating system interaction utilities. It offers a cross-platform abstraction layer for common OS-level tasks, simplifying system programming in Rust.
## Features
- **File System Operations**: Comprehensive file and directory manipulation
- **Download Utilities**: File downloading with automatic extraction support
- **Package Management**: System package manager integration
- **Platform Detection**: Cross-platform OS and architecture detection
- **Rhai Integration**: Full scripting support for all OS operations
## Modules
- `fs`: File system operations (create, copy, delete, find, etc.)
- `download`: File downloading and basic installation
- `package`: System package management
- `platform`: Platform and architecture detection
## Usage
Add this to your `Cargo.toml`:
```toml
[dependencies]
sal-os = "0.1.0"
```
### File System Operations
```rust
use sal_os::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create directory
fs::mkdir("my_dir")?;
// Write and read files
fs::file_write("my_dir/example.txt", "Hello from SAL!")?;
let content = fs::file_read("my_dir/example.txt")?;
// Find files
let files = fs::find_files(".", "*.txt")?;
Ok(())
}
```
### Download Operations
```rust
use sal_os::download;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Download and extract archive
let path = download::download("https://example.com/archive.tar.gz", "/tmp", 1024)?;
// Download specific file
download::download_file("https://example.com/script.sh", "/tmp/script.sh", 0)?;
download::chmod_exec("/tmp/script.sh")?;
Ok(())
}
```
### Platform Detection
```rust
use sal_os::platform;
fn main() {
if platform::is_linux() {
println!("Running on Linux");
}
if platform::is_arm() {
println!("ARM architecture detected");
}
}
```
## Rhai Integration
The package provides full Rhai scripting support:
```rhai
// File operations
mkdir("test_dir");
file_write("test_dir/hello.txt", "Hello World!");
let content = file_read("test_dir/hello.txt");
// Download operations
download("https://example.com/file.zip", "/tmp", 0);
chmod_exec("/tmp/script.sh");
// Platform detection
if is_linux() {
print("Running on Linux");
}
```

View File

@@ -0,0 +1,522 @@
use std::error::Error;
use std::fmt;
use std::fs;
use std::io;
use std::path::Path;
use std::process::Command;
// Define a custom error type for download operations
#[derive(Debug)]
pub enum DownloadError {
CreateDirectoryFailed(io::Error),
CurlExecutionFailed(io::Error),
DownloadFailed(String),
FileMetadataError(io::Error),
FileTooSmall(i64, i64),
RemoveFileFailed(io::Error),
ExtractionFailed(String),
CommandExecutionFailed(io::Error),
InvalidUrl(String),
NotAFile(String),
PlatformNotSupported(String),
InstallationFailed(String),
}
// Implement Display for DownloadError
impl fmt::Display for DownloadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DownloadError::CreateDirectoryFailed(e) => {
write!(f, "Error creating directories: {}", e)
}
DownloadError::CurlExecutionFailed(e) => write!(f, "Error executing curl: {}", e),
DownloadError::DownloadFailed(url) => write!(f, "Error downloading url: {}", url),
DownloadError::FileMetadataError(e) => write!(f, "Error getting file metadata: {}", e),
DownloadError::FileTooSmall(size, min) => write!(
f,
"Error: Downloaded file is too small ({}KB < {}KB)",
size, min
),
DownloadError::RemoveFileFailed(e) => write!(f, "Error removing file: {}", e),
DownloadError::ExtractionFailed(e) => write!(f, "Error extracting archive: {}", e),
DownloadError::CommandExecutionFailed(e) => write!(f, "Error executing command: {}", e),
DownloadError::InvalidUrl(url) => write!(f, "Invalid URL: {}", url),
DownloadError::NotAFile(path) => write!(f, "Not a file: {}", path),
DownloadError::PlatformNotSupported(msg) => write!(f, "{}", msg),
DownloadError::InstallationFailed(msg) => write!(f, "{}", msg),
}
}
}
// Implement Error trait for DownloadError
impl Error for DownloadError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
DownloadError::CreateDirectoryFailed(e) => Some(e),
DownloadError::CurlExecutionFailed(e) => Some(e),
DownloadError::FileMetadataError(e) => Some(e),
DownloadError::RemoveFileFailed(e) => Some(e),
DownloadError::CommandExecutionFailed(e) => Some(e),
_ => None,
}
}
}
/**
* Download a file from URL to destination using the curl command.
* This function is primarily intended for downloading archives that will be extracted
* to a directory.
*
* # Arguments
*
* * `url` - The URL to download from
* * `dest` - The destination directory where the file will be saved or extracted
* * `min_size_kb` - Minimum required file size in KB (0 for no minimum)
*
* # Returns
*
* * `Ok(String)` - The path where the file was saved or extracted
* * `Err(DownloadError)` - An error if the download failed
*
* # Examples
*
* ```no_run
* use sal_os::download;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Download a file with no minimum size requirement
* let path = download("https://example.com/file.txt", "/tmp/", 0)?;
*
* // Download a file with minimum size requirement of 100KB
* let path = download("https://example.com/file.zip", "/tmp/", 100)?;
*
* Ok(())
* }
* ```
*
* # Notes
*
* If the URL ends with .tar.gz, .tgz, .tar, or .zip, the file will be automatically
* extracted to the destination directory.
*/
pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, DownloadError> {
// Create parent directories if they don't exist
let dest_path = Path::new(dest);
fs::create_dir_all(dest_path).map_err(DownloadError::CreateDirectoryFailed)?;
// Extract filename from URL
let filename = match url.split('/').last() {
Some(name) => name,
None => {
return Err(DownloadError::InvalidUrl(
"cannot extract filename".to_string(),
))
}
};
// Create a full path for the downloaded file
let file_path = format!("{}/{}", dest.trim_end_matches('/'), filename);
// Create a temporary path for downloading
let temp_path = format!("{}.download", file_path);
// Use curl to download the file with progress bar
println!("Downloading {} to {}", url, file_path);
let output = Command::new("curl")
.args(&[
"--progress-bar",
"--location",
"--fail",
"--output",
&temp_path,
url,
])
.status()
.map_err(DownloadError::CurlExecutionFailed)?;
if !output.success() {
return Err(DownloadError::DownloadFailed(url.to_string()));
}
// Show file size after download
match fs::metadata(&temp_path) {
Ok(metadata) => {
let size_bytes = metadata.len();
let size_kb = size_bytes / 1024;
let size_mb = size_kb / 1024;
if size_mb > 1 {
println!(
"Download complete! File size: {:.2} MB",
size_bytes as f64 / (1024.0 * 1024.0)
);
} else {
println!(
"Download complete! File size: {:.2} KB",
size_bytes as f64 / 1024.0
);
}
}
Err(_) => println!("Download complete!"),
}
// Check file size if minimum size is specified
if min_size_kb > 0 {
let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?;
let size_kb = metadata.len() as i64 / 1024;
if size_kb < min_size_kb {
fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
return Err(DownloadError::FileTooSmall(size_kb, min_size_kb));
}
}
// Check if it's a compressed file that needs extraction
let lower_url = url.to_lowercase();
let is_archive = lower_url.ends_with(".tar.gz")
|| lower_url.ends_with(".tgz")
|| lower_url.ends_with(".tar")
|| lower_url.ends_with(".zip");
if is_archive {
// Extract the file using the appropriate command with progress indication
println!("Extracting {} to {}", temp_path, dest);
let output = if lower_url.ends_with(".zip") {
Command::new("unzip")
.args(&["-o", &temp_path, "-d", dest]) // Removed -q for verbosity
.status()
} else if lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") {
Command::new("tar")
.args(&["-xzvf", &temp_path, "-C", dest]) // Added v for verbosity
.status()
} else {
Command::new("tar")
.args(&["-xvf", &temp_path, "-C", dest]) // Added v for verbosity
.status()
};
match output {
Ok(status) => {
if !status.success() {
return Err(DownloadError::ExtractionFailed(
"Error extracting archive".to_string(),
));
}
}
Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
}
// Show number of extracted files
match fs::read_dir(dest) {
Ok(entries) => {
let count = entries.count();
println!("Extraction complete! Extracted {} files/directories", count);
}
Err(_) => println!("Extraction complete!"),
}
// Remove the temporary file
fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
Ok(dest.to_string())
} else {
// Just rename the temporary file to the final destination
fs::rename(&temp_path, &file_path).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
Ok(file_path)
}
}
/**
* Download a file from URL to a specific file destination using the curl command.
*
* # Arguments
*
* * `url` - The URL to download from
* * `dest` - The destination file path where the file will be saved
* * `min_size_kb` - Minimum required file size in KB (0 for no minimum)
*
* # Returns
*
* * `Ok(String)` - The path where the file was saved
* * `Err(DownloadError)` - An error if the download failed
*
* # Examples
*
* ```no_run
* use sal_os::download_file;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Download a file with no minimum size requirement
* let path = download_file("https://example.com/file.txt", "/tmp/file.txt", 0)?;
*
* // Download a file with minimum size requirement of 100KB
* let path = download_file("https://example.com/file.zip", "/tmp/file.zip", 100)?;
*
* Ok(())
* }
* ```
*/
pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, DownloadError> {
// Create parent directories if they don't exist
let dest_path = Path::new(dest);
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent).map_err(DownloadError::CreateDirectoryFailed)?;
}
// Create a temporary path for downloading
let temp_path = format!("{}.download", dest);
// Use curl to download the file with progress bar
println!("Downloading {} to {}", url, dest);
let output = Command::new("curl")
.args(&[
"--progress-bar",
"--location",
"--fail",
"--output",
&temp_path,
url,
])
.status()
.map_err(DownloadError::CurlExecutionFailed)?;
if !output.success() {
return Err(DownloadError::DownloadFailed(url.to_string()));
}
// Show file size after download
match fs::metadata(&temp_path) {
Ok(metadata) => {
let size_bytes = metadata.len();
let size_kb = size_bytes / 1024;
let size_mb = size_kb / 1024;
if size_mb > 1 {
println!(
"Download complete! File size: {:.2} MB",
size_bytes as f64 / (1024.0 * 1024.0)
);
} else {
println!(
"Download complete! File size: {:.2} KB",
size_bytes as f64 / 1024.0
);
}
}
Err(_) => println!("Download complete!"),
}
// Check file size if minimum size is specified
if min_size_kb > 0 {
let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?;
let size_kb = metadata.len() as i64 / 1024;
if size_kb < min_size_kb {
fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
return Err(DownloadError::FileTooSmall(size_kb, min_size_kb));
}
}
// Rename the temporary file to the final destination
fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
Ok(dest.to_string())
}
/**
* Make a file executable (equivalent to chmod +x).
*
* # Arguments
*
* * `path` - The path to the file to make executable
*
* # Returns
*
* * `Ok(String)` - A success message including the path
* * `Err(DownloadError)` - An error if the operation failed
*
* # Examples
*
* ```no_run
* use sal_os::chmod_exec;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Make a file executable
* chmod_exec("/path/to/file")?;
* Ok(())
* }
* ```
*/
pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
let path_obj = Path::new(path);
// Check if the path exists and is a file
if !path_obj.exists() {
return Err(DownloadError::NotAFile(format!(
"Path does not exist: {}",
path
)));
}
if !path_obj.is_file() {
return Err(DownloadError::NotAFile(format!(
"Path is not a file: {}",
path
)));
}
// Get current permissions
let metadata = fs::metadata(path).map_err(DownloadError::FileMetadataError)?;
let mut permissions = metadata.permissions();
// Set executable bit for user, group, and others
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = permissions.mode();
// Add executable bit for user, group, and others (equivalent to +x)
let new_mode = mode | 0o111;
permissions.set_mode(new_mode);
}
#[cfg(not(unix))]
{
// On non-Unix platforms, we can't set executable bit directly
// Just return success with a warning
return Ok(format!(
"Made {} executable (note: non-Unix platform, may not be fully supported)",
path
));
}
// Apply the new permissions
fs::set_permissions(path, permissions).map_err(|e| {
DownloadError::CommandExecutionFailed(io::Error::new(
io::ErrorKind::Other,
format!("Failed to set executable permissions: {}", e),
))
})?;
Ok(format!("Made {} executable", path))
}
/**
* Download a file and install it if it's a supported package format.
*
* # Arguments
*
* * `url` - The URL to download from
* * `min_size_kb` - Minimum required file size in KB (0 for no minimum)
*
* # Returns
*
* * `Ok(String)` - The path where the file was saved or extracted
* * `Err(DownloadError)` - An error if the download or installation failed
*
* # Examples
*
* ```no_run
* use sal_os::download_install;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Download and install a .deb package
* let result = download_install("https://example.com/package.deb", 100)?;
* Ok(())
* }
* ```
*
* # Notes
*
* Currently only supports .deb packages on Debian-based systems.
* For other file types, it behaves the same as the download function.
*/
pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadError> {
// Extract filename from URL
let filename = match url.split('/').last() {
Some(name) => name,
None => {
return Err(DownloadError::InvalidUrl(
"cannot extract filename".to_string(),
))
}
};
// Create a proper destination path
let dest_path = format!("/tmp/{}", filename);
// Check if it's a compressed file that needs extraction
let lower_url = url.to_lowercase();
let is_archive = lower_url.ends_with(".tar.gz")
|| lower_url.ends_with(".tgz")
|| lower_url.ends_with(".tar")
|| lower_url.ends_with(".zip");
let download_result = if is_archive {
// For archives, use the directory-based download function
download(url, "/tmp", min_size_kb)?
} else {
// For regular files, use the file-specific download function
download_file(url, &dest_path, min_size_kb)?
};
// Check if the downloaded result is a file
let path = Path::new(&dest_path);
if !path.is_file() {
return Ok(download_result); // Not a file, might be an extracted directory
}
// Check if it's a .deb package
if dest_path.to_lowercase().ends_with(".deb") {
// Check if we're on a Debian-based platform
let platform_check = Command::new("sh")
.arg("-c")
.arg("command -v dpkg > /dev/null && command -v apt > /dev/null || test -f /etc/debian_version")
.status();
match platform_check {
Ok(status) => {
if !status.success() {
return Err(DownloadError::PlatformNotSupported(
"Cannot install .deb package: not on a Debian-based system".to_string(),
));
}
}
Err(_) => {
return Err(DownloadError::PlatformNotSupported(
"Failed to check system compatibility for .deb installation".to_string(),
))
}
}
// Install the .deb package non-interactively
println!("Installing package: {}", dest_path);
let install_result = Command::new("sudo")
.args(&["dpkg", "--install", &dest_path])
.status();
match install_result {
Ok(status) => {
if !status.success() {
// If dpkg fails, try to fix dependencies and retry
println!("Attempting to resolve dependencies...");
let fix_deps = Command::new("sudo")
.args(&["apt-get", "install", "-f", "-y"])
.status();
if let Ok(fix_status) = fix_deps {
if !fix_status.success() {
return Err(DownloadError::InstallationFailed(
"Failed to resolve package dependencies".to_string(),
));
}
} else {
return Err(DownloadError::InstallationFailed(
"Failed to resolve package dependencies".to_string(),
));
}
}
println!("Package installation completed successfully");
}
Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
}
}
Ok(download_result)
}

1185
packages/system/os/src/fs.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
pub mod download;
pub mod fs;
pub mod package;
pub mod platform;
// Re-export all public functions and types
pub use download::*;
pub use fs::*;
pub use package::*;
pub use platform::*;
// Rhai integration module
pub mod rhai;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PlatformError {
#[error("{0}: {1}")]
Generic(String, String),
}
impl PlatformError {
pub fn new(kind: &str, message: &str) -> Self {
PlatformError::Generic(kind.to_string(), message.to_string())
}
}
#[cfg(target_os = "macos")]
pub fn is_osx() -> bool {
true
}
#[cfg(not(target_os = "macos"))]
pub fn is_osx() -> bool {
false
}
#[cfg(target_os = "linux")]
pub fn is_linux() -> bool {
true
}
#[cfg(not(target_os = "linux"))]
pub fn is_linux() -> bool {
false
}
#[cfg(target_arch = "aarch64")]
pub fn is_arm() -> bool {
true
}
#[cfg(not(target_arch = "aarch64"))]
pub fn is_arm() -> bool {
false
}
#[cfg(target_arch = "x86_64")]
pub fn is_x86() -> bool {
true
}
#[cfg(not(target_arch = "x86_64"))]
pub fn is_x86() -> bool {
false
}
pub fn check_linux_x86() -> Result<(), PlatformError> {
if is_linux() && is_x86() {
Ok(())
} else {
Err(PlatformError::new(
"Platform Check Error",
"This operation is only supported on Linux x86_64.",
))
}
}
pub fn check_macos_arm() -> Result<(), PlatformError> {
if is_osx() && is_arm() {
Ok(())
} else {
Err(PlatformError::new(
"Platform Check Error",
"This operation is only supported on macOS ARM.",
))
}
}

View File

@@ -0,0 +1,424 @@
//! Rhai wrappers for OS module functions
//!
//! This module provides Rhai wrappers for the functions in the OS module.
use crate::package::PackHero;
use crate::{download as dl, fs, package};
use rhai::{Array, Engine, EvalAltResult, Position};
/// A trait for converting a Result to a Rhai-compatible error
pub trait ToRhaiError<T> {
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>>;
}
impl<T, E: std::error::Error> ToRhaiError<T> for Result<T, E> {
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>> {
self.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
e.to_string().into(),
Position::NONE,
))
})
}
}
/// Register OS module functions with the Rhai engine
///
/// # Arguments
///
/// * `engine` - The Rhai engine to register the functions with
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_os_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register file system functions
engine.register_fn("copy", copy);
engine.register_fn("copy_bin", copy_bin);
engine.register_fn("exist", exist);
engine.register_fn("find_file", find_file);
engine.register_fn("find_files", find_files);
engine.register_fn("find_dir", find_dir);
engine.register_fn("find_dirs", find_dirs);
engine.register_fn("delete", delete);
engine.register_fn("mkdir", mkdir);
engine.register_fn("file_size", file_size);
engine.register_fn("rsync", rsync);
engine.register_fn("chdir", chdir);
engine.register_fn("file_read", file_read);
engine.register_fn("file_write", file_write);
engine.register_fn("file_write_append", file_write_append);
// Register command check functions
engine.register_fn("which", which);
engine.register_fn("cmd_ensure_exists", cmd_ensure_exists);
// Register download functions
engine.register_fn("download", download);
engine.register_fn("download_file", download_file);
engine.register_fn("download_install", download_install);
engine.register_fn("chmod_exec", chmod_exec);
// Register move function
engine.register_fn("mv", mv);
// Register package management functions
engine.register_fn("package_install", package_install);
engine.register_fn("package_remove", package_remove);
engine.register_fn("package_update", package_update);
engine.register_fn("package_upgrade", package_upgrade);
engine.register_fn("package_list", package_list);
engine.register_fn("package_search", package_search);
engine.register_fn("package_is_installed", package_is_installed);
engine.register_fn("package_set_debug", package_set_debug);
engine.register_fn("package_platform", package_platform);
// Register platform detection functions
engine.register_fn("platform_is_osx", platform_is_osx);
engine.register_fn("platform_is_linux", platform_is_linux);
engine.register_fn("platform_is_arm", platform_is_arm);
engine.register_fn("platform_is_x86", platform_is_x86);
engine.register_fn("platform_check_linux_x86", platform_check_linux_x86);
engine.register_fn("platform_check_macos_arm", platform_check_macos_arm);
Ok(())
}
//
// File System Function Wrappers
//
/// Wrapper for fs::copy
///
/// Recursively copy a file or directory from source to destination.
pub fn copy(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> {
fs::copy(src, dest).to_rhai_error()
}
/// Wrapper for fs::copy_bin
///
/// Copy a binary to the correct location based on OS and user privileges.
pub fn copy_bin(src: &str) -> Result<String, Box<EvalAltResult>> {
fs::copy_bin(src).to_rhai_error()
}
/// Wrapper for fs::exist
///
/// Check if a file or directory exists.
pub fn exist(path: &str) -> bool {
fs::exist(path)
}
/// Wrapper for fs::find_file
///
/// Find a file in a directory (with support for wildcards).
pub fn find_file(dir: &str, filename: &str) -> Result<String, Box<EvalAltResult>> {
fs::find_file(dir, filename).to_rhai_error()
}
/// Wrapper for fs::find_files
///
/// Find multiple files in a directory (recursive, with support for wildcards).
pub fn find_files(dir: &str, filename: &str) -> Result<Array, Box<EvalAltResult>> {
let files = fs::find_files(dir, filename).to_rhai_error()?;
// Convert Vec<String> to Rhai Array
let mut array = Array::new();
for file in files {
array.push(file.into());
}
Ok(array)
}
/// Wrapper for fs::find_dir
///
/// Find a directory in a parent directory (with support for wildcards).
pub fn find_dir(dir: &str, dirname: &str) -> Result<String, Box<EvalAltResult>> {
fs::find_dir(dir, dirname).to_rhai_error()
}
/// Wrapper for fs::find_dirs
///
/// Find multiple directories in a parent directory (recursive, with support for wildcards).
pub fn find_dirs(dir: &str, dirname: &str) -> Result<Array, Box<EvalAltResult>> {
let dirs = fs::find_dirs(dir, dirname).to_rhai_error()?;
// Convert Vec<String> to Rhai Array
let mut array = Array::new();
for dir in dirs {
array.push(dir.into());
}
Ok(array)
}
/// Wrapper for fs::delete
///
/// Delete a file or directory (defensive - doesn't error if file doesn't exist).
pub fn delete(path: &str) -> Result<String, Box<EvalAltResult>> {
fs::delete(path).to_rhai_error()
}
/// Wrapper for fs::mkdir
///
/// Create a directory and all parent directories (defensive - doesn't error if directory exists).
pub fn mkdir(path: &str) -> Result<String, Box<EvalAltResult>> {
fs::mkdir(path).to_rhai_error()
}
/// Wrapper for fs::file_size
///
/// Get the size of a file in bytes.
pub fn file_size(path: &str) -> Result<i64, Box<EvalAltResult>> {
fs::file_size(path).to_rhai_error()
}
/// Wrapper for fs::rsync
///
/// Sync directories using rsync (or platform equivalent).
pub fn rsync(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> {
fs::rsync(src, dest).to_rhai_error()
}
/// Wrapper for fs::chdir
///
/// Change the current working directory.
pub fn chdir(path: &str) -> Result<String, Box<EvalAltResult>> {
fs::chdir(path).to_rhai_error()
}
/// Wrapper for fs::file_read
///
/// Read the contents of a file.
pub fn file_read(path: &str) -> Result<String, Box<EvalAltResult>> {
fs::file_read(path).to_rhai_error()
}
/// Wrapper for fs::file_write
///
/// Write content to a file (creates the file if it doesn't exist, overwrites if it does).
pub fn file_write(path: &str, content: &str) -> Result<String, Box<EvalAltResult>> {
fs::file_write(path, content).to_rhai_error()
}
/// Wrapper for fs::file_write_append
///
/// Append content to a file (creates the file if it doesn't exist).
pub fn file_write_append(path: &str, content: &str) -> Result<String, Box<EvalAltResult>> {
fs::file_write_append(path, content).to_rhai_error()
}
/// Wrapper for fs::mv
///
/// Move a file or directory from source to destination.
pub fn mv(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> {
fs::mv(src, dest).to_rhai_error()
}
//
// Download Function Wrappers
//
/// Wrapper for os::download
///
/// Download a file from URL to destination using the curl command.
pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Box<EvalAltResult>> {
dl::download(url, dest, min_size_kb).to_rhai_error()
}
/// Wrapper for os::download_file
///
/// Download a file from URL to a specific file destination using the curl command.
pub fn download_file(
url: &str,
dest: &str,
min_size_kb: i64,
) -> Result<String, Box<EvalAltResult>> {
dl::download_file(url, dest, min_size_kb).to_rhai_error()
}
/// Wrapper for os::download_install
///
/// Download a file and install it if it's a supported package format.
pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, Box<EvalAltResult>> {
dl::download_install(url, min_size_kb).to_rhai_error()
}
/// Wrapper for os::chmod_exec
///
/// Make a file executable (equivalent to chmod +x).
pub fn chmod_exec(path: &str) -> Result<String, Box<EvalAltResult>> {
dl::chmod_exec(path).to_rhai_error()
}
/// Wrapper for os::which
///
/// Check if a command exists in the system PATH.
pub fn which(command: &str) -> String {
fs::which(command)
}
/// Wrapper for os::cmd_ensure_exists
///
/// Ensure that one or more commands exist in the system PATH.
/// If any command doesn't exist, an error is thrown.
pub fn cmd_ensure_exists(commands: &str) -> Result<String, Box<EvalAltResult>> {
fs::cmd_ensure_exists(commands).to_rhai_error()
}
//
// Package Management Function Wrappers
//
/// Wrapper for os::package::PackHero::install
///
/// Install a package using the system package manager.
pub fn package_install(package: &str) -> Result<String, Box<EvalAltResult>> {
let hero = PackHero::new();
hero.install(package)
.map(|_| format!("Package '{}' installed successfully", package))
.to_rhai_error()
}
/// Wrapper for os::package::PackHero::remove
///
/// Remove a package using the system package manager.
pub fn package_remove(package: &str) -> Result<String, Box<EvalAltResult>> {
let hero = PackHero::new();
hero.remove(package)
.map(|_| format!("Package '{}' removed successfully", package))
.to_rhai_error()
}
/// Wrapper for os::package::PackHero::update
///
/// Update package lists using the system package manager.
pub fn package_update() -> Result<String, Box<EvalAltResult>> {
let hero = PackHero::new();
hero.update()
.map(|_| "Package lists updated successfully".to_string())
.to_rhai_error()
}
/// Wrapper for os::package::PackHero::upgrade
///
/// Upgrade installed packages using the system package manager.
pub fn package_upgrade() -> Result<String, Box<EvalAltResult>> {
let hero = PackHero::new();
hero.upgrade()
.map(|_| "Packages upgraded successfully".to_string())
.to_rhai_error()
}
/// Wrapper for os::package::PackHero::list_installed
///
/// List installed packages using the system package manager.
pub fn package_list() -> Result<Array, Box<EvalAltResult>> {
let hero = PackHero::new();
let packages = hero.list_installed().to_rhai_error()?;
// Convert Vec<String> to Rhai Array
let mut array = Array::new();
for package in packages {
array.push(package.into());
}
Ok(array)
}
/// Wrapper for os::package::PackHero::search
///
/// Search for packages using the system package manager.
pub fn package_search(query: &str) -> Result<Array, Box<EvalAltResult>> {
let hero = PackHero::new();
let packages = hero.search(query).to_rhai_error()?;
// Convert Vec<String> to Rhai Array
let mut array = Array::new();
for package in packages {
array.push(package.into());
}
Ok(array)
}
/// Wrapper for os::package::PackHero::is_installed
///
/// Check if a package is installed using the system package manager.
pub fn package_is_installed(package: &str) -> Result<bool, Box<EvalAltResult>> {
let hero = PackHero::new();
hero.is_installed(package).to_rhai_error()
}
// Thread-local storage for package debug flag
thread_local! {
static PACKAGE_DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false);
}
/// Set the debug mode for package management operations
pub fn package_set_debug(debug: bool) -> bool {
let mut hero = PackHero::new();
hero.set_debug(debug);
// Also set the thread-local debug flag
PACKAGE_DEBUG.with(|cell| {
*cell.borrow_mut() = debug;
});
debug
}
/// Get the current platform name for package management
pub fn package_platform() -> String {
let hero = PackHero::new();
match hero.platform() {
package::Platform::Ubuntu => "Ubuntu".to_string(),
package::Platform::MacOS => "MacOS".to_string(),
package::Platform::Unknown => "Unknown".to_string(),
}
}
//
// Platform Detection Function Wrappers
//
/// Wrapper for platform::is_osx
pub fn platform_is_osx() -> bool {
crate::platform::is_osx()
}
/// Wrapper for platform::is_linux
pub fn platform_is_linux() -> bool {
crate::platform::is_linux()
}
/// Wrapper for platform::is_arm
pub fn platform_is_arm() -> bool {
crate::platform::is_arm()
}
/// Wrapper for platform::is_x86
pub fn platform_is_x86() -> bool {
crate::platform::is_x86()
}
/// Wrapper for platform::check_linux_x86
pub fn platform_check_linux_x86() -> Result<(), Box<EvalAltResult>> {
crate::platform::check_linux_x86().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Platform Check Error: {}", e).into(),
Position::NONE,
))
})
}
/// Wrapper for platform::check_macos_arm
pub fn platform_check_macos_arm() -> Result<(), Box<EvalAltResult>> {
crate::platform::check_macos_arm().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Platform Check Error: {}", e).into(),
Position::NONE,
))
})
}

View File

@@ -0,0 +1,208 @@
use sal_os::{download, DownloadError};
use std::fs;
use tempfile::TempDir;
#[test]
fn test_chmod_exec() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_script.sh");
// Create a test file
fs::write(&test_file, "#!/bin/bash\necho 'test'").unwrap();
// Make it executable
let result = download::chmod_exec(test_file.to_str().unwrap());
assert!(result.is_ok());
// Check if file is executable (Unix only)
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let metadata = fs::metadata(&test_file).unwrap();
let permissions = metadata.permissions();
assert!(permissions.mode() & 0o111 != 0); // Check if any execute bit is set
}
}
#[test]
fn test_download_error_handling() {
let temp_dir = TempDir::new().unwrap();
// Test with invalid URL
let result = download::download("invalid-url", temp_dir.path().to_str().unwrap(), 0);
assert!(result.is_err());
// Test with non-existent domain
let result = download::download(
"https://nonexistentdomain12345.com/file.txt",
temp_dir.path().to_str().unwrap(),
0,
);
assert!(result.is_err());
}
#[test]
fn test_download_file_error_handling() {
let temp_dir = TempDir::new().unwrap();
let dest_file = temp_dir.path().join("downloaded_file.txt");
// Test with invalid URL
let result = download::download_file("invalid-url", dest_file.to_str().unwrap(), 0);
assert!(result.is_err());
// Test with non-existent domain
let result = download::download_file(
"https://nonexistentdomain12345.com/file.txt",
dest_file.to_str().unwrap(),
0,
);
assert!(result.is_err());
}
#[test]
fn test_download_install_error_handling() {
// Test with invalid URL
let result = download::download_install("invalid-url", 0);
assert!(result.is_err());
// Test with non-existent domain
let result = download::download_install("https://nonexistentdomain12345.com/package.deb", 0);
assert!(result.is_err());
}
#[test]
fn test_download_minimum_size_validation() {
let temp_dir = TempDir::new().unwrap();
// Test with a very high minimum size requirement that won't be met
// This should fail even if the URL exists
let result = download::download(
"https://httpbin.org/bytes/10", // This returns only 10 bytes
temp_dir.path().to_str().unwrap(),
1000, // Require 1000KB minimum
);
// This might succeed or fail depending on network, but we're testing the interface
// The important thing is that it doesn't panic
let _ = result;
}
#[test]
fn test_download_to_nonexistent_directory() {
// Test downloading to a directory that doesn't exist
// The download function should create parent directories
let temp_dir = TempDir::new().unwrap();
let nonexistent_dir = temp_dir.path().join("nonexistent").join("nested");
let _ = download::download(
"https://httpbin.org/status/404", // This will fail, but directory creation should work
nonexistent_dir.to_str().unwrap(),
0,
);
// The directory should be created even if download fails
assert!(nonexistent_dir.exists());
}
#[test]
fn test_chmod_exec_nonexistent_file() {
// Test chmod_exec on a file that doesn't exist
let result = download::chmod_exec("/nonexistent/path/file.sh");
assert!(result.is_err());
}
#[test]
fn test_download_file_path_validation() {
let _ = TempDir::new().unwrap();
// Test with invalid destination path
let result = download::download_file(
"https://httpbin.org/status/404",
"/invalid/path/that/does/not/exist/file.txt",
0,
);
assert!(result.is_err());
}
// Integration test that requires network access
// This test is marked with ignore so it doesn't run by default
#[test]
#[ignore]
fn test_download_real_file() {
let temp_dir = TempDir::new().unwrap();
// Download a small file from httpbin (a testing service)
let result = download::download(
"https://httpbin.org/bytes/100", // Returns 100 random bytes
temp_dir.path().to_str().unwrap(),
0,
);
if result.is_ok() {
// If download succeeded, verify the file exists
let downloaded_path = result.unwrap();
assert!(fs::metadata(&downloaded_path).is_ok());
// Verify file size is approximately correct
let metadata = fs::metadata(&downloaded_path).unwrap();
assert!(metadata.len() >= 90 && metadata.len() <= 110); // Allow some variance
}
// If download failed (network issues), that's okay for this test
}
// Integration test for download_file
#[test]
#[ignore]
fn test_download_file_real() {
let temp_dir = TempDir::new().unwrap();
let dest_file = temp_dir.path().join("test_download.bin");
// Download a small file to specific location
let result = download::download_file(
"https://httpbin.org/bytes/50",
dest_file.to_str().unwrap(),
0,
);
if result.is_ok() {
// Verify the file was created at the specified location
assert!(dest_file.exists());
// Verify file size
let metadata = fs::metadata(&dest_file).unwrap();
assert!(metadata.len() >= 40 && metadata.len() <= 60); // Allow some variance
}
}
#[test]
fn test_download_error_types() {
// DownloadError is already imported at the top
// Test that our error types can be created and displayed
let error = DownloadError::InvalidUrl("test".to_string());
assert!(!error.to_string().is_empty());
let error = DownloadError::DownloadFailed("test".to_string());
assert!(!error.to_string().is_empty());
let error = DownloadError::FileTooSmall(50, 100);
assert!(!error.to_string().is_empty());
}
#[test]
fn test_download_url_parsing() {
let temp_dir = TempDir::new().unwrap();
// Test with URL that has no filename
let result = download::download("https://example.com/", temp_dir.path().to_str().unwrap(), 0);
// Should fail with invalid URL error
assert!(result.is_err());
// Test with URL that has query parameters
let result = download::download(
"https://httpbin.org/get?param=value",
temp_dir.path().to_str().unwrap(),
0,
);
// This might succeed or fail depending on network, but shouldn't panic
let _ = result;
}

View File

@@ -0,0 +1,228 @@
use sal_os::fs;
use std::fs as std_fs;
use tempfile::TempDir;
#[test]
fn test_exist() {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
// Test directory exists
assert!(fs::exist(temp_path.to_str().unwrap()));
// Test file doesn't exist
let non_existent = temp_path.join("non_existent.txt");
assert!(!fs::exist(non_existent.to_str().unwrap()));
// Create a file and test it exists
let test_file = temp_path.join("test.txt");
std_fs::write(&test_file, "test content").unwrap();
assert!(fs::exist(test_file.to_str().unwrap()));
}
#[test]
fn test_mkdir() {
let temp_dir = TempDir::new().unwrap();
let new_dir = temp_dir.path().join("new_directory");
// Directory shouldn't exist initially
assert!(!fs::exist(new_dir.to_str().unwrap()));
// Create directory
let result = fs::mkdir(new_dir.to_str().unwrap());
assert!(result.is_ok());
// Directory should now exist
assert!(fs::exist(new_dir.to_str().unwrap()));
// Creating existing directory should not error (defensive)
let result2 = fs::mkdir(new_dir.to_str().unwrap());
assert!(result2.is_ok());
}
#[test]
fn test_file_write_and_read() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_write.txt");
let content = "Hello, World!";
// Write file
let write_result = fs::file_write(test_file.to_str().unwrap(), content);
assert!(write_result.is_ok());
// File should exist
assert!(fs::exist(test_file.to_str().unwrap()));
// Read file
let read_result = fs::file_read(test_file.to_str().unwrap());
assert!(read_result.is_ok());
assert_eq!(read_result.unwrap(), content);
}
#[test]
fn test_file_write_append() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_append.txt");
// Write initial content
let initial_content = "Line 1\n";
let append_content = "Line 2\n";
let write_result = fs::file_write(test_file.to_str().unwrap(), initial_content);
assert!(write_result.is_ok());
// Append content
let append_result = fs::file_write_append(test_file.to_str().unwrap(), append_content);
assert!(append_result.is_ok());
// Read and verify
let read_result = fs::file_read(test_file.to_str().unwrap());
assert!(read_result.is_ok());
assert_eq!(
read_result.unwrap(),
format!("{}{}", initial_content, append_content)
);
}
#[test]
fn test_file_size() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_size.txt");
let content = "Hello, World!"; // 13 bytes
// Write file
fs::file_write(test_file.to_str().unwrap(), content).unwrap();
// Check size
let size_result = fs::file_size(test_file.to_str().unwrap());
assert!(size_result.is_ok());
assert_eq!(size_result.unwrap(), 13);
}
#[test]
fn test_delete() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_delete.txt");
// Create file
fs::file_write(test_file.to_str().unwrap(), "test").unwrap();
assert!(fs::exist(test_file.to_str().unwrap()));
// Delete file
let delete_result = fs::delete(test_file.to_str().unwrap());
assert!(delete_result.is_ok());
// File should no longer exist
assert!(!fs::exist(test_file.to_str().unwrap()));
// Deleting non-existent file should not error (defensive)
let delete_result2 = fs::delete(test_file.to_str().unwrap());
assert!(delete_result2.is_ok());
}
#[test]
fn test_copy() {
let temp_dir = TempDir::new().unwrap();
let source_file = temp_dir.path().join("source.txt");
let dest_file = temp_dir.path().join("dest.txt");
let content = "Copy test content";
// Create source file
fs::file_write(source_file.to_str().unwrap(), content).unwrap();
// Copy file
let copy_result = fs::copy(source_file.to_str().unwrap(), dest_file.to_str().unwrap());
assert!(copy_result.is_ok());
// Destination should exist and have same content
assert!(fs::exist(dest_file.to_str().unwrap()));
let dest_content = fs::file_read(dest_file.to_str().unwrap()).unwrap();
assert_eq!(dest_content, content);
}
#[test]
fn test_mv() {
let temp_dir = TempDir::new().unwrap();
let source_file = temp_dir.path().join("source_mv.txt");
let dest_file = temp_dir.path().join("dest_mv.txt");
let content = "Move test content";
// Create source file
fs::file_write(source_file.to_str().unwrap(), content).unwrap();
// Move file
let mv_result = fs::mv(source_file.to_str().unwrap(), dest_file.to_str().unwrap());
assert!(mv_result.is_ok());
// Source should no longer exist, destination should exist
assert!(!fs::exist(source_file.to_str().unwrap()));
assert!(fs::exist(dest_file.to_str().unwrap()));
// Destination should have same content
let dest_content = fs::file_read(dest_file.to_str().unwrap()).unwrap();
assert_eq!(dest_content, content);
}
#[test]
fn test_which() {
// Test with a command that should exist on all systems
#[cfg(target_os = "windows")]
let existing_cmd = "cmd";
#[cfg(not(target_os = "windows"))]
let existing_cmd = "ls";
let result = fs::which(existing_cmd);
assert!(
!result.is_empty(),
"Command '{}' should exist",
existing_cmd
);
// Test with a command that shouldn't exist
let result = fs::which("nonexistentcommand12345");
assert!(result.is_empty());
}
#[test]
fn test_find_files() {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
// Create test files
fs::file_write(&temp_path.join("test1.txt").to_string_lossy(), "content1").unwrap();
fs::file_write(&temp_path.join("test2.txt").to_string_lossy(), "content2").unwrap();
fs::file_write(
&temp_path.join("other.log").to_string_lossy(),
"log content",
)
.unwrap();
// Find .txt files
let txt_files = fs::find_files(temp_path.to_str().unwrap(), "*.txt");
assert!(txt_files.is_ok());
let files = txt_files.unwrap();
assert_eq!(files.len(), 2);
// Find all files
let all_files = fs::find_files(temp_path.to_str().unwrap(), "*");
assert!(all_files.is_ok());
let files = all_files.unwrap();
assert!(files.len() >= 3); // At least our 3 files
}
#[test]
fn test_find_dirs() {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
// Create test directories
fs::mkdir(&temp_path.join("dir1").to_string_lossy()).unwrap();
fs::mkdir(&temp_path.join("dir2").to_string_lossy()).unwrap();
fs::mkdir(&temp_path.join("subdir").to_string_lossy()).unwrap();
// Find directories
let dirs = fs::find_dirs(temp_path.to_str().unwrap(), "dir*");
assert!(dirs.is_ok());
let found_dirs = dirs.unwrap();
assert!(found_dirs.len() >= 2); // At least dir1 and dir2
}

View File

@@ -0,0 +1,366 @@
use sal_os::package::{PackHero, Platform};
#[test]
fn test_pack_hero_creation() {
// Test that we can create a PackHero instance
let hero = PackHero::new();
// Test that platform detection works
let platform = hero.platform();
match platform {
Platform::Ubuntu | Platform::MacOS | Platform::Unknown => {
// All valid platforms
}
}
}
#[test]
fn test_platform_detection() {
let hero = PackHero::new();
let platform = hero.platform();
// Platform should be deterministic
let platform2 = hero.platform();
assert_eq!(format!("{:?}", platform), format!("{:?}", platform2));
// Test platform display
match platform {
Platform::Ubuntu => {
assert_eq!(format!("{:?}", platform), "Ubuntu");
}
Platform::MacOS => {
assert_eq!(format!("{:?}", platform), "MacOS");
}
Platform::Unknown => {
assert_eq!(format!("{:?}", platform), "Unknown");
}
}
}
#[test]
fn test_debug_mode() {
let mut hero = PackHero::new();
// Test setting debug mode
hero.set_debug(true);
hero.set_debug(false);
// Debug mode setting should not panic
}
#[test]
fn test_package_operations_error_handling() {
let hero = PackHero::new();
// Test with invalid package name
let result = hero.is_installed("nonexistent-package-12345-xyz");
// This should return a result (either Ok(false) or Err)
// Validate that we get a proper result type
match result {
Ok(is_installed) => {
// Should return false for non-existent package
assert!(
!is_installed,
"Non-existent package should not be reported as installed"
);
}
Err(_) => {
// Error is also acceptable (e.g., no package manager available)
// The important thing is it doesn't panic
}
}
// Test install with invalid package
let result = hero.install("nonexistent-package-12345-xyz");
// This should return an error
assert!(result.is_err());
// Test remove with invalid package
let result = hero.remove("nonexistent-package-12345-xyz");
// This might succeed (if package wasn't installed) or fail
// Validate that we get a proper result type
match result {
Ok(_) => {
// Success is acceptable (package wasn't installed)
}
Err(err) => {
// Error is also acceptable
// Verify error message is meaningful
let error_msg = err.to_string();
assert!(!error_msg.is_empty(), "Error message should not be empty");
}
}
}
#[test]
fn test_package_search_basic() {
let hero = PackHero::new();
// Test search with empty query
let result = hero.search("");
// Should handle empty query gracefully
// Validate that we get a proper result type
match result {
Ok(packages) => {
// Empty search might return all packages or empty list
// Verify the result is a valid vector
assert!(
packages.len() < 50000,
"Empty search returned unreasonably large result"
);
}
Err(err) => {
// Error is acceptable for empty query
let error_msg = err.to_string();
assert!(!error_msg.is_empty(), "Error message should not be empty");
}
}
// Test search with very specific query that likely won't match
let result = hero.search("nonexistent-package-xyz-12345");
if let Ok(packages) = result {
// If search succeeded, it should return a vector
// The vector should be valid (we can get its length)
let _count = packages.len();
// Search results should be reasonable (not absurdly large)
assert!(
packages.len() < 10000,
"Search returned unreasonably large result set"
);
}
// If search failed, that's also acceptable
}
#[test]
fn test_package_list_basic() {
let hero = PackHero::new();
// Test listing installed packages
let result = hero.list_installed();
if let Ok(packages) = result {
// If listing succeeded, it should return a vector
// On most systems, there should be at least some packages installed
println!("Found {} installed packages", packages.len());
}
// If listing failed (e.g., no package manager available), that's acceptable
}
#[test]
fn test_package_update_basic() {
let hero = PackHero::new();
// Test package list update
let result = hero.update();
// This might succeed or fail depending on permissions and network
// Validate that we get a proper result type
match result {
Ok(_) => {
// Success is good - package list was updated
}
Err(err) => {
// Error is acceptable (no permissions, no network, etc.)
let error_msg = err.to_string();
assert!(!error_msg.is_empty(), "Error message should not be empty");
// Common error patterns we expect
let error_lower = error_msg.to_lowercase();
assert!(
error_lower.contains("permission")
|| error_lower.contains("network")
|| error_lower.contains("command")
|| error_lower.contains("not found")
|| error_lower.contains("failed"),
"Error message should indicate a reasonable failure cause: {}",
error_msg
);
}
}
}
#[test]
#[ignore] // Skip by default as this can take a very long time and modify the system
fn test_package_upgrade_basic() {
let hero = PackHero::new();
// Test package upgrade (this is a real system operation)
let result = hero.upgrade();
// Validate that we get a proper result type
match result {
Ok(_) => {
// Success means packages were upgraded
println!("Package upgrade completed successfully");
}
Err(err) => {
// Error is acceptable (no permissions, no packages to upgrade, etc.)
let error_msg = err.to_string();
assert!(!error_msg.is_empty(), "Error message should not be empty");
println!("Package upgrade failed as expected: {}", error_msg);
}
}
}
#[test]
fn test_package_upgrade_interface() {
// Test that the upgrade interface works without actually upgrading
let hero = PackHero::new();
// Verify that PackHero has the upgrade method and it returns the right type
// This tests the interface without performing the actual upgrade
let _upgrade_fn = PackHero::upgrade;
// Test that we can call upgrade (it will likely fail due to permissions/network)
// but we're testing that the interface works correctly
let result = hero.upgrade();
// The result should be a proper Result type
match result {
Ok(_) => {
// Upgrade succeeded (unlikely in test environment)
}
Err(err) => {
// Expected in most test environments
// Verify error is meaningful
let error_msg = err.to_string();
assert!(!error_msg.is_empty(), "Error should have a message");
assert!(error_msg.len() > 5, "Error message should be descriptive");
}
}
}
// Platform-specific tests
#[cfg(target_os = "linux")]
#[test]
fn test_linux_platform_detection() {
let hero = PackHero::new();
let platform = hero.platform();
// On Linux, should detect Ubuntu or Unknown (if not Ubuntu-based)
match platform {
Platform::Ubuntu | Platform::Unknown => {
// Expected on Linux
}
Platform::MacOS => {
panic!("Should not detect macOS on Linux system");
}
}
}
#[cfg(target_os = "macos")]
#[test]
fn test_macos_platform_detection() {
let hero = PackHero::new();
let platform = hero.platform();
// On macOS, should detect MacOS
match platform {
Platform::MacOS => {
// Expected on macOS
}
Platform::Ubuntu | Platform::Unknown => {
panic!("Should detect macOS on macOS system, got {:?}", platform);
}
}
}
// Integration tests that require actual package managers
// These are marked with ignore so they don't run by default
#[test]
#[ignore]
fn test_real_package_check() {
let hero = PackHero::new();
// Test with a package that's commonly installed
#[cfg(target_os = "linux")]
let test_package = "bash";
#[cfg(target_os = "macos")]
let test_package = "bash";
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
let test_package = "unknown";
let result = hero.is_installed(test_package);
if let Ok(is_installed) = result {
println!("Package '{}' is installed: {}", test_package, is_installed);
} else {
println!(
"Failed to check if '{}' is installed: {:?}",
test_package, result
);
}
}
#[test]
#[ignore]
fn test_real_package_search() {
let hero = PackHero::new();
// Search for a common package
let result = hero.search("git");
if let Ok(packages) = result {
println!("Found {} packages matching 'git'", packages.len());
if !packages.is_empty() {
println!(
"First few matches: {:?}",
&packages[..std::cmp::min(5, packages.len())]
);
}
} else {
println!("Package search failed: {:?}", result);
}
}
#[test]
#[ignore]
fn test_real_package_list() {
let hero = PackHero::new();
// List installed packages
let result = hero.list_installed();
if let Ok(packages) = result {
println!("Total installed packages: {}", packages.len());
if !packages.is_empty() {
println!(
"First few packages: {:?}",
&packages[..std::cmp::min(10, packages.len())]
);
}
} else {
println!("Package listing failed: {:?}", result);
}
}
#[test]
fn test_platform_enum_properties() {
// Test that Platform enum can be compared
assert_eq!(Platform::Ubuntu, Platform::Ubuntu);
assert_eq!(Platform::MacOS, Platform::MacOS);
assert_eq!(Platform::Unknown, Platform::Unknown);
assert_ne!(Platform::Ubuntu, Platform::MacOS);
assert_ne!(Platform::Ubuntu, Platform::Unknown);
assert_ne!(Platform::MacOS, Platform::Unknown);
}
#[test]
fn test_pack_hero_multiple_instances() {
// Test that multiple PackHero instances work correctly
let hero1 = PackHero::new();
let hero2 = PackHero::new();
// Both should detect the same platform
assert_eq!(
format!("{:?}", hero1.platform()),
format!("{:?}", hero2.platform())
);
// Both should handle debug mode independently
let mut hero1_mut = hero1;
let mut hero2_mut = hero2;
hero1_mut.set_debug(true);
hero2_mut.set_debug(false);
// No assertions here since debug mode doesn't have observable effects in tests
// But this ensures the API works correctly
}

View File

@@ -0,0 +1,205 @@
use sal_os::platform;
#[test]
fn test_platform_detection_consistency() {
// Test that platform detection functions return consistent results
let is_osx = platform::is_osx();
let is_linux = platform::is_linux();
// On any given system, only one of these should be true
// (or both false if running on Windows or other OS)
if is_osx {
assert!(!is_linux, "Cannot be both macOS and Linux");
}
if is_linux {
assert!(!is_osx, "Cannot be both Linux and macOS");
}
}
#[test]
fn test_architecture_detection_consistency() {
// Test that architecture detection functions return consistent results
let is_arm = platform::is_arm();
let is_x86 = platform::is_x86();
// On any given system, only one of these should be true
// (or both false if running on other architectures)
if is_arm {
assert!(!is_x86, "Cannot be both ARM and x86");
}
if is_x86 {
assert!(!is_arm, "Cannot be both x86 and ARM");
}
}
#[test]
fn test_platform_functions_return_bool() {
// Test that all platform detection functions return boolean values
let _: bool = platform::is_osx();
let _: bool = platform::is_linux();
let _: bool = platform::is_arm();
let _: bool = platform::is_x86();
}
#[cfg(target_os = "macos")]
#[test]
fn test_macos_detection() {
// When compiled for macOS, is_osx should return true
assert!(platform::is_osx());
assert!(!platform::is_linux());
}
#[cfg(target_os = "linux")]
#[test]
fn test_linux_detection() {
// When compiled for Linux, is_linux should return true
assert!(platform::is_linux());
assert!(!platform::is_osx());
}
#[cfg(target_arch = "aarch64")]
#[test]
fn test_arm_detection() {
// When compiled for ARM64, is_arm should return true
assert!(platform::is_arm());
assert!(!platform::is_x86());
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_x86_detection() {
// When compiled for x86_64, is_x86 should return true
assert!(platform::is_x86());
assert!(!platform::is_arm());
}
#[test]
fn test_check_linux_x86() {
let result = platform::check_linux_x86();
// The result should depend on the current platform
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
assert!(result.is_ok(), "Should succeed on Linux x86_64");
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
{
assert!(result.is_err(), "Should fail on non-Linux x86_64 platforms");
// Check that the error message is meaningful
let error = result.unwrap_err();
let error_string = error.to_string();
assert!(
error_string.contains("Linux x86_64"),
"Error message should mention Linux x86_64: {}",
error_string
);
}
}
#[test]
fn test_check_macos_arm() {
let result = platform::check_macos_arm();
// The result should depend on the current platform
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{
assert!(result.is_ok(), "Should succeed on macOS ARM");
}
#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
{
assert!(result.is_err(), "Should fail on non-macOS ARM platforms");
// Check that the error message is meaningful
let error = result.unwrap_err();
let error_string = error.to_string();
assert!(
error_string.contains("macOS ARM"),
"Error message should mention macOS ARM: {}",
error_string
);
}
}
#[test]
fn test_platform_error_creation() {
use sal_os::platform::PlatformError;
// Test that we can create platform errors
let error = PlatformError::new("Test Error", "This is a test error message");
let error_string = error.to_string();
assert!(error_string.contains("Test Error"));
assert!(error_string.contains("This is a test error message"));
}
#[test]
fn test_platform_error_display() {
use sal_os::platform::PlatformError;
// Test error display formatting
let error = PlatformError::Generic("Category".to_string(), "Message".to_string());
let error_string = format!("{}", error);
assert!(error_string.contains("Category"));
assert!(error_string.contains("Message"));
}
#[test]
fn test_platform_error_debug() {
use sal_os::platform::PlatformError;
// Test error debug formatting
let error = PlatformError::Generic("Category".to_string(), "Message".to_string());
let debug_string = format!("{:?}", error);
assert!(debug_string.contains("Generic"));
assert!(debug_string.contains("Category"));
assert!(debug_string.contains("Message"));
}
#[test]
fn test_platform_functions_are_deterministic() {
// Platform detection should be deterministic - same result every time
let osx1 = platform::is_osx();
let osx2 = platform::is_osx();
assert_eq!(osx1, osx2);
let linux1 = platform::is_linux();
let linux2 = platform::is_linux();
assert_eq!(linux1, linux2);
let arm1 = platform::is_arm();
let arm2 = platform::is_arm();
assert_eq!(arm1, arm2);
let x86_1 = platform::is_x86();
let x86_2 = platform::is_x86();
assert_eq!(x86_1, x86_2);
}
#[test]
fn test_platform_check_functions_consistency() {
// The check functions should be consistent with the individual detection functions
let is_linux_x86 = platform::is_linux() && platform::is_x86();
let check_linux_x86_result = platform::check_linux_x86().is_ok();
assert_eq!(is_linux_x86, check_linux_x86_result);
let is_macos_arm = platform::is_osx() && platform::is_arm();
let check_macos_arm_result = platform::check_macos_arm().is_ok();
assert_eq!(is_macos_arm, check_macos_arm_result);
}
#[test]
fn test_current_platform_info() {
// Print current platform info for debugging (this will show in test output with --nocapture)
println!("Current platform detection:");
println!(" is_osx(): {}", platform::is_osx());
println!(" is_linux(): {}", platform::is_linux());
println!(" is_arm(): {}", platform::is_arm());
println!(" is_x86(): {}", platform::is_x86());
println!(" check_linux_x86(): {:?}", platform::check_linux_x86());
println!(" check_macos_arm(): {:?}", platform::check_macos_arm());
}

View File

@@ -0,0 +1,111 @@
// 01_file_operations.rhai
// Tests for file system operations in the OS module
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Create a test directory structure
let test_dir = "rhai_test_fs";
let sub_dir = test_dir + "/subdir";
// Test mkdir function
print("Testing mkdir...");
let mkdir_result = mkdir(test_dir);
assert_true(exist(test_dir), "Directory creation failed");
print(`✓ mkdir: ${mkdir_result}`);
// Test nested directory creation
let nested_result = mkdir(sub_dir);
assert_true(exist(sub_dir), "Nested directory creation failed");
print(`✓ mkdir (nested): ${nested_result}`);
// Test file_write function
let test_file = test_dir + "/test.txt";
let file_content = "This is a test file created by Rhai test script.";
let write_result = file_write(test_file, file_content);
assert_true(exist(test_file), "File creation failed");
print(`✓ file_write: ${write_result}`);
// Test file_read function
let read_content = file_read(test_file);
assert_true(read_content == file_content, "File content doesn't match");
print(`✓ file_read: Content matches`);
// Test file_size function
let size = file_size(test_file);
assert_true(size > 0, "File size should be greater than 0");
print(`✓ file_size: ${size} bytes`);
// Test file_write_append function
let append_content = "\nThis is appended content.";
let append_result = file_write_append(test_file, append_content);
let new_content = file_read(test_file);
assert_true(new_content == file_content + append_content, "Appended content doesn't match");
print(`✓ file_write_append: ${append_result}`);
// Test copy function
let copied_file = test_dir + "/copied.txt";
let copy_result = copy(test_file, copied_file);
assert_true(exist(copied_file), "File copy failed");
print(`✓ copy: ${copy_result}`);
// Test mv function
let moved_file = test_dir + "/moved.txt";
let mv_result = mv(copied_file, moved_file);
assert_true(exist(moved_file), "File move failed");
assert_true(!exist(copied_file), "Source file still exists after move");
print(`✓ mv: ${mv_result}`);
// Test find_file function
let found_file = find_file(test_dir, "*.txt");
assert_true(found_file.contains("test.txt") || found_file.contains("moved.txt"), "find_file failed");
print(`✓ find_file: ${found_file}`);
// Test find_files function
let found_files = find_files(test_dir, "*.txt");
assert_true(found_files.len() == 2, "find_files should find 2 files");
print(`✓ find_files: Found ${found_files.len()} files`);
// Test find_dir function
let found_dir = find_dir(test_dir, "sub*");
assert_true(found_dir.contains("subdir"), "find_dir failed");
print(`✓ find_dir: ${found_dir}`);
// Test find_dirs function
let found_dirs = find_dirs(test_dir, "sub*");
assert_true(found_dirs.len() == 1, "find_dirs should find 1 directory");
print(`✓ find_dirs: Found ${found_dirs.len()} directories`);
// Test chdir function
// Save current directory path before changing
let chdir_result = chdir(test_dir);
print(`✓ chdir: ${chdir_result}`);
// Change back to parent directory
chdir("..");
// Test rsync function (if available)
let rsync_dir = test_dir + "/rsync_dest";
mkdir(rsync_dir);
let rsync_result = rsync(test_dir, rsync_dir);
print(`✓ rsync: ${rsync_result}`);
// Test delete function
let delete_file_result = delete(test_file);
assert_true(!exist(test_file), "File deletion failed");
print(`✓ delete (file): ${delete_file_result}`);
// Clean up
delete(moved_file);
delete(sub_dir);
delete(rsync_dir);
delete(test_dir);
assert_true(!exist(test_dir), "Directory deletion failed");
print(`✓ delete (directory): Directory cleaned up`);
print("All file system tests completed successfully!");

View File

@@ -0,0 +1,53 @@
// 02_download_operations.rhai
// Tests for download operations in the OS module
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Create a test directory
let test_dir = "rhai_test_download";
mkdir(test_dir);
print(`Created test directory: ${test_dir}`);
// Test which function to ensure curl is available
let curl_path = which("curl");
if curl_path == "" {
print("Warning: curl not found, download tests may fail");
} else {
print(`✓ which: curl found at ${curl_path}`);
}
// Test cmd_ensure_exists function
let ensure_result = cmd_ensure_exists("curl");
print(`✓ cmd_ensure_exists: ${ensure_result}`);
// Test download function with a small file
let download_url = "https://raw.githubusercontent.com/rust-lang/rust/master/LICENSE-MIT";
let download_dest = test_dir + "/license.txt";
let min_size_kb = 1; // Minimum size in KB
print(`Downloading ${download_url}...`);
let download_result = download_file(download_url, download_dest, min_size_kb);
assert_true(exist(download_dest), "Download failed");
print(`✓ download_file: ${download_result}`);
// Verify the downloaded file
let file_content = file_read(download_dest);
assert_true(file_content.contains("Permission is hereby granted"), "Downloaded file content is incorrect");
print("✓ Downloaded file content verified");
// Test chmod_exec function
let chmod_result = chmod_exec(download_dest);
print(`✓ chmod_exec: ${chmod_result}`);
// Clean up
delete(test_dir);
assert_true(!exist(test_dir), "Directory deletion failed");
print(`✓ Cleanup: Directory ${test_dir} removed`);
print("All download tests completed successfully!");

View File

@@ -0,0 +1,56 @@
// 03_package_operations.rhai
// Tests for package management operations in the OS module
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Test package_platform function
let platform = package_platform();
print(`Current platform: ${platform}`);
// Test package_set_debug function
let debug_enabled = package_set_debug(true);
assert_true(debug_enabled, "Debug mode should be enabled");
print("✓ package_set_debug: Debug mode enabled");
// Disable debug mode for remaining tests
package_set_debug(false);
// Test package_is_installed function with a package that should exist on most systems
let common_packages = ["bash", "curl", "grep"];
let found_package = false;
for pkg in common_packages {
let is_installed = package_is_installed(pkg);
if is_installed {
print(`✓ package_is_installed: ${pkg} is installed`);
found_package = true;
break;
}
}
if !found_package {
print("Warning: None of the common packages were found installed");
}
// Test package_search function with a common term
// Note: This might be slow and produce a lot of output
print("Testing package_search (this might take a moment)...");
let search_results = package_search("lib");
print(`✓ package_search: Found ${search_results.len()} packages containing 'lib'`);
// Test package_list function
// Note: This might be slow and produce a lot of output
print("Testing package_list (this might take a moment)...");
let installed_packages = package_list();
print(`✓ package_list: Found ${installed_packages.len()} installed packages`);
// Note: We're not testing package_install, package_remove, package_update, or package_upgrade
// as they require root privileges and could modify the system state
print("All package management tests completed successfully!");

View File

@@ -0,0 +1,148 @@
// run_all_tests.rhai
// Runs all OS module tests
print("=== Running OS Module Tests ===");
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Run each test directly
let passed = 0;
let failed = 0;
// Test 1: File Operations
print("\n--- Running File Operations Tests ---");
try {
// Create a test directory structure
let test_dir = "rhai_test_fs";
let sub_dir = test_dir + "/subdir";
// Test mkdir function
print("Testing mkdir...");
let mkdir_result = mkdir(test_dir);
assert_true(exist(test_dir), "Directory creation failed");
print(`✓ mkdir: ${mkdir_result}`);
// Test nested directory creation
let nested_result = mkdir(sub_dir);
assert_true(exist(sub_dir), "Nested directory creation failed");
print(`✓ mkdir (nested): ${nested_result}`);
// Test file_write function
let test_file = test_dir + "/test.txt";
let file_content = "This is a test file created by Rhai test script.";
let write_result = file_write(test_file, file_content);
assert_true(exist(test_file), "File creation failed");
print(`✓ file_write: ${write_result}`);
// Test file_read function
let read_content = file_read(test_file);
assert_true(read_content == file_content, "File content doesn't match");
print(`✓ file_read: Content matches`);
// Test file_size function
let size = file_size(test_file);
assert_true(size > 0, "File size should be greater than 0");
print(`✓ file_size: ${size} bytes`);
// Clean up
delete(test_file);
delete(sub_dir);
delete(test_dir);
assert_true(!exist(test_dir), "Directory deletion failed");
print(`✓ delete: Directory cleaned up`);
print("--- File Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in File Operations Tests: ${err}`);
failed += 1;
}
// Test 2: Download Operations
print("\n--- Running Download Operations Tests ---");
try {
// Create a test directory
let test_dir = "rhai_test_download";
mkdir(test_dir);
print(`Created test directory: ${test_dir}`);
// Test which function to ensure curl is available
let curl_path = which("curl");
if curl_path == "" {
print("Warning: curl not found, download tests may fail");
} else {
print(`✓ which: curl found at ${curl_path}`);
}
// Test cmd_ensure_exists function
let ensure_result = cmd_ensure_exists("curl");
print(`✓ cmd_ensure_exists: ${ensure_result}`);
// Test download function with a small file
let download_url = "https://raw.githubusercontent.com/rust-lang/rust/master/LICENSE-MIT";
let download_dest = test_dir + "/license.txt";
let min_size_kb = 1; // Minimum size in KB
print(`Downloading ${download_url}...`);
let download_result = download_file(download_url, download_dest, min_size_kb);
assert_true(exist(download_dest), "Download failed");
print(`✓ download_file: ${download_result}`);
// Verify the downloaded file
let file_content = file_read(download_dest);
assert_true(file_content.contains("Permission is hereby granted"), "Downloaded file content is incorrect");
print("✓ Downloaded file content verified");
// Clean up
delete(test_dir);
assert_true(!exist(test_dir), "Directory deletion failed");
print(`✓ Cleanup: Directory ${test_dir} removed`);
print("--- Download Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Download Operations Tests: ${err}`);
failed += 1;
}
// Test 3: Package Operations
print("\n--- Running Package Operations Tests ---");
try {
// Test package_platform function
let platform = package_platform();
print(`Current platform: ${platform}`);
// Test package_set_debug function
let debug_enabled = package_set_debug(true);
assert_true(debug_enabled, "Debug mode should be enabled");
print("✓ package_set_debug: Debug mode enabled");
// Disable debug mode for remaining tests
package_set_debug(false);
print("--- Package Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Package Operations Tests: ${err}`);
failed += 1;
}
print("\n=== Test Summary ===");
print(`Passed: ${passed}`);
print(`Failed: ${failed}`);
print(`Total: ${passed + failed}`);
if failed == 0 {
print("\n✅ All tests passed!");
} else {
print("\n❌ Some tests failed!");
}
// Return the number of failed tests (0 means success)
failed;

View File

@@ -0,0 +1,364 @@
use rhai::Engine;
use sal_os::rhai::register_os_module;
use tempfile::TempDir;
fn create_test_engine() -> Engine {
let mut engine = Engine::new();
register_os_module(&mut engine).expect("Failed to register OS module");
engine
}
#[test]
fn test_rhai_module_registration() {
// Test that the OS module can be registered without errors
let _engine = create_test_engine();
// If we get here without panicking, the module was registered successfully
// We can't easily test function registration without calling the functions
}
#[test]
fn test_rhai_file_operations() {
let engine = create_test_engine();
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_str().unwrap();
// Test file operations through Rhai
let script = format!(
r#"
let test_dir = "{}/test_rhai";
let test_file = test_dir + "/test.txt";
let content = "Hello from Rhai!";
// Create directory
mkdir(test_dir);
// Check if directory exists
let dir_exists = exist(test_dir);
// Write file
file_write(test_file, content);
// Check if file exists
let file_exists = exist(test_file);
// Read file
let read_content = file_read(test_file);
// Return results
#{{"dir_exists": dir_exists, "file_exists": file_exists, "content_match": read_content == content}}
"#,
temp_path
);
let result: rhai::Map = engine.eval(&script).expect("Script execution failed");
assert_eq!(result["dir_exists"].as_bool().unwrap(), true);
assert_eq!(result["file_exists"].as_bool().unwrap(), true);
assert_eq!(result["content_match"].as_bool().unwrap(), true);
}
#[test]
fn test_rhai_file_size() {
let engine = create_test_engine();
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_str().unwrap();
let script = format!(
r#"
let test_file = "{}/size_test.txt";
let content = "12345"; // 5 bytes
file_write(test_file, content);
let size = file_size(test_file);
size
"#,
temp_path
);
let result: i64 = engine.eval(&script).expect("Script execution failed");
assert_eq!(result, 5);
}
#[test]
fn test_rhai_file_append() {
let engine = create_test_engine();
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_str().unwrap();
let script = format!(
r#"
let test_file = "{}/append_test.txt";
file_write(test_file, "Line 1\n");
file_write_append(test_file, "Line 2\n");
let content = file_read(test_file);
content
"#,
temp_path
);
let result: String = engine.eval(&script).expect("Script execution failed");
assert_eq!(result, "Line 1\nLine 2\n");
}
#[test]
fn test_rhai_copy_and_move() {
let engine = create_test_engine();
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_str().unwrap();
let script = format!(
r#"
let source = "{}/source.txt";
let copy_dest = "{}/copy.txt";
let move_dest = "{}/moved.txt";
let content = "Test content";
// Create source file
file_write(source, content);
// Copy file
copy(source, copy_dest);
// Move the copy
mv(copy_dest, move_dest);
// Check results
let source_exists = exist(source);
let copy_exists = exist(copy_dest);
let move_exists = exist(move_dest);
let move_content = file_read(move_dest);
#{{"source_exists": source_exists, "copy_exists": copy_exists, "move_exists": move_exists, "content_match": move_content == content}}
"#,
temp_path, temp_path, temp_path
);
let result: rhai::Map = engine.eval(&script).expect("Script execution failed");
assert_eq!(result["source_exists"].as_bool().unwrap(), true);
assert_eq!(result["copy_exists"].as_bool().unwrap(), false); // Should be moved
assert_eq!(result["move_exists"].as_bool().unwrap(), true);
assert_eq!(result["content_match"].as_bool().unwrap(), true);
}
#[test]
fn test_rhai_delete() {
let engine = create_test_engine();
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_str().unwrap();
let script = format!(
r#"
let test_file = "{}/delete_test.txt";
// Create file
file_write(test_file, "content");
let exists_before = exist(test_file);
// Delete file
delete(test_file);
let exists_after = exist(test_file);
#{{"before": exists_before, "after": exists_after}}
"#,
temp_path
);
let result: rhai::Map = engine.eval(&script).expect("Script execution failed");
assert_eq!(result["before"].as_bool().unwrap(), true);
assert_eq!(result["after"].as_bool().unwrap(), false);
}
#[test]
fn test_rhai_find_files() {
let engine = create_test_engine();
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_str().unwrap();
let script = format!(
r#"
let test_dir = "{}/find_test";
mkdir(test_dir);
// Create test files
file_write(test_dir + "/file1.txt", "content1");
file_write(test_dir + "/file2.txt", "content2");
file_write(test_dir + "/other.log", "log content");
// Find .txt files
let txt_files = find_files(test_dir, "*.txt");
let all_files = find_files(test_dir, "*");
#{{"txt_count": txt_files.len(), "all_count": all_files.len()}}
"#,
temp_path
);
let result: rhai::Map = engine.eval(&script).expect("Script execution failed");
assert_eq!(result["txt_count"].as_int().unwrap(), 2);
assert!(result["all_count"].as_int().unwrap() >= 3);
}
#[test]
fn test_rhai_which_command() {
let engine = create_test_engine();
let script = r#"
let ls_path = which("ls");
let nonexistent = which("nonexistentcommand12345");
#{"ls_found": ls_path.len() > 0, "nonexistent_found": nonexistent.len() > 0}
"#;
let result: rhai::Map = engine.eval(script).expect("Script execution failed");
assert_eq!(result["ls_found"].as_bool().unwrap(), true);
assert_eq!(result["nonexistent_found"].as_bool().unwrap(), false);
}
#[test]
fn test_rhai_error_handling() {
let engine = create_test_engine();
// Test that errors are properly propagated to Rhai
// Instead of try-catch, just test that the function call fails
let script = r#"file_read("/nonexistent/path/file.txt")"#;
let result = engine.eval::<String>(script);
assert!(
result.is_err(),
"Expected error when reading non-existent file"
);
}
#[test]
fn test_rhai_package_functions() {
let engine = create_test_engine();
// Test that package functions are registered by calling them
let script = r#"
let platform = package_platform();
let debug_result = package_set_debug(true);
#{"platform": platform, "debug": debug_result}
"#;
let result: rhai::Map = engine.eval(script).expect("Script execution failed");
// Platform should be a non-empty string
let platform: String = result["platform"].clone().try_cast().unwrap();
assert!(!platform.is_empty());
// Debug setting should return true
assert_eq!(result["debug"].as_bool().unwrap(), true);
}
#[test]
fn test_rhai_download_functions() {
let engine = create_test_engine();
// Test that download functions are registered by calling them
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_str().unwrap();
let script = format!(
r#"
let test_file = "{}/test_script.sh";
// Create a test script
file_write(test_file, "echo 'test'");
// Make it executable
try {{
let result = chmod_exec(test_file);
result.len() >= 0 // chmod_exec returns a string, so check if it's valid
}} catch {{
false
}}
"#,
temp_path
);
let result: bool = engine.eval(&script).expect("Script execution failed");
assert!(result);
}
#[test]
fn test_rhai_array_returns() {
let engine = create_test_engine();
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_str().unwrap();
let script = format!(
r#"
let test_dir = "{}/array_test";
mkdir(test_dir);
// Create some files
file_write(test_dir + "/file1.txt", "content");
file_write(test_dir + "/file2.txt", "content");
// Test that find_files returns an array
let files = find_files(test_dir, "*.txt");
// Test array operations
let count = files.len();
let first_file = if count > 0 {{ files[0] }} else {{ "" }};
#{{"count": count, "has_files": count > 0, "first_file_exists": first_file.len() > 0}}
"#,
temp_path
);
let result: rhai::Map = engine.eval(&script).expect("Script execution failed");
assert_eq!(result["count"].as_int().unwrap(), 2);
assert_eq!(result["has_files"].as_bool().unwrap(), true);
assert_eq!(result["first_file_exists"].as_bool().unwrap(), true);
}
#[test]
fn test_rhai_platform_functions() {
let engine = create_test_engine();
let script = r#"
let is_osx = platform_is_osx();
let is_linux = platform_is_linux();
let is_arm = platform_is_arm();
let is_x86 = platform_is_x86();
// Test that platform detection is consistent
let platform_consistent = !(is_osx && is_linux);
let arch_consistent = !(is_arm && is_x86);
#{"osx": is_osx, "linux": is_linux, "arm": is_arm, "x86": is_x86, "platform_consistent": platform_consistent, "arch_consistent": arch_consistent}
"#;
let result: rhai::Map = engine.eval(script).expect("Script execution failed");
// Verify platform detection consistency
assert_eq!(result["platform_consistent"].as_bool().unwrap(), true);
assert_eq!(result["arch_consistent"].as_bool().unwrap(), true);
// At least one platform should be detected
let osx = result["osx"].as_bool().unwrap();
let linux = result["linux"].as_bool().unwrap();
// At least one architecture should be detected
let arm = result["arm"].as_bool().unwrap();
let x86 = result["x86"].as_bool().unwrap();
// Print current platform for debugging
println!(
"Platform detection: OSX={}, Linux={}, ARM={}, x86={}",
osx, linux, arm, x86
);
}

View File

@@ -0,0 +1,27 @@
[package]
name = "sal-process"
version = "0.1.0"
edition = "2021"
authors = ["PlanetFirst <info@incubaid.com>"]
description = "SAL Process - Cross-platform process management and command execution"
repository = "https://git.threefold.info/herocode/sal"
license = "Apache-2.0"
[dependencies]
# Core dependencies for process management
tempfile = { workspace = true }
rhai = { workspace = true }
anyhow = { workspace = true }
# SAL dependencies
sal-text = { path = "../text" }
# Optional features for specific OS functionality
[target.'cfg(unix)'.dependencies]
nix = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }

View File

@@ -0,0 +1,178 @@
# SAL Process Package
The `sal-process` package provides comprehensive functionality for managing and interacting with system processes across different platforms (Windows, macOS, and Linux).
## Features
- **Command Execution**: Run commands and scripts with flexible options
- **Process Management**: List, find, and kill processes
- **Cross-Platform**: Works consistently across Windows, macOS, and Linux
- **Builder Pattern**: Fluent API for configuring command execution
- **Rhai Integration**: Full support for Rhai scripting language
- **Error Handling**: Comprehensive error types and handling
## Installation
Add this to your `Cargo.toml`:
```toml
[dependencies]
sal-process = "0.1.0"
```
## Usage
### Basic Command Execution
```rust
use sal_process::{run_command, run_silent};
// Run a command and capture output
let result = run_command("echo hello world")?;
println!("Output: {}", result.stdout);
// Run a command silently
let result = run_silent("ls -la")?;
```
### Builder Pattern
```rust
use sal_process::run;
// Use the builder pattern for more control
let result = run("echo test")
.silent(true)
.die(false)
.log(true)
.execute()?;
```
### Process Management
```rust
use sal_process::{which, process_list, process_get, kill};
// Check if a command exists
if let Some(path) = which("git") {
println!("Git found at: {}", path);
}
// List all processes
let processes = process_list("")?;
println!("Found {} processes", processes.len());
// Find processes by pattern
let chrome_processes = process_list("chrome")?;
// Get a single process (errors if 0 or >1 matches)
let process = process_get("unique_process_name")?;
// Kill processes by pattern
kill("old_server")?;
```
### Multiline Scripts
```rust
let script = r#"
echo "Starting script"
export VAR="test"
echo "Variable: $VAR"
echo "Script complete"
"#;
let result = run_command(script)?;
```
## Rhai Integration
The package provides full Rhai integration for scripting:
```rhai
// Basic command execution
let result = run_command("echo hello");
print(result.stdout);
// Builder pattern
let result = run("echo test")
.silent()
.ignore_error()
.execute();
// Process management
let git_path = which("git");
if git_path != () {
print(`Git found at: ${git_path}`);
}
let processes = process_list("chrome");
print(`Found ${processes.len()} Chrome processes`);
```
## Error Handling
The package provides comprehensive error handling:
```rust
use sal_process::{run, RunError};
match run("some_command").execute() {
Ok(result) => {
if result.success {
println!("Command succeeded: {}", result.stdout);
} else {
println!("Command failed with code: {}", result.code);
}
}
Err(RunError::CommandExecutionFailed(e)) => {
eprintln!("Failed to execute command: {}", e);
}
Err(e) => {
eprintln!("Other error: {}", e);
}
}
```
## Builder Options
The `run()` function returns a builder with these options:
- `.silent(bool)`: Suppress output to stdout/stderr
- `.die(bool)`: Return error if command fails (default: true)
- `.log(bool)`: Log command execution
- `.async_exec(bool)`: Run command asynchronously
## Cross-Platform Support
The package handles platform differences automatically:
- **Windows**: Uses `cmd.exe` for script execution
- **Unix-like**: Uses `/bin/bash` with `-e` flag for error handling
- **Process listing**: Uses appropriate tools (`wmic` on Windows, `ps` on Unix)
- **Command detection**: Uses `where` on Windows, `which` on Unix
## Testing
Run the test suite:
```bash
cargo test
```
The package includes comprehensive tests:
- Unit tests for all functionality
- Integration tests for real-world scenarios
- Rhai script tests for scripting integration
- Cross-platform compatibility tests
## Dependencies
- `tempfile`: For temporary script file creation
- `rhai`: For Rhai scripting integration
- `anyhow`: For error handling
- `sal-text`: For text processing utilities
Platform-specific dependencies:
- `nix` (Unix): For Unix-specific process operations
- `windows` (Windows): For Windows-specific process operations

View File

@@ -0,0 +1,22 @@
//! # SAL Process Package
//!
//! The `sal-process` package provides functionality for managing and interacting with
//! system processes across different platforms. It includes capabilities for:
//!
//! - Running commands and scripts
//! - Listing and filtering processes
//! - Killing processes
//! - Checking for command existence
//! - Screen session management
//!
//! This package is designed to work consistently across Windows, macOS, and Linux.
mod mgmt;
mod run;
mod screen;
pub mod rhai;
pub use mgmt::*;
pub use run::*;
pub use screen::{kill as kill_screen, new as new_screen};

View File

@@ -0,0 +1,351 @@
use std::error::Error;
use std::fmt;
use std::io;
use std::process::Command;
/// Error type for process management operations
///
/// This enum represents various errors that can occur during process management
/// operations such as listing, finding, or killing processes.
#[derive(Debug)]
pub enum ProcessError {
/// An error occurred while executing a command
CommandExecutionFailed(io::Error),
/// A command executed successfully but returned an error
CommandFailed(String),
/// No process was found matching the specified pattern
NoProcessFound(String),
/// Multiple processes were found matching the specified pattern
MultipleProcessesFound(String, usize),
}
/// Implement Display for ProcessError to provide human-readable error messages
impl fmt::Display for ProcessError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProcessError::CommandExecutionFailed(e) => {
write!(f, "Failed to execute command: {}", e)
}
ProcessError::CommandFailed(e) => write!(f, "{}", e),
ProcessError::NoProcessFound(pattern) => {
write!(f, "No processes found matching '{}'", pattern)
}
ProcessError::MultipleProcessesFound(pattern, count) => write!(
f,
"Multiple processes ({}) found matching '{}'",
count, pattern
),
}
}
}
// Implement Error trait for ProcessError
impl Error for ProcessError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ProcessError::CommandExecutionFailed(e) => Some(e),
_ => None,
}
}
}
// Define a struct to represent process information
#[derive(Debug, Clone)]
pub struct ProcessInfo {
pub pid: i64,
pub name: String,
pub memory: f64,
pub cpu: f64,
}
/**
* Check if a command exists in PATH.
*
* # Arguments
*
* * `cmd` - The command to check
*
* # Returns
*
* * `Option<String>` - The full path to the command if found, None otherwise
*
* # Examples
*
* ```
* use sal_process::which;
*
* match which("git") {
* Some(path) => println!("Git is installed at: {}", path),
* None => println!("Git is not installed"),
* }
* ```
*/
pub fn which(cmd: &str) -> Option<String> {
#[cfg(target_os = "windows")]
let which_cmd = "where";
#[cfg(any(target_os = "macos", target_os = "linux"))]
let which_cmd = "which";
let output = Command::new(which_cmd).arg(cmd).output();
match output {
Ok(out) => {
if out.status.success() {
let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
Some(path)
} else {
None
}
}
Err(_) => None,
}
}
/**
* Kill processes matching a pattern.
*
* # Arguments
*
* * `pattern` - The pattern to match against process names
*
* # Returns
*
* * `Ok(String)` - A success message indicating processes were killed or none were found
* * `Err(ProcessError)` - An error if the kill operation failed
*
* # Examples
*
* ```
* // Kill all processes with "server" in their name
* use sal_process::kill;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let result = kill("server")?;
* println!("{}", result);
* Ok(())
* }
* ```
*/
pub fn kill(pattern: &str) -> Result<String, ProcessError> {
// Platform specific implementation
#[cfg(target_os = "windows")]
{
// On Windows, use taskkill with wildcard support
let mut args = vec!["/F"]; // Force kill
if pattern.contains('*') {
// If it contains wildcards, use filter
args.extend(&["/FI", &format!("IMAGENAME eq {}", pattern)]);
} else {
// Otherwise use image name directly
args.extend(&["/IM", pattern]);
}
let output = Command::new("taskkill")
.args(&args)
.output()
.map_err(ProcessError::CommandExecutionFailed)?;
if output.status.success() {
Ok("Successfully killed processes".to_string())
} else {
let error = String::from_utf8_lossy(&output.stderr);
if error.is_empty() {
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains("No tasks") {
Ok("No matching processes found".to_string())
} else {
Err(ProcessError::CommandFailed(format!(
"Failed to kill processes: {}",
stdout
)))
}
} else {
Err(ProcessError::CommandFailed(format!(
"Failed to kill processes: {}",
error
)))
}
}
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
// On Unix-like systems, use pkill which has built-in pattern matching
let output = Command::new("pkill")
.arg("-f") // Match against full process name/args
.arg(pattern)
.output()
.map_err(ProcessError::CommandExecutionFailed)?;
// pkill returns 0 if processes were killed, 1 if none matched
if output.status.success() {
Ok("Successfully killed processes".to_string())
} else if output.status.code() == Some(1) {
Ok("No matching processes found".to_string())
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(ProcessError::CommandFailed(format!(
"Failed to kill processes: {}",
error
)))
}
}
}
/**
* List processes matching a pattern (or all if pattern is empty).
*
* # Arguments
*
* * `pattern` - The pattern to match against process names (empty string for all processes)
*
* # Returns
*
* * `Ok(Vec<ProcessInfo>)` - A vector of process information for matching processes
* * `Err(ProcessError)` - An error if the list operation failed
*
* # Examples
*
* ```
* // List all processes
* use sal_process::process_list;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let processes = process_list("")?;
*
* // List processes with "server" in their name
* let processes = process_list("server")?;
* for proc in processes {
* println!("PID: {}, Name: {}", proc.pid, proc.name);
* }
* Ok(())
* }
* ```
*/
pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
let mut processes = Vec::new();
// Platform specific implementations
#[cfg(target_os = "windows")]
{
// Windows implementation using wmic
let output = Command::new("wmic")
.args(&["process", "list", "brief"])
.output()
.map_err(ProcessError::CommandExecutionFailed)?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
// Parse output (assuming format: Handle Name Priority)
for line in stdout.lines().skip(1) {
// Skip header
let parts: Vec<&str> = line.trim().split_whitespace().collect();
if parts.len() >= 2 {
let pid = parts[0].parse::<i64>().unwrap_or(0);
let name = parts[1].to_string();
// Filter by pattern if provided
if !pattern.is_empty() && !name.contains(pattern) {
continue;
}
processes.push(ProcessInfo {
pid,
name,
memory: 0.0, // Placeholder
cpu: 0.0, // Placeholder
});
}
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
return Err(ProcessError::CommandFailed(format!(
"Failed to list processes: {}",
stderr
)));
}
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
// Unix implementation using ps
let output = Command::new("ps")
.args(&["-eo", "pid,comm"])
.output()
.map_err(ProcessError::CommandExecutionFailed)?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
// Parse output (assuming format: PID COMMAND)
for line in stdout.lines().skip(1) {
// Skip header
let parts: Vec<&str> = line.trim().split_whitespace().collect();
if parts.len() >= 2 {
let pid = parts[0].parse::<i64>().unwrap_or(0);
let name = parts[1].to_string();
// Filter by pattern if provided
if !pattern.is_empty() && !name.contains(pattern) {
continue;
}
processes.push(ProcessInfo {
pid,
name,
memory: 0.0, // Placeholder
cpu: 0.0, // Placeholder
});
}
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
return Err(ProcessError::CommandFailed(format!(
"Failed to list processes: {}",
stderr
)));
}
}
Ok(processes)
}
/**
* Get a single process matching the pattern (error if 0 or more than 1 match).
*
* # Arguments
*
* * `pattern` - The pattern to match against process names
*
* # Returns
*
* * `Ok(ProcessInfo)` - Information about the matching process
* * `Err(ProcessError)` - An error if no process or multiple processes match
*
* # Examples
*
* ```no_run
* use sal_process::process_get;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let process = process_get("unique-server-name")?;
* println!("Found process: {} (PID: {})", process.name, process.pid);
* Ok(())
* }
* ```
*/
pub fn process_get(pattern: &str) -> Result<ProcessInfo, ProcessError> {
let processes = process_list(pattern)?;
match processes.len() {
0 => Err(ProcessError::NoProcessFound(pattern.to_string())),
1 => Ok(processes[0].clone()),
_ => Err(ProcessError::MultipleProcessesFound(
pattern.to_string(),
processes.len(),
)),
}
}

View File

@@ -0,0 +1,212 @@
//! Rhai wrappers for Process module functions
//!
//! This module provides Rhai wrappers for the functions in the Process module.
use crate::{self as process, CommandResult, ProcessError, ProcessInfo, RunError};
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
use std::clone::Clone;
/// Register Process module functions with the Rhai engine
///
/// # Arguments
///
/// * `engine` - The Rhai engine to register the functions with
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_process_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register types
// register_process_types(engine)?; // Removed
// Register CommandResult type and its methods
engine.register_type_with_name::<CommandResult>("CommandResult");
engine.register_get("stdout", |r: &mut CommandResult| r.stdout.clone());
engine.register_get("stderr", |r: &mut CommandResult| r.stderr.clone());
engine.register_get("success", |r: &mut CommandResult| r.success);
engine.register_get("code", |r: &mut CommandResult| r.code);
// Register ProcessInfo type and its methods
engine.register_type_with_name::<ProcessInfo>("ProcessInfo");
engine.register_get("pid", |p: &mut ProcessInfo| p.pid);
engine.register_get("name", |p: &mut ProcessInfo| p.name.clone());
engine.register_get("memory", |p: &mut ProcessInfo| p.memory);
engine.register_get("cpu", |p: &mut ProcessInfo| p.cpu);
// Register CommandBuilder type and its methods
engine.register_type_with_name::<RhaiCommandBuilder>("CommandBuilder");
engine.register_fn("run", RhaiCommandBuilder::new_rhai); // This is the builder entry point
engine.register_fn("silent", RhaiCommandBuilder::silent); // Method on CommandBuilder
engine.register_fn("ignore_error", RhaiCommandBuilder::ignore_error); // Method on CommandBuilder
engine.register_fn("log", RhaiCommandBuilder::log); // Method on CommandBuilder
engine.register_fn("execute", RhaiCommandBuilder::execute_command); // Method on CommandBuilder
// Register other process management functions
engine.register_fn("which", which);
engine.register_fn("kill", kill);
engine.register_fn("process_list", process_list);
engine.register_fn("process_get", process_get);
// Register legacy functions for backward compatibility
engine.register_fn("run_command", run_command);
engine.register_fn("run_silent", run_silent);
engine.register_fn("run", run_with_options);
Ok(())
}
// Helper functions for error conversion
fn run_error_to_rhai_error<T>(result: Result<T, RunError>) -> Result<T, Box<EvalAltResult>> {
result.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Run error: {}", e).into(),
rhai::Position::NONE,
))
})
}
// Define a Rhai-facing builder struct
#[derive(Clone)]
struct RhaiCommandBuilder {
command: String,
die_on_error: bool,
is_silent: bool,
enable_log: bool,
}
impl RhaiCommandBuilder {
// Constructor function for Rhai (registered as `run`)
pub fn new_rhai(command: &str) -> Self {
Self {
command: command.to_string(),
die_on_error: true, // Default: die on error
is_silent: false,
enable_log: false,
}
}
// Rhai method: .silent()
pub fn silent(mut self) -> Self {
self.is_silent = true;
self
}
// Rhai method: .ignore_error()
pub fn ignore_error(mut self) -> Self {
self.die_on_error = false;
self
}
// Rhai method: .log()
pub fn log(mut self) -> Self {
self.enable_log = true;
self
}
// Rhai method: .execute() - Execute the command
pub fn execute_command(self) -> Result<CommandResult, Box<EvalAltResult>> {
let builder = process::run(&self.command)
.die(self.die_on_error)
.silent(self.is_silent)
.log(self.enable_log);
// Execute the command
run_error_to_rhai_error(builder.execute())
}
}
fn process_error_to_rhai_error<T>(
result: Result<T, ProcessError>,
) -> Result<T, Box<EvalAltResult>> {
result.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Process error: {}", e).into(),
rhai::Position::NONE,
))
})
}
//
// Process Management Function Wrappers
//
/// Wrapper for process::which
///
/// Check if a command exists in PATH.
pub fn which(cmd: &str) -> Dynamic {
match process::which(cmd) {
Some(path) => path.into(),
None => Dynamic::UNIT,
}
}
/// Wrapper for process::kill
///
/// Kill processes matching a pattern.
pub fn kill(pattern: &str) -> Result<String, Box<EvalAltResult>> {
process_error_to_rhai_error(process::kill(pattern))
}
/// Wrapper for process::process_list
///
/// List processes matching a pattern (or all if pattern is empty).
pub fn process_list(pattern: &str) -> Result<Array, Box<EvalAltResult>> {
let processes = process_error_to_rhai_error(process::process_list(pattern))?;
// Convert Vec<ProcessInfo> to Rhai Array
let mut array = Array::new();
for process in processes {
array.push(Dynamic::from(process));
}
Ok(array)
}
/// Wrapper for process::process_get
///
/// Get a single process matching the pattern (error if 0 or more than 1 match).
pub fn process_get(pattern: &str) -> Result<ProcessInfo, Box<EvalAltResult>> {
process_error_to_rhai_error(process::process_get(pattern))
}
/// Legacy wrapper for process::run
///
/// Run a command and return the result.
pub fn run_command(cmd: &str) -> Result<CommandResult, Box<EvalAltResult>> {
run_error_to_rhai_error(process::run(cmd).execute())
}
/// Legacy wrapper for process::run with silent option
///
/// Run a command silently and return the result.
pub fn run_silent(cmd: &str) -> Result<CommandResult, Box<EvalAltResult>> {
run_error_to_rhai_error(process::run(cmd).silent(true).execute())
}
/// Legacy wrapper for process::run with options
///
/// Run a command with options and return the result.
pub fn run_with_options(cmd: &str, options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
let mut builder = process::run(cmd);
// Apply options
if let Some(silent) = options.get("silent") {
if let Ok(silent_bool) = silent.as_bool() {
builder = builder.silent(silent_bool);
}
}
if let Some(die) = options.get("die") {
if let Ok(die_bool) = die.as_bool() {
builder = builder.die(die_bool);
}
}
if let Some(log) = options.get("log") {
if let Ok(log_bool) = log.as_bool() {
builder = builder.log(log_bool);
}
}
run_error_to_rhai_error(builder.execute())
}

View File

@@ -0,0 +1,535 @@
use std::error::Error;
use std::fmt;
use std::fs::{self, File};
use std::io;
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use std::thread;
use sal_text;
/// Error type for command and script execution operations
#[derive(Debug)]
pub enum RunError {
/// The command string was empty
EmptyCommand,
/// An error occurred while executing a command
CommandExecutionFailed(io::Error),
/// A command executed successfully but returned an error
CommandFailed(String),
/// An error occurred while preparing a script for execution
ScriptPreparationFailed(String),
/// An error occurred in a child process
ChildProcessError(String),
/// Failed to create a temporary directory
TempDirCreationFailed(io::Error),
/// Failed to create a script file
FileCreationFailed(io::Error),
/// Failed to write to a script file
FileWriteFailed(io::Error),
/// Failed to set file permissions
PermissionError(io::Error),
}
/// Implement Display for RunError to provide human-readable error messages
impl fmt::Display for RunError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RunError::EmptyCommand => write!(f, "Empty command"),
RunError::CommandExecutionFailed(e) => write!(f, "Failed to execute command: {}", e),
RunError::CommandFailed(e) => write!(f, "{}", e),
RunError::ScriptPreparationFailed(e) => write!(f, "{}", e),
RunError::ChildProcessError(e) => write!(f, "{}", e),
RunError::TempDirCreationFailed(e) => {
write!(f, "Failed to create temporary directory: {}", e)
}
RunError::FileCreationFailed(e) => write!(f, "Failed to create script file: {}", e),
RunError::FileWriteFailed(e) => write!(f, "Failed to write to script file: {}", e),
RunError::PermissionError(e) => write!(f, "Failed to set file permissions: {}", e),
}
}
}
// Implement Error trait for RunError
impl Error for RunError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
RunError::CommandExecutionFailed(e) => Some(e),
RunError::TempDirCreationFailed(e) => Some(e),
RunError::FileCreationFailed(e) => Some(e),
RunError::FileWriteFailed(e) => Some(e),
RunError::PermissionError(e) => Some(e),
_ => None,
}
}
}
/// A structure to hold command execution results
#[derive(Debug, Clone)]
pub struct CommandResult {
pub stdout: String,
pub stderr: String,
pub success: bool,
pub code: i32,
}
impl CommandResult {
// Implementation methods can be added here as needed
}
/// Prepare a script file and return the path and interpreter
fn prepare_script_file(
script_content: &str,
) -> Result<(PathBuf, String, tempfile::TempDir), RunError> {
// Dedent the script
let dedented = sal_text::dedent(script_content);
// Create a temporary directory
let temp_dir = tempfile::tempdir().map_err(RunError::TempDirCreationFailed)?;
// Determine script extension and interpreter
#[cfg(target_os = "windows")]
let (ext, interpreter) = (".bat", "cmd.exe".to_string());
#[cfg(any(target_os = "macos", target_os = "linux"))]
let (ext, interpreter) = (".sh", "/bin/bash".to_string());
// Create the script file
let script_path = temp_dir.path().join(format!("script{}", ext));
let mut file = File::create(&script_path).map_err(RunError::FileCreationFailed)?;
// For Unix systems, ensure the script has a shebang line with -e flag
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
let script_with_shebang = if dedented.trim_start().starts_with("#!") {
// Script already has a shebang, use it as is
dedented
} else {
// Add shebang with -e flag to ensure script fails on errors
format!("#!/bin/bash -e\n{}", dedented)
};
// Write the script content with shebang
file.write_all(script_with_shebang.as_bytes())
.map_err(RunError::FileWriteFailed)?;
}
// For Windows, just write the script as is
#[cfg(target_os = "windows")]
{
file.write_all(dedented.as_bytes())
.map_err(RunError::FileWriteFailed)?;
}
// Make the script executable (Unix only)
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&script_path)
.map_err(|e| RunError::PermissionError(e))?
.permissions();
perms.set_mode(0o755); // rwxr-xr-x
fs::set_permissions(&script_path, perms).map_err(RunError::PermissionError)?;
}
Ok((script_path, interpreter, temp_dir))
}
/// Capture output from Child's stdio streams with optional printing
fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult, RunError> {
// Prepare to read stdout & stderr line-by-line
let stdout = child.stdout.take();
let stderr = child.stderr.take();
// Process stdout
let stdout_handle = if let Some(out) = stdout {
let reader = BufReader::new(out);
let silent_clone = silent;
// Spawn a thread to capture and optionally print stdout
Some(std::thread::spawn(move || {
let mut local_buffer = String::new();
for line in reader.lines() {
if let Ok(l) = line {
// Print the line if not silent and flush immediately
if !silent_clone {
println!("{}", l);
std::io::stdout().flush().unwrap_or(());
}
// Store it in our captured buffer
local_buffer.push_str(&l);
local_buffer.push('\n');
}
}
local_buffer
}))
} else {
None
};
// Process stderr
let stderr_handle = if let Some(err) = stderr {
let reader = BufReader::new(err);
let silent_clone = silent;
// Spawn a thread to capture and optionally print stderr
Some(std::thread::spawn(move || {
let mut local_buffer = String::new();
for line in reader.lines() {
if let Ok(l) = line {
// Print the line if not silent and flush immediately
if !silent_clone {
// Print all stderr messages
eprintln!("\x1b[31mERROR: {}\x1b[0m", l); // Red color for errors
std::io::stderr().flush().unwrap_or(());
}
// Store it in our captured buffer
local_buffer.push_str(&l);
local_buffer.push('\n');
}
}
local_buffer
}))
} else {
None
};
// Wait for the child process to exit
let status = child.wait().map_err(|e| {
RunError::ChildProcessError(format!("Failed to wait on child process: {}", e))
})?;
// Join our stdout thread if it exists
let captured_stdout = if let Some(handle) = stdout_handle {
handle.join().unwrap_or_default()
} else {
"Failed to capture stdout".to_string()
};
// Join our stderr thread if it exists
let captured_stderr = if let Some(handle) = stderr_handle {
handle.join().unwrap_or_default()
} else {
"Failed to capture stderr".to_string()
};
// If the command failed, print the stderr if it wasn't already printed
if !status.success() && silent && !captured_stderr.is_empty() {
eprintln!("\x1b[31mCommand failed with error:\x1b[0m");
for line in captured_stderr.lines() {
eprintln!("\x1b[31m{}\x1b[0m", line);
}
}
// Return the command result
Ok(CommandResult {
stdout: captured_stdout,
stderr: captured_stderr,
success: status.success(),
code: status.code().unwrap_or(-1),
})
}
/// Processes Output structure from Command::output() into CommandResult
fn process_command_output(
output: Result<Output, std::io::Error>,
) -> Result<CommandResult, RunError> {
match output {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
// We'll collect stderr but not print it here
// It will be included in the error message if the command fails
// If the command failed, print a clear error message
if !out.status.success() {
eprintln!(
"\x1b[31mCommand failed with exit code: {}\x1b[0m",
out.status.code().unwrap_or(-1)
);
}
Ok(CommandResult {
stdout,
stderr,
success: out.status.success(),
code: out.status.code().unwrap_or(-1),
})
}
Err(e) => Err(RunError::CommandExecutionFailed(e)),
}
}
/// Common logic for running a command with optional silent mode
fn run_command_internal(command: &str, silent: bool) -> Result<CommandResult, RunError> {
let mut parts = command.split_whitespace();
let cmd = match parts.next() {
Some(c) => c,
None => return Err(RunError::EmptyCommand),
};
let args: Vec<&str> = parts.collect();
// Spawn the child process with piped stdout & stderr
let child = Command::new(cmd)
.args(&args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(RunError::CommandExecutionFailed)?;
handle_child_output(child, silent)
}
/// Execute a script with the given interpreter and path
fn execute_script_internal(
interpreter: &str,
script_path: &Path,
silent: bool,
) -> Result<CommandResult, RunError> {
#[cfg(target_os = "windows")]
let command_args = vec!["/c", script_path.to_str().unwrap_or("")];
#[cfg(any(target_os = "macos", target_os = "linux"))]
let command_args = vec!["-e", script_path.to_str().unwrap_or("")];
if silent {
// For silent execution, use output() which captures but doesn't display
let output = Command::new(interpreter).args(&command_args).output();
let result = process_command_output(output)?;
// If the script failed, return an error
if !result.success {
return Err(RunError::CommandFailed(format!(
"Script execution failed with exit code {}: {}",
result.code,
result.stderr.trim()
)));
}
Ok(result)
} else {
// For normal execution, spawn and handle the output streams
let child = Command::new(interpreter)
.args(&command_args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(RunError::CommandExecutionFailed)?;
let result = handle_child_output(child, false)?;
// If the script failed, return an error
if !result.success {
return Err(RunError::CommandFailed(format!(
"Script execution failed with exit code {}: {}",
result.code,
result.stderr.trim()
)));
}
Ok(result)
}
}
/// Run a multiline script with optional silent mode
fn run_script_internal(script: &str, silent: bool) -> Result<CommandResult, RunError> {
// Prepare the script file first to get the content with shebang
let (script_path, interpreter, _temp_dir) = prepare_script_file(script)?;
// Print the script being executed if not silent
if !silent {
println!("\x1b[36mExecuting script:\x1b[0m");
// Read the script file to get the content with shebang
if let Ok(script_content) = fs::read_to_string(&script_path) {
for (i, line) in script_content.lines().enumerate() {
println!("\x1b[36m{:3}: {}\x1b[0m", i + 1, line);
}
} else {
// Fallback to original script if reading fails
for (i, line) in script.lines().enumerate() {
println!("\x1b[36m{:3}: {}\x1b[0m", i + 1, line);
}
}
println!("\x1b[36m---\x1b[0m");
}
// _temp_dir is kept in scope until the end of this function to ensure
// it's not dropped prematurely, which would clean up the directory
// Execute the script and handle the result
let result = execute_script_internal(&interpreter, &script_path, silent);
// If there was an error, print a clear error message only if it's not a CommandFailed error
// (which would already have printed the stderr)
if let Err(ref e) = result {
if !matches!(e, RunError::CommandFailed(_)) {
eprintln!("\x1b[31mScript execution failed: {}\x1b[0m", e);
}
}
result
}
/// A builder for configuring and executing commands or scripts
pub struct RunBuilder<'a> {
/// The command or script to run
cmd: &'a str,
/// Whether to return an error if the command fails (default: true)
die: bool,
/// Whether to suppress output to stdout/stderr (default: false)
silent: bool,
/// Whether to run the command asynchronously (default: false)
async_exec: bool,
/// Whether to log command execution (default: false)
log: bool,
}
impl<'a> RunBuilder<'a> {
/// Create a new RunBuilder with default settings
pub fn new(cmd: &'a str) -> Self {
Self {
cmd,
die: true,
silent: false,
async_exec: false,
log: false,
}
}
/// Set whether to return an error if the command fails
pub fn die(mut self, die: bool) -> Self {
self.die = die;
self
}
/// Set whether to suppress output to stdout/stderr
pub fn silent(mut self, silent: bool) -> Self {
self.silent = silent;
self
}
/// Set whether to run the command asynchronously
pub fn async_exec(mut self, async_exec: bool) -> Self {
self.async_exec = async_exec;
self
}
/// Set whether to log command execution
pub fn log(mut self, log: bool) -> Self {
self.log = log;
self
}
/// Execute the command or script with the configured options
pub fn execute(self) -> Result<CommandResult, RunError> {
let trimmed = self.cmd.trim();
// Log command execution if enabled
if self.log {
println!("\x1b[36m[LOG] Executing command: {}\x1b[0m", trimmed);
}
// Handle async execution
if self.async_exec {
let cmd_copy = trimmed.to_string();
let silent = self.silent;
let log = self.log;
// Spawn a thread to run the command asynchronously
thread::spawn(move || {
if log {
println!("\x1b[36m[ASYNC] Starting execution\x1b[0m");
}
let result = if cmd_copy.contains('\n') {
run_script_internal(&cmd_copy, silent)
} else {
run_command_internal(&cmd_copy, silent)
};
if log {
match &result {
Ok(res) => {
if res.success {
println!("\x1b[32m[ASYNC] Command completed successfully\x1b[0m");
} else {
eprintln!(
"\x1b[31m[ASYNC] Command failed with exit code: {}\x1b[0m",
res.code
);
}
}
Err(e) => {
eprintln!("\x1b[31m[ASYNC] Command failed with error: {}\x1b[0m", e);
}
}
}
});
// Return a placeholder result for async execution
return Ok(CommandResult {
stdout: String::new(),
stderr: String::new(),
success: true,
code: 0,
});
}
// Execute the command or script
let result = if trimmed.contains('\n') {
// This is a multiline script
run_script_internal(trimmed, self.silent)
} else {
// This is a single command
run_command_internal(trimmed, self.silent)
};
// Handle die=false: convert errors to CommandResult with success=false
match result {
Ok(res) => {
// If the command failed but die is false, print a warning
if !res.success && !self.die && !self.silent {
eprintln!("\x1b[33mWarning: Command failed with exit code {} but 'die' is false\x1b[0m", res.code);
}
Ok(res)
}
Err(e) => {
// Print the error only if it's not a CommandFailed error
// (which would already have printed the stderr)
if !matches!(e, RunError::CommandFailed(_)) {
eprintln!("\x1b[31mCommand error: {}\x1b[0m", e);
}
if self.die {
Err(e)
} else {
// Convert error to CommandResult with success=false
Ok(CommandResult {
stdout: String::new(),
stderr: format!("Error: {}", e),
success: false,
code: -1,
})
}
}
}
}
}
/// Create a new RunBuilder for executing a command or script
pub fn run(cmd: &str) -> RunBuilder {
RunBuilder::new(cmd)
}
/// Run a command or multiline script with arguments
pub fn run_command(command: &str) -> Result<CommandResult, RunError> {
run(command).execute()
}
/// Run a command or multiline script with arguments silently
pub fn run_silent(command: &str) -> Result<CommandResult, RunError> {
run(command).silent(true).execute()
}

View File

@@ -0,0 +1,52 @@
use crate::run_command;
use anyhow::Result;
use std::fs;
/// Executes a command in a new screen session.
///
/// # Arguments
///
/// * `name` - The name of the screen session.
/// * `cmd` - The command to execute.
///
/// # Returns
///
/// * `Result<()>` - Ok if the command was executed successfully, otherwise an error.
pub fn new(name: &str, cmd: &str) -> Result<()> {
let script_path = format!("/tmp/cmd_{}.sh", name);
let mut script_content = String::new();
if !cmd.starts_with("#!") {
script_content.push_str("#!/bin/bash\n");
}
script_content.push_str("set -e\n");
script_content.push_str(cmd);
fs::write(&script_path, script_content)?;
fs::set_permissions(
&script_path,
std::os::unix::fs::PermissionsExt::from_mode(0o755),
)?;
let screen_cmd = format!("screen -d -m -S {} {}", name, script_path);
run_command(&screen_cmd)?;
Ok(())
}
/// Kills a screen session.
///
/// # Arguments
///
/// * `name` - The name of the screen session to kill.
///
/// # Returns
///
/// * `Result<()>` - Ok if the session was killed successfully, otherwise an error.
pub fn kill(name: &str) -> Result<()> {
let cmd = format!("screen -S {} -X quit", name);
run_command(&cmd)?;
std::thread::sleep(std::time::Duration::from_millis(500));
Ok(())
}

View File

@@ -0,0 +1,278 @@
use sal_process::{kill, process_get, process_list, which, ProcessError};
#[test]
fn test_which_existing_command() {
// Test with a command that should exist on all systems
#[cfg(target_os = "windows")]
let cmd = "cmd";
#[cfg(not(target_os = "windows"))]
let cmd = "sh";
let result = which(cmd);
assert!(result.is_some());
assert!(!result.unwrap().is_empty());
}
#[test]
fn test_which_nonexistent_command() {
let result = which("nonexistent_command_12345");
assert!(result.is_none());
}
#[test]
fn test_which_common_commands() {
// Test common commands that should exist
let common_commands = if cfg!(target_os = "windows") {
vec!["cmd", "powershell"]
} else {
vec!["sh", "ls", "echo"]
};
for cmd in common_commands {
let result = which(cmd);
assert!(result.is_some(), "Command '{}' should be found", cmd);
assert!(!result.unwrap().is_empty());
}
}
#[test]
fn test_process_list_all() {
let result = process_list("").unwrap();
assert!(
!result.is_empty(),
"Should find at least one running process"
);
// Verify process info structure
let first_process = &result[0];
assert!(first_process.pid > 0, "Process PID should be positive");
assert!(
!first_process.name.is_empty(),
"Process name should not be empty"
);
}
#[test]
fn test_process_list_with_pattern() {
// Try to find processes with common names
let patterns = if cfg!(target_os = "windows") {
vec!["explorer", "winlogon", "System"]
} else {
vec!["init", "kernel", "systemd"]
};
let mut found_any = false;
for pattern in patterns {
if let Ok(processes) = process_list(pattern) {
if !processes.is_empty() {
found_any = true;
for process in processes {
assert!(
process.name.contains(pattern)
|| process
.name
.to_lowercase()
.contains(&pattern.to_lowercase())
);
assert!(process.pid > 0);
}
break;
}
}
}
// At least one pattern should match some processes
assert!(
found_any,
"Should find at least one process with common patterns"
);
}
#[test]
fn test_process_list_nonexistent_pattern() {
let result = process_list("nonexistent_process_12345").unwrap();
assert!(
result.is_empty(),
"Should not find any processes with nonexistent pattern"
);
}
#[test]
fn test_process_info_structure() {
let processes = process_list("").unwrap();
assert!(!processes.is_empty());
let process = &processes[0];
// Test ProcessInfo fields
assert!(process.pid > 0);
assert!(!process.name.is_empty());
// memory and cpu are placeholders, so we just check they exist
assert!(process.memory >= 0.0);
assert!(process.cpu >= 0.0);
}
#[test]
fn test_process_get_single_match() {
// Find a process that should be unique
let processes = process_list("").unwrap();
assert!(!processes.is_empty());
// Try to find a process with a unique enough name
let mut unique_process = None;
for process in &processes {
let matches = process_list(&process.name).unwrap();
if matches.len() == 1 {
unique_process = Some(process.clone());
break;
}
}
if let Some(process) = unique_process {
let result = process_get(&process.name).unwrap();
assert_eq!(result.pid, process.pid);
assert_eq!(result.name, process.name);
}
}
#[test]
fn test_process_get_no_match() {
let result = process_get("nonexistent_process_12345");
assert!(result.is_err());
match result.unwrap_err() {
ProcessError::NoProcessFound(pattern) => {
assert_eq!(pattern, "nonexistent_process_12345");
}
_ => panic!("Expected NoProcessFound error"),
}
}
#[test]
fn test_process_get_multiple_matches() {
// Find a pattern that matches multiple processes
let all_processes = process_list("").unwrap();
assert!(!all_processes.is_empty());
// Try common patterns that might match multiple processes
let patterns = if cfg!(target_os = "windows") {
vec!["svchost", "conhost"]
} else {
vec!["kthread", "ksoftirqd"]
};
let mut _found_multiple = false;
for pattern in patterns {
if let Ok(processes) = process_list(pattern) {
if processes.len() > 1 {
let result = process_get(pattern);
assert!(result.is_err());
match result.unwrap_err() {
ProcessError::MultipleProcessesFound(p, count) => {
assert_eq!(p, pattern);
assert_eq!(count, processes.len());
_found_multiple = true;
break;
}
_ => panic!("Expected MultipleProcessesFound error"),
}
}
}
}
// If we can't find multiple matches with common patterns, that's okay
// The test validates the error handling works correctly
}
#[test]
fn test_kill_nonexistent_process() {
let result = kill("nonexistent_process_12345").unwrap();
assert!(result.contains("No matching processes") || result.contains("Successfully killed"));
}
#[test]
fn test_process_list_performance() {
use std::time::Instant;
let start = Instant::now();
let _processes = process_list("").unwrap();
let duration = start.elapsed();
// Process listing should complete within reasonable time (5 seconds)
assert!(
duration.as_secs() < 5,
"Process listing took too long: {:?}",
duration
);
}
#[test]
fn test_which_performance() {
use std::time::Instant;
let start = Instant::now();
let _result = which("echo");
let duration = start.elapsed();
// Which command should be very fast (1 second)
assert!(
duration.as_secs() < 1,
"Which command took too long: {:?}",
duration
);
}
#[test]
fn test_process_list_filtering_accuracy() {
// Test that filtering actually works correctly
let all_processes = process_list("").unwrap();
assert!(!all_processes.is_empty());
// Pick a process name and filter by it
let test_process = &all_processes[0];
let filtered_processes = process_list(&test_process.name).unwrap();
// All filtered processes should contain the pattern
for process in filtered_processes {
assert!(process.name.contains(&test_process.name));
}
}
#[test]
fn test_process_error_display() {
let error = ProcessError::NoProcessFound("test".to_string());
let error_string = format!("{}", error);
assert!(error_string.contains("No processes found matching 'test'"));
let error = ProcessError::MultipleProcessesFound("test".to_string(), 5);
let error_string = format!("{}", error);
assert!(error_string.contains("Multiple processes (5) found matching 'test'"));
}
#[test]
fn test_cross_platform_process_operations() {
// Test operations that should work on all platforms
// Test which with platform-specific commands
#[cfg(target_os = "windows")]
{
assert!(which("cmd").is_some());
assert!(which("notepad").is_some());
}
#[cfg(target_os = "macos")]
{
assert!(which("sh").is_some());
assert!(which("ls").is_some());
}
#[cfg(target_os = "linux")]
{
assert!(which("sh").is_some());
assert!(which("ls").is_some());
}
// Test process listing works on all platforms
let processes = process_list("").unwrap();
assert!(!processes.is_empty());
}

View File

@@ -0,0 +1,119 @@
// Test script for process command execution functionality
print("=== Process Command Execution Tests ===");
// Test 1: Basic command execution
print("\n--- Test 1: Basic Command Execution ---");
let result = run_command("echo hello world");
assert_true(result.success, "Command should succeed");
assert_true(result.code == 0, "Exit code should be 0");
assert_true(result.stdout.contains("hello world"), "Output should contain 'hello world'");
print("✓ Basic command execution works");
// Test 2: Silent command execution
print("\n--- Test 2: Silent Command Execution ---");
let silent_result = run_silent("echo silent test");
assert_true(silent_result.success, "Silent command should succeed");
assert_true(silent_result.stdout.contains("silent test"), "Silent output should be captured");
print("✓ Silent command execution works");
// Test 3: Builder pattern
print("\n--- Test 3: Builder Pattern ---");
let builder_result = run("echo builder pattern").silent().execute();
assert_true(builder_result.success, "Builder command should succeed");
assert_true(builder_result.stdout.contains("builder pattern"), "Builder output should be captured");
print("✓ Builder pattern works");
// Test 4: Error handling with die=false
print("\n--- Test 4: Error Handling (ignore_error) ---");
let error_result = run("false").ignore_error().silent().execute();
assert_true(!error_result.success, "Command should fail");
assert_true(error_result.code != 0, "Exit code should be non-zero");
print("✓ Error handling with ignore_error works");
// Test 5: Multiline script execution
print("\n--- Test 5: Multiline Script Execution ---");
let script = `
echo "Line 1"
echo "Line 2"
echo "Line 3"
`;
let script_result = run_command(script);
assert_true(script_result.success, "Script should succeed");
assert_true(script_result.stdout.contains("Line 1"), "Should contain Line 1");
assert_true(script_result.stdout.contains("Line 2"), "Should contain Line 2");
assert_true(script_result.stdout.contains("Line 3"), "Should contain Line 3");
print("✓ Multiline script execution works");
// Test 6: Command with arguments
print("\n--- Test 6: Command with Arguments ---");
let args_result = run_command("echo arg1 arg2 arg3");
assert_true(args_result.success, "Command with args should succeed");
assert_true(args_result.stdout.contains("arg1 arg2 arg3"), "Should contain all arguments");
print("✓ Command with arguments works");
// Test 7: Builder with logging
print("\n--- Test 7: Builder with Logging ---");
let log_result = run("echo log test").log().silent().execute();
assert_true(log_result.success, "Logged command should succeed");
assert_true(log_result.stdout.contains("log test"), "Logged output should be captured");
print("✓ Builder with logging works");
// Test 8: Run with options map
print("\n--- Test 8: Run with Options Map ---");
let options = #{
silent: true,
die: false,
log: false
};
let options_result = run("echo options test", options);
assert_true(options_result.success, "Options command should succeed");
assert_true(options_result.stdout.contains("options test"), "Options output should be captured");
print("✓ Run with options map works");
// Test 9: Complex script with variables
print("\n--- Test 9: Complex Script with Variables ---");
let var_script = `
VAR="test_variable"
echo "Variable value: $VAR"
`;
let var_result = run_command(var_script);
assert_true(var_result.success, "Variable script should succeed");
assert_true(var_result.stdout.contains("Variable value: test_variable"), "Should expand variables");
print("✓ Complex script with variables works");
// Test 10: Script with conditionals
print("\n--- Test 10: Script with Conditionals ---");
let cond_script = `
if [ "hello" = "hello" ]; then
echo "Condition passed"
else
echo "Condition failed"
fi
`;
let cond_result = run_command(cond_script);
assert_true(cond_result.success, "Conditional script should succeed");
assert_true(cond_result.stdout.contains("Condition passed"), "Condition should pass");
print("✓ Script with conditionals works");
// Test 11: Builder method chaining
print("\n--- Test 11: Builder Method Chaining ---");
let chain_result = run("echo chaining test")
.silent()
.ignore_error()
.log()
.execute();
assert_true(chain_result.success, "Chained command should succeed");
assert_true(chain_result.stdout.contains("chaining test"), "Chained output should be captured");
print("✓ Builder method chaining works");
// Test 12: CommandResult properties
print("\n--- Test 12: CommandResult Properties ---");
let prop_result = run_command("echo property test");
assert_true(prop_result.success, "Property test command should succeed");
assert_true(prop_result.code == 0, "Exit code property should be 0");
assert_true(prop_result.stdout.len() > 0, "Stdout property should not be empty");
assert_true(prop_result.stderr.len() >= 0, "Stderr property should exist");
print("✓ CommandResult properties work");
print("\n=== All Command Execution Tests Passed! ===");

View File

@@ -0,0 +1,153 @@
// Test script for process management functionality
print("=== Process Management Tests ===");
// Test 1: which function with existing command
print("\n--- Test 1: Which Function (Existing Command) ---");
let echo_path = which("echo");
if echo_path != () {
assert_true(echo_path.len() > 0, "Echo path should not be empty");
print(`✓ which("echo") found at: ${echo_path}`);
} else {
// Try platform-specific commands
let cmd_path = which("cmd");
let sh_path = which("sh");
assert_true(cmd_path != () || sh_path != (), "Should find either cmd or sh");
print("✓ which() function works with platform-specific commands");
}
// Test 2: which function with nonexistent command
print("\n--- Test 2: Which Function (Nonexistent Command) ---");
let nonexistent = which("nonexistent_command_12345");
assert_true(nonexistent == (), "Nonexistent command should return ()");
print("✓ which() correctly handles nonexistent commands");
// Test 3: process_list function
print("\n--- Test 3: Process List Function ---");
let all_processes = process_list("");
assert_true(all_processes.len() > 0, "Should find at least one running process");
print(`✓ process_list("") found ${all_processes.len()} processes`);
// Test 4: process info properties
print("\n--- Test 4: Process Info Properties ---");
if all_processes.len() > 0 {
let first_process = all_processes[0];
assert_true(first_process.pid > 0, "Process PID should be positive");
assert_true(first_process.name.len() > 0, "Process name should not be empty");
assert_true(first_process.memory >= 0.0, "Process memory should be non-negative");
assert_true(first_process.cpu >= 0.0, "Process CPU should be non-negative");
print(`✓ Process properties: PID=${first_process.pid}, Name=${first_process.name}`);
}
// Test 5: process_list with pattern
print("\n--- Test 5: Process List with Pattern ---");
if all_processes.len() > 0 {
let test_process = all_processes[0];
let filtered_processes = process_list(test_process.name);
assert_true(filtered_processes.len() >= 1, "Should find at least the test process");
// Verify all filtered processes contain the pattern
for process in filtered_processes {
assert_true(process.name.contains(test_process.name), "Filtered process should contain pattern");
}
print(`✓ process_list("${test_process.name}") found ${filtered_processes.len()} matching processes`);
}
// Test 6: process_list with nonexistent pattern
print("\n--- Test 6: Process List with Nonexistent Pattern ---");
let empty_list = process_list("nonexistent_process_12345");
assert_true(empty_list.len() == 0, "Should find no processes with nonexistent pattern");
print("✓ process_list() correctly handles nonexistent patterns");
// Test 7: kill function with nonexistent process
print("\n--- Test 7: Kill Function (Nonexistent Process) ---");
let kill_result = kill("nonexistent_process_12345");
assert_true(
kill_result.contains("No matching processes") || kill_result.contains("Successfully killed"),
"Kill should handle nonexistent processes gracefully"
);
print(`✓ kill("nonexistent_process_12345") result: ${kill_result}`);
// Test 8: Common system commands detection
print("\n--- Test 8: Common System Commands Detection ---");
let common_commands = ["echo", "ls", "cat", "grep", "awk", "sed"];
let windows_commands = ["cmd", "powershell", "notepad", "tasklist"];
let found_commands = [];
for cmd in common_commands {
let path = which(cmd);
if path != () {
found_commands.push(cmd);
}
}
for cmd in windows_commands {
let path = which(cmd);
if path != () {
found_commands.push(cmd);
}
}
assert_true(found_commands.len() > 0, "Should find at least one common command");
print(`✓ Found common commands: ${found_commands}`);
// Test 9: Process filtering accuracy
print("\n--- Test 9: Process Filtering Accuracy ---");
if all_processes.len() > 0 {
let test_process = all_processes[0];
let filtered = process_list(test_process.name);
// All filtered processes should contain the pattern
let all_match = true;
for process in filtered {
if !process.name.contains(test_process.name) {
all_match = false;
break;
}
}
assert_true(all_match, "All filtered processes should contain the search pattern");
print("✓ Process filtering is accurate");
}
// Test 10: Process management performance
print("\n--- Test 10: Process Management Performance ---");
let start_time = timestamp();
let perf_processes = process_list("");
let end_time = timestamp();
let duration = end_time - start_time;
assert_true(duration < 5000, "Process listing should complete within 5 seconds");
assert_true(perf_processes.len() > 0, "Performance test should still return processes");
print(`✓ process_list() completed in ${duration}ms`);
// Test 11: which command performance
print("\n--- Test 11: Which Command Performance ---");
let which_start = timestamp();
let which_result = which("echo");
let which_end = timestamp();
let which_duration = which_end - which_start;
assert_true(which_duration < 1000, "which() should complete within 1 second");
print(`✓ which("echo") completed in ${which_duration}ms`);
// Test 12: Cross-platform process operations
print("\n--- Test 12: Cross-Platform Process Operations ---");
let platform_specific_found = false;
// Try Windows-specific
let cmd_found = which("cmd");
if cmd_found != () {
platform_specific_found = true;
print("✓ Windows platform detected (cmd found)");
}
// Try Unix-specific
let sh_found = which("sh");
if sh_found != () {
platform_specific_found = true;
print("✓ Unix-like platform detected (sh found)");
}
assert_true(platform_specific_found, "Should detect platform-specific commands");
print("\n=== All Process Management Tests Passed! ===");

View File

@@ -0,0 +1,167 @@
// Test script for process error handling functionality
print("=== Process Error Handling Tests ===");
// Test 1: Command execution error handling
print("\n--- Test 1: Command Execution Error Handling ---");
try {
let result = run_command("nonexistent_command_12345");
assert_true(false, "Should have thrown an error for nonexistent command");
} catch(e) {
assert_true(true, "Correctly caught error for nonexistent command");
print("✓ Command execution error handling works");
}
// Test 2: Silent error handling with ignore_error
print("\n--- Test 2: Silent Error Handling with ignore_error ---");
let error_result = run("false").ignore_error().silent().execute();
assert_true(!error_result.success, "Command should fail");
assert_true(error_result.code != 0, "Exit code should be non-zero");
print("✓ Silent error handling with ignore_error works");
// Test 3: Process management error handling
print("\n--- Test 3: Process Management Error Handling ---");
try {
let result = process_get("nonexistent_process_12345");
assert_true(false, "Should have thrown an error for nonexistent process");
} catch(e) {
assert_true(true, "Correctly caught error for nonexistent process");
print("✓ Process management error handling works");
}
// Test 4: Script execution error handling
print("\n--- Test 4: Script Execution Error Handling ---");
let error_script = `
echo "Before error"
false
echo "After error"
`;
try {
let result = run_command(error_script);
assert_true(false, "Should have thrown an error for failing script");
} catch(e) {
assert_true(true, "Correctly caught error for failing script");
print("✓ Script execution error handling works");
}
// Test 5: Error handling with die=false in options
print("\n--- Test 5: Error Handling with die=false in Options ---");
let options = #{
silent: true,
die: false,
log: false
};
let no_die_result = run("false", options);
assert_true(!no_die_result.success, "Command should fail but not throw");
assert_true(no_die_result.code != 0, "Exit code should be non-zero");
print("✓ Error handling with die=false in options works");
// Test 6: Builder pattern error handling
print("\n--- Test 6: Builder Pattern Error Handling ---");
try {
let result = run("nonexistent_command_12345").silent().execute();
assert_true(false, "Should have thrown an error for nonexistent command in builder");
} catch(e) {
assert_true(true, "Correctly caught error for nonexistent command in builder");
print("✓ Builder pattern error handling works");
}
// Test 7: Multiple error conditions
print("\n--- Test 7: Multiple Error Conditions ---");
let error_conditions = [
"nonexistent_command_12345",
"false",
"exit 1"
];
for cmd in error_conditions {
try {
let result = run(cmd).silent().execute();
assert_true(false, `Should have thrown an error for: ${cmd}`);
} catch(e) {
// Expected behavior
}
}
print("✓ Multiple error conditions handled correctly");
// Test 8: Error recovery with ignore_error
print("\n--- Test 8: Error Recovery with ignore_error ---");
let recovery_script = `
echo "Starting script"
false
echo "This should not execute"
`;
let recovery_result = run(recovery_script).ignore_error().silent().execute();
assert_true(!recovery_result.success, "Script should fail");
assert_true(recovery_result.stdout.contains("Starting script"), "Should capture output before error");
print("✓ Error recovery with ignore_error works");
// Test 9: Nested error handling
print("\n--- Test 9: Nested Error Handling ---");
try {
try {
let result = run_command("nonexistent_command_12345");
assert_true(false, "Inner try should fail");
} catch(inner_e) {
// Re-throw to test outer catch
throw inner_e;
}
assert_true(false, "Outer try should fail");
} catch(outer_e) {
assert_true(true, "Nested error handling works");
print("✓ Nested error handling works");
}
// Test 10: Error message content validation
print("\n--- Test 10: Error Message Content Validation ---");
try {
let result = process_get("nonexistent_process_12345");
assert_true(false, "Should have thrown an error");
} catch(e) {
let error_msg = `${e}`;
assert_true(error_msg.len() > 0, "Error message should not be empty");
print(`✓ Error message content: ${error_msg}`);
}
// Test 11: Graceful degradation
print("\n--- Test 11: Graceful Degradation ---");
let graceful_commands = [
"echo 'fallback test'",
"printf 'fallback test'",
"print 'fallback test'"
];
let graceful_success = false;
for cmd in graceful_commands {
try {
let result = run_command(cmd);
if result.success {
graceful_success = true;
break;
}
} catch(e) {
// Try next command
continue;
}
}
assert_true(graceful_success, "Should find at least one working command for graceful degradation");
print("✓ Graceful degradation works");
// Test 12: Error handling performance
print("\n--- Test 12: Error Handling Performance ---");
let error_start = timestamp();
try {
let result = run_command("nonexistent_command_12345");
} catch(e) {
// Expected
}
let error_end = timestamp();
let error_duration = error_end - error_start;
assert_true(error_duration < 5000, "Error handling should be fast (< 5 seconds)");
print(`✓ Error handling completed in ${error_duration}ms`);
print("\n=== All Error Handling Tests Passed! ===");

View File

@@ -0,0 +1,326 @@
// Test script for real-world process scenarios
print("=== Real-World Process Scenarios Tests ===");
// Test 1: System information gathering
print("\n--- Test 1: System Information Gathering ---");
let system_info = #{};
// Get current user
try {
let whoami_result = run_command("whoami");
if whoami_result.success {
system_info.user = whoami_result.stdout.trim();
print(`✓ Current user: ${system_info.user}`);
}
} catch(e) {
print("⚠ whoami command not available");
}
// Get current directory
try {
let pwd_result = run_command("pwd");
if pwd_result.success {
system_info.pwd = pwd_result.stdout.trim();
print(`✓ Current directory: ${system_info.pwd}`);
}
} catch(e) {
// Try Windows alternative
try {
let cd_result = run_command("cd");
if cd_result.success {
system_info.pwd = cd_result.stdout.trim();
print(`✓ Current directory (Windows): ${system_info.pwd}`);
}
} catch(e2) {
print("⚠ pwd/cd commands not available");
}
}
assert_true(system_info.len() > 0, "Should gather at least some system information");
// Test 2: File system operations
print("\n--- Test 2: File System Operations ---");
let temp_file = "/tmp/sal_process_test.txt";
let temp_content = "SAL Process Test Content";
// Create a test file
let create_script = `
echo "${temp_content}" > ${temp_file}
`;
try {
let create_result = run_command(create_script);
if create_result.success {
print("✓ Test file created successfully");
// Read the file back
let read_result = run_command(`cat ${temp_file}`);
if read_result.success {
assert_true(read_result.stdout.contains(temp_content), "File content should match");
print("✓ Test file read successfully");
}
// Clean up
let cleanup_result = run_command(`rm -f ${temp_file}`);
if cleanup_result.success {
print("✓ Test file cleaned up successfully");
}
}
} catch(e) {
print("⚠ File system operations not available on this platform");
}
// Test 3: Process monitoring workflow
print("\n--- Test 3: Process Monitoring Workflow ---");
let monitoring_workflow = || {
// Get all processes
let all_processes = process_list("");
assert_true(all_processes.len() > 0, "Should find running processes");
// Find processes with common names
let common_patterns = ["init", "kernel", "system", "explorer", "winlogon"];
let found_patterns = [];
for pattern in common_patterns {
let matches = process_list(pattern);
if matches.len() > 0 {
found_patterns.push(pattern);
}
}
print(`✓ Process monitoring found patterns: ${found_patterns}`);
return found_patterns.len() > 0;
};
assert_true(monitoring_workflow(), "Process monitoring workflow should succeed");
// Test 4: Command availability checking
print("\n--- Test 4: Command Availability Checking ---");
let essential_commands = ["echo"];
let optional_commands = ["git", "curl", "wget", "python", "node", "java"];
let available_commands = [];
let missing_commands = [];
// Check essential commands
for cmd in essential_commands {
let path = which(cmd);
if path != () {
available_commands.push(cmd);
} else {
missing_commands.push(cmd);
}
}
// Check optional commands
for cmd in optional_commands {
let path = which(cmd);
if path != () {
available_commands.push(cmd);
}
}
assert_true(missing_commands.len() == 0, "All essential commands should be available");
print(`✓ Available commands: ${available_commands}`);
print(`✓ Command availability check completed`);
// Test 5: Batch processing simulation
print("\n--- Test 5: Batch Processing Simulation ---");
let batch_commands = [
"echo 'Processing item 1'",
"echo 'Processing item 2'",
"echo 'Processing item 3'"
];
let batch_results = [];
let batch_success = true;
for cmd in batch_commands {
try {
let result = run(cmd).silent().execute();
batch_results.push(result);
if !result.success {
batch_success = false;
}
} catch(e) {
batch_success = false;
break;
}
}
assert_true(batch_success, "Batch processing should succeed");
assert_true(batch_results.len() == batch_commands.len(), "Should process all batch items");
print(`✓ Batch processing completed: ${batch_results.len()} items`);
// Test 6: Environment variable handling
print("\n--- Test 6: Environment Variable Handling ---");
let env_test_script = `
export TEST_VAR="test_value"
echo "TEST_VAR=$TEST_VAR"
`;
try {
let env_result = run_command(env_test_script);
if env_result.success {
assert_true(env_result.stdout.contains("TEST_VAR=test_value"), "Environment variable should be set");
print("✓ Environment variable handling works");
}
} catch(e) {
print("⚠ Environment variable test not available");
}
// Test 7: Pipeline simulation
print("\n--- Test 7: Pipeline Simulation ---");
let pipeline_script = `
echo "line1
line2
line3" | grep "line2"
`;
try {
let pipeline_result = run_command(pipeline_script);
if pipeline_result.success {
assert_true(pipeline_result.stdout.contains("line2"), "Pipeline should filter correctly");
print("✓ Pipeline simulation works");
}
} catch(e) {
print("⚠ Pipeline simulation not available");
}
// Test 8: Error recovery workflow
print("\n--- Test 8: Error Recovery Workflow ---");
let recovery_workflow = || {
let primary_cmd = "nonexistent_primary_command";
let fallback_cmd = "echo 'fallback executed'";
// Try primary command
try {
let primary_result = run_command(primary_cmd);
return primary_result.success;
} catch(e) {
// Primary failed, try fallback
try {
let fallback_result = run_command(fallback_cmd);
return fallback_result.success && fallback_result.stdout.contains("fallback executed");
} catch(e2) {
return false;
}
}
};
assert_true(recovery_workflow(), "Error recovery workflow should succeed");
print("✓ Error recovery workflow works");
// Test 9: Resource monitoring
print("\n--- Test 9: Resource Monitoring ---");
let resource_monitoring = || {
let start_time = timestamp();
// Simulate resource-intensive operation
let intensive_script = `
for i in $(seq 1 10); do
echo "Processing $i"
done
`;
try {
let result = run(intensive_script).silent().execute();
let end_time = timestamp();
let duration = end_time - start_time;
print(`✓ Resource monitoring: operation took ${duration}ms`);
return result.success && duration < 10000; // Should complete within 10 seconds
} catch(e) {
return false;
}
};
assert_true(resource_monitoring(), "Resource monitoring should work");
// Test 10: Cross-platform compatibility
print("\n--- Test 10: Cross-Platform Compatibility ---");
let cross_platform_test = || {
// Test basic commands that should work everywhere
let basic_commands = ["echo hello"];
for cmd in basic_commands {
try {
let result = run_command(cmd);
if !result.success {
return false;
}
} catch(e) {
return false;
}
}
// Test platform detection
let windows_detected = which("cmd") != ();
let unix_detected = which("sh") != ();
return windows_detected || unix_detected;
};
assert_true(cross_platform_test(), "Cross-platform compatibility should work");
print("✓ Cross-platform compatibility verified");
// Test 11: Complex workflow integration
print("\n--- Test 11: Complex Workflow Integration ---");
let complex_workflow = || {
// Step 1: Check prerequisites
let echo_available = which("echo") != ();
if !echo_available {
return false;
}
// Step 2: Execute main task
let main_result = run("echo 'Complex workflow step'").silent().execute();
if !main_result.success {
return false;
}
// Step 3: Verify results
let verify_result = run("echo 'Verification step'").silent().execute();
if !verify_result.success {
return false;
}
// Step 4: Cleanup (always succeeds)
let cleanup_result = run("echo 'Cleanup step'").ignore_error().silent().execute();
return true;
};
assert_true(complex_workflow(), "Complex workflow integration should succeed");
print("✓ Complex workflow integration works");
// Test 12: Performance under load
print("\n--- Test 12: Performance Under Load ---");
let performance_test = || {
let start_time = timestamp();
let iterations = 5;
let success_count = 0;
for i in range(0, iterations) {
try {
let result = run(`echo "Iteration ${i}"`).silent().execute();
if result.success {
success_count += 1;
}
} catch(e) {
// Continue with next iteration
}
}
let end_time = timestamp();
let duration = end_time - start_time;
let avg_time = duration / iterations;
print(`✓ Performance test: ${success_count}/${iterations} succeeded, avg ${avg_time}ms per operation`);
return success_count == iterations && avg_time < 1000; // Each operation should be < 1 second
};
assert_true(performance_test(), "Performance under load should be acceptable");
print("\n=== All Real-World Scenarios Tests Passed! ===");

View File

@@ -0,0 +1,321 @@
use rhai::Engine;
use sal_process::rhai::register_process_module;
fn create_test_engine() -> Engine {
let mut engine = Engine::new();
register_process_module(&mut engine).unwrap();
engine
}
#[test]
fn test_rhai_run_command() {
let engine = create_test_engine();
let script = r#"
let result = run_command("echo hello");
result.success && result.stdout.contains("hello")
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_run_silent() {
let engine = create_test_engine();
let script = r#"
let result = run_silent("echo silent test");
result.success && result.stdout.contains("silent test")
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_run_builder_pattern() {
let engine = create_test_engine();
let script = r#"
let result = run("echo builder test").silent().execute();
result.success && result.stdout.contains("builder test")
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_run_builder_ignore_error() {
let engine = create_test_engine();
let script = r#"
let result = run("false").ignore_error().silent().execute();
!result.success
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_run_builder_with_log() {
let engine = create_test_engine();
let script = r#"
let result = run("echo log test").log().silent().execute();
result.success && result.stdout.contains("log test")
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_which_function() {
let engine = create_test_engine();
// Test with a command that should exist
#[cfg(target_os = "windows")]
let script = r#"
let path = which("cmd");
path != () && path.len() > 0
"#;
#[cfg(not(target_os = "windows"))]
let script = r#"
let path = which("sh");
path != () && path.len() > 0
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_which_nonexistent() {
let engine = create_test_engine();
let script = r#"
let path = which("nonexistent_command_12345");
path == ()
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_process_list() {
let engine = create_test_engine();
let script = r#"
let processes = process_list("");
processes.len() > 0
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_process_list_with_pattern() {
let engine = create_test_engine();
let script = r#"
let all_processes = process_list("");
if all_processes.len() > 0 {
let first_process = all_processes[0];
let filtered = process_list(first_process.name);
filtered.len() >= 1
} else {
false
}
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_process_info_properties() {
let engine = create_test_engine();
let script = r#"
let processes = process_list("");
if processes.len() > 0 {
let process = processes[0];
process.pid > 0 && process.name.len() > 0
} else {
false
}
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_command_result_properties() {
let engine = create_test_engine();
let script = r#"
let result = run_command("echo test");
result.success && result.stdout.contains("test")
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_kill_nonexistent() {
let engine = create_test_engine();
let script = r#"
let result = kill("nonexistent_process_12345");
result.contains("No matching processes") || result.contains("Successfully killed")
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_run_with_options() {
let engine = create_test_engine();
let script = r#"
let options = #{
silent: true,
die: false,
log: false
};
let result = run("echo options test", options);
result.success && result.stdout.contains("options test")
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_run_multiline_script() {
let engine = create_test_engine();
let script = r#"
let bash_script = `
echo "Line 1"
echo "Line 2"
echo "Line 3"
`;
let result = run_command(bash_script);
result.success &&
result.stdout.contains("Line 1") &&
result.stdout.contains("Line 2") &&
result.stdout.contains("Line 3")
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_error_handling() {
let engine = create_test_engine();
// Test that errors are properly converted to Rhai errors
let script = r#"
let error_occurred = false;
try {
run_command("nonexistent_command_12345");
} catch(e) {
error_occurred = true;
}
error_occurred
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_process_get_error_handling() {
let engine = create_test_engine();
let script = r#"
let error_occurred = false;
try {
process_get("nonexistent_process_12345");
} catch(e) {
error_occurred = true;
}
error_occurred
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_builder_chaining() {
let engine = create_test_engine();
let script = r#"
let result = run("echo chaining")
.silent()
.ignore_error()
.log()
.execute();
result.success && result.stdout.contains("chaining")
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_cross_platform_commands() {
let engine = create_test_engine();
// Test platform-specific commands
#[cfg(target_os = "windows")]
let script = r#"
let result = run_command("echo Windows test");
result.success && result.stdout.contains("Windows test")
"#;
#[cfg(not(target_os = "windows"))]
let script = r#"
let result = run_command("echo Unix test");
result.success && result.stdout.contains("Unix test")
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}
#[test]
fn test_rhai_complex_workflow() {
let engine = create_test_engine();
let script = r#"
// Test a complex workflow combining multiple functions
let echo_path = which("echo");
if echo_path == () {
false
} else {
let result = run("echo workflow test").silent().execute();
if !result.success {
false
} else {
let processes = process_list("");
processes.len() > 0
}
}
"#;
let result: bool = engine.eval(script).unwrap();
assert!(result);
}

View File

@@ -0,0 +1,274 @@
use sal_process::{run, run_command, run_silent, RunError};
use std::env;
#[test]
fn test_run_simple_command() {
let result = run_command("echo hello").unwrap();
assert!(result.success);
assert_eq!(result.code, 0);
assert!(result.stdout.contains("hello"));
assert!(result.stderr.is_empty());
}
#[test]
fn test_run_command_with_args() {
let result = run_command("echo hello world").unwrap();
assert!(result.success);
assert_eq!(result.code, 0);
assert!(result.stdout.contains("hello world"));
}
#[test]
fn test_run_silent() {
let result = run_silent("echo silent test").unwrap();
assert!(result.success);
assert_eq!(result.code, 0);
assert!(result.stdout.contains("silent test"));
}
#[test]
fn test_run_builder_pattern() {
let result = run("echo builder test").silent(true).execute().unwrap();
assert!(result.success);
assert_eq!(result.code, 0);
assert!(result.stdout.contains("builder test"));
}
#[test]
fn test_run_builder_die_false() {
let result = run("false") // Command that always fails
.die(false)
.silent(true)
.execute()
.unwrap();
assert!(!result.success);
assert_ne!(result.code, 0);
}
#[test]
fn test_run_builder_die_true() {
// Use a command that will definitely fail
let result = run("exit 1") // Script that always fails
.die(true)
.silent(true)
.execute();
assert!(result.is_err());
}
#[test]
fn test_run_multiline_script() {
let script = r#"
echo "Line 1"
echo "Line 2"
echo "Line 3"
"#;
let result = run_command(script).unwrap();
assert!(result.success);
assert_eq!(result.code, 0);
assert!(result.stdout.contains("Line 1"));
assert!(result.stdout.contains("Line 2"));
assert!(result.stdout.contains("Line 3"));
}
#[test]
fn test_run_script_with_shebang() {
let script = r#"#!/bin/bash
echo "Script with shebang"
exit 0
"#;
let result = run_command(script).unwrap();
assert!(result.success);
assert_eq!(result.code, 0);
assert!(result.stdout.contains("Script with shebang"));
}
#[test]
fn test_run_script_error_handling() {
let script = r#"
echo "Before error"
false
echo "After error"
"#;
let result = run(script).silent(true).execute();
assert!(result.is_err());
}
#[test]
fn test_run_empty_command() {
let result = run_command("");
assert!(result.is_err());
match result.unwrap_err() {
RunError::EmptyCommand => {}
_ => panic!("Expected EmptyCommand error"),
}
}
#[test]
fn test_run_nonexistent_command() {
let result = run("nonexistent_command_12345").silent(true).execute();
assert!(result.is_err());
}
#[test]
fn test_run_with_environment_variables() {
env::set_var("TEST_VAR", "test_value");
#[cfg(target_os = "windows")]
let script = "echo %TEST_VAR%";
#[cfg(not(target_os = "windows"))]
let script = r#"
export TEST_VAR="test_value"
echo $TEST_VAR
"#;
let result = run_command(script).unwrap();
assert!(result.success);
assert!(result.stdout.contains("test_value"));
env::remove_var("TEST_VAR");
}
#[test]
fn test_run_with_working_directory() {
// Test that commands run in the current working directory
#[cfg(target_os = "windows")]
let result = run_command("cd").unwrap();
#[cfg(not(target_os = "windows"))]
let result = run_command("pwd").unwrap();
assert!(result.success);
assert!(!result.stdout.is_empty());
}
#[test]
fn test_command_result_properties() {
let result = run_command("echo test").unwrap();
// Test all CommandResult properties
assert!(!result.stdout.is_empty());
assert!(result.stderr.is_empty());
assert!(result.success);
assert_eq!(result.code, 0);
}
#[test]
fn test_run_builder_log_option() {
// Test that log option doesn't cause errors
let result = run("echo log test")
.log(true)
.silent(true)
.execute()
.unwrap();
assert!(result.success);
assert!(result.stdout.contains("log test"));
}
#[test]
fn test_run_cross_platform_commands() {
// Test commands that work on all platforms
// Test echo command
let result = run_command("echo cross-platform").unwrap();
assert!(result.success);
assert!(result.stdout.contains("cross-platform"));
// Test basic shell operations
#[cfg(target_os = "windows")]
let result = run_command("dir").unwrap();
#[cfg(not(target_os = "windows"))]
let result = run_command("ls").unwrap();
assert!(result.success);
}
#[test]
fn test_run_script_with_variables() {
let script = r#"
VAR="test_variable"
echo "Variable value: $VAR"
"#;
let result = run_command(script).unwrap();
assert!(result.success);
assert!(result.stdout.contains("Variable value: test_variable"));
}
#[test]
fn test_run_script_with_conditionals() {
#[cfg(target_os = "windows")]
let script = r#"
if "hello"=="hello" (
echo Condition passed
) else (
echo Condition failed
)
"#;
#[cfg(not(target_os = "windows"))]
let script = r#"
if [ "hello" = "hello" ]; then
echo "Condition passed"
else
echo "Condition failed"
fi
"#;
let result = run_command(script).unwrap();
assert!(result.success);
assert!(result.stdout.contains("Condition passed"));
}
#[test]
fn test_run_script_with_loops() {
#[cfg(target_os = "windows")]
let script = r#"
for %%i in (1 2 3) do (
echo Number: %%i
)
"#;
#[cfg(not(target_os = "windows"))]
let script = r#"
for i in 1 2 3; do
echo "Number: $i"
done
"#;
let result = run_command(script).unwrap();
assert!(result.success);
assert!(result.stdout.contains("Number: 1"));
assert!(result.stdout.contains("Number: 2"));
assert!(result.stdout.contains("Number: 3"));
}
#[test]
fn test_run_with_stderr_output() {
// Test that stderr field exists and can be accessed
let result = run_command("echo test").unwrap();
assert!(result.success);
// Just verify that stderr field exists and is accessible
let _stderr_len = result.stderr.len(); // This verifies stderr field exists
}
#[test]
fn test_run_builder_chaining() {
let result = run("echo chaining test")
.silent(true)
.die(true)
.log(false)
.execute()
.unwrap();
assert!(result.success);
assert!(result.stdout.contains("chaining test"));
}

View File

@@ -0,0 +1,24 @@
[package]
name = "sal-virt"
version = "0.1.0"
edition = "2021"
authors = ["PlanetFirst <info@incubaid.com>"]
description = "SAL Virt - Virtualization and containerization tools including Buildah, Nerdctl, and RFS"
repository = "https://git.threefold.info/herocode/sal"
license = "Apache-2.0"
[dependencies]
# Core dependencies
anyhow = "1.0.98"
tempfile = "3.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rhai = { version = "1.12.0", features = ["sync"] }
# SAL dependencies
sal-process = { path = "../process" }
sal-os = { path = "../os" }
[dev-dependencies]
tempfile = "3.5"
lazy_static = "1.4.0"

View File

@@ -0,0 +1,176 @@
# SAL Virt Package (`sal-virt`)
The `sal-virt` package provides comprehensive virtualization and containerization tools for building, managing, and deploying containers and filesystem layers.
## Installation
Add this to your `Cargo.toml`:
```toml
[dependencies]
sal-virt = "0.1.0"
```
## Features
- **Buildah**: OCI/Docker image building with builder pattern API
- **Nerdctl**: Container lifecycle management with containerd
- **RFS**: Remote filesystem mounting and layer management
- **Cross-Platform**: Works across Windows, macOS, and Linux
- **Rhai Integration**: Full support for Rhai scripting language
- **Error Handling**: Comprehensive error types and handling
## Modules
### Buildah
Container image building with Buildah, providing:
- Builder pattern for container configuration
- Image management and operations
- Content operations (copy, add, run commands)
- Debug mode support
### Nerdctl
Container management with Nerdctl, providing:
- Container lifecycle management (create, start, stop, remove)
- Image operations (pull, push, build, tag)
- Network and volume management
- Health checks and resource limits
- Builder pattern for container configuration
### RFS
Remote filesystem operations, providing:
- Mount/unmount operations for various filesystem types
- Pack/unpack operations for filesystem layers
- Support for Local, SSH, S3, WebDAV, and custom filesystems
- Store specifications for different backends
## Usage
### Basic Buildah Example
```rust
use sal_virt::buildah::Builder;
// Create a new builder
let mut builder = Builder::new("my-container", "alpine:latest")?;
// Configure the builder
builder.set_debug(true);
// Add content and run commands
builder.copy("./app", "/usr/local/bin/app")?;
builder.run(&["chmod", "+x", "/usr/local/bin/app"])?;
// Commit the image
let image_id = builder.commit("my-app:latest")?;
```
### Basic Nerdctl Example
```rust
use sal_virt::nerdctl::Container;
// Create a container from an image
let container = Container::from_image("web-app", "nginx:alpine")?
.with_port("8080:80")
.with_volume("/host/data:/app/data")
.with_env("ENV_VAR", "production")
.with_restart_policy("always");
// Run the container
let result = container.run()?;
```
### Basic RFS Example
```rust
use sal_virt::rfs::{RfsBuilder, MountType, StoreSpec};
// Mount a remote filesystem
let mount = RfsBuilder::new("user@host:/remote/path", "/local/mount", MountType::SSH)
.with_option("read_only", "true")
.mount()?;
// Pack a directory
let specs = vec![StoreSpec::new("file").with_option("path", "/tmp/store")];
let pack_result = pack_directory("/source/dir", "/output/pack.rfs", &specs)?;
```
## Rhai Integration
All functionality is available in Rhai scripts:
```javascript
// Buildah in Rhai
let builder = bah_new("my-container", "alpine:latest");
builder.copy("./app", "/usr/local/bin/app");
builder.run(["chmod", "+x", "/usr/local/bin/app"]);
// Nerdctl in Rhai
let container = nerdctl_container_from_image("web-app", "nginx:alpine")
.with_port("8080:80")
.with_env("ENV", "production");
container.run();
// RFS in Rhai
let mount_options = #{ "read_only": "true" };
rfs_mount("user@host:/remote", "/local/mount", "ssh", mount_options);
```
## Dependencies
- `sal-process`: For command execution
- `sal-os`: For filesystem operations
- `anyhow`: For error handling
- `serde`: For serialization
- `rhai`: For scripting integration
## Testing
The package includes comprehensive tests:
```bash
# Run all tests
cargo test
# Run specific test suites
cargo test buildah_tests
cargo test nerdctl_tests
cargo test rfs_tests
# Run Rhai integration tests
cargo test --test rhai_integration
```
## Error Handling
Each module provides its own error types:
- `BuildahError`: For Buildah operations
- `NerdctlError`: For Nerdctl operations
- `RfsError`: For RFS operations
All errors implement `std::error::Error` and provide detailed error messages.
## Platform Support
- **Linux**: Full support for all features
- **macOS**: Full support (requires Docker Desktop or similar)
- **Windows**: Full support (requires Docker Desktop or WSL2)
## Security
- Credentials are handled securely and never logged
- URLs with passwords are masked in logs
- All operations respect filesystem permissions
- Network operations use secure defaults
## Configuration
Most operations can be configured through environment variables:
- `BUILDAH_DEBUG`: Enable debug mode for Buildah
- `NERDCTL_DEBUG`: Enable debug mode for Nerdctl
- `RFS_DEBUG`: Enable debug mode for RFS
## License
Apache-2.0

View File

@@ -0,0 +1,232 @@
# SAL Buildah Module (`sal::virt::buildah`)
## Overview
The Buildah module in SAL provides a comprehensive Rust interface for interacting with the `buildah` command-line tool. It allows users to build OCI (Open Container Initiative) and Docker-compatible container images programmatically. The module offers both a high-level `Builder` API for step-by-step image construction and static functions for managing images in local storage.
A Rhai script interface for this module is also available via `sal::rhai::buildah`, making these functionalities accessible from `herodo` scripts.
## Core Components
### 1. `Builder` Struct (`sal::virt::buildah::Builder`)
The `Builder` struct is the primary entry point for constructing container images. It encapsulates a Buildah working container, created from a base image, and provides methods to modify this container and eventually commit it as a new image.
- **Creation**: `Builder::new(name: &str, image: &str) -> Result<Builder, BuildahError>`
- Creates a new working container (or re-attaches to an existing one with the same name) from the specified base `image`.
- **Debug Mode**: `builder.set_debug(true)` / `builder.debug()`
- Enables/disables verbose logging for Buildah commands executed by this builder instance.
#### Working Container Operations:
- `builder.run(command: &str) -> Result<CommandResult, BuildahError>`: Executes a shell command inside the working container (e.g., `buildah run <container> -- <command>`).
- `builder.run_with_isolation(command: &str, isolation: &str) -> Result<CommandResult, BuildahError>`: Runs a command with specified isolation (e.g., "chroot").
- `builder.copy(source_on_host: &str, dest_in_container: &str) -> Result<CommandResult, BuildahError>`: Copies files/directories from the host to the container (`buildah copy`).
- `builder.add(source_on_host: &str, dest_in_container: &str) -> Result<CommandResult, BuildahError>`: Adds files/directories to the container (`buildah add`), potentially handling URLs and archive extraction.
- `builder.config(options: HashMap<String, String>) -> Result<CommandResult, BuildahError>`: Modifies image metadata (e.g., environment variables, labels, entrypoint, cmd). Example options: `{"env": "MYVAR=value", "label": "mylabel=myvalue"}`.
- `builder.set_entrypoint(entrypoint: &str) -> Result<CommandResult, BuildahError>`: Sets the image entrypoint.
- `builder.set_cmd(cmd: &str) -> Result<CommandResult, BuildahError>`: Sets the default command for the image.
- `builder.commit(image_name: &str) -> Result<CommandResult, BuildahError>`: Commits the current state of the working container to a new image named `image_name`.
- `builder.remove() -> Result<CommandResult, BuildahError>`: Removes the working container (`buildah rm`).
- `builder.reset() -> Result<(), BuildahError>`: Removes the working container and resets the builder state.
### 2. Static Image Management Functions (on `Builder`)
These functions operate on images in the local Buildah storage and are not tied to a specific `Builder` instance.
- `Builder::images() -> Result<Vec<Image>, BuildahError>`: Lists all images available locally (`buildah images --json`). Returns a vector of `Image` structs.
- `Builder::image_remove(image_ref: &str) -> Result<CommandResult, BuildahError>`: Removes an image (`buildah rmi <image_ref>`).
- `Builder::image_pull(image_name: &str, tls_verify: bool) -> Result<CommandResult, BuildahError>`: Pulls an image from a registry (`buildah pull`).
- `Builder::image_push(image_ref: &str, destination: &str, tls_verify: bool) -> Result<CommandResult, BuildahError>`: Pushes an image to a registry (`buildah push`).
- `Builder::image_tag(image_ref: &str, new_name: &str) -> Result<CommandResult, BuildahError>`: Tags an image (`buildah tag`).
- `Builder::image_commit(container_ref: &str, image_name: &str, format: Option<&str>, squash: bool, rm: bool) -> Result<CommandResult, BuildahError>`: A static version to commit any existing container to an image, with options for format (e.g., "oci", "docker"), squashing layers, and removing the container post-commit.
- `Builder::build(tag: Option<&str>, context_dir: &str, file: &str, isolation: Option<&str>) -> Result<CommandResult, BuildahError>`: Builds an image from a Dockerfile/Containerfile (`buildah bud`).
*Note: Many static image functions also have a `_with_debug(..., debug: bool)` variant for explicit debug control.*
### 3. `Image` Struct (`sal::virt::buildah::Image`)
Represents a container image as listed by `buildah images`.
```rust
pub struct Image {
pub id: String, // Image ID
pub names: Vec<String>, // Image names/tags
pub size: String, // Image size
pub created: String, // Creation timestamp (as string)
}
```
### 4. `ContentOperations` (`sal::virt::buildah::ContentOperations`)
Provides static methods for reading and writing file content directly within a container, useful for dynamic configuration or inspection.
- `ContentOperations::write_content(container_id: &str, content: &str, dest_path_in_container: &str) -> Result<CommandResult, BuildahError>`: Writes string content to a file inside the specified container.
- `ContentOperations::read_content(container_id: &str, source_path_in_container: &str) -> Result<String, BuildahError>`: Reads the content of a file from within the specified container into a string.
### 5. `BuildahError` Enum (`sal::virt::buildah::BuildahError`)
Defines the error types that can occur during Buildah operations:
- `CommandExecutionFailed(io::Error)`: The `buildah` command itself failed to start.
- `CommandFailed(String)`: The `buildah` command ran but returned a non-zero exit code or error.
- `JsonParseError(String)`: Failed to parse JSON output from Buildah.
- `ConversionError(String)`: Error during data conversion.
- `Other(String)`: Generic error.
## Key Design Points
The SAL Buildah module is designed with the following principles:
- **Builder Pattern**: The `Builder` struct (`sal::virt::buildah::Builder`) employs a builder pattern, enabling a fluent, step-by-step, and stateful approach to constructing container images. Each `Builder` instance manages a specific working container.
- **Separation of Concerns**:
- **Instance Methods**: Operations specific to a working container (e.g., `run`, `copy`, `config`, `commit`) are methods on the `Builder` instance.
- **Static Methods**: General image management tasks (e.g., listing images with `Builder::images()`, removing images with `Builder::image_remove()`, pulling, pushing, tagging, and building from a Dockerfile with `Builder::build()`) are provided as static functions on the `Builder` struct.
- **Direct Content Manipulation**: The `ContentOperations` struct provides static methods (`write_content`, `read_content`) to directly interact with files within a Buildah container. This is typically achieved by temporarily mounting the container or using `buildah add` with temporary files, abstracting the complexity from the user.
- **Debuggability**: Fine-grained control over `buildah` command logging is provided. The `builder.set_debug(true)` method enables verbose output for a specific `Builder` instance. Many static functions also offer `_with_debug(..., debug: bool)` variants. This is managed internally via a thread-local flag passed to the core `execute_buildah_command` function.
- **Comprehensive Rhai Integration**: Most functionalities of the Buildah module are exposed to Rhai scripts executed via `herodo`, allowing for powerful automation of image building workflows. This is facilitated by the `sal::rhai::buildah` module.
## Low-Level Command Execution
- `execute_buildah_command(args: &[&str]) -> Result<CommandResult, BuildahError>` (in `sal::virt::buildah::cmd`):
The core function that executes `buildah` commands. It handles debug logging based on a thread-local flag, which is managed by the higher-level `Builder` methods and `_with_debug` static function variants.
## Usage Example (Rust)
```rust
use sal::virt::buildah::{Builder, BuildahError, ContentOperations};
use std::collections::HashMap;
fn build_custom_image() -> Result<String, BuildahError> {
// Create a new builder from a base image (e.g., alpine)
let mut builder = Builder::new("my-custom-container", "docker.io/library/alpine:latest")?;
builder.set_debug(true);
// Run some commands
builder.run("apk add --no-cache curl")?;
builder.run("mkdir /app")?;
// Add a file
ContentOperations::write_content(builder.container_id().unwrap(), "Hello from SAL!", "/app/hello.txt")?;
// Set image configuration
let mut config_opts = HashMap::new();
config_opts.insert("workingdir".to_string(), "/app".to_string());
config_opts.insert("label".to_string(), "maintainer=sal-user".to_string());
builder.config(config_opts)?;
builder.set_entrypoint("["/usr/bin/curl"]")?;
builder.set_cmd("["http://ifconfig.me"]")?;
// Commit the image
let image_tag = "localhost/my-custom-image:latest";
builder.commit(image_tag)?;
println!("Successfully built image: {}", image_tag);
// Clean up the working container
builder.remove()?;
Ok(image_tag.to_string())
}
fn main() {
match build_custom_image() {
Ok(tag) => println!("Image {} created.", tag),
Err(e) => eprintln!("Error building image: {}", e),
}
}
```
## Rhai Scripting with `herodo`
The Buildah module's capabilities are extensively exposed to Rhai scripts, enabling automation of image building and management tasks via the `herodo` CLI tool. The `sal::rhai::buildah` module registers the necessary functions and types.
Below is a summary of commonly used Rhai functions for Buildah. (Note: `builder` refers to an instance of `BuildahBuilder` obtained typically via `bah_new`).
### Builder Object Management
- `bah_new(name: String, image: String) -> BuildahBuilder`: Creates a new Buildah builder instance (working container) from a base `image` with a given `name`.
- `builder.remove()`: Removes the working container associated with the `builder`.
- `builder.reset()`: Removes the working container and resets the `builder` state.
### Builder Configuration & Operations
- `builder.set_debug(is_debug: bool)`: Enables or disables verbose debug logging for commands executed by this `builder`.
- `builder.debug_mode` (property): Get or set the debug mode (e.g., `let mode = builder.debug_mode; builder.debug_mode = true;`).
- `builder.container_id` (property): Returns the ID of the working container (e.g., `let id = builder.container_id;`).
- `builder.name` (property): Returns the name of the builder/working container.
- `builder.image` (property): Returns the base image name used by the builder.
- `builder.run(command: String)`: Executes a shell command inside the `builder`'s working container.
- `builder.run_with_isolation(command: String, isolation: String)`: Runs a command with specified isolation (e.g., "chroot").
- `builder.copy(source_on_host: String, dest_in_container: String)`: Copies files/directories from the host to the `builder`'s container.
- `builder.add(source_on_host: String, dest_in_container: String)`: Adds files/directories to the `builder`'s container (can handle URLs and auto-extract archives).
- `builder.config(options: Map)`: Modifies image metadata. `options` is a Rhai map, e.g., `#{ "env": "MYVAR=value", "label": "foo=bar" }`.
- `builder.set_entrypoint(entrypoint: String)`: Sets the image entrypoint (e.g., `builder.set_entrypoint("[/app/run.sh]")`).
- `builder.set_cmd(cmd: String)`: Sets the default command for the image (e.g., `builder.set_cmd("[--help]")`).
- `builder.commit(image_tag: String)`: Commits the current state of the `builder`'s working container to a new image with `image_tag`.
### Content Operations (with a Builder instance)
- `bah_write_content(builder: BuildahBuilder, content: String, dest_path_in_container: String)`: Writes string `content` to a file at `dest_path_in_container` inside the `builder`'s container.
- `bah_read_content(builder: BuildahBuilder, source_path_in_container: String) -> String`: Reads the content of a file from `source_path_in_container` within the `builder`'s container.
### Global Image Operations
These functions generally correspond to static methods in Rust and operate on the local Buildah image storage.
- `bah_images() -> Array`: Lists all images available locally. Returns an array of `BuildahImage` objects.
- `bah_image_remove(image_ref: String)`: Removes an image (e.g., by ID or tag) from local storage.
- `bah_image_pull(image_name: String, tls_verify: bool)`: Pulls an image from a registry.
- `bah_image_push(image_ref: String, destination: String, tls_verify: bool)`: Pushes a local image to a registry.
- `bah_image_tag(image_ref: String, new_name: String)`: Adds a new tag (`new_name`) to an existing image (`image_ref`).
- `bah_build(tag: String, context_dir: String, file: String, isolation: String)`: Builds an image from a Dockerfile/Containerfile (equivalent to `buildah bud`). `file` is the path to the Dockerfile relative to `context_dir`. `isolation` can be e.g., "chroot".
### Example `herodo` Rhai Script (Revisited)
```rhai
// Create a new builder
let builder = bah_new("my-rhai-app", "docker.io/library/alpine:latest");
builder.debug_mode = true; // Enable debug logging for this builder
// Run commands in the container
builder.run("apk add --no-cache figlet curl");
builder.run("mkdir /data");
// Write content to a file in the container
bah_write_content(builder, "Hello from SAL Buildah via Rhai!", "/data/message.txt");
// Configure image metadata
builder.config(#{
"env": "APP_VERSION=1.0",
"label": "author=HerodoUser"
});
builder.set_entrypoint('["figlet"]');
builder.set_cmd('["Rhai Build"]');
// Commit the image
let image_name = "localhost/my-rhai-app:v1";
builder.commit(image_name);
print(`Image committed: ${image_name}`);
// Clean up the working container
builder.remove();
print("Builder container removed.");
// List local images
print("Current local images:");
let images = bah_images();
for img in images {
print(` ID: ${img.id}, Name(s): ${img.names}, Size: ${img.size}`);
}
// Example: Build from a Dockerfile (assuming Dockerfile exists at /tmp/build_context/Dockerfile)
// Ensure /tmp/build_context/Dockerfile exists with simple content like:
// FROM alpine
// RUN echo "Built with bah_build" > /built.txt
// CMD cat /built.txt
//
// if exist("/tmp/build_context/Dockerfile") {
// print("Building from Dockerfile...");
// bah_build("localhost/from-dockerfile:latest", "/tmp/build_context", "Dockerfile", "chroot");
// print("Dockerfile build complete.");
// bah_image_remove("localhost/from-dockerfile:latest"); // Clean up
// } else {
// print("Skipping Dockerfile build example: /tmp/build_context/Dockerfile not found.");
// }
```
This README provides a guide to using the SAL Buildah module. For more detailed information on specific functions and their parameters, consult the Rust doc comments within the source code.

View File

@@ -0,0 +1,33 @@
PREFIX := /usr/local
DATADIR := ${PREFIX}/share
MANDIR := $(DATADIR)/man
# Following go-md2man is guaranteed on host
GOMD2MAN ?= ../tests/tools/build/go-md2man
ifeq ($(shell uname -s),FreeBSD)
SED=gsed
else
SED=sed
endif
docs: $(patsubst %.md,%,$(wildcard *.md))
%.1: %.1.md
### sed is used to filter http/s links as well as relative links
### replaces "\" at the end of a line with two spaces
### this ensures that manpages are rendered correctly
@$(SED) -e 's/\((buildah[^)]*\.md\(#.*\)\?)\)//g' \
-e 's/\[\(buildah[^]]*\)\]/\1/g' \
-e 's/\[\([^]]*\)](http[^)]\+)/\1/g' \
-e 's;<\(/\)\?\(a\|a\s\+[^>]*\|sup\)>;;g' \
-e 's/\\$$/ /g' $< | \
$(GOMD2MAN) -in /dev/stdin -out $@
.PHONY: install
install:
install -d ${DESTDIR}/${MANDIR}/man1
install -m 0644 buildah*.1 ${DESTDIR}/${MANDIR}/man1
install -m 0644 links/buildah*.1 ${DESTDIR}/${MANDIR}/man1
.PHONY: clean
clean:
$(RM) buildah*.1

View File

@@ -0,0 +1,162 @@
# buildah-add "1" "April 2021" "buildah"
## NAME
buildah\-add - Add the contents of a file, URL, or a directory to a container.
## SYNOPSIS
**buildah add** [*options*] *container* *src* [[*src* ...] *dest*]
## DESCRIPTION
Adds the contents of a file, URL, or a directory to a container's working
directory or a specified location in the container. If a local source file
appears to be an archive, its contents are extracted and added instead of the
archive file itself. If a local directory is specified as a source, its
*contents* are copied to the destination.
## OPTIONS
**--add-history**
Add an entry to the history which will note the digest of the added content.
Defaults to false.
Note: You can also override the default value of --add-history by setting the
BUILDAH\_HISTORY environment variable. `export BUILDAH_HISTORY=true`
**--cert-dir** *path*
Use certificates at *path* (\*.crt, \*.cert, \*.key) when connecting to
registries for pulling images named with the **--from** flag, and when
connecting to HTTPS servers when fetching sources from locations specified with
HTTPS URLs. The default certificates directory is _/etc/containers/certs.d_.
**--checksum** *checksum*
Checksum the source content. The value of *checksum* must be a standard
container digest string. Only supported for HTTP sources.
**--chmod** *permissions*
Sets the access permissions of the destination content. Accepts the numerical format.
**--chown** *owner*:*group*
Sets the user and group ownership of the destination content.
**--contextdir** *directory*
Build context directory. Specifying a context directory causes Buildah to
chroot into that context directory. This means copying files pointed at
by symbolic links outside of the chroot will fail.
**--exclude** *pattern*
Exclude copying files matching the specified pattern. Option can be specified
multiple times. See containerignore(5) for supported formats.
**--from** *containerOrImage*
Use the root directory of the specified working container or image as the root
directory when resolving absolute source paths and the path of the context
directory. If an image needs to be pulled, options recognized by `buildah pull`
can be used.
**--ignorefile** *file*
Path to an alternative .containerignore (.dockerignore) file. Requires \-\-contextdir be specified.
**--quiet**, **-q**
Refrain from printing a digest of the added content.
**--retry** *attempts*
Number of times to retry in case of failure when pulling images from registries
or retrieving content from HTTPS URLs.
Defaults to `3`.
**--retry-delay** *duration*
Duration of delay between retry attempts in case of failure when pulling images
from registries or retrieving content from HTTPS URLs.
Defaults to `2s`.
**--tls-verify** *bool-value*
Require verification of certificates when retrieving sources from HTTPS
locations, or when pulling images referred to with the **--from*** flag
(defaults to true). TLS verification cannot be used when talking to an
insecure registry.
## EXAMPLE
buildah add containerID '/myapp/app.conf' '/myapp/app.conf'
buildah add --chown myuser:mygroup containerID '/myapp/app.conf' '/myapp/app.conf'
buildah add --chmod 660 containerID '/myapp/app.conf' '/myapp/app.conf'
buildah add containerID '/home/myuser/myproject.go'
buildah add containerID '/home/myuser/myfiles.tar' '/tmp'
buildah add containerID '/tmp/workingdir' '/tmp/workingdir'
buildah add containerID 'https://github.com/containers/buildah/blob/main/README.md' '/tmp'
buildah add containerID 'passwd' 'certs.d' /etc
## FILES
### .containerignore or .dockerignore
If a .containerignore or .dockerignore file exists in the context directory,
`buildah add` reads its contents. If both exist, then .containerignore is used.
When the `--ignorefile` option is specified Buildah reads it and
uses it to decide which content to exclude when copying content into the
working container.
Users can specify a series of Unix shell glob patterns in an ignore file to
identify files/directories to exclude.
Buildah supports a special wildcard string `**` which matches any number of
directories (including zero). For example, **/*.go will exclude all files that
end with .go that are found in all directories.
Example .containerignore/.dockerignore file:
```
# here are files we want to exclude
*/*.c
**/output*
src
```
`*/*.c`
Excludes files and directories whose names end with .c in any top level subdirectory. For example, the source file include/rootless.c.
`**/output*`
Excludes files and directories starting with `output` from any directory.
`src`
Excludes files named src and the directory src as well as any content in it.
Lines starting with ! (exclamation mark) can be used to make exceptions to
exclusions. The following is an example .containerignore file that uses this
mechanism:
```
*.doc
!Help.doc
```
Exclude all doc files except Help.doc when copying content into the container.
This functionality is compatible with the handling of .containerignore files described here:
https://github.com/containers/common/blob/main/docs/containerignore.5.md
## SEE ALSO
buildah(1), containerignore(5)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,393 @@
# buildah-commit "1" "March 2017" "buildah"
## NAME
buildah\-commit - Create an image from a working container.
## SYNOPSIS
**buildah commit** [*options*] *container* [*image*]
## DESCRIPTION
Writes a new image using the specified container's read-write layer and if it
is based on an image, the layers of that image. If *image* does not begin
with a registry name component, `localhost` will be added to the name. If
*image* is not provided, the image will have no name. When an image has no
name, the `buildah images` command will display `<none>` in the `REPOSITORY` and
`TAG` columns.
The *image* value supports all transports from `containers-transports(5)`. If no transport is specified, the `containers-storage` (i.e., local storage) transport is used.
## RETURN VALUE
The image ID of the image that was created. On error, 1 is returned and errno is returned.
## OPTIONS
**--add-file** *source[:destination]*
Read the contents of the file `source` and add it to the committed image as a
file at `destination`. If `destination` is not specified, the path of `source`
will be used. The new file will be owned by UID 0, GID 0, have 0644
permissions, and be given a current timestamp unless the **--timestamp** option
is also specified. This option can be specified multiple times.
**--authfile** *path*
Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. See containers-auth.json(5) for more information. This file is created using `buildah login`.
If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`.
Note: You can also override the default path of the authentication file by setting the REGISTRY\_AUTH\_FILE
environment variable. `export REGISTRY_AUTH_FILE=path`
**--cert-dir** *path*
Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry.
The default certificates directory is _/etc/containers/certs.d_.
**--change**, **-c** *"INSTRUCTION"*
Apply the change to the committed image that would have been made if it had
been built using a Containerfile which included the specified instruction.
This option can be specified multiple times.
**--config** *filename*
Read a JSON-encoded version of an image configuration object from the specified
file, and merge the values from it with the configuration of the image being
committed.
**--creds** *creds*
The [username[:password]] to use to authenticate with the registry if required.
If one or both values are not supplied, a command line prompt will appear and the
value can be entered. The password is entered without echo.
**--cw** *options*
Produce an image suitable for use as a confidential workload running in a
trusted execution environment (TEE) using krun (i.e., *crun* built with the
libkrun feature enabled and invoked as *krun*). Instead of the conventional
contents, the root filesystem of the image will contain an encrypted disk image
and configuration information for krun.
The value for *options* is a comma-separated list of key=value pairs, supplying
configuration information which is needed for producing the additional data
which will be included in the container image.
Recognized _keys_ are:
*attestation_url*: The location of a key broker / attestation server.
If a value is specified, the new image's workload ID, along with the passphrase
used to encrypt the disk image, will be registered with the server, and the
server's location will be stored in the container image.
At run-time, krun is expected to contact the server to retrieve the passphrase
using the workload ID, which is also stored in the container image.
If no value is specified, a *passphrase* value *must* be specified.
*cpus*: The number of virtual CPUs which the image expects to be run with at
run-time. If not specified, a default value will be supplied.
*firmware_library*: The location of the libkrunfw-sev shared library. If not
specified, `buildah` checks for its presence in a number of hard-coded
locations.
*memory*: The amount of memory which the image expects to be run with at
run-time, as a number of megabytes. If not specified, a default value will be
supplied.
*passphrase*: The passphrase to use to encrypt the disk image which will be
included in the container image.
If no value is specified, but an *attestation_url* value is specified, a
randomly-generated passphrase will be used.
The authors recommend setting an *attestation_url* but not a *passphrase*.
*slop*: Extra space to allocate for the disk image compared to the size of the
container image's contents, expressed either as a percentage (..%) or a size
value (bytes, or larger units if suffixes like KB or MB are present), or a sum
of two or more such specifications separated by "+". If not specified,
`buildah` guesses that 25% more space than the contents will be enough, but
this option is provided in case its guess is wrong. If the specified or
computed size is less than 10 megabytes, it will be increased to 10 megabytes.
*type*: The type of trusted execution environment (TEE) which the image should
be marked for use with. Accepted values are "SEV" (AMD Secure Encrypted
Virtualization - Encrypted State) and "SNP" (AMD Secure Encrypted
Virtualization - Secure Nested Paging). If not specified, defaults to "SNP".
*workload_id*: A workload identifier which will be recorded in the container
image, to be used at run-time for retrieving the passphrase which was used to
encrypt the disk image. If not specified, a semi-random value will be derived
from the base image's image ID.
**--disable-compression**, **-D**
Don't compress filesystem layers when building the image unless it is required
by the location where the image is being written. This is the default setting,
because image layers are compressed automatically when they are pushed to
registries, and images being written to local storage would only need to be
decompressed again to be stored. Compression can be forced in all cases by
specifying **--disable-compression=false**.
**--encrypt-layer** *layer(s)*
Layer(s) to encrypt: 0-indexed layer indices with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer). If not defined, will encrypt all layers if encryption-key flag is specified.
**--encryption-key** *key*
The [protocol:keyfile] specifies the encryption protocol, which can be JWE (RFC7516), PGP (RFC4880), and PKCS7 (RFC2315) and the key material required for image encryption. For instance, jwe:/path/to/key.pem or pgp:admin@example.com or pkcs7:/path/to/x509-file.
**--format**, **-f** *[oci | docker]*
Control the format for the image manifest and configuration data. Recognized
formats include *oci* (OCI image-spec v1.0, the default) and *docker* (version
2, using schema format 2 for the manifest).
Note: You can also override the default format by setting the BUILDAH_FORMAT
environment variable. `export BUILDAH_FORMAT=docker`
**--identity-label** *bool-value*
Adds default identity label `io.buildah.version` if set. (default true).
**--iidfile** *ImageIDfile*
Write the image ID to the file.
**--manifest** "listName"
Name of the manifest list to which the built image will be added. Creates the manifest list
if it does not exist. This option is useful for building multi architecture images.
**--omit-history** *bool-value*
Omit build history information in the built image. (default false).
This option is useful for the cases where end users explicitly
want to set `--omit-history` to omit the optional `History` from
built images or when working with images built using build tools that
do not include `History` information in their images.
**--pull**
When the *--pull* flag is enabled or set explicitly to `true` (with
*--pull=true*), attempt to pull the latest versions of SBOM scanner images from
the registries listed in registries.conf if a local SBOM scanner image does not
exist or the image in the registry is newer than the one in local storage.
Raise an error if the SBOM scanner image is not in any listed registry and is
not present locally.
If the flag is disabled (with *--pull=false*), do not pull SBOM scanner images
from registries, use only local versions. Raise an error if a SBOM scanner
image is not present locally.
If the pull flag is set to `always` (with *--pull=always*), pull SBOM scanner
images from the registries listed in registries.conf. Raise an error if a SBOM
scanner image is not found in the registries, even if an image with the same
name is present locally.
If the pull flag is set to `missing` (with *--pull=missing*), pull SBOM scanner
images only if they could not be found in the local containers storage. Raise
an error if no image could be found and the pull fails.
If the pull flag is set to `never` (with *--pull=never*), do not pull SBOM
scanner images from registries, use only the local versions. Raise an error if
the image is not present locally.
**--quiet**, **-q**
When writing the output image, suppress progress output.
**--rm**
Remove the working container and its contents after creating the image.
Default leaves the container and its content in place.
**--sbom** *preset*
Generate SBOMs (Software Bills Of Materials) for the output image by scanning
the working container and build contexts using the named combination of scanner
image, scanner commands, and merge strategy. Must be specified with one or
more of **--sbom-image-output**, **--sbom-image-purl-output**, **--sbom-output**,
and **--sbom-purl-output**. Recognized presets, and the set of options which
they equate to:
- "syft", "syft-cyclonedx":
--sbom-scanner-image=ghcr.io/anchore/syft
--sbom-scanner-command="/syft scan -q dir:{ROOTFS} --output cyclonedx-json={OUTPUT}"
--sbom-scanner-command="/syft scan -q dir:{CONTEXT} --output cyclonedx-json={OUTPUT}"
--sbom-merge-strategy=merge-cyclonedx-by-component-name-and-version
- "syft-spdx":
--sbom-scanner-image=ghcr.io/anchore/syft
--sbom-scanner-command="/syft scan -q dir:{ROOTFS} --output spdx-json={OUTPUT}"
--sbom-scanner-command="/syft scan -q dir:{CONTEXT} --output spdx-json={OUTPUT}"
--sbom-merge-strategy=merge-spdx-by-package-name-and-versioninfo
- "trivy", "trivy-cyclonedx":
--sbom-scanner-image=ghcr.io/aquasecurity/trivy
--sbom-scanner-command="trivy filesystem -q {ROOTFS} --format cyclonedx --output {OUTPUT}"
--sbom-scanner-command="trivy filesystem -q {CONTEXT} --format cyclonedx --output {OUTPUT}"
--sbom-merge-strategy=merge-cyclonedx-by-component-name-and-version
- "trivy-spdx":
--sbom-scanner-image=ghcr.io/aquasecurity/trivy
--sbom-scanner-command="trivy filesystem -q {ROOTFS} --format spdx-json --output {OUTPUT}"
--sbom-scanner-command="trivy filesystem -q {CONTEXT} --format spdx-json --output {OUTPUT}"
--sbom-merge-strategy=merge-spdx-by-package-name-and-versioninfo
**--sbom-image-output** *path*
When generating SBOMs, store the generated SBOM in the specified path in the
output image. There is no default.
**--sbom-image-purl-output** *path*
When generating SBOMs, scan them for PURL ([package
URL](https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst))
information, and save a list of found PURLs to the named file in the local
filesystem. There is no default.
**--sbom-merge-strategy** *method*
If more than one **--sbom-scanner-command** value is being used, use the
specified method to merge the output from later commands with output from
earlier commands. Recognized values include:
- cat
Concatenate the files.
- merge-cyclonedx-by-component-name-and-version
Merge the "component" fields of JSON documents, ignoring values from
documents when the combination of their "name" and "version" values is
already present. Documents are processed in the order in which they are
generated, which is the order in which the commands that generate them
were specified.
- merge-spdx-by-package-name-and-versioninfo
Merge the "package" fields of JSON documents, ignoring values from
documents when the combination of their "name" and "versionInfo" values is
already present. Documents are processed in the order in which they are
generated, which is the order in which the commands that generate them
were specified.
**--sbom-output** *file*
When generating SBOMs, store the generated SBOM in the named file on the local
filesystem. There is no default.
**--sbom-purl-output** *file*
When generating SBOMs, scan them for PURL ([package
URL](https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst))
information, and save a list of found PURLs to the named file in the local
filesystem. There is no default.
**--sbom-scanner-command** *image*
Generate SBOMs by running the specified command from the scanner image. If
multiple commands are specified, they are run in the order in which they are
specified. These text substitutions are performed:
- {ROOTFS}
The root of the built image's filesystem, bind mounted.
- {CONTEXT}
The build context and additional build contexts, bind mounted.
- {OUTPUT}
The name of a temporary output file, to be read and merged with others or copied elsewhere.
**--sbom-scanner-image** *image*
Generate SBOMs using the specified scanner image.
**--sign-by** *fingerprint*
Sign the new image using the GPG key that matches the specified fingerprint.
**--squash**
Squash all of the new image's layers (including those inherited from a base image) into a single new layer.
**--timestamp** *seconds*
Set the create timestamp to seconds since epoch to allow for deterministic builds (defaults to current time).
By default, the created timestamp is changed and written into the image manifest with every commit,
causing the image's sha256 hash to be different even if the sources are exactly the same otherwise.
When --timestamp is set, the created timestamp is always set to the time specified and therefore not changed, allowing the image's sha256 to remain the same. All files committed to the layers of the image will be created with the timestamp.
**--tls-verify** *bool-value*
Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry.
**--unsetenv** *env*
Unset environment variables from the final image.
## EXAMPLE
This example saves an image based on the container.
`buildah commit containerID newImageName`
This example saves an image named newImageName based on the container and removes the working container.
`buildah commit --rm containerID newImageName`
This example commits to an OCI archive file named /tmp/newImageName based on the container.
`buildah commit containerID oci-archive:/tmp/newImageName`
This example saves an image with no name, removes the working container, and creates a new container using the image's ID.
`buildah from $(buildah commit --rm containerID)`
This example saves an image based on the container disabling compression.
`buildah commit --disable-compression containerID`
This example saves an image named newImageName based on the container disabling compression.
`buildah commit --disable-compression containerID newImageName`
This example commits the container to the image on the local registry while turning off tls verification.
`buildah commit --tls-verify=false containerID docker://localhost:5000/imageId`
This example commits the container to the image on the local registry using credentials and certificates for authentication.
`buildah commit --cert-dir ~/auth --tls-verify=true --creds=username:password containerID docker://localhost:5000/imageId`
This example commits the container to the image on the local registry using credentials from the /tmp/auths/myauths.json file and certificates for authentication.
`buildah commit --authfile /tmp/auths/myauths.json --cert-dir ~/auth --tls-verify=true --creds=username:password containerID docker://localhost:5000/imageName`
This example saves an image based on the container, but stores dates based on epoch time.
`buildah commit --timestamp=0 containerID newImageName`
### Building an multi-architecture image using the --manifest option (requires emulation software)
```
#!/bin/sh
build() {
ctr=$(./bin/buildah from --arch $1 ubi8)
./bin/buildah run $ctr dnf install -y iputils
./bin/buildah commit --manifest ubi8ping $ctr
}
build arm
build amd64
build s390x
```
## ENVIRONMENT
**BUILD\_REGISTRY\_SOURCES**
BUILD\_REGISTRY\_SOURCES, if set, is treated as a JSON object which contains
lists of registry names under the keys `insecureRegistries`,
`blockedRegistries`, and `allowedRegistries`.
When committing an image, if the image is to be given a name, the portion of
the name that corresponds to a registry is compared to the items in the
`blockedRegistries` list, and if it matches any of them, the commit attempt is
denied. If there are registries in the `allowedRegistries` list, and the
portion of the name that corresponds to the registry is not in the list, the
commit attempt is denied.
**TMPDIR**
The TMPDIR environment variable allows the user to specify where temporary files
are stored while pulling and pushing images. Defaults to '/var/tmp'.
## FILES
**registries.conf** (`/etc/containers/registries.conf`)
registries.conf is the configuration file which specifies which container registries should be consulted when completing image names which do not include a registry or domain portion.
**policy.json** (`/etc/containers/policy.json`)
Signature policy file. This defines the trust policy for container images. Controls which container registries can be used for image, and whether or not the tool should trust the images.
## SEE ALSO
buildah(1), buildah-images(1), containers-policy.json(5), containers-registries.conf(5), containers-transports(5), containers-auth.json(5)

View File

@@ -0,0 +1,302 @@
# buildah-config "1" "March 2017" "buildah"
## NAME
buildah\-config - Update image configuration settings.
## SYNOPSIS
**buildah config** [*options*] *container*
## DESCRIPTION
Updates one or more of the settings kept for a container.
## OPTIONS
**--add-history**
Add an entry to the image's history which will note changes to the settings for
**--cmd**, **--entrypoint**, **--env**, **--healthcheck**, **--label**,
**--onbuild**, **--port**, **--shell**, **--stop-signal**, **--user**,
**--volume**, and **--workingdir**.
Defaults to false.
Note: You can also override the default value of --add-history by setting the
BUILDAH\_HISTORY environment variable. `export BUILDAH_HISTORY=true`
**--annotation**, **-a** *annotation*=*annotation*
Add an image *annotation* (e.g. annotation=*annotation*) to the image manifest
of any images which will be built using the specified container. Can be used multiple times.
If *annotation* has a trailing `-`, then the *annotation* is removed from the config.
If the *annotation* is set to "-" then all annotations are removed from the config.
**--arch** *architecture*
Set the target *architecture* for any images which will be built using the
specified container. By default, if the container was based on an image, that
image's target architecture is kept, otherwise the host's architecture is
recorded.
**--author** *author*
Set contact information for the *author* for any images which will be built
using the specified container.
**--cmd** *command*
Set the default *command* to run for containers based on any images which will
be built using the specified container. When used in combination with an
*entry point*, this specifies the default parameters for the *entry point*.
**--comment** *comment*
Set the image-level comment for any images which will be built using the
specified container.
Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats.
**--created-by** *created*
Set the description of how the topmost layer was *created* for any images which
will be created using the specified container.
**--domainname** *domain*
Set the domainname to set when running containers based on any images built
using the specified container.
Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats.
**--entrypoint** *"command"* | *'["command", "arg1", ...]'*
Set the *entry point* for containers based on any images which will be built
using the specified container. buildah supports two formats for entrypoint. It
can be specified as a simple string, or as an array of commands.
Note: When the entrypoint is specified as a string, container runtimes will
ignore the `cmd` value of the container image. However if you use the array
form, then the cmd will be appended onto the end of the entrypoint cmd and be
executed together.
Note: The string form is appended to the `sh -c` command as the entrypoint. The array form
replaces entrypoint entirely.
String Format:
```
$ buildah from scratch
$ buildah config --entrypoint "/usr/bin/notashell" working-container
$ buildah inspect --format '{{ .OCIv1.Config.Entrypoint }}' working-container
[/bin/sh -c /usr/bin/notshell]
$ buildah inspect --format '{{ .Docker.Config.Entrypoint }}' working-container
[/bin/sh -c /usr/bin/notshell]
```
Array Format:
```
$ buildah config --entrypoint '["/usr/bin/notashell"]' working-container
$ buildah inspect --format '{{ .OCIv1.Config.Entrypoint }}' working-container
[/usr/bin/notashell]
$ buildah inspect --format '{{ .Docker.Config.Entrypoint }}' working-container
[/usr/bin/notashell]
```
**--env**, **-e** *env[=value]*
Add a value (e.g. env=*value*) to the environment for containers based on any
images which will be built using the specified container. Can be used multiple times.
If *env* is named but neither `=` nor a `value` is specified, then the value
will be taken from the current process environment.
If *env* has a trailing `-`, then the *env* is removed from the config.
If the *env* is set to "-" then all environment variables are removed from the config.
**--healthcheck** *command*
Specify a command which should be run to check if a container is running correctly.
Values can be *NONE*, "*CMD* ..." (run the specified command directly), or
"*CMD-SHELL* ..." (run the specified command using the system's shell), or the
empty value (remove a previously-set value and related settings).
Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats.
**--healthcheck-interval** *interval*
Specify how often the command specified using the *--healthcheck* option should
be run.
Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats.
**--healthcheck-retries** *count*
Specify how many times the command specified using the *--healthcheck* option
can fail before the container is considered to be unhealthy.
Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats.
**--healthcheck-start-interval** *interval*
Specify the time between health checks during the start period.
Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats.
**--healthcheck-start-period** *interval*
Specify how much time can elapse after a container has started before a failure
to run the command specified using the *--healthcheck* option should be treated
as an indication that the container is failing. During this time period,
failures will be attributed to the container not yet having fully started, and
will not be counted as errors. After the command succeeds, or the time period
has elapsed, failures will be counted as errors.
Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats.
**--healthcheck-timeout** *interval*
Specify how long to wait after starting the command specified using the
*--healthcheck* option to wait for the command to return its exit status. If
the command has not returned within this time, it should be considered to have
failed.
Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats.
**--history-comment** *comment*
Sets a comment on the topmost layer in any images which will be created
using the specified container.
**--hostname** *host*
Set the hostname to set when running containers based on any images built using
the specified container.
Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats.
**--label**, **-l** *label*=*value*
Add an image *label* (e.g. label=*value*) to the image configuration of any
images which will be built using the specified container. Can be used multiple times.
If *label* has a trailing `-`, then the *label* is removed from the config.
If the *label* is set to "-" then all labels are removed from the config.
**--onbuild** *onbuild command*
Add an ONBUILD command to the image. ONBUILD commands are automatically run
when images are built based on the image you are creating.
Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats.
**--os** *operating system*
Set the target *operating system* for any images which will be built using
the specified container. By default, if the container was based on an image,
its OS is kept, otherwise the host's OS's name is recorded.
**--os-feature** *feature*
Set the name of a required operating system *feature* for any images which will
be built using the specified container. By default, if the container was based
on an image, the base image's required OS feature list is kept, if it specified
one. This option is typically only meaningful when the image's OS is Windows.
If *feature* has a trailing `-`, then the *feature* is removed from the set of
required features which will be listed in the image. If the *feature* is set
to "-" then the entire features list is removed from the config.
**--os-version** *version*
Set the exact required operating system *version* for any images which will be
built using the specified container. By default, if the container was based on
an image, the base image's required OS version is kept, if it specified one.
This option is typically only meaningful when the image's OS is Windows, and is
typically set in Windows base images, so using this option is usually
unnecessary.
**--port**, **-p** *port/protocol*
Add a *port* to expose when running containers based on any images which
will be built using the specified container. Can be used multiple times.
To specify whether the port listens on TCP or UDP, use "port/protocol".
The default is TCP if the protocol is not specified. To expose the port on both TCP and UDP,
specify the port option multiple times. If *port* has a trailing `-` and is already set,
then the *port* is removed from the configuration. If the port is set to `-` then all exposed
ports settings are removed from the configuration.
**--shell** *shell*
Set the default *shell* to run inside of the container image.
The shell instruction allows the default shell used for the shell form of commands to be overridden. The default shell for Linux containers is "/bin/sh -c".
Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats.
**--stop-signal** *signal*
Set default *stop signal* for container. This signal will be sent when container is stopped, default is SIGINT.
**--unsetlabel** *label*
Unset the image label, causing the label not to be inherited from the base image.
**--user**, **-u** *user*[:*group*]
Set the default *user* to be used when running containers based on this image.
The user can be specified as a user name
or UID, optionally followed by a group name or GID, separated by a colon (':').
If names are used, the container should include entries for those names in its
*/etc/passwd* and */etc/group* files.
**--variant** *variant*
Set the target architecture *variant* for any images which will be built using
the specified container. By default, if the container was based on an image,
that image's target architecture and variant information is kept, otherwise the
host's architecture and variant are recorded.
**--volume**, **-v** *volume*
Add a location in the directory tree which should be marked as a *volume* in any images which will be built using the specified container. Can be used multiple times. If *volume* has a trailing `-`, and is already set, then the *volume* is removed from the config.
If the *volume* is set to "-" then all volumes are removed from the config.
**--workingdir** *directory*
Set the initial working *directory* for containers based on images which will
be built using the specified container.
## EXAMPLE
buildah config --author='Jane Austen' --workingdir='/etc/mycontainers' containerID
buildah config --entrypoint /entrypoint.sh containerID
buildah config --entrypoint '[ "/entrypoint.sh", "dev" ]' containerID
buildah config --env foo=bar --env PATH=$PATH containerID
buildah config --env foo- containerID
buildah config --label Name=Mycontainer --label Version=1.0 containerID
buildah config --label Name- containerID
buildah config --annotation note=myNote containerID
buildah config --annotation note-
buildah config --volume /usr/myvol containerID
buildah config --volume /usr/myvol- containerID
buildah config --port 1234 --port 8080 containerID
buildah config --port 514/tcp --port 514/udp containerID
buildah config --env 1234=5678 containerID
buildah config --env 1234- containerID
buildah config --os-version 10.0.19042.1645 containerID
buildah config --os-feature win32k containerID
buildah config --os-feature win32k- containerID
## SEE ALSO
buildah(1)

View File

@@ -0,0 +1,123 @@
# buildah-containers "1" "March 2017" "buildah"
## NAME
buildah\-containers - List the working containers and their base images.
## SYNOPSIS
**buildah containers** [*options*]
## DESCRIPTION
Lists containers which appear to be Buildah working containers, their names and
IDs, and the names and IDs of the images from which they were initialized.
## OPTIONS
**--all**, **-a**
List information about all containers, including those which were not created
by and are not being used by Buildah. Containers created by Buildah are
denoted with an '*' in the 'BUILDER' column.
**--filter**, **-f**
Filter output based on conditions provided.
Valid filters are listed below:
| **Filter** | **Description** |
| --------------- | ------------------------------------------------------------------- |
| id | [ID] Container's ID |
| name | [Name] Container's name |
| ancestor | [ImageName] Image or descendant used to create container |
**--format**
Pretty-print containers using a Go template.
Valid placeholders for the Go template are listed below:
| **Placeholder** | **Description** |
| --------------- | -----------------------------------------|
| .ContainerID | Container ID |
| .Builder | Whether container was created by buildah |
| .ImageID | Image ID |
| .ImageName | Image name |
| .ContainerName | Container name |
**--json**
Output in JSON format.
**--noheading**, **-n**
Omit the table headings from the listing of containers.
**--notruncate**
Do not truncate IDs and image names in the output.
**--quiet**, **-q**
Displays only the container IDs.
## EXAMPLE
buildah containers
```
CONTAINER ID BUILDER IMAGE ID IMAGE NAME CONTAINER NAME
ccf84de04b80 * 53ce4390f2ad registry.access.redhat.com/ub... ubi8-working-container
45be1d806fc5 * 16ea53ea7c65 docker.io/library/busybox:latest busybox-working-container
```
buildah containers --quiet
```
ccf84de04b80c309ce6586997c79a769033dc4129db903c1882bc24a058438b8
45be1d806fc533fcfc2beee77e424d87e5990d3ce9214d6b374677d6630bba07
```
buildah containers -q --noheading --notruncate
```
ccf84de04b80c309ce6586997c79a769033dc4129db903c1882bc24a058438b8
45be1d806fc533fcfc2beee77e424d87e5990d3ce9214d6b374677d6630bba07
```
buildah containers --json
```
[
{
"id": "ccf84de04b80c309ce6586997c79a769033dc4129db903c1882bc24a058438b8",
"builder": true,
"imageid": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e",
"imagename": "registry.access.redhat.com/ubi8:latest",
"containername": "ubi8-working-container"
},
{
"id": "45be1d806fc533fcfc2beee77e424d87e5990d3ce9214d6b374677d6630bba07",
"builder": true,
"imageid": "16ea53ea7c652456803632d67517b78a4f9075a10bfdc4fc6b7b4cbf2bc98497",
"imagename": "docker.io/library/busybox:latest",
"containername": "busybox-working-container"
}
]
```
buildah containers --format "{{.ContainerID}} {{.ContainerName}}"
```
ccf84de04b80c309ce6586997c79a769033dc4129db903c1882bc24a058438b8 ubi8-working-container
45be1d806fc533fcfc2beee77e424d87e5990d3ce9214d6b374677d6630bba07 busybox-working-container
```
buildah containers --format "Container ID: {{.ContainerID}}"
```
Container ID: ccf84de04b80c309ce6586997c79a769033dc4129db903c1882bc24a058438b8
Container ID: 45be1d806fc533fcfc2beee77e424d87e5990d3ce9214d6b374677d6630bba07
```
buildah containers --filter ancestor=ubuntu
```
CONTAINER ID BUILDER IMAGE ID IMAGE NAME CONTAINER NAME
fbfd3505376e * 0ff04b2e7b63 docker.io/library/ubuntu:latest ubuntu-working-container
```
## SEE ALSO
buildah(1)

View File

@@ -0,0 +1,169 @@
# buildah-copy "1" "April 2021" "buildah"
## NAME
buildah\-copy - Copies the contents of a file, URL, or directory into a container's working directory.
## SYNOPSIS
**buildah copy** *container* *src* [[*src* ...] *dest*]
## DESCRIPTION
Copies the contents of a file, URL, or a directory to a container's working
directory or a specified location in the container. If a local directory is
specified as a source, its *contents* are copied to the destination.
## OPTIONS
**--add-history**
Add an entry to the history which will note the digest of the added content.
Defaults to false.
Note: You can also override the default value of --add-history by setting the
BUILDAH\_HISTORY environment variable. `export BUILDAH_HISTORY=true`
**--cert-dir** *path*
Use certificates at *path* (\*.crt, \*.cert, \*.key) when connecting to
registries for pulling images named with the **--from** flag. The default
certificates directory is _/etc/containers/certs.d_.
**--checksum** *checksum*
Checksum the source content. The value of *checksum* must be a standard
container digest string. Only supported for HTTP sources.
**--chmod** *permissions*
Sets the access permissions of the destination content. Accepts the numerical
format. If `--from` is not used, defaults to `0755`.
**--chown** *owner*:*group*
Sets the user and group ownership of the destination content. If `--from` is
not used, defaults to `0:0`.
**--contextdir** *directory*
Build context directory. Specifying a context directory causes Buildah to
chroot into the context directory. This means copying files pointed at
by symbolic links outside of the chroot will fail.
**--exclude** *pattern*
Exclude copying files matching the specified pattern. Option can be specified
multiple times. See containerignore(5) for supported formats.
**--from** *containerOrImage*
Use the root directory of the specified working container or image as the root
directory when resolving absolute source paths and the path of the context
directory. If an image needs to be pulled, options recognized by `buildah pull`
can be used. If `--chown` or `--chmod` are not used, permissions and ownership
is preserved.
**--ignorefile** *file*
Path to an alternative .containerignore (.dockerignore) file. Requires \-\-contextdir be specified.
**--parents**
Preserve leading directories in the paths of items being copied, relative to either the
top of the build context, or to the "pivot point", a location in the source path marked
by a path component named "." (i.e., where "/./" occurs in the path).
**--quiet**, **-q**
Refrain from printing a digest of the copied content.
**--retry** *attempts*
Number of times to retry in case of failure when performing pull of images from registry.
Defaults to `3`.
**--retry-delay** *duration*
Duration of delay between retry attempts in case of failure when performing pull of images from registry.
Defaults to `2s`.
**--tls-verify** *bool-value*
Require verification of certificates when pulling images referred to with the
**--from*** flag (defaults to true). TLS verification cannot be used when
talking to an insecure registry.
## EXAMPLE
buildah copy containerID '/myapp/app.conf' '/myapp/app.conf'
buildah copy --exclude=**/*.md docs containerID 'docs' '/docs'
buildah copy --parents containerID './x/a.txt' './y/a.txt' '/parents'
buildah copy --chown myuser:mygroup containerID '/myapp/app.conf' '/myapp/app.conf'
buildah copy --chmod 660 containerID '/myapp/app.conf' '/myapp/app.conf'
buildah copy containerID '/home/myuser/myproject.go'
buildah copy containerID '/home/myuser/myfiles.tar' '/tmp'
buildah copy containerID '/tmp/workingdir' '/tmp/workingdir'
buildah copy containerID 'https://github.com/containers/buildah' '/tmp'
buildah copy containerID 'passwd' 'certs.d' /etc
## FILES
### .containerignore/.dockerignore
If the .containerignore/.dockerignore file exists in the context directory,
`buildah copy` reads its contents. If both exist, then .containerignore is used.
When the `--ignorefile` option is specified Buildah reads it and
uses it to decide which content to exclude when copying content into the
working container.
Users can specify a series of Unix shell glob patterns in an ignore file to
identify files/directories to exclude.
Buildah supports a special wildcard string `**` which matches any number of
directories (including zero). For example, `**/*.go` will exclude all files that
end with .go that are found in all directories.
Example .containerignore/.dockerignore file:
```
# here are files we want to exclude
*/*.c
**/output*
src
```
`*/*.c`
Excludes files and directories whose names end with .c in any top level subdirectory. For example, the source file include/rootless.c.
`**/output*`
Excludes files and directories starting with `output` from any directory.
`src`
Excludes files named src and the directory src as well as any content in it.
Lines starting with ! (exclamation mark) can be used to make exceptions to
exclusions. The following is an example .containerignore/.dockerignore file that uses this
mechanism:
```
*.doc
!Help.doc
```
Exclude all doc files except Help.doc when copying content into the container.
This functionality is compatible with the handling of .containerignore files described here:
https://github.com/containers/common/blob/main/docs/containerignore.5.md
## SEE ALSO
buildah(1), containerignore(5)

View File

@@ -0,0 +1,244 @@
# Buildah Essential Commands Guide
Buildah is a command-line tool for building OCI-compatible container images. Unlike other container build tools, Buildah doesn't require a daemon to be running and allows for granular control over the container building process.
## Creating Containers = BUILD STEP
### buildah from
Creates a new working container, either from scratch or using a specified image.
```bash
# Create a container from an image
buildah from [options] <image-name>
# Create a container from scratch
buildah from scratch
# Examples
buildah from fedora:latest
buildah from docker://ubuntu:22.04
buildah from --name my-container alpine:latest
```
Important options:
- `--name <name>`: Set a name for the container
- `--pull`: Pull image policy (missing, always, never, newer)
- `--authfile <path>`: Path to authentication file
- `--creds <username:password>`: Registry credentials
## Working with Containers
### buildah run
Runs a command inside of the container.
```bash
# Basic syntax
buildah run [options] <container-id> <command>
# Examples
buildah run my-container yum install -y httpd
buildah run my-container -- sh -c "echo 'Hello World' > /etc/motd"
buildah run --hostname myhost my-container ps -auxw
```
Important options:
- `--tty`, `-t`: Allocate a pseudo-TTY
- `--env`, `-e <env=value>`: Set environment variables
- `--volume`, `-v <host-dir:container-dir:opts>`: Mount a volume
- `--workingdir <directory>`: Set the working directory
### buildah copy
Copy files from the host into the container.
```bash
# Basic syntax
buildah copy [options] <container-id> <source> <destination>
# Examples
buildah copy my-container ./app /app
buildah copy my-container config.json /etc/myapp/
```
### buildah add
Add content from a file, URL, or directory to the container.
```bash
# Basic syntax
buildah add [options] <container-id> <source> <destination>
# Examples
buildah add my-container https://example.com/archive.tar.gz /tmp/
buildah add my-container ./local/dir /app/
```
## Configuring Containers
### buildah config
Updates container configuration settings.
```bash
# Basic syntax
buildah config [options] <container-id>
# Examples
buildah config --author="John Doe" my-container
buildah config --port 8080 my-container
buildah config --env PATH=$PATH my-container
buildah config --label version=1.0 my-container
buildah config --entrypoint "/entrypoint.sh" my-container
```
Important options:
- `--author <author>`: Set the author
- `--cmd <command>`: Set the default command
- `--entrypoint <command>`: Set the entry point
- `--env`, `-e <env=value>`: Set environment variables
- `--label`, `-l <label=value>`: Add image labels
- `--port`, `-p <port>`: Expose ports
- `--user`, `-u <user[:group]>`: Set the default user
- `--workingdir <directory>`: Set the working directory
- `--volume`, `-v <volume>`: Add a volume
## Building Images
### buildah commit
Create an image from a working container.
```bash
# Basic syntax
buildah commit [options] <container-id> [<image-name>]
# Examples
buildah commit my-container new-image:latest
buildah commit --format docker my-container docker.io/username/image:tag
buildah commit --rm my-container localhost/myimage:v1.0
```
Important options:
- `--format`, `-f`: Output format (oci or docker)
- `--rm`: Remove the container after committing
- `--quiet`, `-q`: Suppress output
- `--squash`: Squash all layers into a single layer
### buildah build
Build an image using instructions from Containerfiles or Dockerfiles.
```bash
# Basic syntax
buildah build [options] <context>
# Examples
buildah build .
buildah build -t myimage:latest .
buildah build -f Containerfile.custom .
buildah build --layers --format docker -t username/myapp:1.0 .
```
Important options:
- `--file`, `-f <Containerfile>`: Path to Containerfile/Dockerfile
- `--tag`, `-t <name:tag>`: Tag to apply to the built image
- `--layers`: Cache intermediate layers during build
- `--pull`: Force pull of newer base images
- `--no-cache`: Do not use cache during build
- `--build-arg <key=value>`: Set build-time variables
- `--format`: Output format (oci or docker)
## Managing Images
### buildah images
List images in local storage.
```bash
buildah images [options]
```
### buildah rmi
Remove one or more images.
```bash
buildah rmi [options] <image>
```
### buildah push
Push an image to a registry.
```bash
# Basic syntax
buildah push [options] <image> [destination]
# Examples
buildah push myimage:latest docker://registry.example.com/myimage:latest
buildah push --tls-verify=false localhost/myimage docker://localhost:5000/myimage
```
Important options:
- `--authfile <path>`: Path to authentication file
- `--creds <username:password>`: Registry credentials
- `--tls-verify <bool>`: Require HTTPS and verify certificates
### buildah tag
Add an additional name to a local image.
```bash
# Basic syntax
buildah tag <image> <new-name>
# Example
buildah tag localhost/myimage:latest myimage:v1.0
```
### buildah pull
Pull an image from a registry.
```bash
# Basic syntax
buildah pull [options] <image-name>
# Examples
buildah pull docker.io/library/ubuntu:latest
buildah pull --tls-verify=false registry.example.com/myimage:latest
```
Important options:
- `--authfile <path>`: Path to authentication file
- `--creds <username:password>`: Registry credentials
- `--tls-verify <bool>`: Require HTTPS and verify certificates
## Typical Workflow Example
```bash
# Create a container from an existing image
container=$(buildah from fedora:latest)
# Run a command to install software
buildah run $container dnf install -y nginx
# Copy local configuration files to the container
buildah copy $container ./nginx.conf /etc/nginx/nginx.conf
# Configure container metadata
buildah config --port 80 $container
buildah config --label maintainer="example@example.com" $container
buildah config --entrypoint "/usr/sbin/nginx" $container
# Commit the container to create a new image
buildah commit --rm $container my-nginx:latest
# Or build using a Containerfile
buildah build -t my-nginx:latest .
# Push the image to a registry
buildah push my-nginx:latest docker://docker.io/username/my-nginx:latest
```

View File

@@ -0,0 +1,758 @@
# buildah-from "1" "March 2017" "buildah"
## NAME
buildah\-from - Creates a new working container, either from scratch or using a specified image as a starting point.
## SYNOPSIS
**buildah from** [*options*] *image*
## DESCRIPTION
Creates a working container based upon the specified image name. If the
supplied image name is "scratch" a new empty container is created. Image names
use a "transport":"details" format.
Multiple transports are supported:
**dir:**_path_
An existing local directory _path_ containing the manifest, layer tarballs, and signatures in individual files. This is a non-standardized format, primarily useful for debugging or noninvasive image inspection.
**docker://**_docker-reference_ (Default)
An image in a registry implementing the "Docker Registry HTTP API V2". By default, uses the authorization state in `$XDG_RUNTIME_DIR/containers/auth.json`, which is set using `(buildah login)`. See containers-auth.json(5) for more information. If the authorization state is not found there, `$HOME/.docker/config.json` is checked, which is set using `(docker login)`.
If _docker-reference_ does not include a registry name, *localhost* will be consulted first, followed by any registries named in the registries configuration.
**docker-archive:**_path_
An image is retrieved as a `podman load` formatted file.
**docker-daemon:**_docker-reference_
An image _docker-reference_ stored in the docker daemon's internal storage. _docker-reference_ must include either a tag or a digest. Alternatively, when reading images, the format can also be docker-daemon:algo:digest (an image ID).
**oci:**_path_**:**_tag_**
An image tag in a directory compliant with "Open Container Image Layout Specification" at _path_.
**oci-archive:**_path_**:**_tag_
An image _tag_ in a directory compliant with "Open Container Image Layout Specification" at _path_.
### DEPENDENCIES
Buildah resolves the path to the registry to pull from by using the /etc/containers/registries.conf
file, containers-registries.conf(5). If the `buildah from` command fails with an "image not known" error,
first verify that the registries.conf file is installed and configured appropriately.
## RETURN VALUE
The container ID of the container that was created. On error 1 is returned.
## OPTIONS
**--add-host**=[]
Add a custom host-to-IP mapping (host:ip)
Add a line to /etc/hosts. The format is hostname:ip. The **--add-host** option can be set multiple times.
**--arch**="ARCH"
Set the ARCH of the image to be pulled to the provided value instead of using the architecture of the host. (Examples: arm, arm64, 386, amd64, ppc64le, s390x)
**--authfile** *path*
Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. See containers-auth.json(5) for more information. This file is created using `buildah login`.
If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`.
Note: You can also override the default path of the authentication file by setting the REGISTRY\_AUTH\_FILE
environment variable. `export REGISTRY_AUTH_FILE=path`
**--cap-add**=*CAP\_xxx*
Add the specified capability to the default set of capabilities which will be
supplied for subsequent *buildah run* invocations which use this container.
Certain capabilities are granted by default; this option can be used to add
more.
**--cap-drop**=*CAP\_xxx*
Remove the specified capability from the default set of capabilities which will
be supplied for subsequent *buildah run* invocations which use this container.
The CAP\_CHOWN, CAP\_DAC\_OVERRIDE, CAP\_FOWNER, CAP\_FSETID, CAP\_KILL,
CAP\_NET\_BIND\_SERVICE, CAP\_SETFCAP, CAP\_SETGID, CAP\_SETPCAP,
and CAP\_SETUID capabilities are granted by default; this option can be used to remove them. The list of default capabilities is managed in containers.conf(5).
If a capability is specified to both the **--cap-add** and **--cap-drop**
options, it will be dropped, regardless of the order in which the options were
given.
**--cert-dir** *path*
Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry.
The default certificates directory is _/etc/containers/certs.d_.
**--cgroup-parent**=""
Path to cgroups under which the cgroup for the container will be created. If the path is not absolute, the path is considered to be relative to the cgroups path of the init process. Cgroups will be created if they do not already exist.
**--cgroupns** *how*
Sets the configuration for IPC namespaces when the container is subsequently
used for `buildah run`.
The configured value can be "" (the empty string) or "private" to indicate
that a new cgroup namespace should be created, or it can be "host" to indicate
that the cgroup namespace in which `buildah` itself is being run should be reused.
**--cidfile** *ContainerIDFile*
Write the container ID to the file.
**--cpu-period**=*0*
Limit the CPU CFS (Completely Fair Scheduler) period
Limit the container's CPU usage. This flag tells the kernel to restrict the container's CPU usage to the period you specify.
**--cpu-quota**=*0*
Limit the CPU CFS (Completely Fair Scheduler) quota
Limit the container's CPU usage. By default, containers run with the full
CPU resource. This flag tells the kernel to restrict the container's CPU usage
to the quota you specify.
**--cpu-shares**, **-c**=*0*
CPU shares (relative weight)
By default, all containers get the same proportion of CPU cycles. This proportion
can be modified by changing the container's CPU share weighting relative
to the weighting of all other running containers.
To modify the proportion from the default of 1024, use the **--cpu-shares**
flag to set the weighting to 2 or higher.
The proportion will only apply when CPU-intensive processes are running.
When tasks in one container are idle, other containers can use the
left-over CPU time. The actual amount of CPU time will vary depending on
the number of containers running on the system.
For example, consider three containers, one has a cpu-share of 1024 and
two others have a cpu-share setting of 512. When processes in all three
containers attempt to use 100% of CPU, the first container would receive
50% of the total CPU time. If you add a fourth container with a cpu-share
of 1024, the first container only gets 33% of the CPU. The remaining containers
receive 16.5%, 16.5% and 33% of the CPU.
On a multi-core system, the shares of CPU time are distributed over all CPU
cores. Even if a container is limited to less than 100% of CPU time, it can
use 100% of each individual CPU core.
For example, consider a system with more than three cores. If you start one
container **{C0}** with **-c=512** running one process, and another container
**{C1}** with **-c=1024** running two processes, this can result in the following
division of CPU shares:
PID container CPU CPU share
100 {C0} 0 100% of CPU0
101 {C1} 1 100% of CPU1
102 {C1} 2 100% of CPU2
**--cpuset-cpus**=""
CPUs in which to allow execution (0-3, 0,1)
**--cpuset-mems**=""
Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems.
If you have four memory nodes on your system (0-3), use `--cpuset-mems=0,1`
then processes in your container will only use memory from the first
two memory nodes.
**--creds** *creds*
The [username[:password]] to use to authenticate with the registry if required.
If one or both values are not supplied, a command line prompt will appear and the
value can be entered. The password is entered without echo.
**--decryption-key** *key[:passphrase]*
The [key[:passphrase]] to be used for decryption of images. Key can point to keys and/or certificates. Decryption will be tried with all keys. If the key is protected by a passphrase, it is required to be passed in the argument and omitted otherwise.
**--device**=*device*
Add a host device, or devices under a directory, to the environment of
subsequent **buildah run** invocations for the new working container. The
optional *permissions* parameter can be used to specify device permissions,
using any one or more of **r** for read, **w** for write, and **m** for
**mknod**(2).
Example: **--device=/dev/sdc:/dev/xvdc:rwm**.
Note: if _host-device_ is a symbolic link then it will be resolved first.
The container will only store the major and minor numbers of the host device.
The device to share can also be specified using a Container Device Interface
(CDI) specification (https://github.com/cncf-tags/container-device-interface).
Note: if the user only has access rights via a group, accessing the device
from inside a rootless container will fail. The **crun**(1) runtime offers a
workaround for this by adding the option **--annotation run.oci.keep_original_groups=1**.
**--dns**=[]
Set custom DNS servers
This option can be used to override the DNS configuration passed to the container. Typically this is necessary when the host DNS configuration is invalid for the container (e.g., 127.0.0.1). When this is the case the `--dns` flag is necessary for every run.
The special value **none** can be specified to disable creation of /etc/resolv.conf in the container by Buildah. The /etc/resolv.conf file in the image will be used without changes.
**--dns-option**=[]
Set custom DNS options
**--dns-search**=[]
Set custom DNS search domains
**--format**, **-f** *oci* | *docker*
Control the format for the built image's manifest and configuration data.
Recognized formats include *oci* (OCI image-spec v1.0, the default) and
*docker* (version 2, using schema format 2 for the manifest).
Note: You can also override the default format by setting the BUILDAH\_FORMAT
environment variable. `export BUILDAH_FORMAT=docker`
**--group-add**=*group* | *keep-groups*
Assign additional groups to the primary user running within the container
process.
- `keep-groups` is a special flag that tells Buildah to keep the supplementary
group access.
Allows container to use the user's supplementary group access. If file systems
or devices are only accessible by the rootless user's group, this flag tells the
OCI runtime to pass the group access into the container. Currently only
available with the `crun` OCI runtime. Note: `keep-groups` is exclusive, other
groups cannot be specified with this flag.
**--http-proxy**
By default proxy environment variables are passed into the container if set
for the Buildah process. This can be disabled by setting the `--http-proxy`
option to `false`. The environment variables passed in include `http_proxy`,
`https_proxy`, `ftp_proxy`, `no_proxy`, and also the upper case versions of
those.
Defaults to `true`
**--ipc** *how*
Sets the configuration for IPC namespaces when the container is subsequently
used for `buildah run`.
The configured value can be "" (the empty string) or "container" to indicate
that a new IPC namespace should be created, or it can be "host" to indicate
that the IPC namespace in which `Buildah` itself is being run should be reused,
or it can be the path to an IPC namespace which is already in use by
another process.
**--isolation** *type*
Controls what type of isolation is used for running processes under `buildah
run`. Recognized types include *oci* (OCI-compatible runtime, the default),
*rootless* (OCI-compatible runtime invoked using a modified
configuration, with *--no-new-keyring* added to its *create* invocation,
reusing the host's network and UTS namespaces, and creating private IPC, PID,
mount, and user namespaces; the default for unprivileged users), and *chroot*
(an internal wrapper that leans more toward chroot(1) than container
technology, reusing the host's control group, network, IPC, and PID namespaces,
and creating private mount and UTS namespaces, and creating user namespaces
only when they're required for ID mapping).
Note: You can also override the default isolation type by setting the
BUILDAH\_ISOLATION environment variable. `export BUILDAH_ISOLATION=oci`
**--memory**, **-m**=""
Memory limit (format: <number>[<unit>], where unit = b, k, m or g)
Allows you to constrain the memory available to a container. If the host
supports swap memory, then the **-m** memory setting can be larger than physical
RAM. If a limit of 0 is specified (not using **-m**), the container's memory is
not limited. The actual limit may be rounded up to a multiple of the operating
system's page size (the value would be very large, that's millions of trillions).
**--memory-swap**="LIMIT"
A limit value equal to memory plus swap. Must be used with the **-m**
(**--memory**) flag. The swap `LIMIT` should always be larger than **-m**
(**--memory**) value. By default, the swap `LIMIT` will be set to double
the value of --memory.
The format of `LIMIT` is `<number>[<unit>]`. Unit can be `b` (bytes),
`k` (kilobytes), `m` (megabytes), or `g` (gigabytes). If you don't specify a
unit, `b` is used. Set LIMIT to `-1` to enable unlimited swap.
**--name** *name*
A *name* for the working container
**--network**=*mode*, **--net**=*mode*
Sets the configuration for network namespaces when the container is subsequently
used for `buildah run`.
Valid _mode_ values are:
- **none**: no networking. Invalid if using **--dns**, **--dns-opt**, or **--dns-search**;
- **host**: use the host network stack. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure;
- **ns:**_path_: path to a network namespace to join;
- **private**: create a new namespace for the container (default)
- **\<network name|ID\>**: Join the network with the given name or ID, e.g. use `--network mynet` to join the network with the name mynet. Only supported for rootful users.
- **slirp4netns[:OPTIONS,...]**: use **slirp4netns**(1) to create a user network stack. This is the default for rootless containers. It is possible to specify these additional options, they can also be set with `network_cmd_options` in containers.conf:
- **allow_host_loopback=true|false**: Allow slirp4netns to reach the host loopback IP (default is 10.0.2.2 or the second IP from slirp4netns cidr subnet when changed, see the cidr option below). The default is false.
- **mtu=MTU**: Specify the MTU to use for this network. (Default is `65520`).
- **cidr=CIDR**: Specify ip range to use for this network. (Default is `10.0.2.0/24`).
- **enable_ipv6=true|false**: Enable IPv6. Default is true. (Required for `outbound_addr6`).
- **outbound_addr=INTERFACE**: Specify the outbound interface slirp binds to (ipv4 traffic only).
- **outbound_addr=IPv4**: Specify the outbound ipv4 address slirp binds to.
- **outbound_addr6=INTERFACE**: Specify the outbound interface slirp binds to (ipv6 traffic only).
- **outbound_addr6=IPv6**: Specify the outbound ipv6 address slirp binds to.
- **pasta[:OPTIONS,...]**: use **pasta**(1) to create a user-mode networking
stack. \
This is only supported in rootless mode. \
By default, IPv4 and IPv6 addresses and routes, as well as the pod interface
name, are copied from the host. If port forwarding isn't configured, ports
are forwarded dynamically as services are bound on either side (init
namespace or container namespace). Port forwarding preserves the original
source IP address. Options described in pasta(1) can be specified as
comma-separated arguments. \
In terms of pasta(1) options, **--config-net** is given by default, in
order to configure networking when the container is started, and
**--no-map-gw** is also assumed by default, to avoid direct access from
container to host using the gateway address. The latter can be overridden
by passing **--map-gw** in the pasta-specific options (despite not being an
actual pasta(1) option). \
Also, **-t none** and **-u none** are passed to disable
automatic port forwarding based on bound ports. Similarly, **-T none** and
**-U none** are given to disable the same functionality from container to
host. \
Some examples:
- **pasta:--map-gw**: Allow the container to directly reach the host using the
gateway address.
- **pasta:--mtu,1500**: Specify a 1500 bytes MTU for the _tap_ interface in
the container.
- **pasta:--ipv4-only,-a,10.0.2.0,-n,24,-g,10.0.2.2,--dns-forward,10.0.2.3,-m,1500,--no-ndp,--no-dhcpv6,--no-dhcp**,
equivalent to default slirp4netns(1) options: disable IPv6, assign
`10.0.2.0/24` to the `tap0` interface in the container, with gateway
`10.0.2.3`, enable DNS forwarder reachable at `10.0.2.3`, set MTU to 1500
bytes, disable NDP, DHCPv6 and DHCP support.
- **pasta:-I,tap0,--ipv4-only,-a,10.0.2.0,-n,24,-g,10.0.2.2,--dns-forward,10.0.2.3,--no-ndp,--no-dhcpv6,--no-dhcp**,
equivalent to default slirp4netns(1) options with Podman overrides: same as
above, but leave the MTU to 65520 bytes
- **pasta:-t,auto,-u,auto,-T,auto,-U,auto**: enable automatic port forwarding
based on observed bound ports from both host and container sides
- **pasta:-T,5201**: enable forwarding of TCP port 5201 from container to
host, using the loopback interface instead of the tap interface for improved
performance
**--os**="OS"
Set the OS of the image to be pulled to the provided value instead of using the current operating system of the host.
**--pid** *how*
Sets the configuration for PID namespaces when the container is subsequently
used for `buildah run`.
The configured value can be "" (the empty string) or "container" to indicate
that a new PID namespace should be created, or it can be "host" to indicate
that the PID namespace in which `Buildah` itself is being run should be reused,
or it can be the path to a PID namespace which is already in use by another
process.
**--platform**="OS/ARCH[/VARIANT]"
Set the OS/ARCH of the image to be pulled
to the provided value instead of using the current operating system and
architecture of the host (for example `linux/arm`).
OS/ARCH pairs are those used by the Go Programming Language. In several cases
the ARCH value for a platform differs from one produced by other tools such as
the `arch` command. Valid OS and architecture name combinations are listed as
values for $GOOS and $GOARCH at https://golang.org/doc/install/source#environment,
and can also be found by running `go tool dist list`.
While `buildah from` is happy to pull an image for any platform that exists,
`buildah run` will not be able to run binaries provided by that image without
the help of emulation provided by packages like `qemu-user-static`.
**NOTE:** The `--platform` option may not be used in combination with the `--arch`, `--os`, or `--variant` options.
**--pull**
Pull image policy. The default is **missing**.
- **always**: Pull base and SBOM scanner images from the registries listed in
registries.conf. Raise an error if a base or SBOM scanner image is not found
in the registries, even if an image with the same name is present locally.
- **missing**: SBOM scanner images only if they could not be found in the local
containers storage. Raise an error if no image could be found and the pull
fails.
- **never**: Do not pull base and SBOM scanner images from registries, use only
the local versions. Raise an error if the image is not present locally.
- **newer**: Pull base and SBOM scanner images from the registries listed in
registries.conf if newer. Raise an error if a base or SBOM scanner image is
not found in the registries when image with the same name is not present
locally.
**--quiet**, **-q**
If an image needs to be pulled from the registry, suppress progress output.
**--retry** *attempts*
Number of times to retry in case of failure when performing pull of images from registry.
Defaults to `3`.
**--retry-delay** *duration*
Duration of delay between retry attempts in case of failure when performing pull of images from registry.
Defaults to `2s`.
**--security-opt**=[]
Security Options
"label=user:USER" : Set the label user for the container
"label=role:ROLE" : Set the label role for the container
"label=type:TYPE" : Set the label type for the container
"label=level:LEVEL" : Set the label level for the container
"label=disable" : Turn off label confinement for the container
"no-new-privileges" : Not supported
"seccomp=unconfined" : Turn off seccomp confinement for the container
"seccomp=profile.json : White listed syscalls seccomp Json file to be used as a seccomp filter
"apparmor=unconfined" : Turn off apparmor confinement for the container
"apparmor=your-profile" : Set the apparmor confinement profile for the container
**--shm-size**=""
Size of `/dev/shm`. The format is `<number><unit>`. `number` must be greater than `0`.
Unit is optional and can be `b` (bytes), `k` (kilobytes), `m`(megabytes), or `g` (gigabytes).
If you omit the unit, the system uses bytes. If you omit the size entirely, the system uses `64m`.
**--tls-verify** *bool-value*
Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry.
**--ulimit** *type*=*soft-limit*[:*hard-limit*]
Specifies resource limits to apply to processes launched during `buildah run`.
This option can be specified multiple times. Recognized resource types
include:
"core": maximum core dump size (ulimit -c)
"cpu": maximum CPU time (ulimit -t)
"data": maximum size of a process's data segment (ulimit -d)
"fsize": maximum size of new files (ulimit -f)
"locks": maximum number of file locks (ulimit -x)
"memlock": maximum amount of locked memory (ulimit -l)
"msgqueue": maximum amount of data in message queues (ulimit -q)
"nice": niceness adjustment (nice -n, ulimit -e)
"nofile": maximum number of open files (ulimit -n)
"nofile": maximum number of open files (1048576); when run by root
"nproc": maximum number of processes (ulimit -u)
"nproc": maximum number of processes (1048576); when run by root
"rss": maximum size of a process's (ulimit -m)
"rtprio": maximum real-time scheduling priority (ulimit -r)
"rttime": maximum amount of real-time execution between blocking syscalls
"sigpending": maximum number of pending signals (ulimit -i)
"stack": maximum stack size (ulimit -s)
**--userns** *how*
Sets the configuration for user namespaces when the container is subsequently
used for `buildah run`.
The configured value can be "" (the empty string) or "container" to indicate
that a new user namespace should be created, it can be "host" to indicate that
the user namespace in which `Buildah` itself is being run should be reused, or
it can be the path to an user namespace which is already in use by another
process.
**--userns-gid-map** *mapping*
Directly specifies a GID mapping which should be used to set ownership, at the
filesystem level, on the working container's contents.
Commands run when handling `RUN` instructions will default to being run in
their own user namespaces, configured using the UID and GID maps.
Entries in this map take the form of one or more colon-separated triples of a starting
in-container GID, a corresponding starting host-level GID, and the number of
consecutive IDs which the map entry represents.
This option overrides the *remap-gids* setting in the *options* section of
/etc/containers/storage.conf.
If this option is not specified, but a global --userns-gid-map setting is
supplied, settings from the global option will be used.
**--userns-gid-map-group** *mapping*
Directly specifies a GID mapping which should be used to set ownership, at the
filesystem level, on the container's contents.
Commands run using `buildah run` will default to being run in their own user
namespaces, configured using the UID and GID maps.
Entries in this map take the form of one or more triples of a starting
in-container GID, a corresponding starting host-level GID, and the number of
consecutive IDs which the map entry represents.
This option overrides the *remap-gids* setting in the *options* section of
/etc/containers/storage.conf.
If this option is not specified, but a global --userns-gid-map setting is
supplied, settings from the global option will be used.
If none of --userns-uid-map-user, --userns-gid-map-group, or --userns-gid-map
are specified, but --userns-uid-map is specified, the GID map will be set to
use the same numeric values as the UID map.
**NOTE:** When this option is specified by a rootless user, the specified mappings are relative to the rootless usernamespace in the container, rather than being relative to the host as it would be when run rootful.
**--userns-gid-map-group** *group*
Specifies that a GID mapping which should be used to set ownership, at the
filesystem level, on the container's contents, can be found in entries in the
`/etc/subgid` file which correspond to the specified group.
Commands run using `buildah run` will default to being run in their own user
namespaces, configured using the UID and GID maps.
If --userns-uid-map-user is specified, but --userns-gid-map-group is not
specified, `Buildah` will assume that the specified user name is also a
suitable group name to use as the default setting for this option.
**--userns-uid-map** *mapping*
Directly specifies a UID mapping which should be used to set ownership, at the
filesystem level, on the working container's contents.
Commands run when handling `RUN` instructions will default to being run in
their own user namespaces, configured using the UID and GID maps.
Entries in this map take the form of one or more colon-separated triples of a starting
in-container UID, a corresponding starting host-level UID, and the number of
consecutive IDs which the map entry represents.
This option overrides the *remap-uids* setting in the *options* section of
/etc/containers/storage.conf.
If this option is not specified, but a global --userns-uid-map setting is
supplied, settings from the global option will be used.
**--userns-uid-map-user** *mapping*
Directly specifies a UID mapping which should be used to set ownership, at the
filesystem level, on the container's contents.
Commands run using `buildah run` will default to being run in their own user
namespaces, configured using the UID and GID maps.
Entries in this map take the form of one or more triples of a starting
in-container UID, a corresponding starting host-level UID, and the number of
consecutive IDs which the map entry represents.
This option overrides the *remap-uids* setting in the *options* section of
/etc/containers/storage.conf.
If this option is not specified, but a global --userns-uid-map setting is
supplied, settings from the global option will be used.
If none of --userns-uid-map-user, --userns-gid-map-group, or --userns-uid-map
are specified, but --userns-gid-map is specified, the UID map will be set to
use the same numeric values as the GID map.
**NOTE:** When this option is specified by a rootless user, the specified mappings are relative to the rootless usernamespace in the container, rather than being relative to the host as it would be when run rootful.
**--userns-uid-map-user** *user*
Specifies that a UID mapping which should be used to set ownership, at the
filesystem level, on the container's contents, can be found in entries in the
`/etc/subuid` file which correspond to the specified user.
Commands run using `buildah run` will default to being run in their own user
namespaces, configured using the UID and GID maps.
If --userns-gid-map-group is specified, but --userns-uid-map-user is not
specified, `Buildah` will assume that the specified group name is also a
suitable user name to use as the default setting for this option.
**--uts** *how*
Sets the configuration for UTS namespaces when the container is subsequently
used for `buildah run`.
The configured value can be "" (the empty string) or "container" to indicate
that a new UTS namespace should be created, or it can be "host" to indicate
that the UTS namespace in which `Buildah` itself is being run should be reused,
or it can be the path to a UTS namespace which is already in use by another
process.
**--variant**=""
Set the architecture variant of the image to be pulled.
**--volume**, **-v**[=*[HOST-DIR:CONTAINER-DIR[:OPTIONS]]*]
Create a bind mount. If you specify, ` -v /HOST-DIR:/CONTAINER-DIR`, Buildah
bind mounts `/HOST-DIR` in the host to `/CONTAINER-DIR` in the Buildah
container. The `OPTIONS` are a comma delimited list and can be:
* [rw|ro]
* [U]
* [z|Z|O]
* [`[r]shared`|`[r]slave`|`[r]private`|`[r]unbindable`] <sup>[[1]](#Footnote1)</sup>
The `CONTAINER-DIR` must be an absolute path such as `/src/docs`. The `HOST-DIR`
must be an absolute path as well. Buildah bind-mounts the `HOST-DIR` to the
path you specify. For example, if you supply `/foo` as the host path,
Buildah copies the contents of `/foo` to the container filesystem on the host
and bind mounts that into the container.
You can specify multiple **-v** options to mount one or more mounts to a
container.
`Write Protected Volume Mounts`
You can add the `:ro` or `:rw` suffix to a volume to mount it read-only or
read-write mode, respectively. By default, the volumes are mounted read-write.
See examples.
`Chowning Volume Mounts`
By default, Buildah does not change the owner and group of source volume directories mounted into containers. If a container is created in a new user namespace, the UID and GID in the container may correspond to another UID and GID on the host.
The `:U` suffix tells Buildah to use the correct host UID and GID based on the UID and GID within the container, to change the owner and group of the source volume.
`Labeling Volume Mounts`
Labeling systems like SELinux require that proper labels are placed on volume
content mounted into a container. Without a label, the security system might
prevent the processes running inside the container from using the content. By
default, Buildah does not change the labels set by the OS.
To change a label in the container context, you can add either of two suffixes
`:z` or `:Z` to the volume mount. These suffixes tell Buildah to relabel file
objects on the shared volumes. The `z` option tells Buildah that two containers
share the volume content. As a result, Buildah labels the content with a shared
content label. Shared volume labels allow all containers to read/write content.
The `Z` option tells Buildah to label the content with a private unshared label.
Only the current container can use a private volume.
`Overlay Volume Mounts`
The `:O` flag tells Buildah to mount the directory from the host as a temporary storage using the Overlay file system. The `RUN` command containers are allowed to modify contents within the mountpoint and are stored in the container storage in a separate directory. In Overlay FS terms the source directory will be the lower, and the container storage directory will be the upper. Modifications to the mount point are destroyed when the `RUN` command finishes executing, similar to a tmpfs mount point.
Any subsequent execution of `RUN` commands sees the original source directory content, any changes from previous RUN commands no longer exist.
One use case of the `overlay` mount is sharing the package cache from the host into the container to allow speeding up builds.
Note:
- The `O` flag is not allowed to be specified with the `Z` or `z` flags. Content mounted into the container is labeled with the private label.
On SELinux systems, labels in the source directory need to be readable by the container label. If not, SELinux container separation must be disabled for the container to work.
- Modification of the directory volume mounted into the container with an overlay mount can cause unexpected failures. It is recommended that you do not modify the directory until the container finishes running.
By default bind mounted volumes are `private`. That means any mounts done
inside container will not be visible on the host and vice versa. This behavior can
be changed by specifying a volume mount propagation property.
When the mount propagation policy is set to `shared`, any mounts completed inside
the container on that volume will be visible to both the host and container. When
the mount propagation policy is set to `slave`, one way mount propagation is enabled
and any mounts completed on the host for that volume will be visible only inside of the container.
To control the mount propagation property of the volume use the `:[r]shared`,
`:[r]slave`, `[r]private` or `[r]unbindable`propagation flag. The propagation property can
be specified only for bind mounted volumes and not for internal volumes or
named volumes. For mount propagation to work on the source mount point (the mount point
where source dir is mounted on) it has to have the right propagation properties. For
shared volumes, the source mount point has to be shared. And for slave volumes,
the source mount has to be either shared or slave. <sup>[[1]](#Footnote1)</sup>
Use `df <source-dir>` to determine the source mount and then use
`findmnt -o TARGET,PROPAGATION <source-mount-dir>` to determine propagation
properties of source mount, if `findmnt` utility is not available, the source mount point
can be determined by looking at the mount entry in `/proc/self/mountinfo`. Look
at `optional fields` and see if any propagation properties are specified.
`shared:X` means the mount is `shared`, `master:X` means the mount is `slave` and if
nothing is there that means the mount is `private`. <sup>[[1]](#Footnote1)</sup>
To change propagation properties of a mount point use the `mount` command. For
example, to bind mount the source directory `/foo` do
`mount --bind /foo /foo` and `mount --make-private --make-shared /foo`. This
will convert /foo into a `shared` mount point. The propagation properties of the source
mount can be changed directly. For instance if `/` is the source mount for
`/foo`, then use `mount --make-shared /` to convert `/` into a `shared` mount.
## EXAMPLE
buildah from --pull imagename
buildah from --pull docker://myregistry.example.com/imagename
buildah from docker-daemon:imagename:imagetag
buildah from --name mycontainer docker-archive:filename
buildah from oci-archive:filename
buildah from --name mycontainer dir:directoryname
buildah from --pull-always --name "mycontainer" myregistry.example.com/imagename
buildah from --tls-verify=false myregistry/myrepository/imagename:imagetag
buildah from --creds=myusername:mypassword --cert-dir ~/auth myregistry/myrepository/imagename:imagetag
buildah from --authfile=/tmp/auths/myauths.json myregistry/myrepository/imagename:imagetag
buildah from --memory 40m --cpu-shares 2 --cpuset-cpus 0,2 --security-opt label=level:s0:c100,c200 myregistry/myrepository/imagename:imagetag
buildah from --ulimit nofile=1024:1028 --cgroup-parent /path/to/cgroup/parent myregistry/myrepository/imagename:imagetag
buildah from --volume /home/test:/myvol:ro,Z myregistry/myrepository/imagename:imagetag
buildah from -v /home/test:/myvol:z,U myregistry/myrepository/imagename:imagetag
buildah from -v /var/lib/yum:/var/lib/yum:O myregistry/myrepository/imagename:imagetag
buildah from --arch=arm --variant v7 myregistry/myrepository/imagename:imagetag
## ENVIRONMENT
**BUILD\_REGISTRY\_SOURCES**
BUILD\_REGISTRY\_SOURCES, if set, is treated as a JSON object which contains
lists of registry names under the keys `insecureRegistries`,
`blockedRegistries`, and `allowedRegistries`.
When pulling an image from a registry, if the name of the registry matches any
of the items in the `blockedRegistries` list, the image pull attempt is denied.
If there are registries in the `allowedRegistries` list, and the registry's
name is not in the list, the pull attempt is denied.
**TMPDIR**
The TMPDIR environment variable allows the user to specify where temporary files
are stored while pulling and pushing images. Defaults to '/var/tmp'.
## FILES
**registries.conf** (`/etc/containers/registries.conf`)
registries.conf is the configuration file which specifies which container registries should be consulted when completing image names which do not include a registry or domain portion.
**policy.json** (`/etc/containers/policy.json`)
Signature policy file. This defines the trust policy for container images. Controls which container registries can be used for image, and whether or not the tool should trust the images.
## SEE ALSO
buildah(1), buildah-pull(1), buildah-login(1), docker-login(1), namespaces(7), pid\_namespaces(7), containers-policy.json(5), containers-registries.conf(5), user\_namespaces(7), containers.conf(5), containers-auth.json(5)
## FOOTNOTES
<a name="Footnote1">1</a>: The Buildah project is committed to inclusivity, a core value of open source. The `master` and `slave` mount propagation terminology used here is problematic and divisive, and should be changed. However, these terms are currently used within the Linux kernel and must be used as-is at this time. When the kernel maintainers rectify this usage, Buildah will follow suit immediately.

View File

@@ -0,0 +1,137 @@
# buildah-images "1" "March 2017" "buildah"
## NAME
buildah\-images - List images in local storage.
## SYNOPSIS
**buildah images** [*options*] [*image*]
## DESCRIPTION
Displays locally stored images, their names, sizes, created date and their IDs.
The created date is displayed in the time locale of the local machine.
## OPTIONS
**--all**, **-a**
Show all images, including intermediate images from a build.
**--digests**
Show the image digests.
**--filter**, **-f**=[]
Filter output based on conditions provided (default []).
Filters:
**after,since=image**
Filter on images created since the given image.
**before=image**
Filter on images created before the given image.
**dangling=true|false**
Show dangling images. An images is considered to be dangling if it has no associated names and tags.
**id=id**
Show image with this specific ID.
**intermediate=true|false**
Show intermediate images. An images is considered to be an indermediate image if it is dangling and has no children.
**label=key[=value]**
Filter by images labels key and/or value.
**readonly=true|false**
Show only read only images or Read/Write images. The default is to show both. Read/Only images can be configured by modifying the "additionalimagestores" in the /etc/containers/storage.conf file.
**reference=reference**
Show images matching the specified reference. Wildcards are supported (e.g., "reference=*fedora:3*").
**--format**="TEMPLATE"
Pretty-print images using a Go template.
Valid placeholders for the Go template are listed below:
| **Placeholder** | **Description** |
| --------------- | -----------------------------------------|
| .Created | Creation date in epoch time |
| .CreatedAt | Creation date Pretty Formatted |
| .CreatedAtRaw | Creation date in raw format |
| .Digest | Image Digest |
| .ID | Image ID |
| .Name | Image Name |
| .ReadOnly | Indicates if image came from a R/O store |
| .Size | Image Size |
| .Tag | Image Tag |
**--history**
Display the image name history.
**--json**
Display the output in JSON format.
**--no-trunc**
Do not truncate output.
**--noheading**, **-n**
Omit the table headings from the listing of images.
**--quiet**, **-q**
Displays only the image IDs.
## EXAMPLE
buildah images
buildah images fedora:latest
buildah images --json
buildah images --quiet
buildah images -q --noheading --no-trunc
buildah images --quiet fedora:latest
buildah images --filter dangling=true
buildah images --format "ImageID: {{.ID}}"
```
$ buildah images
REPOSITORY TAG IMAGE ID CREATED SIZE
registry.access.redhat.com/ubi8 latest 53ce4390f2ad 3 weeks ago 233 MB
docker.io/library/busybox latest 16ea53ea7c65 3 weeks ago 1.46 MB
quay.io/libpod/testimage 20210610 9f9ec7f2fdef 4 months ago 7.99 MB
```
```
# buildah images -a
IMAGE NAME IMAGE TAG IMAGE ID CREATED AT SIZE
registry.access.redhat.com/ubi8 latest 53ce4390f2ad 3 weeks ago 233 MB
<none> <none> 8c6e16890c2b Jun 13, 2018 15:52 4.42 MB
localhost/test latest c0cfe75da054 Jun 13, 2018 15:52 4.42 MB
```
```
# buildah images --format '{{.ID}} {{.CreatedAtRaw}}'
3f53bb00af943dfdf815650be70c0fa7b426e56a66f5e3362b47a129d57d5991 2018-12-20 19:21:30.122610396 -0500 EST
8e09da8f6701d7cde1526d79e3123b0f1109b78d925dfe9f9bac6d59d702a390 2019-01-08 09:22:52.330623532 -0500 EST
```
```
# buildah images --format '{{.ID}} {{.Name}} {{.Digest}} {{.CreatedAt}} {{.Size}} {{.CreatedAtRaw}}'
3f53bb00af943dfdf815650be70c0fa7b426e56a66f5e3362b47a129d57d5991 docker.io/library/alpine sha256:3d2e482b82608d153a374df3357c0291589a61cc194ec4a9ca2381073a17f58e Dec 20, 2018 19:21 4.67 MB 2018-12-20 19:21:30.122610396 -0500 EST
8e09da8f6701d7cde1526d79e3123b0f1109b78d925dfe9f9bac6d59d702a390 <none> sha256:894532ec56e0205ce68ca7230b00c18aa3c8ee39fcdb310615c60e813057229c Jan 8, 2019 09:22 4.67 MB 2019-01-08 09:22:52.330623532 -0500 EST
```
## SEE ALSO
buildah(1), containers-storage.conf(5)

View File

@@ -0,0 +1,73 @@
# buildah-info "1" "November 2018" "Buildah"
## NAME
buildah\-info - Display Buildah system information.
## SYNOPSIS
**buildah info** [*options*]
## DESCRIPTION
The information displayed pertains to the host and current storage statistics which is useful when reporting issues.
## OPTIONS
**--debug**, **-d**
Show additional information.
**--format** *template*
Use *template* as a Go template when formatting the output.
## EXAMPLE
Run buildah info response:
```
$ buildah info
{
"host": {
"Distribution": {
"distribution": "ubuntu",
"version": "18.04"
},
"MemTotal": 16702980096,
"MemFree": 309428224,
"SwapFree": 2146693120,
"SwapTotal": 2147479552,
"arch": "amd64",
"cpus": 4,
"hostname": "localhost.localdomain",
"kernel": "4.15.0-36-generic",
"os": "linux",
"rootless": false,
"uptime": "91h 30m 59.9s (Approximately 3.79 days)"
},
"store": {
"ContainerStore": {
"number": 2
},
"GraphDriverName": "overlay",
"GraphOptions": [
"overlay.override_kernel_check=true"
],
"GraphRoot": "/var/lib/containers/storage",
"GraphStatus": {
"Backing Filesystem": "extfs",
"Native Overlay Diff": "true",
"Supports d_type": "true"
},
"ImageStore": {
"number": 1
},
"RunRoot": "/run/containers/storage"
}
}
```
Run buildah info and retrieve only the store information:
```
$ buildah info --format={{".store"}}
map[GraphOptions:[overlay.override_kernel_check=true] GraphStatus:map[Backing Filesystem:extfs Supports d_type:true Native Overlay Diff:true] ImageStore:map[number:1] ContainerStore:map[number:2] GraphRoot:/var/lib/containers/storage RunRoot:/run/containers/storage GraphDriverName:overlay]
```
## SEE ALSO
buildah(1)

View File

@@ -0,0 +1,39 @@
# buildah-inspect "1" "May 2017" "buildah"
## NAME
buildah\-inspect - Display information about working containers or images or manifest lists.
## SYNOPSIS
**buildah inspect** [*options*] [**--**] *object*
## DESCRIPTION
Prints the low-level information on Buildah object(s) (e.g. container, images, manifest lists) identified by name or ID. By default, this will render all results in a
JSON array. If the container, image, or manifest lists have the same name, this will return container JSON for an unspecified type. If a format is specified,
the given template will be executed for each result.
## OPTIONS
**--format**, **-f** *template*
Use *template* as a Go template when formatting the output.
Users of this option should be familiar with the [*text/template*
package](https://golang.org/pkg/text/template/) in the Go standard library, and
of internals of Buildah's implementation.
**--type**, **-t** **container** | **image** | **manifest**
Specify whether *object* is a container, image or a manifest list.
## EXAMPLE
buildah inspect containerID
buildah inspect --type container containerID
buildah inspect --type image imageID
buildah inspect --format '{{.OCIv1.Config.Env}}' alpine
## SEE ALSO
buildah(1)

View File

@@ -0,0 +1,114 @@
# buildah-login "1" "Apr 2019" "buildah"
## NAME
buildah\-login - Login to a container registry
## SYNOPSIS
**buildah login** [*options*] *registry*
## DESCRIPTION
**buildah login** logs into a specified registry server with the correct username
and password. **buildah login** reads in the username and password from STDIN.
The username and password can also be set using the **username** and **password** flags.
The path of the authentication file can be specified by the user by setting the **authfile**
flag. The default path used is **${XDG\_RUNTIME_DIR}/containers/auth.json**. If XDG_RUNTIME_DIR
is not set, the default is /run/user/$UID/containers/auth.json.
**buildah [GLOBAL OPTIONS]**
**buildah login [GLOBAL OPTIONS]**
**buildah login [OPTIONS] REGISTRY [GLOBAL OPTIONS]**
## OPTIONS
**--authfile**
Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. See containers-auth.json(5) for more information. This file is created using `buildah login`.
Note: You can also override the default path of the authentication file by setting the REGISTRY\_AUTH\_FILE
environment variable. `export REGISTRY_AUTH_FILE=path`
**--cert-dir** *path*
Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry.
The default certificates directory is _/etc/containers/certs.d_.
**--compat-auth-file**=*path*
Instead of updating the default credentials file, update the one at *path*, and use a Docker-compatible format.
**--get-login**
Return the logged-in user for the registry. Return error if no login is found.
**--help**, **-h**
Print usage statement
**--password**, **-p**
Password for registry
**--password-stdin**
Take the password from stdin
**--tls-verify**
Require HTTPS and verification of certificates when talking to container registries (default: true). If explicitly set to true,
then TLS verification will be used. If set to false, then TLS verification will not be used. If not specified,
TLS verification will be used unless the target registry is listed as an insecure registry in registries.conf.
TLS verification cannot be used when talking to an insecure registry.
**--username**, **-u**
Username for registry
**--verbose**, **-v**
print detailed information about credential store
## EXAMPLES
```
$ buildah login quay.io
Username: qiwanredhat
Password:
Login Succeeded!
```
```
$ buildah login -u testuser -p testpassword localhost:5000
Login Succeeded!
```
```
$ buildah login --authfile ./auth.json quay.io
Username: qiwanredhat
Password:
Login Succeeded!
```
```
$ buildah login --tls-verify=false -u test -p test localhost:5000
Login Succeeded!
```
```
$ buildah login --cert-dir /etc/containers/certs.d/ -u foo -p bar localhost:5000
Login Succeeded!
```
```
$ buildah login -u testuser --password-stdin < pw.txt quay.io
Login Succeeded!
```
```
$ echo $testpassword | buildah login -u testuser --password-stdin quay.io
Login Succeeded!
```
## SEE ALSO
buildah(1), buildah-logout(1), containers-auth.json(5)

View File

@@ -0,0 +1,60 @@
# buildah-logout "1" "Apr 2019" "buildah"
## NAME
buildah\-logout - Logout of a container registry
## SYNOPSIS
**buildah logout** [*options*] *registry*
## DESCRIPTION
**buildah logout** logs out of a specified registry server by deleting the cached credentials
stored in the **auth.json** file. The path of the authentication file can be overridden by the user by setting the **authfile** flag.
The default path used is **${XDG\_RUNTIME_DIR}/containers/auth.json**. See containers-auth.json(5) for more information.
All the cached credentials can be removed by setting the **all** flag.
**buildah [GLOBAL OPTIONS]**
**buildah logout [GLOBAL OPTIONS]**
**buildah logout [OPTIONS] REGISTRY [GLOBAL OPTIONS]**
## OPTIONS
**--all**, **-a**
Remove the cached credentials for all registries in the auth file
**--authfile**
Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. See containers-auth.json(5) for more information.
Note: You can also override the default path of the authentication file by setting the REGISTRY\_AUTH\_FILE
environment variable. `export REGISTRY_AUTH_FILE=path`
**--compat-auth-file**=*path*
Instead of updating the default credentials file, update the one at *path*, and use a Docker-compatible format.
**--help**, **-h**
Print usage statement
## EXAMPLES
```
$ buildah logout quay.io
Removed login credentials for quay.io
```
```
$ buildah logout --authfile authdir/myauths.json quay.io
Removed login credentials for quay.io
```
```
$ buildah logout --all
Remove login credentials for all registries
```
## SEE ALSO
buildah(1), buildah-login(1), containers-auth.json(5)

View File

@@ -0,0 +1,170 @@
# buildah-manifest-add "1" "September 2019" "buildah"
## NAME
buildah\-manifest\-add - Add an image or artifact to a manifest list or image index.
## SYNOPSIS
**buildah manifest add** [options...] *listNameOrIndexName* *imageOrArtifactName* [...]
## DESCRIPTION
Adds the specified image to the specified manifest list or image index, or
creates an artifact manifest and adds it to the specified image index.
## RETURN VALUE
The list image's ID and the digest of the image's manifest.
## OPTIONS
**--all**
If the image which should be added to the list or index is itself a list or
index, add all of the contents to the local list. By default, only one image
from such a list or index will be added to the list or index. Combining
*--all* with any of the other options described below is NOT recommended.
**--annotation** *annotation=value*
Set an annotation on the entry for the newly-added image or artifact manifest.
**--arch**
Override the architecture which the list or index records as a requirement for
the image. If *imageName* refers to a manifest list or image index, the
architecture information will be retrieved from it. Otherwise, it will be
retrieved from the image's configuration information.
**--artifact**
Create an artifact manifest and add it to the image index. Arguments after the
index name will be interpreted as file names rather than as image references.
In most scenarios, the **--artifact-type** option should also be specified.
**--artifact-annotation** *annotation=value*
When creating an artifact manifest and adding it to the image index, set an
annotation in the artifact manifest.
**--artifact-config** *filename*
When creating an artifact manifest and adding it to the image index, use the
specified file's contents as the configuration blob in the artifact manifest.
In most scenarios, leaving the default value, which signifies an empty
configuration, unchanged, is the preferred option.
**--artifact-config-type** *type*
When creating an artifact manifest and adding it to the image index, use the
specified MIME type as the `mediaType` associated with the configuration blob
in the artifact manifest. In most scenarios, leaving the default value, which
signifies either an empty configuration or the standard OCI configuration type,
unchanged, is the preferred option.
**--artifact-exclude-titles**
When creating an artifact manifest and adding it to the image index, do not
set "org.opencontainers.image.title" annotations equal to the file's basename
for each file added to the artifact manifest. Tools which retrieve artifacts
from a registry may use these values to choose names for files when saving
artifacts to disk, so this option is not recommended unless it is required
for interoperability with a particular registry.
**--artifact-layer-type** *type*
When creating an artifact manifest and adding it to the image index, use the
specified MIME type as the `mediaType` associated with the files' contents. If
not specified, guesses based on either the files names or their contents will
be made and used, but the option should be specified if certainty is needed.
**--artifact-subject** *imageName*
When creating an artifact manifest and adding it to the image index, set the
*subject* field in the artifact manifest to mark the artifact manifest as being
associated with the specified image in some way. An artifact manifest can only
be associated with, at most, one subject.
**--artifact-type** *type*
When creating an artifact manifest, use the specified MIME type as the
manifest's `artifactType` value instead of the less informative default value.
**--authfile** *path*
Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. See containers-auth.json(5) for more information. This file is created using `buildah login`.
If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`.
Note: You can also override the default path of the authentication file by setting the REGISTRY\_AUTH\_FILE
environment variable. `export REGISTRY_AUTH_FILE=path`
**--cert-dir** *path*
Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry.
The default certificates directory is _/etc/containers/certs.d_.
**--creds** *creds*
The [username[:password]] to use to authenticate with the registry if required.
If one or both values are not supplied, a command line prompt will appear and the
value can be entered. The password is entered without echo.
**--features**
Specify the features list which the list or index records as requirements for
the image. This option is rarely used.
**--os**
Override the OS which the list or index records as a requirement for the image.
If *imageName* refers to a manifest list or image index, the OS information
will be retrieved from it. Otherwise, it will be retrieved from the image's
configuration information.
**--os-features**
Specify the OS features list which the list or index records as requirements
for the image. This option is rarely used.
**--os-version**
Specify the OS version which the list or index records as a requirement for the
image. This option is rarely used.
**--tls-verify** *bool-value*
Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry.
**--variant**
Specify the variant which the list or index records for the image. This option
is typically used to distinguish between multiple entries which share the same
architecture value, but which expect different versions of its instruction set.
## EXAMPLE
```
buildah manifest add mylist:v1.11 docker://fedora
506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:f81f09918379d5442d20dff82a298f29698197035e737f76e511d5af422cabd7
```
```
buildah manifest add --all mylist:v1.11 docker://fedora
506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:f81f09918379d5442d20dff82a298f29698197035e737f76e511d5af422cabd7
```
```
buildah manifest add --arch arm64 --variant v8 mylist:v1.11 docker://fedora@sha256:c829b1810d2dbb456e74a695fd3847530c8319e5a95dca623e9f1b1b89020d8b
506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:c829b1810d2dbb456e74a695fd3847530c8319e5a95dca623e9f1b1b89020d8b
```
```
buildah manifest add --artifact --artifact-type application/x-cd-image mylist:v1.11 ./imagefile.iso
506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:1768fae728f6f8ff3d0f8c7df409d7f4f0ca5c89b070810bd4aa4a2ed2eca8bb
```
## SEE ALSO
buildah(1), buildah-login(1), buildah-manifest(1), buildah-manifest-create(1), buildah-manifest-remove(1), buildah-manifest-annotate(1), buildah-manifest-inspect(1), buildah-manifest-push(1), buildah-rmi(1), docker-login(1), containers-auth.json(5)

View File

@@ -0,0 +1,84 @@
# buildah-manifest-annotate "1" "September 2019" "buildah"
## NAME
buildah\-manifest\-annotate - Add and update information about an image or artifact to a manifest list or image index.
## SYNOPSIS
**buildah manifest annotate** [options...] *listNameOrIndexName* *imageManifestDigestOrImageOrArtifactName*
## DESCRIPTION
Adds or updates information about an image or artifact included in a manifest list or image index.
## RETURN VALUE
The list image's ID and the digest of the image's manifest.
## OPTIONS
**--annotation** *annotation=value*
Set an annotation on the entry for the specified image or artifact. If
**--index** is also specified, sets the annotation on the entire image index.
**--arch**
Override the architecture which the list or index records as a requirement for
the image. This is usually automatically retrieved from the image's
configuration information, so it is rarely necessary to use this option.
**--features**
Specify the features list which the list or index records as requirements for
the image. This option is rarely used.
**--index**
Treats arguments to the **--annotation** option as annotation values to be set
on the image index itself rather than on an entry in the image index. Implied
for **--subject**.
**--os**
Override the OS which the list or index records as a requirement for the image.
This is usually automatically retrieved from the image's configuration
information, so it is rarely necessary to use this option.
**--os-features**
Specify the OS features list which the list or index records as requirements
for the image. This option is rarely used.
**--os-version**
Specify the OS version which the list or index records as a requirement for the
image. This option is rarely used.
**--subject** *imageName*
Set the *subject* field in the image index to mark the image index as being
associated with the specified image in some way. An image index can only be
associated with, at most, one subject.
**--variant**
Specify the variant which the list or index records for the image. This option
is typically used to distinguish between multiple entries which share the same
architecture value, but which expect different versions of its instruction set.
## EXAMPLE
```
buildah manifest annotate --arch arm64 --variant v8 mylist:v1.11 sha256:c829b1810d2dbb456e74a695fd3847530c8319e5a95dca623e9f1b1b89020d8b
506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:c829b1810d2dbb456e74a695fd3847530c8319e5a95dca623e9f1b1b89020d8b
```
```
buildah manifest annotate --index --annotation food=yummy mylist:v1.11
506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:c829b1810d2dbb456e74a695fd3847530c8319e5a95dca623e9f1b1b89020d8b
```
## SEE ALSO
buildah(1), buildah-manifest(1), buildah-manifest-create(1), buildah-manifest-add(1), buildah-manifest-remove(1), buildah-manifest-inspect(1), buildah-manifest-push(1), buildah-rmi(1)

View File

@@ -0,0 +1,66 @@
# buildah-manifest-create "1" "August 2022" "buildah"
## NAME
buildah\-manifest\-create - Create a manifest list or image index.
## SYNOPSIS
**buildah manifest create** [options...] *listNameOrIndexName* [*imageName* ...]
## DESCRIPTION
Creates a new manifest list and stores it as an image in local storage using
the specified name.
If additional images are specified, they are added to the newly-created list or
index.
## RETURN VALUE
The randomly-generated image ID of the newly-created list or index. The image
can be deleted using the *buildah rmi* command.
## OPTIONS
**--all**
If any of the images which should be added to the new list or index are
themselves lists or indexes, add all of their contents. By default, only one
image from such a list will be added to the newly-created list or index.
**--amend**
If a manifest list named *listNameOrIndexName* already exists, modify the
preexisting list instead of exiting with an error. The contents of
*listNameOrIndexName* are not modified if no *imageName*s are given.
**--annotation** *annotation=value*
Set an annotation on the newly-created image index.
**--tls-verify** *bool-value*
Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry.
## EXAMPLE
```
buildah manifest create mylist:v1.11
941c1259e4b85bebf23580a044e4838aa3c1e627528422c9bf9262ff1661fca9
buildah manifest create --amend mylist:v1.11
941c1259e4b85bebf23580a044e4838aa3c1e627528422c9bf9262ff1661fca9
```
```
buildah manifest create mylist:v1.11 docker://fedora
941c1259e4b85bebf23580a044e4838aa3c1e627528422c9bf9262ff1661fca9
```
```
buildah manifest create --all mylist:v1.11 docker://fedora
941c1259e4b85bebf23580a044e4838aa3c1e627528422c9bf9262ff1661fca9
```
## SEE ALSO
buildah(1), buildah-manifest(1), buildah-manifest-add(1), buildah-manifest-remove(1), buildah-manifest-annotate(1), buildah-manifest-inspect(1), buildah-manifest-push(1), buildah-rmi(1)

View File

@@ -0,0 +1,40 @@
% buildah-manifest-exists(1)
## NAME
buildah\-manifest\-exists - Check if the given manifest list exists in local storage
## SYNOPSIS
**buildah manifest exists** *manifest*
## DESCRIPTION
**buildah manifest exists** checks if a manifest list exists in local storage. Buildah will
return an exit code of `0` when the manifest list is found. A `1` will be returned otherwise.
An exit code of `125` indicates there was another issue.
## OPTIONS
#### **--help**, **-h**
Print usage statement.
## EXAMPLE
Check if a manifest list called `list1` exists (the manifest list does actually exist).
```
$ buildah manifest exists list1
$ echo $?
0
$
```
Check if an manifest called `mylist` exists (the manifest list does not actually exist).
```
$ buildah manifest exists mylist
$ echo $?
1
$
```
## SEE ALSO
**[buildah(1)](buildah.1.md)**, **[buildah-manifest(1)](buildah-manifest.1.md)**

View File

@@ -0,0 +1,37 @@
# buildah-manifest-inspect "1" "September 2019" "buildah"
## NAME
buildah\-manifest\-inspect - Display a manifest list or image index.
## SYNOPSIS
**buildah manifest inspect** *listNameOrIndexName*
## DESCRIPTION
Displays the manifest list or image index stored using the specified image name.
## RETURN VALUE
A formatted JSON representation of the manifest list or image index.
## OPTIONS
**--authfile** *path*
Path of the authentication file. Default is ${XDG\_RUNTIME\_DIR}/containers/auth.json, which is set using `buildah login`.
If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`.
**--tls-verify** *bool-value*
Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry.
## EXAMPLE
```
buildah manifest inspect mylist:v1.11
```
## SEE ALSO
buildah(1), buildah-manifest(1), buildah-manifest-create(1), buildah-manifest-add(1), buildah-manifest-remove(1), buildah-manifest-annotate(1), buildah-manifest-push(1), buildah-rmi(1)

View File

@@ -0,0 +1,113 @@
# buildah-manifest-push "1" "September 2019" "buildah"
## NAME
buildah\-manifest\-push - Push a manifest list or image index to a registry.
## SYNOPSIS
**buildah manifest push** [options...] *listNameOrIndexName* *transport:details*
## DESCRIPTION
Pushes a manifest list or image index to a registry.
## RETURN VALUE
The list image's ID and the digest of the image's manifest.
## OPTIONS
**--add-compression** *compression*
Makes sure that requested compression variant for each platform is added to the manifest list keeping original instance
intact in the same manifest list. Supported values are (`gzip`, `zstd` and `zstd:chunked`)
Note: This is different than `--compression` which replaces the instance with requested with specified compression
while `--add-compression` makes sure than each instance has it variant added to manifest list without modifying the
original instance.
**--all**
Push the images mentioned in the manifest list or image index, in addition to
the list or index itself. (Default true)
**--authfile** *path*
Path of the authentication file. Default is ${XDG\_RUNTIME\_DIR}/containers/auth.json, which is set using `buildah login`.
If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`.
**--cert-dir** *path*
Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry.
The default certificates directory is _/etc/containers/certs.d_.
**--compression-format** *format*
Specifies the compression format to use. Supported values are: `gzip`, `zstd` and `zstd:chunked`.
**--compression-level** *level*
Specify the compression level used with the compression.
Specifies the compression level to use. The value is specific to the compression algorithm used, e.g. for zstd the accepted values are in the range 1-20 (inclusive), while for gzip it is 1-9 (inclusive).
**--creds** *creds*
The [username[:password]] to use to authenticate with the registry if required.
If one or both values are not supplied, a command line prompt will appear and the
value can be entered. The password is entered without echo.
**--digestfile** *Digestfile*
After copying the image, write the digest of the resulting image to the file.
**--force-compression**
If set, push uses the specified compression algorithm even if the destination contains a differently-compressed variant already.
Defaults to `true` if `--compression-format` is explicitly specified on the command-line, `false` otherwise.
**--format**, **-f**
Manifest list type (oci or v2s2) to use when pushing the list (default is oci).
**--quiet**, **-q**
Don't output progress information when pushing lists.
**--remove-signatures**
Don't copy signatures when pushing images.
**--retry** *attempts*
Number of times to retry in case of failure when performing push of images to registry.
Defaults to `3`.
**--retry-delay** *duration*
Duration of delay between retry attempts in case of failure when performing push of images to registry.
Defaults to `2s`.
**--rm**
Delete the manifest list or image index from local storage if pushing succeeds.
**--sign-by** *fingerprint*
Sign the pushed images using the GPG key that matches the specified fingerprint.
**--tls-verify** *bool-value*
Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry.
## EXAMPLE
```
buildah manifest push mylist:v1.11 registry.example.org/mylist:v1.11
```
## SEE ALSO
buildah(1), buildah-login(1), buildah-manifest(1), buildah-manifest-create(1), buildah-manifest-add(1), buildah-manifest-remove(1), buildah-manifest-annotate(1), buildah-manifest-inspect(1), buildah-rmi(1), docker-login(1)

View File

@@ -0,0 +1,28 @@
# buildah-manifest-remove "1" "September 2019" "buildah"
## NAME
buildah\-manifest\-remove - Remove an image from a manifest list or image index.
## SYNOPSIS
**buildah manifest remove** *listNameOrIndexName* *imageNameOrManifestDigestOrArtifactName*
## DESCRIPTION
Removes the image with the specified name or digest from the specified manifest
list or image index, or the specified artifact from the specified image index.
## RETURN VALUE
The list image's ID and the digest of the removed image's manifest.
## EXAMPLE
```
buildah manifest remove mylist:v1.11 sha256:f81f09918379d5442d20dff82a298f29698197035e737f76e511d5af422cabd7
506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:f81f09918379d5442d20dff82a298f29698197035e737f76e511d5af422cabd7
```
## SEE ALSO
buildah(1), buildah-manifest(1), buildah-manifest-create(1), buildah-manifest-add(1), buildah-manifest-annotate(1), buildah-manifest-inspect(1), buildah-manifest-push(1), buildah-rmi(1)

View File

@@ -0,0 +1,25 @@
# buildah-manifest-rm "1" "April 2021" "buildah"
## NAME
buildah\-manifest\-rm - Removes one or more manifest lists.
## SYNOPSIS
**buildah manifest rm** [*listNameOrIndexName* ...]
## DESCRIPTION
Removes one or more locally stored manifest lists.
## EXAMPLE
buildah manifest rm <list>
buildah manifest-rm listID1 listID2
**storage.conf** (`/etc/containers/storage.conf`)
storage.conf is the storage configuration file for all tools using containers/storage
The storage configuration file specifies all of the available container storage options for tools using shared container storage.
## SEE ALSO
buildah(1), containers-storage.conf(5), buildah-manifest(1)

View File

@@ -0,0 +1,77 @@
# buildah-manifest "1" "September 2019" "buildah"
## NAME
buildah-manifest - Create and manipulate manifest lists and image indexes.
## SYNOPSIS
buildah manifest COMMAND [OPTIONS] [ARG...]
## DESCRIPTION
The `buildah manifest` command provides subcommands which can be used to:
* Create a working Docker manifest list or OCI image index.
* Add an entry to a manifest list or image index for a specified image.
* Add an entry to an image index for an artifact manifest referring to a file.
* Add or update information about an entry in a manifest list or image index.
* Delete a working container or an image.
* Push a manifest list or image index to a registry or other location.
## SUBCOMMANDS
| Command | Man Page | Description |
| ------- | -------------------------------------------------------------- | --------------------------------------------------------------------------- |
| add | [buildah-manifest-add(1)](buildah-manifest-add.1.md) | Add an image or artifact to a manifest list or image index. |
| annotate | [buildah-manifest-annotate(1)](buildah-manifest-annotate.1.md) | Add or update information about an image or artifact in a manifest list or image index. |
| create | [buildah-manifest-create(1)](buildah-manifest-create.1.md) | Create a manifest list or image index. |
| exists | [buildah-manifest-exists(1)](buildah-manifest-exists.1.md) | Check if a manifest list exists in local storage. |
| inspect | [buildah-manifest-inspect(1)](buildah-manifest-inspect.1.md) | Display the contents of a manifest list or image index. |
| push | [buildah-manifest-push(1)](buildah-manifest-push.1.md) | Push a manifest list or image index to a registry or other location. |
| remove | [buildah-manifest-remove(1)](buildah-manifest-remove.1.md) | Remove an image from a manifest list or image index. |
| rm | [buildah-manifest-rm(1)](buildah-manifest-rm.1.md) | Remove manifest list from local storage. |
## EXAMPLES
### Building a multi-arch manifest list from a Containerfile
Assuming the `Containerfile` uses `RUN` instructions, the host needs
a way to execute non-native binaries. Configuring this is beyond
the scope of this example. Building a multi-arch manifest list
`shazam` in parallel across 4-threads can be done like this:
$ platarch=linux/amd64,linux/ppc64le,linux/arm64,linux/s390x
$ buildah build --jobs=4 --platform=$platarch --manifest shazam .
**Note:** The `--jobs` argument is optional, and the `--manifest` option
should be used instead of the`-t` or `--tag` options.
### Assembling a multi-arch manifest from separately built images
Assuming `example.com/example/shazam:$arch` images are built separately
on other hosts and pushed to the `example.com` registry. They may
be combined into a manifest list, and pushed using a simple loop:
$ REPO=example.com/example/shazam
$ buildah manifest create $REPO:latest
$ for IMGTAG in amd64 s390x ppc64le arm64; do \
buildah manifest add $REPO:latest docker://$REPO:IMGTAG; \
done
$ buildah manifest push --all $REPO:latest
**Note:** The `add` instruction argument order is `<manifest>` then `<image>`.
Also, the `--all` push option is required to ensure all contents are
pushed, not just the native platform/arch.
### Removing and tagging a manifest list before pushing
Special care is needed when removing and pushing manifest lists, as opposed
to the contents. You almost always want to use the `manifest rm` and
`manifest push --all` subcommands. For example, a rename and push could
be performed like this:
$ buildah tag localhost/shazam example.com/example/shazam
$ buildah manifest rm localhost/shazam
$ buildah manifest push --all example.com/example/shazam
## SEE ALSO
buildah(1), buildah-manifest-create(1), buildah-manifest-add(1), buildah-manifest-remove(1), buildah-manifest-annotate(1), buildah-manifest-inspect(1), buildah-manifest-push(1), buildah-manifest-rm(1)

View File

@@ -0,0 +1,86 @@
# buildah-mkcw "1" "July 2023" "buildah"
## NAME
buildah\-mkcw - Convert a conventional container image into a confidential workload image.
## SYNOPSIS
**buildah mkcw** [*options*] *source* *destination*
## DESCRIPTION
Converts the contents of a container image into a new container image which is
suitable for use in a trusted execution environment (TEE), typically run using
krun (i.e., crun built with the libkrun feature enabled and invoked as *krun*).
Instead of the conventional contents, the root filesystem of the created image
will contain an encrypted disk image and configuration information for krun.
## source
A container image, stored locally or in a registry
## destination
A container image, stored locally or in a registry
## OPTIONS
**--add-file** *source[:destination]*
Read the contents of the file `source` and add it to the committed image as a
file at `destination`. If `destination` is not specified, the path of `source`
will be used. The new file will be owned by UID 0, GID 0, have 0644
permissions, and be given a current timestamp. This option can be specified
multiple times.
**--attestation-url**, **-u** *url*
The location of a key broker / attestation server.
If a value is specified, the new image's workload ID, along with the passphrase
used to encrypt the disk image, will be registered with the server, and the
server's location will be stored in the container image.
At run-time, krun is expected to contact the server to retrieve the passphrase
using the workload ID, which is also stored in the container image.
If no value is specified, a *passphrase* value *must* be specified.
**--base-image**, **-b** *image*
An alternate image to use as the base for the output image. By default,
the *scratch* non-image is used.
**--cpus**, **-c** *number*
The number of virtual CPUs which the image expects to be run with at run-time.
If not specified, a default value will be supplied.
**--firmware-library**, **-f** *file*
The location of the libkrunfw-sev shared library. If not specified, `buildah`
checks for its presence in a number of hard-coded locations.
**--memory**, **-m** *number*
The amount of memory which the image expects to be run with at run-time, as a
number of megabytes. If not specified, a default value will be supplied.
**--passphrase**, **-p** *text*
The passphrase to use to encrypt the disk image which will be included in the
container image.
If no value is specified, but an *--attestation-url* value is specified, a
randomly-generated passphrase will be used.
The authors recommend setting an *--attestation-url* but not a *--passphrase*.
**--slop**, **-s** *{percentage%|sizeKB|sizeMB|sizeGB}*
Extra space to allocate for the disk image compared to the size of the
container image's contents, expressed either as a percentage (..%) or a size
value (bytes, or larger units if suffixes like KB or MB are present), or a sum
of two or more such specifications. If not specified, `buildah` guesses that
25% more space than the contents will be enough, but this option is provided in
case its guess is wrong. If the specified or computed size is less than 10
megabytes, it will be increased to 10 megabytes.
**--type**, **-t** {SEV|SNP}
The type of trusted execution environment (TEE) which the image should be
marked for use with. Accepted values are "SEV" (AMD Secure Encrypted
Virtualization - Encrypted State) and "SNP" (AMD Secure Encrypted
Virtualization - Secure Nested Paging). If not specified, defaults to "SNP".
**--workload-id**, **-w** *id*
A workload identifier which will be recorded in the container image, to be used
at run-time for retrieving the passphrase which was used to encrypt the disk
image. If not specified, a semi-random value will be derived from the base
image's image ID.
## SEE ALSO
buildah(1)

View File

@@ -0,0 +1,66 @@
# buildah-mount "1" "March 2017" "buildah"
## NAME
buildah\-mount - Mount a working container's root filesystem.
## SYNOPSIS
**buildah mount** [*container* ...]
## DESCRIPTION
Mounts the specified container's root file system in a location which can be
accessed from the host, and returns its location.
If the mount command is invoked without any arguments, the tool will list all of the currently mounted containers.
When running in rootless mode, mount runs in a different namespace so
that the mounted volume might not be accessible from the host when
using a driver different than `vfs`. To be able to access the file
system mounted, you might need to create the mount namespace
separately as part of `buildah unshare`. In the environment created
with `buildah unshare` you can then use `buildah mount` and have
access to the mounted file system.
## RETURN VALUE
The location of the mounted file system. On error an empty string and errno is
returned.
## OPTIONS
**--json**
Output in JSON format.
## EXAMPLE
```
buildah mount working-container
/var/lib/containers/storage/overlay2/f3ac502d97b5681989dff84dfedc8354239bcecbdc2692f9a639f4e080a02364/merged
```
```
buildah mount
working-container /var/lib/containers/storage/overlay2/f3ac502d97b5681989dff84dfedc8354239bcecbdc2692f9a639f4e080a02364/merged
fedora-working-container /var/lib/containers/storage/overlay2/0ff7d7ca68bed1ace424f9df154d2dd7b5a125c19d887f17653cbcd5b6e30ba1/merged
```
```
buildah mount working-container fedora-working-container ubi8-working-container
working-container /var/lib/containers/storage/overlay/f8cac5cce73e5102ab321cc5b57c0824035b5cb82b6822e3c86ebaff69fefa9c/merged
fedora-working-container /var/lib/containers/storage/overlay/c3ec418be5bda5b72dca74c4d397e05829fe62ecd577dd7518b5f7fc1ca5f491/merged
ubi8-working-container /var/lib/containers/storage/overlay/03a071f206f70f4fcae5379bd5126be86b5352dc2a0c3449cd6fca01b77ea868/merged
```
If running in rootless mode, you need to do a buildah unshare first to use
the mount point.
```
$ buildah unshare
# buildah mount working-container
/var/lib/containers/storage/overlay/f8cac5cce73e5102ab321cc5b57c0824035b5cb82b6822e3c86ebaff69fefa9c/merged
# cp foobar /var/lib/containers/storage/overlay/f8cac5cce73e5102ab321cc5b57c0824035b5cb82b6822e3c86ebaff69fefa9c/merged
# buildah unmount working-container
# exit
$ buildah commit working-container newimage
```
## SEE ALSO
buildah(1), buildah-unshare(1), buildah-umount(1)

View File

@@ -0,0 +1,33 @@
# buildah-rmi "1" "Jan 2023" "buildah"
## NAME
buildah\-prune - Cleanup intermediate images as well as build and mount cache.
## SYNOPSIS
**buildah prune**
## DESCRIPTION
Cleanup intermediate images as well as build and mount cache.
## OPTIONS
**--all**, **-a**
All local images will be removed from the system that do not have containers using the image as a reference image.
**--force**, **-f**
This option will cause Buildah to remove all containers that are using the image before removing the image from the system.
## EXAMPLE
buildah prune
buildah prune --force
## SEE ALSO
buildah(1), containers-registries.conf(5), containers-storage.conf(5)

View File

@@ -0,0 +1,162 @@
# buildah-pull "1" "July 2018" "buildah"
## NAME
buildah\-pull - Pull an image from a registry.
## SYNOPSIS
**buildah pull** [*options*] *image*
## DESCRIPTION
Pulls an image based upon the specified input. It supports all transports from `containers-transports(5)` (see examples below). If no transport is specified, the input is subject to short-name resolution (see `containers-registries.conf(5)`) and the `docker` (i.e., container registry) transport is used.
### DEPENDENCIES
Buildah resolves the path to the registry to pull from by using the /etc/containers/registries.conf
file, containers-registries.conf(5). If the `buildah pull` command fails with an "image not known" error,
first verify that the registries.conf file is installed and configured appropriately.
## RETURN VALUE
The image ID of the image that was pulled. On error 1 is returned.
## OPTIONS
**--all-tags**, **-a**
All tagged images in the repository will be pulled.
**--arch**="ARCH"
Set the ARCH of the image to be pulled to the provided value instead of using the architecture of the host. (Examples: arm, arm64, 386, amd64, ppc64le, s390x)
**--authfile** *path*
Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. See containers-auth.json(5) for more information. This file is created using `buildah login`.
If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`.
Note: You can also override the default path of the authentication file by setting the REGISTRY\_AUTH\_FILE
environment variable. `export REGISTRY_AUTH_FILE=path`
**--cert-dir** *path*
Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry.
The default certificates directory is _/etc/containers/certs.d_.
**--creds** *creds*
The [username[:password]] to use to authenticate with the registry if required.
If one or both values are not supplied, a command line prompt will appear and the
value can be entered. The password is entered without echo.
**--decryption-key** *key[:passphrase]*
The [key[:passphrase]] to be used for decryption of images. Key can point to keys and/or certificates. Decryption will be tried with all keys. If the key is protected by a passphrase, it is required to be passed in the argument and omitted otherwise.
**--os**="OS"
Set the OS of the image to be pulled instead of using the current operating system of the host.
**--platform**="OS/ARCH[/VARIANT]"
Set the OS/ARCH of the image to be pulled
to the provided value instead of using the current operating system and
architecture of the host (for example `linux/arm`).
OS/ARCH pairs are those used by the Go Programming Language. In several cases
the ARCH value for a platform differs from one produced by other tools such as
the `arch` command. Valid OS and architecture name combinations are listed as
values for $GOOS and $GOARCH at https://golang.org/doc/install/source#environment,
and can also be found by running `go tool dist list`.
**NOTE:** The `--platform` option may not be used in combination with the `--arch`, `--os`, or `--variant` options.
**--policy**=**always**|**missing**|**never**|**newer**
Pull image policy. The default is **missing**.
- **always**: Always pull the image and throw an error if the pull fails.
- **missing**: Pull the image only if it could not be found in the local containers storage. Throw an error if no image could be found and the pull fails.
- **never**: Never pull the image but use the one from the local containers storage. Throw an error if no image could be found.
- **newer**: Pull if the image on the registry is newer than the one in the local containers storage. An image is considered to be newer when the digests are different. Comparing the time stamps is prone to errors. Pull errors are suppressed if a local image was found.
**--quiet**, **-q**
If an image needs to be pulled from the registry, suppress progress output.
**--remove-signatures**
Don't copy signatures when pulling images.
**--retry** *attempts*
Number of times to retry in case of failure when performing pull of images from registry.
Defaults to `3`.
**--retry-delay** *duration*
Duration of delay between retry attempts in case of failure when performing pull of images from registry.
Defaults to `2s`.
**--tls-verify** *bool-value*
Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry.
**--variant**=""
Set the architecture variant of the image to be pulled.
## EXAMPLE
buildah pull imagename
buildah pull docker://myregistry.example.com/imagename
buildah pull docker-daemon:imagename:imagetag
buildah pull docker-archive:filename
buildah pull oci-archive:filename
buildah pull dir:directoryname
buildah pull --tls-verify=false myregistry/myrepository/imagename:imagetag
buildah pull --creds=myusername:mypassword --cert-dir ~/auth myregistry/myrepository/imagename:imagetag
buildah pull --authfile=/tmp/auths/myauths.json myregistry/myrepository/imagename:imagetag
buildah pull --arch=aarch64 myregistry/myrepository/imagename:imagetag
buildah pull --arch=arm --variant=v7 myregistry/myrepository/imagename:imagetag
## ENVIRONMENT
**BUILD\_REGISTRY\_SOURCES**
BUILD\_REGISTRY\_SOURCES, if set, is treated as a JSON object which contains
lists of registry names under the keys `insecureRegistries`,
`blockedRegistries`, and `allowedRegistries`.
When pulling an image from a registry, if the name of the registry matches any
of the items in the `blockedRegistries` list, the image pull attempt is denied.
If there are registries in the `allowedRegistries` list, and the registry's
name is not in the list, the pull attempt is denied.
**TMPDIR**
The TMPDIR environment variable allows the user to specify where temporary files
are stored while pulling and pushing images. Defaults to '/var/tmp'.
## FILES
**registries.conf** (`/etc/containers/registries.conf`)
registries.conf is the configuration file which specifies which container registries should be consulted when completing image names which do not include a registry or domain portion.
**policy.json** (`/etc/containers/policy.json`)
Signature policy file. This defines the trust policy for container images. Controls which container registries can be used for image, and whether or not the tool should trust the images.
## SEE ALSO
buildah(1), buildah-from(1), buildah-login(1), docker-login(1), containers-policy.json(5), containers-registries.conf(5), containers-transports(5), containers-auth.json(5)

View File

@@ -0,0 +1,185 @@
# buildah-push "1" "June 2017" "buildah"
## NAME
buildah\-push - Push an image, manifest list or image index from local storage to elsewhere.
## SYNOPSIS
**buildah push** [*options*] *image* [*destination*]
## DESCRIPTION
Pushes an image from local storage to a specified destination, decompressing
and recompessing layers as needed.
## imageID
Image stored in local container/storage
## DESTINATION
DESTINATION is the location the container image is pushed to. It supports all transports from `containers-transports(5)` (see examples below). If no transport is specified, the `docker` (i.e., container registry) transport is used.
## OPTIONS
**--all**
If specified image is a manifest list or image index, push the images in addition to
the list or index itself.
**--authfile** *path*
Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. See containers-auth.json(5) for more information. This file is created using `buildah login`.
If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`.
Note: You can also override the default path of the authentication file by setting the REGISTRY\_AUTH\_FILE
environment variable. `export REGISTRY_AUTH_FILE=path`
**--cert-dir** *path*
Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry.
The default certificates directory is _/etc/containers/certs.d_.
**--compression-format** *format*
Specifies the compression format to use. Supported values are: `gzip`, `zstd` and `zstd:chunked`.
`zstd:chunked` is incompatible with encrypting images, and will be treated as `zstd` with a warning in that case.
**--compression-level** *level*
Specify the compression level used with the compression.
Specifies the compression level to use. The value is specific to the compression algorithm used, e.g. for zstd the accepted values are in the range 1-20 (inclusive), while for gzip it is 1-9 (inclusive).
**--creds** *creds*
The [username[:password]] to use to authenticate with the registry if required.
If one or both values are not supplied, a command line prompt will appear and the
value can be entered. The password is entered without echo.
**--digestfile** *Digestfile*
After copying the image, write the digest of the resulting image to the file.
**--disable-compression**, **-D**
Don't compress copies of filesystem layers which will be pushed.
**--encrypt-layer** *layer(s)*
Layer(s) to encrypt: 0-indexed layer indices with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer). If not defined, will encrypt all layers if encryption-key flag is specified.
**--encryption-key** *key*
The [protocol:keyfile] specifies the encryption protocol, which can be JWE (RFC7516), PGP (RFC4880), and PKCS7 (RFC2315) and the key material required for image encryption. For instance, jwe:/path/to/key.pem or pgp:admin@example.com or pkcs7:/path/to/x509-file.
**--force-compression**
If set, push uses the specified compression algorithm even if the destination contains a differently-compressed variant already.
Defaults to `true` if `--compression-format` is explicitly specified on the command-line, `false` otherwise.
**--format**, **-f**
Manifest Type (oci, v2s2, or v2s1) to use when pushing an image. (default is manifest type of the source image, with fallbacks)
**--quiet**, **-q**
When writing the output image, suppress progress output.
**--remove-signatures**
Don't copy signatures when pushing images.
**--retry** *attempts*
Number of times to retry in case of failure when performing push of images to registry.
Defaults to `3`.
**--retry-delay** *duration*
Duration of delay between retry attempts in case of failure when performing push of images to registry.
Defaults to `2s`.
**--rm**
When pushing a manifest list or image index, delete them from local storage if pushing succeeds.
**--sign-by** *fingerprint*
Sign the pushed image using the GPG key that matches the specified fingerprint.
**--tls-verify** *bool-value*
Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry.
## EXAMPLE
This example pushes the image specified by the imageID to a local directory in docker format.
`# buildah push imageID dir:/path/to/image`
This example pushes the image specified by the imageID to a local directory in oci format.
`# buildah push imageID oci:/path/to/layout:image:tag`
This example pushes the image specified by the imageID to a tar archive in oci format.
`# buildah push imageID oci-archive:/path/to/archive:image:tag`
This example pushes the image specified by the imageID to a container registry named registry.example.com.
`# buildah push imageID docker://registry.example.com/repository:tag`
This example pushes the image specified by the imageID to a container registry named registry.example.com and saves the digest in the specified digestfile.
`# buildah push --digestfile=/tmp/mydigest imageID docker://registry.example.com/repository:tag`
This example works like **docker push**, assuming *registry.example.com/my_image* is a local image.
`# buildah push registry.example.com/my_image`
This example pushes the image specified by the imageID to a private container registry named registry.example.com with authentication from /tmp/auths/myauths.json.
`# buildah push --authfile /tmp/auths/myauths.json imageID docker://registry.example.com/repository:tag`
This example pushes the image specified by the imageID and puts it into the local docker container store.
`# buildah push imageID docker-daemon:image:tag`
This example pushes the image specified by the imageID and puts it into the registry on the localhost while turning off tls verification.
`# buildah push --tls-verify=false imageID localhost:5000/my-imageID`
This example pushes the image specified by the imageID and puts it into the registry on the localhost using credentials and certificates for authentication.
`# buildah push --cert-dir ~/auth --tls-verify=true --creds=username:password imageID localhost:5000/my-imageID`
## ENVIRONMENT
**BUILD\_REGISTRY\_SOURCES**
BUILD\_REGISTRY\_SOURCES, if set, is treated as a JSON object which contains
lists of registry names under the keys `insecureRegistries`,
`blockedRegistries`, and `allowedRegistries`.
When pushing an image to a registry, if the portion of the destination image
name that corresponds to a registry is compared to the items in the
`blockedRegistries` list, and if it matches any of them, the push attempt is
denied. If there are registries in the `allowedRegistries` list, and the
portion of the name that corresponds to the registry is not in the list, the
push attempt is denied.
**TMPDIR**
The TMPDIR environment variable allows the user to specify where temporary files
are stored while pulling and pushing images. Defaults to '/var/tmp'.
## FILES
**registries.conf** (`/etc/containers/registries.conf`)
registries.conf is the configuration file which specifies which container registries should be consulted when completing image names which do not include a registry or domain portion.
**policy.json** (`/etc/containers/policy.json`)
Signature policy file. This defines the trust policy for container images. Controls which container registries can be used for image, and whether or not the tool should trust the images.
## SEE ALSO
buildah(1), buildah-login(1), containers-policy.json(5), docker-login(1), containers-registries.conf(5), buildah-manifest(1), containers-transports(5), containers-auth.json(5)

Some files were not shown because too many files have changed in this diff Show More