Compare commits
4 Commits
b4dfa7733d
...
developmen
Author | SHA1 | Date | |
---|---|---|---|
|
0e63efda61 | ||
|
568a5b0a49 | ||
|
d431705501 | ||
|
b4e370b668 |
227
.github/workflows/publish.yml
vendored
227
.github/workflows/publish.yml
vendored
@@ -1,227 +0,0 @@
|
||||
name: Publish SAL Crates
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to publish (e.g., 0.1.0)'
|
||||
required: true
|
||||
type: string
|
||||
dry_run:
|
||||
description: 'Dry run (do not actually publish)'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish to crates.io
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Cache Cargo dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- name: Install cargo-edit for version management
|
||||
run: cargo install cargo-edit
|
||||
|
||||
- name: Set version from release tag
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
echo "PUBLISH_VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "Publishing version: $VERSION"
|
||||
|
||||
- name: Set version from workflow input
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
echo "PUBLISH_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
|
||||
echo "Publishing version: ${{ github.event.inputs.version }}"
|
||||
|
||||
- name: Update version in all crates
|
||||
run: |
|
||||
echo "Updating version to $PUBLISH_VERSION"
|
||||
|
||||
# Update root Cargo.toml
|
||||
cargo set-version $PUBLISH_VERSION
|
||||
|
||||
# Update each crate
|
||||
CRATES=(os process text net git vault kubernetes virt redisclient postgresclient zinit_client mycelium rhai)
|
||||
for crate in "${CRATES[@]}"; do
|
||||
if [ -d "$crate" ]; then
|
||||
cd "$crate"
|
||||
cargo set-version $PUBLISH_VERSION
|
||||
cd ..
|
||||
echo "Updated $crate to version $PUBLISH_VERSION"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --workspace --verbose
|
||||
|
||||
- name: Check formatting
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Run clippy
|
||||
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
|
||||
- name: Dry run publish (check packages)
|
||||
run: |
|
||||
echo "Checking all packages can be published..."
|
||||
|
||||
CRATES=(os process text net git vault kubernetes virt redisclient postgresclient zinit_client mycelium rhai)
|
||||
for crate in "${CRATES[@]}"; do
|
||||
if [ -d "$crate" ]; then
|
||||
echo "Checking $crate..."
|
||||
cd "$crate"
|
||||
cargo publish --dry-run
|
||||
cd ..
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Checking main crate..."
|
||||
cargo publish --dry-run
|
||||
|
||||
- name: Publish crates (dry run)
|
||||
if: github.event.inputs.dry_run == 'true'
|
||||
run: |
|
||||
echo "🔍 DRY RUN MODE - Would publish the following crates:"
|
||||
echo "Individual crates: sal-os, sal-process, sal-text, sal-net, sal-git, sal-vault, sal-kubernetes, sal-virt, sal-redisclient, sal-postgresclient, sal-zinit-client, sal-mycelium, sal-rhai"
|
||||
echo "Meta-crate: sal"
|
||||
echo "Version: $PUBLISH_VERSION"
|
||||
|
||||
- name: Publish individual crates
|
||||
if: github.event.inputs.dry_run != 'true'
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
echo "Publishing individual crates..."
|
||||
|
||||
# Crates in dependency order
|
||||
CRATES=(os process text net git vault kubernetes virt redisclient postgresclient zinit_client mycelium rhai)
|
||||
|
||||
for crate in "${CRATES[@]}"; do
|
||||
if [ -d "$crate" ]; then
|
||||
echo "Publishing sal-$crate..."
|
||||
cd "$crate"
|
||||
|
||||
# Retry logic for transient failures
|
||||
for attempt in 1 2 3; do
|
||||
if cargo publish --token $CARGO_REGISTRY_TOKEN; then
|
||||
echo "✅ sal-$crate published successfully"
|
||||
break
|
||||
else
|
||||
if [ $attempt -eq 3 ]; then
|
||||
echo "❌ Failed to publish sal-$crate after 3 attempts"
|
||||
exit 1
|
||||
else
|
||||
echo "⚠️ Attempt $attempt failed, retrying in 30 seconds..."
|
||||
sleep 30
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
cd ..
|
||||
|
||||
# Wait for crates.io to process
|
||||
if [ "$crate" != "rhai" ]; then
|
||||
echo "⏳ Waiting 30 seconds for crates.io to process..."
|
||||
sleep 30
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Publish main crate
|
||||
if: github.event.inputs.dry_run != 'true'
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
echo "Publishing main sal crate..."
|
||||
|
||||
# Wait a bit longer before publishing the meta-crate
|
||||
echo "⏳ Waiting 60 seconds for all individual crates to be available..."
|
||||
sleep 60
|
||||
|
||||
# Retry logic for the main crate
|
||||
for attempt in 1 2 3; do
|
||||
if cargo publish --token $CARGO_REGISTRY_TOKEN; then
|
||||
echo "✅ Main sal crate published successfully"
|
||||
break
|
||||
else
|
||||
if [ $attempt -eq 3 ]; then
|
||||
echo "❌ Failed to publish main sal crate after 3 attempts"
|
||||
exit 1
|
||||
else
|
||||
echo "⚠️ Attempt $attempt failed, retrying in 60 seconds..."
|
||||
sleep 60
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Create summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## 📦 SAL Publishing Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version:** $PUBLISH_VERSION" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then
|
||||
echo "**Mode:** Dry Run" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "**Mode:** Live Publishing" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Published Crates" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- sal-os" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- sal-process" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- sal-text" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- sal-net" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- sal-git" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- sal-vault" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- sal-kubernetes" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- sal-virt" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- sal-redisclient" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- sal-postgresclient" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- sal-zinit-client" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- sal-mycelium" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- sal-rhai" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- sal (meta-crate)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Usage" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```bash' >> $GITHUB_STEP_SUMMARY
|
||||
echo "# Individual crates" >> $GITHUB_STEP_SUMMARY
|
||||
echo "cargo add sal-os sal-process sal-text" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "# Meta-crate with features" >> $GITHUB_STEP_SUMMARY
|
||||
echo "cargo add sal --features core" >> $GITHUB_STEP_SUMMARY
|
||||
echo "cargo add sal --features all" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
233
.github/workflows/test-publish.yml
vendored
233
.github/workflows/test-publish.yml
vendored
@@ -1,233 +0,0 @@
|
||||
name: Test Publishing Setup
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
paths:
|
||||
- '**/Cargo.toml'
|
||||
- 'scripts/publish-all.sh'
|
||||
- '.github/workflows/publish.yml'
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
paths:
|
||||
- '**/Cargo.toml'
|
||||
- 'scripts/publish-all.sh'
|
||||
- '.github/workflows/publish.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test-publish-setup:
|
||||
name: Test Publishing Setup
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Cache Cargo dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-publish-test-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-publish-test-
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- name: Install cargo-edit
|
||||
run: cargo install cargo-edit
|
||||
|
||||
- name: Test workspace structure
|
||||
run: |
|
||||
echo "Testing workspace structure..."
|
||||
|
||||
# Check that all expected crates exist
|
||||
EXPECTED_CRATES=(os process text net git vault kubernetes virt redisclient postgresclient zinit_client mycelium rhai herodo)
|
||||
|
||||
for crate in "${EXPECTED_CRATES[@]}"; do
|
||||
if [ -d "$crate" ] && [ -f "$crate/Cargo.toml" ]; then
|
||||
echo "✅ $crate exists"
|
||||
else
|
||||
echo "❌ $crate missing or invalid"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Test feature configuration
|
||||
run: |
|
||||
echo "Testing feature configuration..."
|
||||
|
||||
# Test that features work correctly
|
||||
cargo check --features os
|
||||
cargo check --features process
|
||||
cargo check --features text
|
||||
cargo check --features net
|
||||
cargo check --features git
|
||||
cargo check --features vault
|
||||
cargo check --features kubernetes
|
||||
cargo check --features virt
|
||||
cargo check --features redisclient
|
||||
cargo check --features postgresclient
|
||||
cargo check --features zinit_client
|
||||
cargo check --features mycelium
|
||||
cargo check --features rhai
|
||||
|
||||
echo "✅ All individual features work"
|
||||
|
||||
# Test feature groups
|
||||
cargo check --features core
|
||||
cargo check --features clients
|
||||
cargo check --features infrastructure
|
||||
cargo check --features scripting
|
||||
|
||||
echo "✅ All feature groups work"
|
||||
|
||||
# Test all features
|
||||
cargo check --features all
|
||||
|
||||
echo "✅ All features together work"
|
||||
|
||||
- name: Test dry-run publishing
|
||||
run: |
|
||||
echo "Testing dry-run publishing..."
|
||||
|
||||
# Test each individual crate can be packaged
|
||||
CRATES=(os process text net git vault kubernetes virt redisclient postgresclient zinit_client mycelium rhai)
|
||||
|
||||
for crate in "${CRATES[@]}"; do
|
||||
echo "Testing sal-$crate..."
|
||||
cd "$crate"
|
||||
cargo publish --dry-run
|
||||
cd ..
|
||||
echo "✅ sal-$crate can be published"
|
||||
done
|
||||
|
||||
# Test main crate
|
||||
echo "Testing main sal crate..."
|
||||
cargo publish --dry-run
|
||||
echo "✅ Main sal crate can be published"
|
||||
|
||||
- name: Test publishing script
|
||||
run: |
|
||||
echo "Testing publishing script..."
|
||||
|
||||
# Make script executable
|
||||
chmod +x scripts/publish-all.sh
|
||||
|
||||
# Test dry run
|
||||
./scripts/publish-all.sh --dry-run --version 0.1.0-test
|
||||
|
||||
echo "✅ Publishing script works"
|
||||
|
||||
- name: Test version consistency
|
||||
run: |
|
||||
echo "Testing version consistency..."
|
||||
|
||||
# Get version from root Cargo.toml
|
||||
ROOT_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
|
||||
echo "Root version: $ROOT_VERSION"
|
||||
|
||||
# Check all crates have the same version
|
||||
CRATES=(os process text net git vault kubernetes virt redisclient postgresclient zinit_client mycelium rhai herodo)
|
||||
|
||||
for crate in "${CRATES[@]}"; do
|
||||
if [ -f "$crate/Cargo.toml" ]; then
|
||||
CRATE_VERSION=$(grep '^version = ' "$crate/Cargo.toml" | head -1 | sed 's/version = "\(.*\)"/\1/')
|
||||
if [ "$CRATE_VERSION" = "$ROOT_VERSION" ]; then
|
||||
echo "✅ $crate version matches: $CRATE_VERSION"
|
||||
else
|
||||
echo "❌ $crate version mismatch: $CRATE_VERSION (expected $ROOT_VERSION)"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Test metadata completeness
|
||||
run: |
|
||||
echo "Testing metadata completeness..."
|
||||
|
||||
# Check that all crates have required metadata
|
||||
CRATES=(os process text net git vault kubernetes virt redisclient postgresclient zinit_client mycelium rhai)
|
||||
|
||||
for crate in "${CRATES[@]}"; do
|
||||
echo "Checking sal-$crate metadata..."
|
||||
cd "$crate"
|
||||
|
||||
# Check required fields exist
|
||||
if ! grep -q '^name = "sal-' Cargo.toml; then
|
||||
echo "❌ $crate missing or incorrect name"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q '^description = ' Cargo.toml; then
|
||||
echo "❌ $crate missing description"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q '^repository = ' Cargo.toml; then
|
||||
echo "❌ $crate missing repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q '^license = ' Cargo.toml; then
|
||||
echo "❌ $crate missing license"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ sal-$crate metadata complete"
|
||||
cd ..
|
||||
done
|
||||
|
||||
- name: Test dependency resolution
|
||||
run: |
|
||||
echo "Testing dependency resolution..."
|
||||
|
||||
# Test that all workspace dependencies resolve correctly
|
||||
cargo tree --workspace > /dev/null
|
||||
echo "✅ All dependencies resolve correctly"
|
||||
|
||||
# Test that there are no dependency conflicts
|
||||
cargo check --workspace
|
||||
echo "✅ No dependency conflicts"
|
||||
|
||||
- name: Generate publishing report
|
||||
if: always()
|
||||
run: |
|
||||
echo "## 🧪 Publishing Setup Test Report" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### ✅ Tests Passed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Workspace structure validation" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Feature configuration testing" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Dry-run publishing simulation" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Publishing script validation" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Version consistency check" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Metadata completeness verification" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Dependency resolution testing" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📦 Ready for Publishing" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "All SAL crates are ready for publishing to crates.io!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Individual Crates:** 13 modules" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Meta-crate:** sal with optional features" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Binary:** herodo script executor" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🚀 Next Steps" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "1. Create a release tag (e.g., v0.1.0)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "2. The publish workflow will automatically trigger" >> $GITHUB_STEP_SUMMARY
|
||||
echo "3. All crates will be published to crates.io" >> $GITHUB_STEP_SUMMARY
|
||||
echo "4. Users can install with: \`cargo add sal-os\` or \`cargo add sal --features all\`" >> $GITHUB_STEP_SUMMARY
|
84
Cargo.toml
84
Cargo.toml
@@ -11,23 +11,7 @@ categories = ["os", "filesystem", "api-bindings"]
|
||||
readme = "README.md"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
"vault",
|
||||
"git",
|
||||
"redisclient",
|
||||
"mycelium",
|
||||
"text",
|
||||
"os",
|
||||
"net",
|
||||
"zinit_client",
|
||||
"process",
|
||||
"virt",
|
||||
"postgresclient",
|
||||
"kubernetes",
|
||||
"rhai",
|
||||
"herodo",
|
||||
]
|
||||
members = [".", "vault", "git", "redisclient", "mycelium", "text", "os", "net", "zinit_client", "process", "virt", "postgresclient", "rhai", "herodo"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.metadata]
|
||||
@@ -88,57 +72,15 @@ tokio-test = "0.4.4"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "2.0.12" # For error handling in the main Error enum
|
||||
|
||||
# Optional dependencies - users can choose which modules to include
|
||||
sal-git = { path = "git", optional = true }
|
||||
sal-kubernetes = { path = "kubernetes", optional = true }
|
||||
sal-redisclient = { path = "redisclient", optional = true }
|
||||
sal-mycelium = { path = "mycelium", optional = true }
|
||||
sal-text = { path = "text", optional = true }
|
||||
sal-os = { path = "os", optional = true }
|
||||
sal-net = { path = "net", optional = true }
|
||||
sal-zinit-client = { path = "zinit_client", optional = true }
|
||||
sal-process = { path = "process", optional = true }
|
||||
sal-virt = { path = "virt", optional = true }
|
||||
sal-postgresclient = { path = "postgresclient", optional = true }
|
||||
sal-vault = { path = "vault", optional = true }
|
||||
sal-rhai = { path = "rhai", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
# Individual module features
|
||||
git = ["dep:sal-git"]
|
||||
kubernetes = ["dep:sal-kubernetes"]
|
||||
redisclient = ["dep:sal-redisclient"]
|
||||
mycelium = ["dep:sal-mycelium"]
|
||||
text = ["dep:sal-text"]
|
||||
os = ["dep:sal-os"]
|
||||
net = ["dep:sal-net"]
|
||||
zinit_client = ["dep:sal-zinit-client"]
|
||||
process = ["dep:sal-process"]
|
||||
virt = ["dep:sal-virt"]
|
||||
postgresclient = ["dep:sal-postgresclient"]
|
||||
vault = ["dep:sal-vault"]
|
||||
rhai = ["dep:sal-rhai"]
|
||||
|
||||
# Convenience feature groups
|
||||
core = ["os", "process", "text", "net"]
|
||||
clients = ["redisclient", "postgresclient", "zinit_client", "mycelium"]
|
||||
infrastructure = ["git", "vault", "kubernetes", "virt"]
|
||||
scripting = ["rhai"]
|
||||
all = [
|
||||
"git",
|
||||
"kubernetes",
|
||||
"redisclient",
|
||||
"mycelium",
|
||||
"text",
|
||||
"os",
|
||||
"net",
|
||||
"zinit_client",
|
||||
"process",
|
||||
"virt",
|
||||
"postgresclient",
|
||||
"vault",
|
||||
"rhai",
|
||||
]
|
||||
sal-git = { path = "git" }
|
||||
sal-redisclient = { path = "redisclient" }
|
||||
sal-mycelium = { path = "mycelium" }
|
||||
sal-text = { path = "text" }
|
||||
sal-os = { path = "os" }
|
||||
sal-net = { path = "net" }
|
||||
sal-zinit-client = { path = "zinit_client" }
|
||||
sal-process = { path = "process" }
|
||||
sal-virt = { path = "virt" }
|
||||
sal-postgresclient = { path = "postgresclient" }
|
||||
sal-vault = { path = "vault" }
|
||||
sal-rhai = { path = "rhai" }
|
||||
|
239
PUBLISHING.md
239
PUBLISHING.md
@@ -1,239 +0,0 @@
|
||||
# SAL Publishing Guide
|
||||
|
||||
This guide explains how to publish SAL crates to crates.io and how users can consume them.
|
||||
|
||||
## 🎯 Publishing Strategy
|
||||
|
||||
SAL uses a **modular publishing approach** where each module is published as an individual crate. This allows users to install only the functionality they need, reducing compilation time and binary size.
|
||||
|
||||
## 📦 Crate Structure
|
||||
|
||||
### Individual Crates
|
||||
|
||||
Each SAL module is published as a separate crate:
|
||||
|
||||
| Crate Name | Description | Category |
|
||||
|------------|-------------|----------|
|
||||
| `sal-os` | Operating system operations | Core |
|
||||
| `sal-process` | Process management | Core |
|
||||
| `sal-text` | Text processing utilities | Core |
|
||||
| `sal-net` | Network operations | Core |
|
||||
| `sal-git` | Git repository management | Infrastructure |
|
||||
| `sal-vault` | Cryptographic operations | Infrastructure |
|
||||
| `sal-kubernetes` | Kubernetes cluster management | Infrastructure |
|
||||
| `sal-virt` | Virtualization tools (Buildah, nerdctl) | Infrastructure |
|
||||
| `sal-redisclient` | Redis database client | Clients |
|
||||
| `sal-postgresclient` | PostgreSQL database client | Clients |
|
||||
| `sal-zinit-client` | Zinit process supervisor client | Clients |
|
||||
| `sal-mycelium` | Mycelium network client | Clients |
|
||||
| `sal-rhai` | Rhai scripting integration | Scripting |
|
||||
|
||||
### Meta-crate
|
||||
|
||||
The main `sal` crate serves as a meta-crate that re-exports all modules with optional features:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sal = { version = "0.1.0", features = ["os", "process", "text"] }
|
||||
```
|
||||
|
||||
## 🚀 Publishing Process
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Crates.io Account**: Ensure you have a crates.io account and API token
|
||||
2. **Repository Access**: Ensure the repository URL is accessible
|
||||
3. **Version Consistency**: All crates should use the same version number
|
||||
|
||||
### Publishing Individual Crates
|
||||
|
||||
Each crate can be published independently:
|
||||
|
||||
```bash
|
||||
# Publish core modules
|
||||
cd os && cargo publish
|
||||
cd ../process && cargo publish
|
||||
cd ../text && cargo publish
|
||||
cd ../net && cargo publish
|
||||
|
||||
# Publish infrastructure modules
|
||||
cd ../git && cargo publish
|
||||
cd ../vault && cargo publish
|
||||
cd ../kubernetes && cargo publish
|
||||
cd ../virt && cargo publish
|
||||
|
||||
# Publish client modules
|
||||
cd ../redisclient && cargo publish
|
||||
cd ../postgresclient && cargo publish
|
||||
cd ../zinit_client && cargo publish
|
||||
cd ../mycelium && cargo publish
|
||||
|
||||
# Publish scripting module
|
||||
cd ../rhai && cargo publish
|
||||
|
||||
# Finally, publish the meta-crate
|
||||
cd .. && cargo publish
|
||||
```
|
||||
|
||||
### Automated Publishing
|
||||
|
||||
Use the comprehensive publishing script:
|
||||
|
||||
```bash
|
||||
# Test the publishing process (safe)
|
||||
./scripts/publish-all.sh --dry-run --version 0.1.0
|
||||
|
||||
# Actually publish to crates.io
|
||||
./scripts/publish-all.sh --version 0.1.0
|
||||
```
|
||||
|
||||
The script handles:
|
||||
- ✅ **Dependency order** - Publishes crates in correct dependency order
|
||||
- ✅ **Path dependencies** - Automatically updates path deps to version deps
|
||||
- ✅ **Rate limiting** - Waits between publishes to avoid rate limits
|
||||
- ✅ **Error handling** - Stops on failures with clear error messages
|
||||
- ✅ **Dry run mode** - Test without actually publishing
|
||||
|
||||
## 👥 User Consumption
|
||||
|
||||
### Installation Options
|
||||
|
||||
#### Option 1: Individual Crates (Recommended)
|
||||
|
||||
Users install only what they need:
|
||||
|
||||
```bash
|
||||
# Core functionality
|
||||
cargo add sal-os sal-process sal-text sal-net
|
||||
|
||||
# Database operations
|
||||
cargo add sal-redisclient sal-postgresclient
|
||||
|
||||
# Infrastructure management
|
||||
cargo add sal-git sal-vault sal-kubernetes
|
||||
|
||||
# Service integration
|
||||
cargo add sal-zinit-client sal-mycelium
|
||||
|
||||
# Scripting
|
||||
cargo add sal-rhai
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```rust
|
||||
use sal_os::fs;
|
||||
use sal_process::run;
|
||||
use sal_git::GitManager;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let files = fs::list_files(".")?;
|
||||
let result = run::command("echo hello")?;
|
||||
let git = GitManager::new(".")?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
#### Option 2: Meta-crate with Features
|
||||
|
||||
Users can use the main crate with selective features:
|
||||
|
||||
```bash
|
||||
# Specific modules
|
||||
cargo add sal --features os,process,text
|
||||
|
||||
# Feature groups
|
||||
cargo add sal --features core # os, process, text, net
|
||||
cargo add sal --features clients # redisclient, postgresclient, zinit_client, mycelium
|
||||
cargo add sal --features infrastructure # git, vault, kubernetes, virt
|
||||
cargo add sal --features scripting # rhai
|
||||
|
||||
# Everything
|
||||
cargo add sal --features all
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```rust
|
||||
// Cargo.toml: sal = { version = "0.1.0", features = ["os", "process", "git"] }
|
||||
use sal::os::fs;
|
||||
use sal::process::run;
|
||||
use sal::git::GitManager;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let files = fs::list_files(".")?;
|
||||
let result = run::command("echo hello")?;
|
||||
let git = GitManager::new(".")?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Feature Groups
|
||||
|
||||
The meta-crate provides convenient feature groups:
|
||||
|
||||
- **`core`**: Essential system operations (os, process, text, net)
|
||||
- **`clients`**: Database and service clients (redisclient, postgresclient, zinit_client, mycelium)
|
||||
- **`infrastructure`**: Infrastructure management tools (git, vault, kubernetes, virt)
|
||||
- **`scripting`**: Rhai scripting support (rhai)
|
||||
- **`all`**: Everything included
|
||||
|
||||
## 📋 Version Management
|
||||
|
||||
### Semantic Versioning
|
||||
|
||||
All SAL crates follow semantic versioning:
|
||||
|
||||
- **Major version**: Breaking API changes
|
||||
- **Minor version**: New features, backward compatible
|
||||
- **Patch version**: Bug fixes, backward compatible
|
||||
|
||||
### Synchronized Releases
|
||||
|
||||
All crates are released with the same version number to ensure compatibility:
|
||||
|
||||
```toml
|
||||
# All crates use the same version
|
||||
sal-os = "0.1.0"
|
||||
sal-process = "0.1.0"
|
||||
sal-git = "0.1.0"
|
||||
# etc.
|
||||
```
|
||||
|
||||
## 🔧 Maintenance
|
||||
|
||||
### Updating Dependencies
|
||||
|
||||
When updating dependencies:
|
||||
|
||||
1. Update `Cargo.toml` in the workspace root
|
||||
2. Update individual crate dependencies if needed
|
||||
3. Test all crates: `cargo test --workspace`
|
||||
4. Publish with incremented version numbers
|
||||
|
||||
### Adding New Modules
|
||||
|
||||
To add a new SAL module:
|
||||
|
||||
1. Create the new crate directory
|
||||
2. Add to workspace members in root `Cargo.toml`
|
||||
3. Add optional dependency in root `Cargo.toml`
|
||||
4. Add feature flag in root `Cargo.toml`
|
||||
5. Add conditional re-export in `src/lib.rs`
|
||||
6. Update documentation
|
||||
|
||||
## 🎉 Benefits
|
||||
|
||||
### For Users
|
||||
|
||||
- **Minimal Dependencies**: Install only what you need
|
||||
- **Faster Builds**: Smaller dependency trees compile faster
|
||||
- **Smaller Binaries**: Reduced binary size
|
||||
- **Clear Dependencies**: Explicit about what functionality is used
|
||||
|
||||
### For Maintainers
|
||||
|
||||
- **Independent Releases**: Can release individual crates as needed
|
||||
- **Focused Testing**: Test individual modules in isolation
|
||||
- **Clear Ownership**: Each crate has clear responsibility
|
||||
- **Easier Maintenance**: Smaller, focused codebases
|
||||
|
||||
This publishing strategy provides the best of both worlds: modularity for users who want minimal dependencies, and convenience for users who prefer a single crate with features.
|
171
README.md
171
README.md
@@ -22,158 +22,6 @@ This workspace structure provides excellent build performance, dependency manage
|
||||
- **Modular Architecture**: Each module is independently maintainable while sharing common infrastructure
|
||||
- **Production Ready**: 100% test coverage with comprehensive Rhai integration tests
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
SAL is designed to be modular - install only the components you need!
|
||||
|
||||
### Option 1: Individual Crates (Recommended)
|
||||
|
||||
Install only the modules you need:
|
||||
|
||||
```bash
|
||||
# Core system operations
|
||||
cargo add sal-os sal-process sal-text sal-net
|
||||
|
||||
# Database clients
|
||||
cargo add sal-redisclient sal-postgresclient
|
||||
|
||||
# Infrastructure tools
|
||||
cargo add sal-git sal-vault sal-kubernetes sal-virt
|
||||
|
||||
# Service clients
|
||||
cargo add sal-zinit-client sal-mycelium
|
||||
|
||||
# Scripting support
|
||||
cargo add sal-rhai
|
||||
```
|
||||
|
||||
### Option 2: Meta-crate with Features
|
||||
|
||||
Use the main `sal` crate with specific features:
|
||||
|
||||
```bash
|
||||
# Install specific modules
|
||||
cargo add sal --features os,process,text
|
||||
|
||||
# Install feature groups
|
||||
cargo add sal --features core # os, process, text, net
|
||||
cargo add sal --features clients # redisclient, postgresclient, zinit_client, mycelium
|
||||
cargo add sal --features infrastructure # git, vault, kubernetes, virt
|
||||
cargo add sal --features scripting # rhai
|
||||
|
||||
# Install everything
|
||||
cargo add sal --features all
|
||||
```
|
||||
|
||||
### Quick Start Examples
|
||||
|
||||
#### Using Individual Crates (Recommended)
|
||||
|
||||
```rust
|
||||
use sal_os::fs;
|
||||
use sal_process::run;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// File system operations
|
||||
let files = fs::list_files(".")?;
|
||||
println!("Found {} files", files.len());
|
||||
|
||||
// Process execution
|
||||
let result = run::command("echo hello")?;
|
||||
println!("Output: {}", result.stdout);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
#### Using Meta-crate with Features
|
||||
|
||||
```rust
|
||||
// In Cargo.toml: sal = { version = "0.1.0", features = ["os", "process"] }
|
||||
use sal::os::fs;
|
||||
use sal::process::run;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// File system operations
|
||||
let files = fs::list_files(".")?;
|
||||
println!("Found {} files", files.len());
|
||||
|
||||
// Process execution
|
||||
let result = run::command("echo hello")?;
|
||||
println!("Output: {}", result.stdout);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
#### Using Herodo for Scripting
|
||||
|
||||
```bash
|
||||
# Build and install herodo
|
||||
git clone https://github.com/PlanetFirst/sal.git
|
||||
cd sal
|
||||
./build_herodo.sh
|
||||
|
||||
# Create a script file
|
||||
cat > example.rhai << 'EOF'
|
||||
// File operations
|
||||
let files = find_files(".", "*.rs");
|
||||
print("Found " + files.len() + " Rust files");
|
||||
|
||||
// Process execution
|
||||
let result = run("echo 'Hello from SAL!'");
|
||||
print("Output: " + result.stdout);
|
||||
|
||||
// Network operations
|
||||
let reachable = http_check("https://github.com");
|
||||
print("GitHub reachable: " + reachable);
|
||||
EOF
|
||||
|
||||
# Execute the script
|
||||
herodo example.rhai
|
||||
```
|
||||
|
||||
## 📦 Available Packages
|
||||
|
||||
SAL is published as individual crates, allowing you to install only what you need:
|
||||
|
||||
| Package | Description | Install Command |
|
||||
|---------|-------------|-----------------|
|
||||
| [`sal-os`](https://crates.io/crates/sal-os) | Operating system operations | `cargo add sal-os` |
|
||||
| [`sal-process`](https://crates.io/crates/sal-process) | Process management | `cargo add sal-process` |
|
||||
| [`sal-text`](https://crates.io/crates/sal-text) | Text processing utilities | `cargo add sal-text` |
|
||||
| [`sal-net`](https://crates.io/crates/sal-net) | Network operations | `cargo add sal-net` |
|
||||
| [`sal-git`](https://crates.io/crates/sal-git) | Git repository management | `cargo add sal-git` |
|
||||
| [`sal-vault`](https://crates.io/crates/sal-vault) | Cryptographic operations | `cargo add sal-vault` |
|
||||
| [`sal-kubernetes`](https://crates.io/crates/sal-kubernetes) | Kubernetes management | `cargo add sal-kubernetes` |
|
||||
| [`sal-virt`](https://crates.io/crates/sal-virt) | Virtualization tools | `cargo add sal-virt` |
|
||||
| `sal-redisclient` | Redis database client | `cargo add sal-redisclient` ⏳ |
|
||||
| `sal-postgresclient` | PostgreSQL client | `cargo add sal-postgresclient` ⏳ |
|
||||
| `sal-zinit-client` | Zinit process supervisor | `cargo add sal-zinit-client` ⏳ |
|
||||
| `sal-mycelium` | Mycelium network client | `cargo add sal-mycelium` ⏳ |
|
||||
| `sal-rhai` | Rhai scripting integration | `cargo add sal-rhai` ⏳ |
|
||||
| `sal` | Meta-crate with features | `cargo add sal --features all` ⏳ |
|
||||
| `herodo` | Script executor binary | Build from source ⏳ |
|
||||
|
||||
**Legend**: ✅ Published | ⏳ Publishing soon (rate limited)
|
||||
|
||||
### 📢 **Publishing Status**
|
||||
|
||||
**Currently Available on crates.io:**
|
||||
- ✅ [`sal-os`](https://crates.io/crates/sal-os) - Operating system operations
|
||||
- ✅ [`sal-text`](https://crates.io/crates/sal-text) - Text processing utilities
|
||||
- ✅ [`sal-net`](https://crates.io/crates/sal-net) - Network operations
|
||||
- ✅ [`sal-git`](https://crates.io/crates/sal-git) - Git repository management
|
||||
- ✅ [`sal-vault`](https://crates.io/crates/sal-vault) - Cryptographic operations
|
||||
- ✅ [`sal-kubernetes`](https://crates.io/crates/sal-kubernetes) - Kubernetes management
|
||||
|
||||
**Publishing Soon** (hit crates.io rate limit):
|
||||
- ⏳ `sal-redisclient`, `sal-postgresclient`, `sal-zinit-client`, `sal-mycelium`
|
||||
- ⏳ `sal-process`, `sal-virt`, `sal-rhai`
|
||||
- ⏳ `sal` (meta-crate), `herodo` (binary)
|
||||
|
||||
**Estimated Timeline**: Remaining packages will be published within 24 hours once the rate limit resets.
|
||||
|
||||
## Core Features
|
||||
|
||||
SAL offers a broad spectrum of functionalities, including:
|
||||
@@ -302,25 +150,6 @@ async fn main() {
|
||||
```
|
||||
*(Note: The Redis client API might have evolved; please refer to `src/redisclient/mod.rs` and its documentation for the most current usage.)*
|
||||
|
||||
## 🎯 **Why Choose SAL?**
|
||||
|
||||
### **Modular Architecture**
|
||||
- **Install Only What You Need**: Each package is independent - no bloated dependencies
|
||||
- **Faster Compilation**: Smaller dependency trees mean faster build times
|
||||
- **Smaller Binaries**: Only include the functionality you actually use
|
||||
- **Clear Dependencies**: Explicit about what functionality your project uses
|
||||
|
||||
### **Developer Experience**
|
||||
- **Consistent APIs**: All packages follow the same design patterns and conventions
|
||||
- **Comprehensive Documentation**: Each package has detailed documentation and examples
|
||||
- **Real-World Tested**: All functionality is production-tested, no placeholder code
|
||||
- **Type Safety**: Leverages Rust's type system for safe, reliable operations
|
||||
|
||||
### **Scripting Power**
|
||||
- **Herodo Integration**: Execute Rhai scripts with full access to SAL functionality
|
||||
- **Cross-Platform**: Works consistently across Windows, macOS, and Linux
|
||||
- **Automation Ready**: Perfect for DevOps, CI/CD, and system administration tasks
|
||||
|
||||
## 📦 **Workspace Modules Overview**
|
||||
|
||||
SAL is organized as a Cargo workspace with the following crates:
|
||||
|
@@ -1,72 +0,0 @@
|
||||
//! Basic Kubernetes operations example
|
||||
//!
|
||||
//! This script demonstrates basic Kubernetes operations using the SAL Kubernetes module.
|
||||
//!
|
||||
//! Prerequisites:
|
||||
//! - A running Kubernetes cluster
|
||||
//! - Valid kubeconfig file or in-cluster configuration
|
||||
//! - Appropriate permissions for the operations
|
||||
//!
|
||||
//! Usage:
|
||||
//! herodo examples/kubernetes/basic_operations.rhai
|
||||
|
||||
print("=== SAL Kubernetes Basic Operations Example ===");
|
||||
|
||||
// Create a KubernetesManager for the default namespace
|
||||
print("Creating KubernetesManager for 'default' namespace...");
|
||||
let km = kubernetes_manager_new("default");
|
||||
print("✓ KubernetesManager created for namespace: " + namespace(km));
|
||||
|
||||
// List all pods in the namespace
|
||||
print("\n--- Listing Pods ---");
|
||||
let pods = pods_list(km);
|
||||
print("Found " + pods.len() + " pods in the namespace:");
|
||||
for pod in pods {
|
||||
print(" - " + pod);
|
||||
}
|
||||
|
||||
// List all services in the namespace
|
||||
print("\n--- Listing Services ---");
|
||||
let services = services_list(km);
|
||||
print("Found " + services.len() + " services in the namespace:");
|
||||
for service in services {
|
||||
print(" - " + service);
|
||||
}
|
||||
|
||||
// List all deployments in the namespace
|
||||
print("\n--- Listing Deployments ---");
|
||||
let deployments = deployments_list(km);
|
||||
print("Found " + deployments.len() + " deployments in the namespace:");
|
||||
for deployment in deployments {
|
||||
print(" - " + deployment);
|
||||
}
|
||||
|
||||
// Get resource counts
|
||||
print("\n--- Resource Counts ---");
|
||||
let counts = resource_counts(km);
|
||||
print("Resource counts in namespace '" + namespace(km) + "':");
|
||||
for resource_type in counts.keys() {
|
||||
print(" " + resource_type + ": " + counts[resource_type]);
|
||||
}
|
||||
|
||||
// List all namespaces (cluster-wide operation)
|
||||
print("\n--- Listing All Namespaces ---");
|
||||
let namespaces = namespaces_list(km);
|
||||
print("Found " + namespaces.len() + " namespaces in the cluster:");
|
||||
for ns in namespaces {
|
||||
print(" - " + ns);
|
||||
}
|
||||
|
||||
// Check if specific namespaces exist
|
||||
print("\n--- Checking Namespace Existence ---");
|
||||
let test_namespaces = ["default", "kube-system", "non-existent-namespace"];
|
||||
for ns in test_namespaces {
|
||||
let exists = namespace_exists(km, ns);
|
||||
if exists {
|
||||
print("✓ Namespace '" + ns + "' exists");
|
||||
} else {
|
||||
print("✗ Namespace '" + ns + "' does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
print("\n=== Example completed successfully! ===");
|
@@ -1,208 +0,0 @@
|
||||
//! Multi-namespace Kubernetes operations example
|
||||
//!
|
||||
//! This script demonstrates working with multiple namespaces and comparing resources across them.
|
||||
//!
|
||||
//! Prerequisites:
|
||||
//! - A running Kubernetes cluster
|
||||
//! - Valid kubeconfig file or in-cluster configuration
|
||||
//! - Appropriate permissions for the operations
|
||||
//!
|
||||
//! Usage:
|
||||
//! herodo examples/kubernetes/multi_namespace_operations.rhai
|
||||
|
||||
print("=== SAL Kubernetes Multi-Namespace Operations Example ===");
|
||||
|
||||
// Define namespaces to work with
|
||||
let target_namespaces = ["default", "kube-system"];
|
||||
let managers = #{};
|
||||
|
||||
print("Creating managers for multiple namespaces...");
|
||||
|
||||
// Create managers for each namespace
|
||||
for ns in target_namespaces {
|
||||
try {
|
||||
let km = kubernetes_manager_new(ns);
|
||||
managers[ns] = km;
|
||||
print("✓ Created manager for namespace: " + ns);
|
||||
} catch(e) {
|
||||
print("✗ Failed to create manager for " + ns + ": " + e);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to safely get resource counts
|
||||
fn get_safe_counts(km) {
|
||||
try {
|
||||
return resource_counts(km);
|
||||
} catch(e) {
|
||||
print(" Warning: Could not get resource counts - " + e);
|
||||
return #{};
|
||||
}
|
||||
}
|
||||
|
||||
// Function to safely get pod list
|
||||
fn get_safe_pods(km) {
|
||||
try {
|
||||
return pods_list(km);
|
||||
} catch(e) {
|
||||
print(" Warning: Could not list pods - " + e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Compare resource counts across namespaces
|
||||
print("\n--- Resource Comparison Across Namespaces ---");
|
||||
let total_resources = #{};
|
||||
|
||||
for ns in target_namespaces {
|
||||
if ns in managers {
|
||||
let km = managers[ns];
|
||||
print("\nNamespace: " + ns);
|
||||
let counts = get_safe_counts(km);
|
||||
|
||||
for resource_type in counts.keys() {
|
||||
let count = counts[resource_type];
|
||||
print(" " + resource_type + ": " + 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("\n--- Total Resources Across All Namespaces ---");
|
||||
for resource_type in total_resources.keys() {
|
||||
print("Total " + resource_type + ": " + total_resources[resource_type]);
|
||||
}
|
||||
|
||||
// Find namespaces with the most resources
|
||||
print("\n--- Namespace Resource Analysis ---");
|
||||
let namespace_totals = #{};
|
||||
|
||||
for ns in target_namespaces {
|
||||
if ns in managers {
|
||||
let km = managers[ns];
|
||||
let counts = get_safe_counts(km);
|
||||
let total = 0;
|
||||
|
||||
for resource_type in counts.keys() {
|
||||
total = total + counts[resource_type];
|
||||
}
|
||||
|
||||
namespace_totals[ns] = total;
|
||||
print("Namespace '" + ns + "' has " + total + " total resources");
|
||||
}
|
||||
}
|
||||
|
||||
// Find the busiest namespace
|
||||
let busiest_ns = "";
|
||||
let max_resources = 0;
|
||||
for ns in namespace_totals.keys() {
|
||||
if namespace_totals[ns] > max_resources {
|
||||
max_resources = namespace_totals[ns];
|
||||
busiest_ns = ns;
|
||||
}
|
||||
}
|
||||
|
||||
if busiest_ns != "" {
|
||||
print("🏆 Busiest namespace: '" + busiest_ns + "' with " + max_resources + " resources");
|
||||
}
|
||||
|
||||
// Detailed pod analysis
|
||||
print("\n--- Pod Analysis Across Namespaces ---");
|
||||
let all_pods = [];
|
||||
|
||||
for ns in target_namespaces {
|
||||
if ns in managers {
|
||||
let km = managers[ns];
|
||||
let pods = get_safe_pods(km);
|
||||
|
||||
print("\nNamespace '" + ns + "' pods:");
|
||||
if pods.len() == 0 {
|
||||
print(" (no pods)");
|
||||
} else {
|
||||
for pod in pods {
|
||||
print(" - " + pod);
|
||||
all_pods.push(ns + "/" + pod);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print("\n--- All Pods Summary ---");
|
||||
print("Total pods across all namespaces: " + all_pods.len());
|
||||
|
||||
// Look for common pod name patterns
|
||||
print("\n--- Pod Name Pattern Analysis ---");
|
||||
let patterns = #{
|
||||
"system": 0,
|
||||
"kube": 0,
|
||||
"coredns": 0,
|
||||
"proxy": 0,
|
||||
"controller": 0
|
||||
};
|
||||
|
||||
for pod_full_name in all_pods {
|
||||
let pod_name = pod_full_name.to_lower();
|
||||
|
||||
for pattern in patterns.keys() {
|
||||
if pod_name.contains(pattern) {
|
||||
patterns[pattern] = patterns[pattern] + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print("Common pod name patterns found:");
|
||||
for pattern in patterns.keys() {
|
||||
if patterns[pattern] > 0 {
|
||||
print(" '" + pattern + "': " + patterns[pattern] + " pods");
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace health check
|
||||
print("\n--- Namespace Health Check ---");
|
||||
for ns in target_namespaces {
|
||||
if ns in managers {
|
||||
let km = managers[ns];
|
||||
print("\nChecking namespace: " + ns);
|
||||
|
||||
// Check if namespace exists (should always be true for our managers)
|
||||
let exists = namespace_exists(km, ns);
|
||||
if exists {
|
||||
print(" ✓ Namespace exists and is accessible");
|
||||
} else {
|
||||
print(" ✗ Namespace existence check failed");
|
||||
}
|
||||
|
||||
// Try to get resource counts as a health indicator
|
||||
let counts = get_safe_counts(km);
|
||||
if counts.len() > 0 {
|
||||
print(" ✓ Can access resources (" + counts.len() + " resource types)");
|
||||
} else {
|
||||
print(" ⚠ No resources found or access limited");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a summary report
|
||||
print("\n--- Summary Report ---");
|
||||
print("Namespaces analyzed: " + target_namespaces.len());
|
||||
print("Total unique resource types: " + total_resources.len());
|
||||
|
||||
let grand_total = 0;
|
||||
for resource_type in total_resources.keys() {
|
||||
grand_total = grand_total + total_resources[resource_type];
|
||||
}
|
||||
print("Grand total resources: " + grand_total);
|
||||
|
||||
print("\nResource breakdown:");
|
||||
for resource_type in total_resources.keys() {
|
||||
let count = total_resources[resource_type];
|
||||
let percentage = (count * 100) / grand_total;
|
||||
print(" " + resource_type + ": " + count + " (" + percentage + "%)");
|
||||
}
|
||||
|
||||
print("\n=== Multi-namespace operations example completed! ===");
|
@@ -1,95 +0,0 @@
|
||||
//! Kubernetes namespace management example
|
||||
//!
|
||||
//! This script demonstrates namespace creation and management operations.
|
||||
//!
|
||||
//! Prerequisites:
|
||||
//! - A running Kubernetes cluster
|
||||
//! - Valid kubeconfig file or in-cluster configuration
|
||||
//! - Permissions to create and manage namespaces
|
||||
//!
|
||||
//! Usage:
|
||||
//! herodo examples/kubernetes/namespace_management.rhai
|
||||
|
||||
print("=== SAL Kubernetes Namespace Management Example ===");
|
||||
|
||||
// Create a KubernetesManager
|
||||
let km = kubernetes_manager_new("default");
|
||||
print("Created KubernetesManager for namespace: " + namespace(km));
|
||||
|
||||
// Define test namespace names
|
||||
let test_namespaces = [
|
||||
"sal-test-namespace-1",
|
||||
"sal-test-namespace-2",
|
||||
"sal-example-app"
|
||||
];
|
||||
|
||||
print("\n--- Creating Test Namespaces ---");
|
||||
for ns in test_namespaces {
|
||||
print("Creating namespace: " + ns);
|
||||
try {
|
||||
namespace_create(km, ns);
|
||||
print("✓ Successfully created namespace: " + ns);
|
||||
} catch(e) {
|
||||
print("✗ Failed to create namespace " + ns + ": " + e);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait a moment for namespaces to be created
|
||||
print("\nWaiting for namespaces to be ready...");
|
||||
|
||||
// Verify namespaces were created
|
||||
print("\n--- Verifying Namespace Creation ---");
|
||||
for ns in test_namespaces {
|
||||
let exists = namespace_exists(km, ns);
|
||||
if exists {
|
||||
print("✓ Namespace '" + ns + "' exists");
|
||||
} else {
|
||||
print("✗ Namespace '" + ns + "' was not found");
|
||||
}
|
||||
}
|
||||
|
||||
// List all namespaces to see our new ones
|
||||
print("\n--- Current Namespaces ---");
|
||||
let all_namespaces = namespaces_list(km);
|
||||
print("Total namespaces in cluster: " + all_namespaces.len());
|
||||
for ns in all_namespaces {
|
||||
if ns.starts_with("sal-") {
|
||||
print(" 🔹 " + ns + " (created by this example)");
|
||||
} else {
|
||||
print(" - " + ns);
|
||||
}
|
||||
}
|
||||
|
||||
// Test idempotent creation (creating the same namespace again)
|
||||
print("\n--- Testing Idempotent Creation ---");
|
||||
let test_ns = test_namespaces[0];
|
||||
print("Attempting to create existing namespace: " + test_ns);
|
||||
try {
|
||||
namespace_create(km, test_ns);
|
||||
print("✓ Idempotent creation successful (no error for existing namespace)");
|
||||
} catch(e) {
|
||||
print("✗ Unexpected error during idempotent creation: " + e);
|
||||
}
|
||||
|
||||
// Create managers for the new namespaces and check their properties
|
||||
print("\n--- Creating Managers for New Namespaces ---");
|
||||
for ns in test_namespaces {
|
||||
try {
|
||||
let ns_km = kubernetes_manager_new(ns);
|
||||
print("✓ Created manager for namespace: " + namespace(ns_km));
|
||||
|
||||
// Get resource counts for the new namespace (should be mostly empty)
|
||||
let counts = resource_counts(ns_km);
|
||||
print(" Resource counts: " + counts);
|
||||
} catch(e) {
|
||||
print("✗ Failed to create manager for " + ns + ": " + e);
|
||||
}
|
||||
}
|
||||
|
||||
print("\n--- Cleanup Instructions ---");
|
||||
print("To clean up the test namespaces created by this example, run:");
|
||||
for ns in test_namespaces {
|
||||
print(" kubectl delete namespace " + ns);
|
||||
}
|
||||
|
||||
print("\n=== Namespace management example completed! ===");
|
@@ -1,157 +0,0 @@
|
||||
//! Kubernetes pattern-based deletion example
|
||||
//!
|
||||
//! This script demonstrates how to use PCRE patterns to delete multiple resources.
|
||||
//!
|
||||
//! ⚠️ WARNING: This example includes actual deletion operations!
|
||||
//! ⚠️ Only run this in a test environment!
|
||||
//!
|
||||
//! Prerequisites:
|
||||
//! - A running Kubernetes cluster (preferably a test cluster)
|
||||
//! - Valid kubeconfig file or in-cluster configuration
|
||||
//! - Permissions to delete resources
|
||||
//!
|
||||
//! Usage:
|
||||
//! herodo examples/kubernetes/pattern_deletion.rhai
|
||||
|
||||
print("=== SAL Kubernetes Pattern Deletion Example ===");
|
||||
print("⚠️ WARNING: This example will delete resources matching patterns!");
|
||||
print("⚠️ Only run this in a test environment!");
|
||||
|
||||
// Create a KubernetesManager for a test namespace
|
||||
let test_namespace = "sal-pattern-test";
|
||||
let km = kubernetes_manager_new("default");
|
||||
|
||||
print("\nCreating test namespace: " + test_namespace);
|
||||
try {
|
||||
namespace_create(km, test_namespace);
|
||||
print("✓ Test namespace created");
|
||||
} catch(e) {
|
||||
print("Note: " + e);
|
||||
}
|
||||
|
||||
// Switch to the test namespace
|
||||
let test_km = kubernetes_manager_new(test_namespace);
|
||||
print("Switched to namespace: " + namespace(test_km));
|
||||
|
||||
// Show current resources before any operations
|
||||
print("\n--- Current Resources in Test Namespace ---");
|
||||
let counts = resource_counts(test_km);
|
||||
print("Resource counts before operations:");
|
||||
for resource_type in counts.keys() {
|
||||
print(" " + resource_type + ": " + counts[resource_type]);
|
||||
}
|
||||
|
||||
// List current pods to see what we're working with
|
||||
let current_pods = pods_list(test_km);
|
||||
print("\nCurrent pods in namespace:");
|
||||
if current_pods.len() == 0 {
|
||||
print(" (no pods found)");
|
||||
} else {
|
||||
for pod in current_pods {
|
||||
print(" - " + pod);
|
||||
}
|
||||
}
|
||||
|
||||
// Demonstrate pattern matching without deletion first
|
||||
print("\n--- Pattern Matching Demo (Dry Run) ---");
|
||||
let test_patterns = [
|
||||
"test-.*", // Match anything starting with "test-"
|
||||
".*-temp$", // Match anything ending with "-temp"
|
||||
"demo-pod-.*", // Match demo pods
|
||||
"nginx-.*", // Match nginx pods
|
||||
"app-[0-9]+", // Match app-1, app-2, etc.
|
||||
];
|
||||
|
||||
for pattern in test_patterns {
|
||||
print("Testing pattern: '" + pattern + "'");
|
||||
|
||||
// Check which pods would match this pattern
|
||||
let matching_pods = [];
|
||||
for pod in current_pods {
|
||||
// Simple pattern matching simulation (Rhai doesn't have regex, so this is illustrative)
|
||||
if pod.contains("test") && pattern == "test-.*" {
|
||||
matching_pods.push(pod);
|
||||
} else if pod.contains("temp") && pattern == ".*-temp$" {
|
||||
matching_pods.push(pod);
|
||||
} else if pod.contains("demo") && pattern == "demo-pod-.*" {
|
||||
matching_pods.push(pod);
|
||||
} else if pod.contains("nginx") && pattern == "nginx-.*" {
|
||||
matching_pods.push(pod);
|
||||
}
|
||||
}
|
||||
|
||||
print(" Would match " + matching_pods.len() + " pods: " + matching_pods);
|
||||
}
|
||||
|
||||
// Example of safe deletion patterns
|
||||
print("\n--- Safe Deletion Examples ---");
|
||||
print("These patterns are designed to be safe for testing:");
|
||||
|
||||
let safe_patterns = [
|
||||
"test-example-.*", // Very specific test resources
|
||||
"sal-demo-.*", // SAL demo resources
|
||||
"temp-resource-.*", // Temporary resources
|
||||
];
|
||||
|
||||
for pattern in safe_patterns {
|
||||
print("\nTesting safe pattern: '" + pattern + "'");
|
||||
|
||||
try {
|
||||
// This will actually attempt deletion, but should be safe in a test environment
|
||||
let deleted_count = delete(test_km, pattern);
|
||||
print("✓ Pattern '" + pattern + "' matched and deleted " + deleted_count + " resources");
|
||||
} catch(e) {
|
||||
print("Note: Pattern '" + pattern + "' - " + e);
|
||||
}
|
||||
}
|
||||
|
||||
// Show resources after deletion attempts
|
||||
print("\n--- Resources After Deletion Attempts ---");
|
||||
let final_counts = resource_counts(test_km);
|
||||
print("Final resource counts:");
|
||||
for resource_type in final_counts.keys() {
|
||||
print(" " + resource_type + ": " + final_counts[resource_type]);
|
||||
}
|
||||
|
||||
// Example of individual resource deletion
|
||||
print("\n--- Individual Resource Deletion Examples ---");
|
||||
print("These functions delete specific resources by name:");
|
||||
|
||||
// These are examples - they will fail if the resources don't exist, which is expected
|
||||
let example_deletions = [
|
||||
["pod", "test-pod-example"],
|
||||
["service", "test-service-example"],
|
||||
["deployment", "test-deployment-example"],
|
||||
];
|
||||
|
||||
for deletion in example_deletions {
|
||||
let resource_type = deletion[0];
|
||||
let resource_name = deletion[1];
|
||||
|
||||
print("Attempting to delete " + resource_type + ": " + resource_name);
|
||||
try {
|
||||
if resource_type == "pod" {
|
||||
pod_delete(test_km, resource_name);
|
||||
} else if resource_type == "service" {
|
||||
service_delete(test_km, resource_name);
|
||||
} else if resource_type == "deployment" {
|
||||
deployment_delete(test_km, resource_name);
|
||||
}
|
||||
print("✓ Successfully deleted " + resource_type + ": " + resource_name);
|
||||
} catch(e) {
|
||||
print("Note: " + resource_type + " '" + resource_name + "' - " + e);
|
||||
}
|
||||
}
|
||||
|
||||
print("\n--- Best Practices for Pattern Deletion ---");
|
||||
print("1. Always test patterns in a safe environment first");
|
||||
print("2. Use specific patterns rather than broad ones");
|
||||
print("3. Consider using dry-run approaches when possible");
|
||||
print("4. Have backups or be able to recreate resources");
|
||||
print("5. Use descriptive naming conventions for easier pattern matching");
|
||||
|
||||
print("\n--- Cleanup ---");
|
||||
print("To clean up the test namespace:");
|
||||
print(" kubectl delete namespace " + test_namespace);
|
||||
|
||||
print("\n=== Pattern deletion example completed! ===");
|
@@ -1,33 +0,0 @@
|
||||
//! Test Kubernetes module registration
|
||||
//!
|
||||
//! This script tests that the Kubernetes module is properly registered
|
||||
//! and available in the Rhai environment.
|
||||
|
||||
print("=== Testing Kubernetes Module Registration ===");
|
||||
|
||||
// Test that we can reference the kubernetes functions
|
||||
print("Testing function registration...");
|
||||
|
||||
// These should not error even if we can't connect to a cluster
|
||||
let functions_to_test = [
|
||||
"kubernetes_manager_new",
|
||||
"pods_list",
|
||||
"services_list",
|
||||
"deployments_list",
|
||||
"delete",
|
||||
"namespace_create",
|
||||
"namespace_exists",
|
||||
"resource_counts",
|
||||
"pod_delete",
|
||||
"service_delete",
|
||||
"deployment_delete",
|
||||
"namespace"
|
||||
];
|
||||
|
||||
for func_name in functions_to_test {
|
||||
print("✓ Function '" + func_name + "' is available");
|
||||
}
|
||||
|
||||
print("\n=== All Kubernetes functions are properly registered! ===");
|
||||
print("Note: To test actual functionality, you need a running Kubernetes cluster.");
|
||||
print("See other examples in this directory for real cluster operations.");
|
@@ -1,18 +1,9 @@
|
||||
# SAL Git Package (`sal-git`)
|
||||
# SAL `git` Module
|
||||
|
||||
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.
|
||||
The `git` module in SAL 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:
|
||||
|
@@ -18,8 +18,8 @@ path = "src/main.rs"
|
||||
env_logger = { workspace = true }
|
||||
rhai = { workspace = true }
|
||||
|
||||
# SAL library for Rhai module registration (with all features for herodo)
|
||||
sal = { path = "..", features = ["all"] }
|
||||
# SAL library for Rhai module registration
|
||||
sal = { path = ".." }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
@@ -15,32 +15,14 @@ Herodo is a command-line utility that executes Rhai scripts with full access to
|
||||
|
||||
## Installation
|
||||
|
||||
### Build and Install
|
||||
Build the herodo binary:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/PlanetFirst/sal.git
|
||||
cd sal
|
||||
./build_herodo.sh
|
||||
cd herodo
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
This script will:
|
||||
- Build herodo in debug mode
|
||||
- Install it to `~/hero/bin/herodo` (non-root) or `/usr/local/bin/herodo` (root)
|
||||
- Make it available in your PATH
|
||||
|
||||
**Note**: If using the non-root installation, make sure `~/hero/bin` is in your PATH:
|
||||
```bash
|
||||
export PATH="$HOME/hero/bin:$PATH"
|
||||
```
|
||||
|
||||
### Install from crates.io (Coming Soon)
|
||||
|
||||
```bash
|
||||
# This will be available once herodo is published to crates.io
|
||||
cargo install herodo
|
||||
```
|
||||
|
||||
**Note**: `herodo` is not yet published to crates.io due to publishing rate limits. It will be available soon.
|
||||
The executable will be available at `target/release/herodo`.
|
||||
|
||||
## Usage
|
||||
|
||||
|
@@ -1,56 +0,0 @@
|
||||
[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 }
|
||||
|
||||
# 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"]
|
@@ -1,227 +0,0 @@
|
||||
# SAL Kubernetes (`sal-kubernetes`)
|
||||
|
||||
Kubernetes cluster management and operations for the System Abstraction Layer (SAL).
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sal-kubernetes = "0.1.0"
|
||||
```
|
||||
|
||||
## ⚠️ **IMPORTANT SECURITY NOTICE**
|
||||
|
||||
**This package includes destructive operations that can permanently delete Kubernetes resources!**
|
||||
|
||||
- The `delete(pattern)` function uses PCRE regex patterns to bulk delete resources
|
||||
- **Always test patterns in a safe environment first**
|
||||
- Use specific patterns to avoid accidental deletion of critical resources
|
||||
- Consider the impact on dependent resources before deletion
|
||||
- **No confirmation prompts** - deletions are immediate and irreversible
|
||||
|
||||
## Overview
|
||||
|
||||
This package provides a high-level interface for managing Kubernetes clusters using the `kube-rs` SDK. It focuses on namespace-scoped operations through the `KubernetesManager` factory pattern.
|
||||
|
||||
### Production Safety Features
|
||||
|
||||
- **Configurable Timeouts**: All operations have configurable timeouts to prevent hanging
|
||||
- **Exponential Backoff Retry**: Automatic retry logic for transient failures
|
||||
- **Rate Limiting**: Built-in rate limiting to prevent API overload
|
||||
- **Comprehensive Error Handling**: Detailed error types and proper error propagation
|
||||
- **Structured Logging**: Production-ready logging for monitoring and debugging
|
||||
|
||||
## Features
|
||||
|
||||
- **Namespace-scoped Management**: Each `KubernetesManager` instance operates on a single namespace
|
||||
- **Pod Management**: List, create, and manage pods
|
||||
- **Pattern-based Deletion**: Delete resources using PCRE pattern matching
|
||||
- **Namespace Operations**: Create and manage namespaces (idempotent operations)
|
||||
- **Resource Management**: Support for pods, services, deployments, configmaps, secrets, and more
|
||||
- **Rhai Integration**: Full scripting support through Rhai wrappers
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Operations
|
||||
|
||||
```rust
|
||||
use sal_kubernetes::KubernetesManager;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a manager for the "default" namespace
|
||||
let km = KubernetesManager::new("default").await?;
|
||||
|
||||
// List all pods in the namespace
|
||||
let pods = km.pods_list().await?;
|
||||
println!("Found {} pods", pods.len());
|
||||
|
||||
// Create a namespace (no error if it already exists)
|
||||
km.namespace_create("my-namespace").await?;
|
||||
|
||||
// Delete resources matching a pattern
|
||||
km.delete("test-.*").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Rhai Scripting
|
||||
|
||||
```javascript
|
||||
// Create Kubernetes manager for namespace
|
||||
let km = kubernetes_manager_new("default");
|
||||
|
||||
// List pods
|
||||
let pods = pods_list(km);
|
||||
print("Found " + pods.len() + " pods");
|
||||
|
||||
// Create namespace
|
||||
namespace_create(km, "my-app");
|
||||
|
||||
// Delete test resources
|
||||
delete(km, "test-.*");
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `kube`: Kubernetes client library
|
||||
- `k8s-openapi`: Kubernetes API types
|
||||
- `tokio`: Async runtime
|
||||
- `regex`: Pattern matching for resource deletion
|
||||
- `rhai`: Scripting integration (optional)
|
||||
|
||||
## Configuration
|
||||
|
||||
### Kubernetes Authentication
|
||||
|
||||
The package uses the standard Kubernetes configuration methods:
|
||||
- In-cluster configuration (when running in a pod)
|
||||
- Kubeconfig file (`~/.kube/config` or `KUBECONFIG` environment variable)
|
||||
- Service account tokens
|
||||
|
||||
### Production Safety Configuration
|
||||
|
||||
```rust
|
||||
use sal_kubernetes::{KubernetesManager, KubernetesConfig};
|
||||
use std::time::Duration;
|
||||
|
||||
// Create with custom configuration
|
||||
let config = KubernetesConfig::new()
|
||||
.with_timeout(Duration::from_secs(60))
|
||||
.with_retries(5, Duration::from_secs(1), Duration::from_secs(30))
|
||||
.with_rate_limit(20, 50);
|
||||
|
||||
let km = KubernetesManager::with_config("my-namespace", config).await?;
|
||||
```
|
||||
|
||||
### Pre-configured Profiles
|
||||
|
||||
```rust
|
||||
// High-throughput environment
|
||||
let config = KubernetesConfig::high_throughput();
|
||||
|
||||
// Low-latency environment
|
||||
let config = KubernetesConfig::low_latency();
|
||||
|
||||
// Development/testing
|
||||
let config = KubernetesConfig::development();
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
All operations return `Result<T, KubernetesError>` with comprehensive error types for different failure scenarios including API errors, configuration issues, and permission problems.
|
||||
|
||||
## API Reference
|
||||
|
||||
### KubernetesManager
|
||||
|
||||
The main interface for Kubernetes operations. Each instance is scoped to a single namespace.
|
||||
|
||||
#### Constructor
|
||||
|
||||
- `KubernetesManager::new(namespace)` - Create a manager for the specified namespace
|
||||
|
||||
#### Resource Listing
|
||||
|
||||
- `pods_list()` - List all pods in the namespace
|
||||
- `services_list()` - List all services in the namespace
|
||||
- `deployments_list()` - List all deployments in the namespace
|
||||
- `configmaps_list()` - List all configmaps in the namespace
|
||||
- `secrets_list()` - List all secrets in the namespace
|
||||
|
||||
#### Resource Management
|
||||
|
||||
- `pod_get(name)` - Get a specific pod by name
|
||||
- `service_get(name)` - Get a specific service by name
|
||||
- `deployment_get(name)` - Get a specific deployment by name
|
||||
- `pod_delete(name)` - Delete a specific pod by name
|
||||
- `service_delete(name)` - Delete a specific service by name
|
||||
- `deployment_delete(name)` - Delete a specific deployment by name
|
||||
|
||||
#### Pattern-based Operations
|
||||
|
||||
- `delete(pattern)` - Delete all resources matching a PCRE pattern
|
||||
|
||||
#### Namespace Operations
|
||||
|
||||
- `namespace_create(name)` - Create a namespace (idempotent)
|
||||
- `namespace_exists(name)` - Check if a namespace exists
|
||||
- `namespaces_list()` - List all namespaces (cluster-wide)
|
||||
|
||||
#### Utility Functions
|
||||
|
||||
- `resource_counts()` - Get counts of all resource types in the namespace
|
||||
- `namespace()` - Get the namespace this manager operates on
|
||||
|
||||
### Rhai Functions
|
||||
|
||||
When using the Rhai integration, the following functions are available:
|
||||
|
||||
- `kubernetes_manager_new(namespace)` - Create a KubernetesManager
|
||||
- `pods_list(km)` - List pods
|
||||
- `services_list(km)` - List services
|
||||
- `deployments_list(km)` - List deployments
|
||||
- `namespaces_list(km)` - List all namespaces
|
||||
- `delete(km, pattern)` - Delete resources matching pattern
|
||||
- `namespace_create(km, name)` - Create namespace
|
||||
- `namespace_exists(km, name)` - Check namespace existence
|
||||
- `resource_counts(km)` - Get resource counts
|
||||
- `pod_delete(km, name)` - Delete specific pod
|
||||
- `service_delete(km, name)` - Delete specific service
|
||||
- `deployment_delete(km, name)` - Delete specific deployment
|
||||
- `namespace(km)` - Get manager's namespace
|
||||
|
||||
## Examples
|
||||
|
||||
The `examples/kubernetes/` directory contains comprehensive examples:
|
||||
|
||||
- `basic_operations.rhai` - Basic listing and counting operations
|
||||
- `namespace_management.rhai` - Creating and managing namespaces
|
||||
- `pattern_deletion.rhai` - Using PCRE patterns for bulk deletion
|
||||
- `multi_namespace_operations.rhai` - Working across multiple namespaces
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests with:
|
||||
|
||||
```bash
|
||||
# Unit tests (no cluster required)
|
||||
cargo test --package sal-kubernetes
|
||||
|
||||
# Integration tests (requires cluster)
|
||||
KUBERNETES_TEST_ENABLED=1 cargo test --package sal-kubernetes
|
||||
|
||||
# Rhai integration tests
|
||||
KUBERNETES_TEST_ENABLED=1 cargo test --package sal-kubernetes --features rhai
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Always use specific PCRE patterns to avoid accidental deletion of important resources
|
||||
- Test deletion patterns in a safe environment first
|
||||
- Ensure proper RBAC permissions are configured
|
||||
- Be cautious with cluster-wide operations like namespace listing
|
||||
- Consider using dry-run approaches when possible
|
@@ -1,113 +0,0 @@
|
||||
//! 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,
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,85 +0,0 @@
|
||||
//! 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
@@ -1,49 +0,0 @@
|
||||
//! 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};
|
@@ -1,555 +0,0 @@
|
||||
//! 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 rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
|
||||
|
||||
/// Helper function to execute async operations with proper runtime handling
|
||||
fn execute_async<F, T>(future: F) -> Result<T, Box<EvalAltResult>>
|
||||
where
|
||||
F: std::future::Future<Output = Result<T, KubernetesError>>,
|
||||
{
|
||||
match tokio::runtime::Handle::try_current() {
|
||||
Ok(handle) => handle
|
||||
.block_on(future)
|
||||
.map_err(kubernetes_error_to_rhai_error),
|
||||
Err(_) => {
|
||||
// No runtime available, create a new one
|
||||
let rt = tokio::runtime::Runtime::new().map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to create Tokio runtime: {}", e).into(),
|
||||
rhai::Position::NONE,
|
||||
))
|
||||
})?;
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
///
|
||||
/// # 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))?;
|
||||
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,
|
||||
) -> 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 deployment =
|
||||
execute_async(km.deployment_create(&name, &image, replicas as i32, labels_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)
|
||||
}
|
||||
|
||||
/// 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("namespaces_list", namespaces_list);
|
||||
|
||||
// Register resource creation methods (object-oriented style)
|
||||
engine.register_fn("create_pod", pod_create);
|
||||
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);
|
||||
|
||||
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,
|
||||
))
|
||||
}
|
@@ -1,174 +0,0 @@
|
||||
//! 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())).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)).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).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");
|
||||
}
|
||||
}
|
@@ -1,385 +0,0 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,231 +0,0 @@
|
||||
//! 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");
|
||||
}
|
||||
}
|
@@ -1,62 +0,0 @@
|
||||
//! 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");
|
||||
}
|
@@ -1,200 +0,0 @@
|
||||
//! 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");
|
@@ -1,85 +0,0 @@
|
||||
//! 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! ===");
|
@@ -1,137 +0,0 @@
|
||||
//! 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! ===");
|
@@ -1,86 +0,0 @@
|
||||
//! 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"
|
||||
];
|
||||
|
||||
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"
|
||||
];
|
||||
|
||||
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());
|
||||
print("Basic validation: " + (passed_tests > 0 ? "PASSED" : "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 ===");
|
@@ -1,90 +0,0 @@
|
||||
//! 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');");
|
@@ -1,368 +0,0 @@
|
||||
//! 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_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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async 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
|
||||
let script = r#"
|
||||
let km = kubernetes_manager_new("invalid-namespace-name-that-should-fail");
|
||||
pods_list(km)
|
||||
"#;
|
||||
|
||||
let result = engine.eval::<rhai::Array>(script);
|
||||
assert!(result.is_err(), "Expected error for invalid configuration");
|
||||
|
||||
if let Err(e) = result {
|
||||
let error_msg = e.to_string();
|
||||
println!("Got expected error: {}", error_msg);
|
||||
assert!(error_msg.contains("Kubernetes error") || error_msg.contains("error"));
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,303 +0,0 @@
|
||||
//! 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"
|
||||
);
|
||||
}
|
@@ -1,16 +1,7 @@
|
||||
# SAL Mycelium (`sal-mycelium`)
|
||||
# SAL Mycelium
|
||||
|
||||
A Rust client library for interacting with Mycelium node's HTTP API, with Rhai scripting support.
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sal-mycelium = "0.1.0"
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
SAL Mycelium provides async HTTP client functionality for managing Mycelium nodes, including:
|
||||
|
@@ -1,16 +1,7 @@
|
||||
# SAL Network Package (`sal-net`)
|
||||
# SAL Network Package
|
||||
|
||||
Network connectivity utilities for TCP, HTTP, and SSH operations.
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sal-net = "0.1.0"
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
The `sal-net` package provides a comprehensive set of network connectivity tools for the SAL (System Abstraction Layer) ecosystem. It includes utilities for TCP port checking, HTTP/HTTPS connectivity testing, and SSH command execution.
|
||||
|
@@ -165,18 +165,9 @@ fn test_mv() {
|
||||
|
||||
#[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 should exist on most systems
|
||||
let result = fs::which("ls");
|
||||
assert!(!result.is_empty());
|
||||
|
||||
// Test with a command that shouldn't exist
|
||||
let result = fs::which("nonexistentcommand12345");
|
||||
|
@@ -1,16 +1,7 @@
|
||||
# SAL PostgreSQL Client (`sal-postgresclient`)
|
||||
# SAL PostgreSQL Client
|
||||
|
||||
The SAL PostgreSQL Client (`sal-postgresclient`) is an independent package that provides a simple and efficient way to interact with PostgreSQL databases in Rust. It offers connection management, query execution, a builder pattern for flexible configuration, and PostgreSQL installer functionality using nerdctl.
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sal-postgresclient = "0.1.0"
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Connection Management**: Automatic connection handling and reconnection
|
||||
|
@@ -17,7 +17,7 @@ Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sal-process = "0.1.0"
|
||||
sal-process = { path = "../process" }
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
@@ -138,12 +138,7 @@ fn test_run_with_environment_variables() {
|
||||
#[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());
|
||||
}
|
||||
@@ -205,16 +200,6 @@ fn test_run_script_with_variables() {
|
||||
|
||||
#[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"
|
||||
@@ -230,14 +215,6 @@ fn test_run_script_with_conditionals() {
|
||||
|
||||
#[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"
|
||||
|
@@ -1,16 +1,7 @@
|
||||
# SAL Redis Client (`sal-redisclient`)
|
||||
# Redis Client Module
|
||||
|
||||
A robust Redis client wrapper for Rust applications that provides connection management, automatic reconnection, and a simple interface for executing Redis commands.
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sal-redisclient = "0.1.0"
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Singleton Pattern**: Maintains a global Redis client instance, so we don't re-int all the time.
|
||||
|
@@ -29,7 +29,6 @@ sal-mycelium = { path = "../mycelium" }
|
||||
sal-text = { path = "../text" }
|
||||
sal-net = { path = "../net" }
|
||||
sal-zinit-client = { path = "../zinit_client" }
|
||||
sal-kubernetes = { path = "../kubernetes" }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
@@ -1,16 +1,7 @@
|
||||
# SAL Rhai - Rhai Integration Module (`sal-rhai`)
|
||||
# SAL Rhai - Rhai Integration Module
|
||||
|
||||
The `sal-rhai` package provides Rhai scripting integration for the SAL (System Abstraction Layer) ecosystem. This package serves as the central integration point that registers all SAL modules with the Rhai scripting engine, enabling powerful automation and scripting capabilities.
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sal-rhai = "0.1.0"
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Module Registration**: Automatically registers all SAL packages with Rhai engine
|
||||
|
@@ -99,10 +99,6 @@ pub use sal_net::rhai::register_net_module;
|
||||
// Re-export crypto module
|
||||
pub use sal_vault::rhai::register_crypto_module;
|
||||
|
||||
// Re-export kubernetes module
|
||||
pub use sal_kubernetes::rhai::register_kubernetes_module;
|
||||
pub use sal_kubernetes::KubernetesManager;
|
||||
|
||||
// Rename copy functions to avoid conflicts
|
||||
pub use sal_os::rhai::copy as os_copy;
|
||||
|
||||
@@ -158,9 +154,6 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
// Register Crypto module functions
|
||||
register_crypto_module(engine)?;
|
||||
|
||||
// Register Kubernetes module functions
|
||||
register_kubernetes_module(engine)?;
|
||||
|
||||
// Register Redis client module functions
|
||||
sal_redisclient::rhai::register_redisclient_module(engine)?;
|
||||
|
||||
|
@@ -21,12 +21,8 @@ fn assert_eq(actual, expected, message) {
|
||||
|
||||
// Helper function to check if buildah is available
|
||||
fn is_buildah_available() {
|
||||
try {
|
||||
let result = run("which buildah");
|
||||
return result.success;
|
||||
} catch(err) {
|
||||
return false;
|
||||
}
|
||||
let command = run("which buildah");
|
||||
return command.silent().execute().success;
|
||||
}
|
||||
|
||||
print("=== Testing Buildah Builder Pattern ===");
|
||||
@@ -35,8 +31,7 @@ print("=== Testing Buildah Builder Pattern ===");
|
||||
let buildah_available = is_buildah_available();
|
||||
if !buildah_available {
|
||||
print("Buildah is not available. Skipping Buildah tests.");
|
||||
// Exit gracefully without error
|
||||
return;
|
||||
throw err;
|
||||
}
|
||||
|
||||
print("✓ Buildah is available");
|
||||
@@ -121,7 +116,7 @@ try {
|
||||
// Test committing to an image
|
||||
print("Testing commit()...");
|
||||
let image_name = "rhai_test_image:latest";
|
||||
builder.commit(image_name);
|
||||
builder.commit(image_name, []);
|
||||
print("✓ commit(): Container committed to image successfully");
|
||||
|
||||
// Test removing the container
|
||||
@@ -154,19 +149,21 @@ try {
|
||||
// Clean up in case of error
|
||||
try {
|
||||
// Remove test container if it exists
|
||||
run("buildah rm rhai_test_container");
|
||||
} catch(_) {}
|
||||
let command = run("buildah rm rhai_test_container");
|
||||
command.execute();
|
||||
} catch(err) {}
|
||||
|
||||
try {
|
||||
// Remove test image if it exists
|
||||
run("buildah rmi rhai_test_image:latest");
|
||||
} catch(_) {}
|
||||
let command = run("buildah rmi alpine");
|
||||
command.execute();
|
||||
} catch(err) {}
|
||||
|
||||
try {
|
||||
// Remove test files if they exist
|
||||
delete("test_add_file.txt");
|
||||
delete("test_copy_file.txt");
|
||||
} catch(_) {}
|
||||
} catch(err) {}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
@@ -21,19 +21,25 @@ fn assert_eq(actual, expected, message) {
|
||||
|
||||
// Helper function to check if buildah is available
|
||||
fn is_buildah_available() {
|
||||
try {
|
||||
let result = run("which buildah");
|
||||
return result.success;
|
||||
} catch(err) {
|
||||
return false;
|
||||
}
|
||||
let command = run("which buildah");
|
||||
return command.silent().execute().success;
|
||||
}
|
||||
|
||||
// Helper function to check if an image exists
|
||||
fn image_exists(image_name) {
|
||||
try {
|
||||
let result = run(`buildah images -q ${image_name}`);
|
||||
return result.success && result.stdout.trim() != "";
|
||||
// First, check for the exact image name
|
||||
let command = run(`buildah images -q ${image_name}`);
|
||||
let result = command.execute();
|
||||
if result.success && result.stdout.trim() != "" {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If not found, check for the localhost-prefixed version
|
||||
let prefixed_image_name = `localhost/${image_name}`;
|
||||
let command = run(`buildah images -q ${prefixed_image_name}`);
|
||||
let result_prefixed = command.execute();
|
||||
return result_prefixed.success && result_prefixed.stdout.trim() != "";
|
||||
} catch(err) {
|
||||
return false;
|
||||
}
|
||||
@@ -45,8 +51,7 @@ print("=== Testing Buildah Image Operations ===");
|
||||
let buildah_available = is_buildah_available();
|
||||
if !buildah_available {
|
||||
print("Buildah is not available. Skipping Buildah tests.");
|
||||
// Exit gracefully without error
|
||||
return;
|
||||
throw err;
|
||||
}
|
||||
|
||||
print("✓ Buildah is available");
|
||||
@@ -82,8 +87,10 @@ try {
|
||||
|
||||
// Find our tagged image
|
||||
let found_tag = false;
|
||||
let expected_tag = "rhai_test_tag:latest";
|
||||
for image in images {
|
||||
if image.names.contains("rhai_test_tag:latest") {
|
||||
// The tag might be prefixed with 'localhost/' if no registry is specified.
|
||||
if image.names.contains(expected_tag) || image.names.contains("localhost/" + expected_tag) {
|
||||
found_tag = true;
|
||||
break;
|
||||
}
|
||||
@@ -95,10 +102,11 @@ try {
|
||||
print("Testing build()...");
|
||||
|
||||
// Create a simple Dockerfile
|
||||
let dockerfile_content = `FROM alpine:latest
|
||||
RUN echo "Hello from Dockerfile" > /hello.txt
|
||||
CMD ["cat", "/hello.txt"]
|
||||
`;
|
||||
let dockerfile_content = `
|
||||
FROM alpine:latest
|
||||
RUN echo "Hello from Dockerfile" > /hello.txt
|
||||
CMD ["cat", "/hello.txt"]
|
||||
`;
|
||||
file_write(`${test_dir}/Dockerfile`, dockerfile_content);
|
||||
|
||||
// Build the image
|
||||
@@ -133,18 +141,23 @@ CMD ["cat", "/hello.txt"]
|
||||
// Clean up in case of error
|
||||
try {
|
||||
// Remove test container if it exists
|
||||
run("buildah rm rhai_test_container");
|
||||
} catch(_) {}
|
||||
let command = run("buildah rm rhai_test_container");
|
||||
command.execute();
|
||||
} catch(err) {}
|
||||
|
||||
try {
|
||||
// Remove test images if they exist
|
||||
run("buildah rmi rhai_test_tag:latest");
|
||||
run("buildah rmi rhai_test_build:latest");
|
||||
} catch(_) {}
|
||||
let command = run("buildah rmi rhai_test_tag:latest");
|
||||
command.execute();
|
||||
let command = run("buildah rmi rhai_test_build:latest");
|
||||
command.execute();
|
||||
} catch(err) {}
|
||||
|
||||
try {
|
||||
// Remove test directory if it exists
|
||||
delete(test_dir);
|
||||
print("✓ Cleanup: Test directory removed");
|
||||
} catch (err) {}
|
||||
|
||||
throw err;
|
||||
} finally {
|
||||
// Clean up test directory
|
||||
delete(test_dir);
|
||||
print("✓ Cleanup: Test directory removed");
|
||||
}
|
||||
|
@@ -21,12 +21,8 @@ fn assert_eq(actual, expected, message) {
|
||||
|
||||
// Helper function to check if buildah is available
|
||||
fn is_buildah_available() {
|
||||
try {
|
||||
let result = run("which buildah");
|
||||
return result.success;
|
||||
} catch(err) {
|
||||
return false;
|
||||
}
|
||||
let command = run("which buildah");
|
||||
return command.silent().execute().success;
|
||||
}
|
||||
|
||||
print("=== Testing Buildah Container Operations ===");
|
||||
@@ -35,8 +31,7 @@ print("=== Testing Buildah Container Operations ===");
|
||||
let buildah_available = is_buildah_available();
|
||||
if !buildah_available {
|
||||
print("Buildah is not available. Skipping Buildah tests.");
|
||||
// Exit gracefully without error
|
||||
return;
|
||||
throw err;
|
||||
}
|
||||
|
||||
print("✓ Buildah is available");
|
||||
@@ -59,10 +54,12 @@ try {
|
||||
|
||||
// Test config
|
||||
print("Testing config()...");
|
||||
let config_options = #{
|
||||
"LABEL": "rhai_test=true",
|
||||
"ENV": "TEST_VAR=test_value"
|
||||
};
|
||||
let config_options = [
|
||||
["label", "rhai_test_true"],
|
||||
["env", "TEST_VAR=test_value"],
|
||||
["env", "ANOTHER_VAR=another_value"],
|
||||
["author", "Rhai Test With Spaces"]
|
||||
];
|
||||
builder.config(config_options);
|
||||
print("✓ config(): Container configured successfully");
|
||||
|
||||
@@ -77,9 +74,10 @@ try {
|
||||
print("Testing content operations...");
|
||||
|
||||
// Write content to a file
|
||||
let script_content = `#!/bin/sh
|
||||
echo "Hello from script"
|
||||
`;
|
||||
let script_content = `
|
||||
#!/bin/sh
|
||||
echo "Hello from script"
|
||||
`;
|
||||
builder.write_content(script_content, "/script.sh");
|
||||
|
||||
// Make the script executable
|
||||
@@ -91,14 +89,10 @@ echo "Hello from script"
|
||||
assert_true(script_result.stdout.contains("Hello from script"), "Script output should contain expected text");
|
||||
print("✓ Content operations: Script created and executed successfully");
|
||||
|
||||
// Test commit with config
|
||||
print("Testing commit with config...");
|
||||
let commit_options = #{
|
||||
"author": "Rhai Test",
|
||||
"message": "Test commit"
|
||||
};
|
||||
builder.commit("rhai_test_commit:latest", commit_options);
|
||||
print("✓ commit(): Container committed with config successfully");
|
||||
// Test commit
|
||||
print("Testing commit...");
|
||||
builder.commit("rhai_test_commit:latest", [["q", ""]]);
|
||||
print("✓ commit(): Container committed successfully");
|
||||
|
||||
// Clean up
|
||||
builder.remove();
|
||||
@@ -115,13 +109,15 @@ echo "Hello from script"
|
||||
// Clean up in case of error
|
||||
try {
|
||||
// Remove test container if it exists
|
||||
run("buildah rm rhai_test_container");
|
||||
} catch(_) {}
|
||||
let command = run("buildah rm rhai_test_container");
|
||||
command.execute();
|
||||
} catch(err) {}
|
||||
|
||||
try {
|
||||
// Remove test image if it exists
|
||||
run("buildah rmi rhai_test_commit:latest");
|
||||
} catch(_) {}
|
||||
let command = run("buildah rmi rhai_test_commit:latest");
|
||||
command.execute();
|
||||
} catch(err) {}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
@@ -1,155 +0,0 @@
|
||||
// run_all_tests.rhai
|
||||
// Runs all Buildah module tests
|
||||
|
||||
print("=== Running Buildah Module Tests ===");
|
||||
|
||||
// Custom assert function
|
||||
fn assert_true(condition, message) {
|
||||
if !condition {
|
||||
print(`ASSERTION FAILED: ${message}`);
|
||||
throw message;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if buildah is available
|
||||
fn is_buildah_available() {
|
||||
try {
|
||||
let result = run("which buildah");
|
||||
return result.success;
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Run each test directly
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
let skipped = 0;
|
||||
let total = 0;
|
||||
|
||||
// Check if buildah is available
|
||||
let buildah_available = is_buildah_available();
|
||||
if !buildah_available {
|
||||
print("Buildah is not available. Skipping all Buildah tests.");
|
||||
skipped = 3; // Skip all three tests
|
||||
total = 3;
|
||||
} else {
|
||||
// Test 1: Builder Pattern
|
||||
print("\n--- Running Builder Pattern Tests ---");
|
||||
try {
|
||||
// Create a builder
|
||||
let builder = bah_new("rhai_test_container", "alpine:latest");
|
||||
|
||||
// Test basic properties
|
||||
assert_true(builder.container_id != "", "Container ID should not be empty");
|
||||
assert_true(builder.name == "rhai_test_container", "Container name should match");
|
||||
|
||||
// Run a simple command
|
||||
let result = builder.run("echo 'Hello from container'");
|
||||
assert_true(result.success, "Command should succeed");
|
||||
|
||||
// Clean up
|
||||
builder.remove();
|
||||
|
||||
print("--- Builder Pattern Tests completed successfully ---");
|
||||
passed += 1;
|
||||
} catch(err) {
|
||||
print(`!!! Error in Builder Pattern Tests: ${err}`);
|
||||
failed += 1;
|
||||
|
||||
// Clean up in case of error
|
||||
try {
|
||||
run("buildah rm rhai_test_container");
|
||||
} catch(e) {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
}
|
||||
total += 1;
|
||||
|
||||
// Test 2: Image Operations
|
||||
print("\n--- Running Image Operations Tests ---");
|
||||
try {
|
||||
// Create a temporary directory for testing
|
||||
let test_dir = "rhai_test_buildah";
|
||||
mkdir(test_dir);
|
||||
|
||||
// Create a builder
|
||||
let builder = bah_new("rhai_test_container", "alpine:latest");
|
||||
|
||||
// List images
|
||||
let images = builder.images();
|
||||
assert_true(images.len() > 0, "There should be at least one image");
|
||||
|
||||
// Clean up
|
||||
builder.remove();
|
||||
delete(test_dir);
|
||||
|
||||
print("--- Image Operations Tests completed successfully ---");
|
||||
passed += 1;
|
||||
} catch(err) {
|
||||
print(`!!! Error in Image Operations Tests: ${err}`);
|
||||
failed += 1;
|
||||
|
||||
// Clean up in case of error
|
||||
try {
|
||||
run("buildah rm rhai_test_container");
|
||||
delete("rhai_test_buildah");
|
||||
} catch(e) {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
}
|
||||
total += 1;
|
||||
|
||||
// Test 3: Container Operations
|
||||
print("\n--- Running Container Operations Tests ---");
|
||||
try {
|
||||
// Create a builder
|
||||
let builder = bah_new("rhai_test_container", "alpine:latest");
|
||||
|
||||
// Test reset
|
||||
builder.reset();
|
||||
|
||||
// Create a new container
|
||||
builder = bah_new("rhai_test_container", "alpine:latest");
|
||||
|
||||
// Run a command
|
||||
let result = builder.run("echo 'Hello from container'");
|
||||
assert_true(result.success, "Command should succeed");
|
||||
|
||||
// Clean up
|
||||
builder.remove();
|
||||
|
||||
print("--- Container Operations Tests completed successfully ---");
|
||||
passed += 1;
|
||||
} catch(err) {
|
||||
print(`!!! Error in Container Operations Tests: ${err}`);
|
||||
failed += 1;
|
||||
|
||||
// Clean up in case of error
|
||||
try {
|
||||
run("buildah rm rhai_test_container");
|
||||
} catch(e) {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
}
|
||||
total += 1;
|
||||
}
|
||||
|
||||
print("\n=== Test Summary ===");
|
||||
print(`Passed: ${passed}`);
|
||||
print(`Failed: ${failed}`);
|
||||
print(`Skipped: ${skipped}`);
|
||||
print(`Total: ${total}`);
|
||||
|
||||
if failed == 0 {
|
||||
if skipped > 0 {
|
||||
print("\n⚠️ All tests skipped or passed!");
|
||||
} else {
|
||||
print("\n✅ All tests passed!");
|
||||
}
|
||||
} else {
|
||||
print("\n❌ Some tests failed!");
|
||||
}
|
||||
|
||||
// Return the number of failed tests (0 means success)
|
||||
failed;
|
@@ -1,152 +0,0 @@
|
||||
#!/usr/bin/env rhai
|
||||
|
||||
// Test 1: Namespace Operations
|
||||
// This test covers namespace creation, existence checking, and listing
|
||||
|
||||
// Helper function to generate timestamp for unique names
|
||||
fn timestamp() {
|
||||
let now = 1640995200; // Base timestamp
|
||||
let random = (now % 1000000).to_string();
|
||||
random
|
||||
}
|
||||
|
||||
print("=== Kubernetes Namespace Operations Test ===");
|
||||
print("");
|
||||
|
||||
// Test namespace creation and existence checking
|
||||
print("Test 1: Namespace Creation and Existence");
|
||||
print("----------------------------------------");
|
||||
|
||||
// Create a test namespace
|
||||
let test_namespace = "sal-test-ns-" + timestamp();
|
||||
print("Creating test namespace: " + test_namespace);
|
||||
|
||||
try {
|
||||
let km = kubernetes_manager_new("default");
|
||||
|
||||
// Check if namespace exists before creation
|
||||
let exists_before = km.namespace_exists(test_namespace);
|
||||
print("Namespace exists before creation: " + exists_before);
|
||||
|
||||
if exists_before {
|
||||
print("⚠️ Namespace already exists, this is unexpected");
|
||||
} else {
|
||||
print("✅ Namespace doesn't exist yet (expected)");
|
||||
}
|
||||
|
||||
// Create the namespace
|
||||
print("Creating namespace...");
|
||||
km.create_namespace(test_namespace);
|
||||
print("✅ Namespace created successfully");
|
||||
|
||||
// Check if namespace exists after creation
|
||||
let exists_after = km.namespace_exists(test_namespace);
|
||||
print("Namespace exists after creation: " + exists_after);
|
||||
|
||||
if exists_after {
|
||||
print("✅ Namespace exists after creation (expected)");
|
||||
} else {
|
||||
print("❌ Namespace doesn't exist after creation (unexpected)");
|
||||
throw "Namespace creation verification failed";
|
||||
}
|
||||
|
||||
// Test idempotent creation (should not error)
|
||||
print("Testing idempotent creation...");
|
||||
km.create_namespace(test_namespace);
|
||||
print("✅ Idempotent creation successful");
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Namespace creation test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test namespace listing
|
||||
print("Test 2: Namespace Listing");
|
||||
print("-------------------------");
|
||||
|
||||
try {
|
||||
let km = kubernetes_manager_new("default");
|
||||
|
||||
// List all namespaces
|
||||
let namespaces = km.namespaces_list();
|
||||
print("Found " + namespaces.len() + " namespaces");
|
||||
|
||||
if namespaces.len() == 0 {
|
||||
print("⚠️ No namespaces found, this might indicate a connection issue");
|
||||
} else {
|
||||
print("✅ Successfully retrieved namespace list");
|
||||
|
||||
// Check if our test namespace is in the list
|
||||
let found_test_ns = false;
|
||||
for ns in namespaces {
|
||||
if ns.name == test_namespace {
|
||||
found_test_ns = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if found_test_ns {
|
||||
print("✅ Test namespace found in namespace list");
|
||||
} else {
|
||||
print("⚠️ Test namespace not found in list (might be propagation delay)");
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Namespace listing test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test namespace manager creation
|
||||
print("Test 3: Namespace Manager Creation");
|
||||
print("----------------------------------");
|
||||
|
||||
try {
|
||||
// Create manager for our test namespace
|
||||
let test_km = kubernetes_manager_new(test_namespace);
|
||||
|
||||
// Verify the manager's namespace
|
||||
let manager_namespace = namespace(test_km);
|
||||
print("Manager namespace: " + manager_namespace);
|
||||
|
||||
if manager_namespace == test_namespace {
|
||||
print("✅ Manager created for correct namespace");
|
||||
} else {
|
||||
print("❌ Manager namespace mismatch");
|
||||
throw "Manager namespace verification failed";
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Namespace manager creation test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Cleanup
|
||||
print("Test 4: Namespace Cleanup");
|
||||
print("-------------------------");
|
||||
|
||||
try {
|
||||
let km = kubernetes_manager_new("default");
|
||||
|
||||
// Delete the test namespace
|
||||
print("Deleting test namespace: " + test_namespace);
|
||||
km.delete_namespace(test_namespace);
|
||||
print("✅ Namespace deletion initiated");
|
||||
|
||||
// Note: Namespace deletion is asynchronous, so we don't immediately check existence
|
||||
print("ℹ️ Namespace deletion is asynchronous and may take time to complete");
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Namespace cleanup failed: " + error);
|
||||
// Don't throw here as this is cleanup
|
||||
}
|
||||
|
||||
print("");
|
||||
print("=== Namespace Operations Test Complete ===");
|
||||
print("✅ All namespace operation tests passed");
|
@@ -1,217 +0,0 @@
|
||||
#!/usr/bin/env rhai
|
||||
|
||||
// Test 2: Pod Management Operations
|
||||
// This test covers pod creation, listing, retrieval, and deletion
|
||||
|
||||
// Helper function to generate timestamp for unique names
|
||||
fn timestamp() {
|
||||
let now = 1640995200; // Base timestamp
|
||||
let random = (now % 1000000).to_string();
|
||||
random
|
||||
}
|
||||
|
||||
print("=== Kubernetes Pod Management Test ===");
|
||||
print("");
|
||||
|
||||
// Setup test namespace
|
||||
let test_namespace = "sal-test-pods-" + timestamp();
|
||||
print("Setting up test namespace: " + test_namespace);
|
||||
|
||||
try {
|
||||
let setup_km = kubernetes_manager_new("default");
|
||||
setup_km.create_namespace(test_namespace);
|
||||
print("✅ Test namespace created");
|
||||
} catch (error) {
|
||||
print("❌ Failed to create test namespace: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Create manager for test namespace
|
||||
let km = kubernetes_manager_new(test_namespace);
|
||||
|
||||
print("");
|
||||
|
||||
// Test pod listing (should be empty initially)
|
||||
print("Test 1: Initial Pod Listing");
|
||||
print("---------------------------");
|
||||
|
||||
try {
|
||||
let initial_pods = km.pods_list();
|
||||
print("Initial pod count: " + initial_pods.len());
|
||||
|
||||
if initial_pods.len() == 0 {
|
||||
print("✅ Namespace is empty as expected");
|
||||
} else {
|
||||
print("⚠️ Found " + initial_pods.len() + " existing pods in test namespace");
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Initial pod listing failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test pod creation
|
||||
print("Test 2: Pod Creation");
|
||||
print("-------------------");
|
||||
|
||||
let test_pod_name = "test-pod-" + timestamp();
|
||||
let test_image = "nginx:alpine";
|
||||
let test_labels = #{
|
||||
"app": "test",
|
||||
"environment": "testing",
|
||||
"created-by": "sal-integration-test"
|
||||
};
|
||||
|
||||
try {
|
||||
print("Creating pod: " + test_pod_name);
|
||||
print("Image: " + test_image);
|
||||
print("Labels: " + test_labels);
|
||||
|
||||
let created_pod = km.create_pod(test_pod_name, test_image, test_labels);
|
||||
print("✅ Pod created successfully");
|
||||
|
||||
// Verify pod name
|
||||
if created_pod.name == test_pod_name {
|
||||
print("✅ Pod name matches expected: " + created_pod.name);
|
||||
} else {
|
||||
print("❌ Pod name mismatch. Expected: " + test_pod_name + ", Got: " + created_pod.name);
|
||||
throw "Pod name verification failed";
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Pod creation failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test pod listing after creation
|
||||
print("Test 3: Pod Listing After Creation");
|
||||
print("----------------------------------");
|
||||
|
||||
try {
|
||||
let pods_after_creation = km.pods_list();
|
||||
print("Pod count after creation: " + pods_after_creation.len());
|
||||
|
||||
if pods_after_creation.len() > 0 {
|
||||
print("✅ Pods found after creation");
|
||||
|
||||
// Find our test pod
|
||||
let found_test_pod = false;
|
||||
for pod in pods_after_creation {
|
||||
if pod.name == test_pod_name {
|
||||
found_test_pod = true;
|
||||
print("✅ Test pod found in list: " + pod.name);
|
||||
print(" Status: " + pod.status);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !found_test_pod {
|
||||
print("❌ Test pod not found in pod list");
|
||||
throw "Test pod not found in listing";
|
||||
}
|
||||
|
||||
} else {
|
||||
print("❌ No pods found after creation");
|
||||
throw "Pod listing verification failed";
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Pod listing after creation failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test pod retrieval
|
||||
print("Test 4: Individual Pod Retrieval");
|
||||
print("--------------------------------");
|
||||
|
||||
try {
|
||||
let retrieved_pod = km.get_pod(test_pod_name);
|
||||
print("✅ Pod retrieved successfully");
|
||||
print("Pod name: " + retrieved_pod.name);
|
||||
print("Pod status: " + retrieved_pod.status);
|
||||
|
||||
if retrieved_pod.name == test_pod_name {
|
||||
print("✅ Retrieved pod name matches expected");
|
||||
} else {
|
||||
print("❌ Retrieved pod name mismatch");
|
||||
throw "Pod retrieval verification failed";
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Pod retrieval failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test resource counts
|
||||
print("Test 5: Resource Counts");
|
||||
print("-----------------------");
|
||||
|
||||
try {
|
||||
let counts = km.resource_counts();
|
||||
print("Resource counts: " + counts);
|
||||
|
||||
if counts.pods >= 1 {
|
||||
print("✅ Pod count reflects created pod: " + counts.pods);
|
||||
} else {
|
||||
print("⚠️ Pod count doesn't reflect created pod: " + counts.pods);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Resource counts failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test pod deletion
|
||||
print("Test 6: Pod Deletion");
|
||||
print("--------------------");
|
||||
|
||||
try {
|
||||
print("Deleting pod: " + test_pod_name);
|
||||
km.delete_pod(test_pod_name);
|
||||
print("✅ Pod deletion initiated");
|
||||
|
||||
// Wait a moment for deletion to propagate
|
||||
print("Waiting for deletion to propagate...");
|
||||
|
||||
// Check if pod is gone (may take time)
|
||||
try {
|
||||
let deleted_pod = km.get_pod(test_pod_name);
|
||||
print("⚠️ Pod still exists after deletion (may be terminating): " + deleted_pod.status);
|
||||
} catch (get_error) {
|
||||
print("✅ Pod no longer retrievable (deletion successful)");
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Pod deletion failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Cleanup
|
||||
print("Test 7: Cleanup");
|
||||
print("---------------");
|
||||
|
||||
try {
|
||||
let cleanup_km = kubernetes_manager_new("default");
|
||||
cleanup_km.delete_namespace(test_namespace);
|
||||
print("✅ Test namespace cleanup initiated");
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Cleanup failed: " + error);
|
||||
// Don't throw here as this is cleanup
|
||||
}
|
||||
|
||||
print("");
|
||||
print("=== Pod Management Test Complete ===");
|
||||
print("✅ All pod management tests passed");
|
@@ -1,292 +0,0 @@
|
||||
#!/usr/bin/env rhai
|
||||
|
||||
// Test 3: PCRE Pattern Matching for Bulk Operations
|
||||
// This test covers the powerful pattern-based deletion functionality
|
||||
|
||||
// Helper function to generate timestamp for unique names
|
||||
fn timestamp() {
|
||||
let now = 1640995200; // Base timestamp
|
||||
let random = (now % 1000000).to_string();
|
||||
random
|
||||
}
|
||||
|
||||
print("=== Kubernetes PCRE Pattern Matching Test ===");
|
||||
print("");
|
||||
|
||||
// Setup test namespace
|
||||
let test_namespace = "sal-test-patterns-" + timestamp();
|
||||
print("Setting up test namespace: " + test_namespace);
|
||||
|
||||
try {
|
||||
let setup_km = kubernetes_manager_new("default");
|
||||
setup_km.create_namespace(test_namespace);
|
||||
print("✅ Test namespace created");
|
||||
} catch (error) {
|
||||
print("❌ Failed to create test namespace: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Create manager for test namespace
|
||||
let km = kubernetes_manager_new(test_namespace);
|
||||
|
||||
print("");
|
||||
|
||||
// Create multiple test resources with different naming patterns
|
||||
print("Test 1: Creating Test Resources");
|
||||
print("------------------------------");
|
||||
|
||||
let test_resources = [
|
||||
"test-app-frontend",
|
||||
"test-app-backend",
|
||||
"test-app-database",
|
||||
"prod-app-frontend",
|
||||
"prod-app-backend",
|
||||
"staging-service",
|
||||
"dev-service",
|
||||
"temp-worker-1",
|
||||
"temp-worker-2",
|
||||
"permanent-service"
|
||||
];
|
||||
|
||||
try {
|
||||
print("Creating " + test_resources.len() + " test pods...");
|
||||
|
||||
for resource_name in test_resources {
|
||||
let labels = #{
|
||||
"app": resource_name,
|
||||
"test": "pattern-matching",
|
||||
"created-by": "sal-integration-test"
|
||||
};
|
||||
|
||||
km.create_pod(resource_name, "nginx:alpine", labels);
|
||||
print(" ✅ Created: " + resource_name);
|
||||
}
|
||||
|
||||
print("✅ All test resources created");
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Test resource creation failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Verify all resources exist
|
||||
print("Test 2: Verify Resource Creation");
|
||||
print("--------------------------------");
|
||||
|
||||
try {
|
||||
let all_pods = km.pods_list();
|
||||
print("Total pods created: " + all_pods.len());
|
||||
|
||||
if all_pods.len() >= test_resources.len() {
|
||||
print("✅ Expected number of pods found");
|
||||
} else {
|
||||
print("❌ Missing pods. Expected: " + test_resources.len() + ", Found: " + all_pods.len());
|
||||
throw "Resource verification failed";
|
||||
}
|
||||
|
||||
// List all pod names for verification
|
||||
print("Created pods:");
|
||||
for pod in all_pods {
|
||||
print(" - " + pod.name);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Resource verification failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test pattern matching - delete all "test-app-*" resources
|
||||
print("Test 3: Pattern Deletion - test-app-*");
|
||||
print("--------------------------------------");
|
||||
|
||||
try {
|
||||
let pattern = "test-app-.*";
|
||||
print("Deleting resources matching pattern: " + pattern);
|
||||
|
||||
// Count pods before deletion
|
||||
let pods_before = km.pods_list();
|
||||
let count_before = pods_before.len();
|
||||
print("Pods before deletion: " + count_before);
|
||||
|
||||
// Perform pattern deletion
|
||||
km.delete(pattern);
|
||||
print("✅ Pattern deletion executed");
|
||||
|
||||
// Wait for deletion to propagate
|
||||
print("Waiting for deletion to propagate...");
|
||||
|
||||
// Count pods after deletion
|
||||
let pods_after = km.pods_list();
|
||||
let count_after = pods_after.len();
|
||||
print("Pods after deletion: " + count_after);
|
||||
|
||||
// Should have deleted 3 pods (test-app-frontend, test-app-backend, test-app-database)
|
||||
let expected_deleted = 3;
|
||||
let actual_deleted = count_before - count_after;
|
||||
|
||||
if actual_deleted >= expected_deleted {
|
||||
print("✅ Pattern deletion successful. Deleted " + actual_deleted + " pods");
|
||||
} else {
|
||||
print("⚠️ Pattern deletion may still be propagating. Expected to delete " + expected_deleted + ", deleted " + actual_deleted);
|
||||
}
|
||||
|
||||
// Verify specific pods are gone
|
||||
print("Remaining pods:");
|
||||
for pod in pods_after {
|
||||
print(" - " + pod.name);
|
||||
|
||||
// Check that no test-app-* pods remain
|
||||
if pod.name.starts_with("test-app-") {
|
||||
print("❌ Found test-app pod that should have been deleted: " + pod.name);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Pattern deletion test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test more specific pattern - delete all "temp-*" resources
|
||||
print("Test 4: Pattern Deletion - temp-*");
|
||||
print("----------------------------------");
|
||||
|
||||
try {
|
||||
let pattern = "temp-.*";
|
||||
print("Deleting resources matching pattern: " + pattern);
|
||||
|
||||
// Count pods before deletion
|
||||
let pods_before = km.pods_list();
|
||||
let count_before = pods_before.len();
|
||||
print("Pods before deletion: " + count_before);
|
||||
|
||||
// Perform pattern deletion
|
||||
km.delete(pattern);
|
||||
print("✅ Pattern deletion executed");
|
||||
|
||||
// Wait for deletion to propagate
|
||||
print("Waiting for deletion to propagate...");
|
||||
|
||||
// Count pods after deletion
|
||||
let pods_after = km.pods_list();
|
||||
let count_after = pods_after.len();
|
||||
print("Pods after deletion: " + count_after);
|
||||
|
||||
// Should have deleted 2 pods (temp-worker-1, temp-worker-2)
|
||||
let expected_deleted = 2;
|
||||
let actual_deleted = count_before - count_after;
|
||||
|
||||
if actual_deleted >= expected_deleted {
|
||||
print("✅ Pattern deletion successful. Deleted " + actual_deleted + " pods");
|
||||
} else {
|
||||
print("⚠️ Pattern deletion may still be propagating. Expected to delete " + expected_deleted + ", deleted " + actual_deleted);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Temp pattern deletion test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test complex pattern - delete all "*-service" resources
|
||||
print("Test 5: Pattern Deletion - *-service");
|
||||
print("------------------------------------");
|
||||
|
||||
try {
|
||||
let pattern = ".*-service$";
|
||||
print("Deleting resources matching pattern: " + pattern);
|
||||
|
||||
// Count pods before deletion
|
||||
let pods_before = km.pods_list();
|
||||
let count_before = pods_before.len();
|
||||
print("Pods before deletion: " + count_before);
|
||||
|
||||
// Perform pattern deletion
|
||||
km.delete(pattern);
|
||||
print("✅ Pattern deletion executed");
|
||||
|
||||
// Wait for deletion to propagate
|
||||
print("Waiting for deletion to propagate...");
|
||||
|
||||
// Count pods after deletion
|
||||
let pods_after = km.pods_list();
|
||||
let count_after = pods_after.len();
|
||||
print("Pods after deletion: " + count_after);
|
||||
|
||||
// Should have deleted service pods (staging-service, dev-service, permanent-service)
|
||||
let actual_deleted = count_before - count_after;
|
||||
print("✅ Pattern deletion executed. Deleted " + actual_deleted + " pods");
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Service pattern deletion test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test safety - verify remaining resources
|
||||
print("Test 6: Verify Remaining Resources");
|
||||
print("----------------------------------");
|
||||
|
||||
try {
|
||||
let remaining_pods = km.pods_list();
|
||||
print("Remaining pods: " + remaining_pods.len());
|
||||
|
||||
print("Remaining pod names:");
|
||||
for pod in remaining_pods {
|
||||
print(" - " + pod.name);
|
||||
}
|
||||
|
||||
// Should only have prod-app-* pods remaining
|
||||
let expected_remaining = ["prod-app-frontend", "prod-app-backend"];
|
||||
|
||||
for pod in remaining_pods {
|
||||
let is_expected = false;
|
||||
for expected in expected_remaining {
|
||||
if pod.name == expected {
|
||||
is_expected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if is_expected {
|
||||
print("✅ Expected pod remains: " + pod.name);
|
||||
} else {
|
||||
print("⚠️ Unexpected pod remains: " + pod.name);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Remaining resources verification failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Cleanup
|
||||
print("Test 7: Cleanup");
|
||||
print("---------------");
|
||||
|
||||
try {
|
||||
let cleanup_km = kubernetes_manager_new("default");
|
||||
cleanup_km.delete_namespace(test_namespace);
|
||||
print("✅ Test namespace cleanup initiated");
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Cleanup failed: " + error);
|
||||
// Don't throw here as this is cleanup
|
||||
}
|
||||
|
||||
print("");
|
||||
print("=== PCRE Pattern Matching Test Complete ===");
|
||||
print("✅ All pattern matching tests passed");
|
||||
print("");
|
||||
print("⚠️ IMPORTANT: Pattern deletion is a powerful feature!");
|
||||
print(" Always test patterns in safe environments first.");
|
||||
print(" Use specific patterns to avoid accidental deletions.");
|
@@ -1,307 +0,0 @@
|
||||
#!/usr/bin/env rhai
|
||||
|
||||
// Test 4: Error Handling and Edge Cases
|
||||
// This test covers error scenarios and edge cases
|
||||
|
||||
// Helper function to generate timestamp for unique names
|
||||
fn timestamp() {
|
||||
let now = 1640995200; // Base timestamp
|
||||
let random = (now % 1000000).to_string();
|
||||
random
|
||||
}
|
||||
|
||||
print("=== Kubernetes Error Handling Test ===");
|
||||
print("");
|
||||
|
||||
// Test connection validation
|
||||
print("Test 1: Connection Validation");
|
||||
print("-----------------------------");
|
||||
|
||||
try {
|
||||
// This should work if cluster is available
|
||||
let km = kubernetes_manager_new("default");
|
||||
print("✅ Successfully connected to Kubernetes cluster");
|
||||
|
||||
// Test basic operation to verify connection
|
||||
let namespaces = km.namespaces_list();
|
||||
print("✅ Successfully retrieved " + namespaces.len() + " namespaces");
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Kubernetes connection failed: " + error);
|
||||
print("");
|
||||
print("This test requires a running Kubernetes cluster.");
|
||||
print("Please ensure:");
|
||||
print(" - kubectl is configured");
|
||||
print(" - Cluster is accessible");
|
||||
print(" - Proper RBAC permissions are set");
|
||||
print("");
|
||||
throw "Kubernetes cluster not available";
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test invalid namespace handling
|
||||
print("Test 2: Invalid Namespace Handling");
|
||||
print("----------------------------------");
|
||||
|
||||
try {
|
||||
// Try to create manager for invalid namespace name
|
||||
let invalid_names = [
|
||||
"INVALID-UPPERCASE",
|
||||
"invalid_underscore",
|
||||
"invalid.dot",
|
||||
"invalid space",
|
||||
"invalid@symbol",
|
||||
"123-starts-with-number",
|
||||
"ends-with-dash-",
|
||||
"-starts-with-dash"
|
||||
];
|
||||
|
||||
for invalid_name in invalid_names {
|
||||
try {
|
||||
print("Testing invalid namespace: '" + invalid_name + "'");
|
||||
let km = kubernetes_manager_new(invalid_name);
|
||||
|
||||
// If we get here, the name was accepted (might be valid after all)
|
||||
print(" ⚠️ Name was accepted: " + invalid_name);
|
||||
|
||||
} catch (name_error) {
|
||||
print(" ✅ Properly rejected invalid name: " + invalid_name);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Invalid namespace test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test resource not found errors
|
||||
print("Test 3: Resource Not Found Errors");
|
||||
print("---------------------------------");
|
||||
|
||||
try {
|
||||
let km = kubernetes_manager_new("default");
|
||||
|
||||
// Try to get a pod that doesn't exist
|
||||
let nonexistent_pod = "nonexistent-pod-" + timestamp();
|
||||
|
||||
try {
|
||||
let pod = km.get_pod(nonexistent_pod);
|
||||
print("❌ Expected error for nonexistent pod, but got result: " + pod.name);
|
||||
throw "Should have failed to get nonexistent pod";
|
||||
} catch (not_found_error) {
|
||||
print("✅ Properly handled nonexistent pod error: " + not_found_error);
|
||||
}
|
||||
|
||||
// Try to delete a pod that doesn't exist
|
||||
try {
|
||||
km.delete_pod(nonexistent_pod);
|
||||
print("✅ Delete nonexistent pod handled gracefully");
|
||||
} catch (delete_error) {
|
||||
print("✅ Delete nonexistent pod error handled: " + delete_error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Resource not found test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test invalid resource names
|
||||
print("Test 4: Invalid Resource Names");
|
||||
print("------------------------------");
|
||||
|
||||
try {
|
||||
let km = kubernetes_manager_new("default");
|
||||
|
||||
let invalid_resource_names = [
|
||||
"INVALID-UPPERCASE",
|
||||
"invalid_underscore",
|
||||
"invalid.multiple.dots",
|
||||
"invalid space",
|
||||
"invalid@symbol",
|
||||
"toolong" + "a".repeat(100), // Too long name
|
||||
"", // Empty name
|
||||
"-starts-with-dash",
|
||||
"ends-with-dash-"
|
||||
];
|
||||
|
||||
for invalid_name in invalid_resource_names {
|
||||
try {
|
||||
print("Testing invalid resource name: '" + invalid_name + "'");
|
||||
|
||||
let labels = #{ "test": "invalid-name" };
|
||||
km.create_pod(invalid_name, "nginx:alpine", labels);
|
||||
|
||||
print(" ⚠️ Invalid name was accepted: " + invalid_name);
|
||||
|
||||
// Clean up if it was created
|
||||
try {
|
||||
km.delete_pod(invalid_name);
|
||||
} catch (cleanup_error) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
} catch (name_error) {
|
||||
print(" ✅ Properly rejected invalid resource name: " + invalid_name);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Invalid resource names test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test invalid patterns
|
||||
print("Test 5: Invalid PCRE Patterns");
|
||||
print("------------------------------");
|
||||
|
||||
try {
|
||||
let km = kubernetes_manager_new("default");
|
||||
|
||||
let invalid_patterns = [
|
||||
"[unclosed-bracket",
|
||||
"(?invalid-group",
|
||||
"*invalid-quantifier",
|
||||
"(?P<invalid-named-group>)",
|
||||
"\\invalid-escape"
|
||||
];
|
||||
|
||||
for invalid_pattern in invalid_patterns {
|
||||
try {
|
||||
print("Testing invalid pattern: '" + invalid_pattern + "'");
|
||||
km.delete(invalid_pattern);
|
||||
print(" ⚠️ Invalid pattern was accepted: " + invalid_pattern);
|
||||
|
||||
} catch (pattern_error) {
|
||||
print(" ✅ Properly rejected invalid pattern: " + invalid_pattern);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Invalid patterns test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test permission errors (if applicable)
|
||||
print("Test 6: Permission Handling");
|
||||
print("---------------------------");
|
||||
|
||||
try {
|
||||
let km = kubernetes_manager_new("default");
|
||||
|
||||
// Try to create a namespace (might require cluster-admin)
|
||||
let test_ns = "sal-permission-test-" + timestamp();
|
||||
|
||||
try {
|
||||
km.create_namespace(test_ns);
|
||||
print("✅ Namespace creation successful (sufficient permissions)");
|
||||
|
||||
// Clean up
|
||||
try {
|
||||
km.delete_namespace(test_ns);
|
||||
print("✅ Namespace deletion successful");
|
||||
} catch (delete_error) {
|
||||
print("⚠️ Namespace deletion failed: " + delete_error);
|
||||
}
|
||||
|
||||
} catch (permission_error) {
|
||||
print("⚠️ Namespace creation failed (may be permission issue): " + permission_error);
|
||||
print(" This is expected if running with limited RBAC permissions");
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Permission handling test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test empty operations
|
||||
print("Test 7: Empty Operations");
|
||||
print("------------------------");
|
||||
|
||||
try {
|
||||
// Create a temporary namespace for testing
|
||||
let test_namespace = "sal-empty-test-" + timestamp();
|
||||
let setup_km = kubernetes_manager_new("default");
|
||||
|
||||
try {
|
||||
setup_km.create_namespace(test_namespace);
|
||||
let km = kubernetes_manager_new(test_namespace);
|
||||
|
||||
// Test operations on empty namespace
|
||||
let empty_pods = km.pods_list();
|
||||
print("Empty namespace pod count: " + empty_pods.len());
|
||||
|
||||
if empty_pods.len() == 0 {
|
||||
print("✅ Empty namespace handled correctly");
|
||||
} else {
|
||||
print("⚠️ Expected empty namespace, found " + empty_pods.len() + " pods");
|
||||
}
|
||||
|
||||
// Test pattern deletion on empty namespace
|
||||
km.delete(".*");
|
||||
print("✅ Pattern deletion on empty namespace handled");
|
||||
|
||||
// Test resource counts on empty namespace
|
||||
let counts = km.resource_counts();
|
||||
print("✅ Resource counts on empty namespace: " + counts);
|
||||
|
||||
// Cleanup
|
||||
setup_km.delete_namespace(test_namespace);
|
||||
|
||||
} catch (empty_error) {
|
||||
print("❌ Empty operations test failed: " + empty_error);
|
||||
throw empty_error;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Empty operations setup failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test concurrent operations (basic)
|
||||
print("Test 8: Basic Concurrent Operations");
|
||||
print("-----------------------------------");
|
||||
|
||||
try {
|
||||
let km = kubernetes_manager_new("default");
|
||||
|
||||
// Test multiple rapid operations
|
||||
print("Testing rapid successive operations...");
|
||||
|
||||
for i in range(0, 3) {
|
||||
let namespaces = km.namespaces_list();
|
||||
print(" Iteration " + i + ": " + namespaces.len() + " namespaces");
|
||||
}
|
||||
|
||||
print("✅ Rapid successive operations handled");
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Concurrent operations test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
print("=== Error Handling Test Complete ===");
|
||||
print("✅ All error handling tests completed");
|
||||
print("");
|
||||
print("Summary:");
|
||||
print("- Connection validation: ✅");
|
||||
print("- Invalid namespace handling: ✅");
|
||||
print("- Resource not found errors: ✅");
|
||||
print("- Invalid resource names: ✅");
|
||||
print("- Invalid PCRE patterns: ✅");
|
||||
print("- Permission handling: ✅");
|
||||
print("- Empty operations: ✅");
|
||||
print("- Basic concurrent operations: ✅");
|
@@ -1,323 +0,0 @@
|
||||
#!/usr/bin/env rhai
|
||||
|
||||
// Test 5: Production Safety Features
|
||||
// This test covers timeouts, rate limiting, retry logic, and safety features
|
||||
|
||||
print("=== Kubernetes Production Safety Test ===");
|
||||
print("");
|
||||
|
||||
// Test basic safety features
|
||||
print("Test 1: Basic Safety Features");
|
||||
print("-----------------------------");
|
||||
|
||||
try {
|
||||
let km = kubernetes_manager_new("default");
|
||||
|
||||
// Test that manager creation includes safety features
|
||||
print("✅ KubernetesManager created with safety features");
|
||||
|
||||
// Test basic operations work with safety features
|
||||
let namespaces = km.namespaces_list();
|
||||
print("✅ Operations work with safety features enabled");
|
||||
print(" Found " + namespaces.len() + " namespaces");
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Basic safety features test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test rate limiting behavior
|
||||
print("Test 2: Rate Limiting Behavior");
|
||||
print("------------------------------");
|
||||
|
||||
try {
|
||||
let km = kubernetes_manager_new("default");
|
||||
|
||||
print("Testing rapid API calls to verify rate limiting...");
|
||||
|
||||
let start_time = timestamp();
|
||||
|
||||
// Make multiple rapid calls
|
||||
for i in range(0, 10) {
|
||||
let namespaces = km.namespaces_list();
|
||||
print(" Call " + i + ": " + namespaces.len() + " namespaces");
|
||||
}
|
||||
|
||||
let end_time = timestamp();
|
||||
let duration = end_time - start_time;
|
||||
|
||||
print("✅ Rate limiting test completed");
|
||||
print(" Duration: " + duration + " seconds");
|
||||
|
||||
if duration > 0 {
|
||||
print("✅ Operations took measurable time (rate limiting may be active)");
|
||||
} else {
|
||||
print("⚠️ Operations completed very quickly (rate limiting may not be needed)");
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Rate limiting test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test timeout behavior (simulated)
|
||||
print("Test 3: Timeout Handling");
|
||||
print("------------------------");
|
||||
|
||||
try {
|
||||
let km = kubernetes_manager_new("default");
|
||||
|
||||
print("Testing timeout handling with normal operations...");
|
||||
|
||||
// Test operations that should complete within timeout
|
||||
let start_time = timestamp();
|
||||
|
||||
try {
|
||||
let namespaces = km.namespaces_list();
|
||||
let end_time = timestamp();
|
||||
let duration = end_time - start_time;
|
||||
|
||||
print("✅ Operation completed within timeout");
|
||||
print(" Duration: " + duration + " seconds");
|
||||
|
||||
if duration < 30 {
|
||||
print("✅ Operation completed quickly (good performance)");
|
||||
} else {
|
||||
print("⚠️ Operation took longer than expected: " + duration + " seconds");
|
||||
}
|
||||
|
||||
} catch (timeout_error) {
|
||||
print("❌ Operation timed out: " + timeout_error);
|
||||
print(" This might indicate network issues or cluster problems");
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Timeout handling test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test retry logic (simulated)
|
||||
print("Test 4: Retry Logic");
|
||||
print("-------------------");
|
||||
|
||||
try {
|
||||
let km = kubernetes_manager_new("default");
|
||||
|
||||
print("Testing retry logic with normal operations...");
|
||||
|
||||
// Test operations that should succeed (retry logic is internal)
|
||||
let success_count = 0;
|
||||
let total_attempts = 5;
|
||||
|
||||
for i in range(0, total_attempts) {
|
||||
try {
|
||||
let namespaces = km.namespaces_list();
|
||||
success_count = success_count + 1;
|
||||
print(" Attempt " + i + ": ✅ Success (" + namespaces.len() + " namespaces)");
|
||||
} catch (attempt_error) {
|
||||
print(" Attempt " + i + ": ❌ Failed - " + attempt_error);
|
||||
}
|
||||
}
|
||||
|
||||
print("✅ Retry logic test completed");
|
||||
print(" Success rate: " + success_count + "/" + total_attempts);
|
||||
|
||||
if success_count == total_attempts {
|
||||
print("✅ All operations succeeded (good cluster health)");
|
||||
} else if success_count > 0 {
|
||||
print("⚠️ Some operations failed (retry logic may be helping)");
|
||||
} else {
|
||||
print("❌ All operations failed (cluster may be unavailable)");
|
||||
throw "All retry attempts failed";
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Retry logic test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test resource limits and safety
|
||||
print("Test 5: Resource Limits and Safety");
|
||||
print("----------------------------------");
|
||||
|
||||
try {
|
||||
// Create a test namespace for safety testing
|
||||
let test_namespace = "sal-safety-test-" + timestamp();
|
||||
let setup_km = kubernetes_manager_new("default");
|
||||
|
||||
try {
|
||||
setup_km.create_namespace(test_namespace);
|
||||
let km = kubernetes_manager_new(test_namespace);
|
||||
|
||||
print("Testing resource creation limits...");
|
||||
|
||||
// Create a reasonable number of test resources
|
||||
let max_resources = 5; // Keep it reasonable for testing
|
||||
let created_count = 0;
|
||||
|
||||
for i in range(0, max_resources) {
|
||||
try {
|
||||
let resource_name = "safety-test-" + i;
|
||||
let labels = #{ "test": "safety", "index": i };
|
||||
|
||||
km.create_pod(resource_name, "nginx:alpine", labels);
|
||||
created_count = created_count + 1;
|
||||
print(" ✅ Created resource " + i + ": " + resource_name);
|
||||
|
||||
} catch (create_error) {
|
||||
print(" ❌ Failed to create resource " + i + ": " + create_error);
|
||||
}
|
||||
}
|
||||
|
||||
print("✅ Resource creation safety test completed");
|
||||
print(" Created " + created_count + "/" + max_resources + " resources");
|
||||
|
||||
// Test bulk operations safety
|
||||
print("Testing bulk operations safety...");
|
||||
|
||||
let pods_before = km.pods_list();
|
||||
print(" Pods before bulk operation: " + pods_before.len());
|
||||
|
||||
// Use a safe pattern that only matches our test resources
|
||||
let safe_pattern = "safety-test-.*";
|
||||
km.delete(safe_pattern);
|
||||
print(" ✅ Bulk deletion with safe pattern executed");
|
||||
|
||||
// Cleanup
|
||||
setup_km.delete_namespace(test_namespace);
|
||||
print("✅ Test namespace cleaned up");
|
||||
|
||||
} catch (safety_error) {
|
||||
print("❌ Resource safety test failed: " + safety_error);
|
||||
throw safety_error;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Resource limits and safety test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test logging and monitoring readiness
|
||||
print("Test 6: Logging and Monitoring");
|
||||
print("------------------------------");
|
||||
|
||||
try {
|
||||
let km = kubernetes_manager_new("default");
|
||||
|
||||
print("Testing operations for logging and monitoring...");
|
||||
|
||||
// Perform operations that should generate logs
|
||||
let operations = [
|
||||
"namespaces_list",
|
||||
"resource_counts"
|
||||
];
|
||||
|
||||
for operation in operations {
|
||||
try {
|
||||
if operation == "namespaces_list" {
|
||||
let result = km.namespaces_list();
|
||||
print(" ✅ " + operation + ": " + result.len() + " items");
|
||||
} else if operation == "resource_counts" {
|
||||
let result = km.resource_counts();
|
||||
print(" ✅ " + operation + ": " + result);
|
||||
}
|
||||
} catch (op_error) {
|
||||
print(" ❌ " + operation + " failed: " + op_error);
|
||||
}
|
||||
}
|
||||
|
||||
print("✅ Logging and monitoring test completed");
|
||||
print(" All operations should generate structured logs");
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Logging and monitoring test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test configuration validation
|
||||
print("Test 7: Configuration Validation");
|
||||
print("--------------------------------");
|
||||
|
||||
try {
|
||||
print("Testing configuration validation...");
|
||||
|
||||
// Test that manager creation validates configuration
|
||||
let km = kubernetes_manager_new("default");
|
||||
print("✅ Configuration validation passed");
|
||||
|
||||
// Test that manager has expected namespace
|
||||
let manager_namespace = namespace(km);
|
||||
if manager_namespace == "default" {
|
||||
print("✅ Manager namespace correctly set: " + manager_namespace);
|
||||
} else {
|
||||
print("❌ Manager namespace mismatch: " + manager_namespace);
|
||||
throw "Configuration validation failed";
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Configuration validation test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Test graceful degradation
|
||||
print("Test 8: Graceful Degradation");
|
||||
print("----------------------------");
|
||||
|
||||
try {
|
||||
let km = kubernetes_manager_new("default");
|
||||
|
||||
print("Testing graceful degradation scenarios...");
|
||||
|
||||
// Test operations that might fail gracefully
|
||||
try {
|
||||
// Try to access a namespace that might not exist
|
||||
let test_km = kubernetes_manager_new("nonexistent-namespace-" + timestamp());
|
||||
let pods = test_km.pods_list();
|
||||
print(" ⚠️ Nonexistent namespace operation succeeded: " + pods.len() + " pods");
|
||||
} catch (graceful_error) {
|
||||
print(" ✅ Graceful degradation: " + graceful_error);
|
||||
}
|
||||
|
||||
print("✅ Graceful degradation test completed");
|
||||
|
||||
} catch (error) {
|
||||
print("❌ Graceful degradation test failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
print("");
|
||||
print("=== Production Safety Test Complete ===");
|
||||
print("✅ All production safety tests completed");
|
||||
print("");
|
||||
print("Production Safety Summary:");
|
||||
print("- Basic safety features: ✅");
|
||||
print("- Rate limiting behavior: ✅");
|
||||
print("- Timeout handling: ✅");
|
||||
print("- Retry logic: ✅");
|
||||
print("- Resource limits and safety: ✅");
|
||||
print("- Logging and monitoring: ✅");
|
||||
print("- Configuration validation: ✅");
|
||||
print("- Graceful degradation: ✅");
|
||||
print("");
|
||||
print("🛡️ Production safety features are working correctly!");
|
||||
|
||||
// Helper function to generate timestamp for unique names
|
||||
fn timestamp() {
|
||||
let now = 1640995200; // Base timestamp
|
||||
let random = (now % 1000000).to_string();
|
||||
random
|
||||
}
|
@@ -1,187 +0,0 @@
|
||||
#!/usr/bin/env rhai
|
||||
|
||||
// Kubernetes Integration Tests - Main Test Runner
|
||||
// This script runs all Kubernetes integration tests in sequence
|
||||
|
||||
print("===============================================");
|
||||
print(" SAL Kubernetes Integration Tests");
|
||||
print("===============================================");
|
||||
print("");
|
||||
|
||||
// Helper function to generate timestamp for unique names
|
||||
fn timestamp() {
|
||||
let now = 1640995200; // Base timestamp
|
||||
let random = (now % 1000000).to_string();
|
||||
random
|
||||
}
|
||||
|
||||
// Test configuration
|
||||
let test_files = [
|
||||
"01_namespace_operations.rhai",
|
||||
"02_pod_management.rhai",
|
||||
"03_pcre_pattern_matching.rhai",
|
||||
"04_error_handling.rhai",
|
||||
"05_production_safety.rhai"
|
||||
];
|
||||
|
||||
let total_tests = test_files.len();
|
||||
let passed_tests = 0;
|
||||
let failed_tests = 0;
|
||||
let test_results = [];
|
||||
|
||||
print("🚀 Starting Kubernetes integration tests...");
|
||||
print("Total test files: " + total_tests);
|
||||
print("");
|
||||
|
||||
// Pre-flight checks
|
||||
print("=== Pre-flight Checks ===");
|
||||
|
||||
// Check if Kubernetes cluster is available
|
||||
try {
|
||||
let km = kubernetes_manager_new("default");
|
||||
let namespaces = km.namespaces_list();
|
||||
print("✅ Kubernetes cluster is accessible");
|
||||
print(" Found " + namespaces.len() + " namespaces");
|
||||
|
||||
// Check basic permissions
|
||||
try {
|
||||
let test_ns = "sal-preflight-" + timestamp();
|
||||
km.create_namespace(test_ns);
|
||||
print("✅ Namespace creation permissions available");
|
||||
|
||||
// Clean up
|
||||
km.delete_namespace(test_ns);
|
||||
print("✅ Namespace deletion permissions available");
|
||||
|
||||
} catch (perm_error) {
|
||||
print("⚠️ Limited permissions detected: " + perm_error);
|
||||
print(" Some tests may fail due to RBAC restrictions");
|
||||
}
|
||||
|
||||
} catch (cluster_error) {
|
||||
print("❌ Kubernetes cluster not accessible: " + cluster_error);
|
||||
print("");
|
||||
print("Please ensure:");
|
||||
print(" - Kubernetes cluster is running");
|
||||
print(" - kubectl is configured correctly");
|
||||
print(" - Proper RBAC permissions are set");
|
||||
print(" - Network connectivity to cluster");
|
||||
print("");
|
||||
throw "Pre-flight checks failed";
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Run each test file
|
||||
for i in range(0, test_files.len()) {
|
||||
let test_file = test_files[i];
|
||||
let test_number = i + 1;
|
||||
|
||||
print("=== Test " + test_number + "/" + total_tests + ": " + test_file + " ===");
|
||||
|
||||
let test_start_time = timestamp();
|
||||
|
||||
try {
|
||||
// Note: In a real implementation, we would use eval_file or similar
|
||||
// For now, we'll simulate the test execution
|
||||
print("🔄 Running " + test_file + "...");
|
||||
|
||||
// Simulate test execution based on file name
|
||||
if test_file == "01_namespace_operations.rhai" {
|
||||
print("✅ Namespace operations test completed");
|
||||
} else if test_file == "02_pod_management.rhai" {
|
||||
print("✅ Pod management test completed");
|
||||
} else if test_file == "03_pcre_pattern_matching.rhai" {
|
||||
print("✅ PCRE pattern matching test completed");
|
||||
} else if test_file == "04_error_handling.rhai" {
|
||||
print("✅ Error handling test completed");
|
||||
} else if test_file == "05_production_safety.rhai" {
|
||||
print("✅ Production safety test completed");
|
||||
}
|
||||
|
||||
passed_tests = passed_tests + 1;
|
||||
test_results.push(#{ "file": test_file, "status": "PASSED", "error": "" });
|
||||
|
||||
print("✅ " + test_file + " PASSED");
|
||||
|
||||
} catch (test_error) {
|
||||
failed_tests = failed_tests + 1;
|
||||
test_results.push(#{ "file": test_file, "status": "FAILED", "error": test_error });
|
||||
|
||||
print("❌ " + test_file + " FAILED: " + test_error);
|
||||
}
|
||||
|
||||
let test_end_time = timestamp();
|
||||
print(" Duration: " + (test_end_time - test_start_time) + " seconds");
|
||||
print("");
|
||||
}
|
||||
|
||||
// Print summary
|
||||
print("===============================================");
|
||||
print(" Test Summary");
|
||||
print("===============================================");
|
||||
print("");
|
||||
print("Total tests: " + total_tests);
|
||||
print("Passed: " + passed_tests);
|
||||
print("Failed: " + failed_tests);
|
||||
print("Success rate: " + ((passed_tests * 100) / total_tests) + "%");
|
||||
print("");
|
||||
|
||||
// Print detailed results
|
||||
print("Detailed Results:");
|
||||
print("-----------------");
|
||||
for result in test_results {
|
||||
let status_icon = if result.status == "PASSED" { "✅" } else { "❌" };
|
||||
print(status_icon + " " + result.file + " - " + result.status);
|
||||
|
||||
if result.status == "FAILED" && result.error != "" {
|
||||
print(" Error: " + result.error);
|
||||
}
|
||||
}
|
||||
|
||||
print("");
|
||||
|
||||
// Final assessment
|
||||
if failed_tests == 0 {
|
||||
print("🎉 ALL TESTS PASSED!");
|
||||
print("✅ Kubernetes module is ready for production use");
|
||||
print("");
|
||||
print("Key features verified:");
|
||||
print(" ✅ Namespace operations");
|
||||
print(" ✅ Pod management");
|
||||
print(" ✅ PCRE pattern matching");
|
||||
print(" ✅ Error handling");
|
||||
print(" ✅ Production safety features");
|
||||
|
||||
} else if passed_tests > failed_tests {
|
||||
print("⚠️ MOSTLY SUCCESSFUL");
|
||||
print("Most tests passed, but some issues were found.");
|
||||
print("Review failed tests before production deployment.");
|
||||
|
||||
} else {
|
||||
print("❌ SIGNIFICANT ISSUES FOUND");
|
||||
print("Multiple tests failed. Review and fix issues before proceeding.");
|
||||
throw "Integration tests failed";
|
||||
}
|
||||
|
||||
print("");
|
||||
print("===============================================");
|
||||
print(" Kubernetes Integration Tests Complete");
|
||||
print("===============================================");
|
||||
|
||||
// Additional notes
|
||||
print("");
|
||||
print("📝 Notes:");
|
||||
print(" - These tests require a running Kubernetes cluster");
|
||||
print(" - Some tests create and delete resources");
|
||||
print(" - Pattern deletion tests demonstrate powerful bulk operations");
|
||||
print(" - All test resources are cleaned up automatically");
|
||||
print(" - Tests are designed to be safe and non-destructive");
|
||||
print("");
|
||||
print("🔒 Security Reminders:");
|
||||
print(" - Pattern deletion is powerful - always test patterns first");
|
||||
print(" - Use specific patterns to avoid accidental deletions");
|
||||
print(" - Review RBAC permissions for production use");
|
||||
print(" - Monitor resource usage and API rate limits");
|
||||
print("");
|
||||
print("🚀 Ready for production deployment!");
|
@@ -21,22 +21,31 @@ fn assert_eq(actual, expected, message) {
|
||||
|
||||
// Helper function to check if nerdctl is available
|
||||
fn is_nerdctl_available() {
|
||||
try {
|
||||
let result = run("which nerdctl");
|
||||
return result.success;
|
||||
} catch(err) {
|
||||
return false;
|
||||
}
|
||||
let command = run("which nerdctl");
|
||||
return command.silent().execute().success;
|
||||
}
|
||||
|
||||
// Helper function to check if a container exists
|
||||
fn container_exists(container_name) {
|
||||
try {
|
||||
let result = run(`nerdctl ps -a --format "{{.Names}}" | grep -w ${container_name}`);
|
||||
return result.success;
|
||||
} catch(err) {
|
||||
// let command = run(`nerdctl ps -a --format "{{.Names}}" | grep -w ${container_name}`);
|
||||
let command = run(`nerdctl ps -a --format "{{.Names}}"`);
|
||||
let result = command.silent().execute();
|
||||
|
||||
// Check if the command was successful
|
||||
if !result.success {
|
||||
print(`Error executing 'nerdctl ps': ${result.stderr}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Split the output into individual lines (names)
|
||||
// and check if any of them is an exact match for our container name.
|
||||
for line in result.stdout.split('\n') {
|
||||
if line.trim() == container_name {
|
||||
return true; // Found the container
|
||||
}
|
||||
}
|
||||
|
||||
return false; // Did not find the container
|
||||
}
|
||||
|
||||
// Helper function to clean up a container if it exists
|
||||
@@ -49,6 +58,8 @@ fn cleanup_container(container_name) {
|
||||
} catch(err) {
|
||||
print(`Error cleaning up container ${container_name}: ${err}`);
|
||||
}
|
||||
} else {
|
||||
print!(`No container with name ${container_name} found. Nothing to clean up.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +69,7 @@ print("=== Testing Nerdctl Container Operations ===");
|
||||
let nerdctl_available = is_nerdctl_available();
|
||||
if !nerdctl_available {
|
||||
print("nerdctl is not available. Skipping Nerdctl tests.");
|
||||
// Exit gracefully without error
|
||||
return;
|
||||
throw err;
|
||||
}
|
||||
|
||||
print("✓ nerdctl is available");
|
||||
@@ -81,84 +91,132 @@ try {
|
||||
assert_eq(container.container_id, "", "Container ID should be empty initially");
|
||||
|
||||
// Test setting container image
|
||||
print("Testing with_image()...");
|
||||
container.with_image("alpine:latest");
|
||||
print("Testing image setter...");
|
||||
container.image = "alpine:latest";
|
||||
assert_eq(container.image, "alpine:latest", "Container image should match");
|
||||
|
||||
// Test setting detach mode
|
||||
print("Testing with_detach()...");
|
||||
container.with_detach(true);
|
||||
assert_true(container.detach, "Container detach mode should be true");
|
||||
// Test setting container config
|
||||
print("Testing config setter...");
|
||||
let config_options = #{"key1": "value1", "key2": "value2"};
|
||||
container.config = config_options;
|
||||
assert_eq(container.config, config_options, "Container config options should match");
|
||||
|
||||
// Test setting environment variables
|
||||
print("Testing with_env()...");
|
||||
container.with_env("TEST_VAR", "test_value");
|
||||
// Test container_id setter and getter
|
||||
print("Testing container_id setter...");
|
||||
container.container_id = "test-id";
|
||||
assert_eq(container.container_id, "test-id", "Container ID should be 'test-id'");
|
||||
|
||||
// Test setting multiple environment variables
|
||||
print("Testing with_envs()...");
|
||||
let env_map = #{
|
||||
"VAR1": "value1",
|
||||
"VAR2": "value2"
|
||||
};
|
||||
container.with_envs(env_map);
|
||||
// Test ports setter and getter
|
||||
print("Testing ports setter and getter...");
|
||||
let ports_list = ["1234", "2345"];
|
||||
container.ports = ports_list;
|
||||
assert_eq(container.ports, ports_list, "Container ports should match");
|
||||
|
||||
// Test setting ports
|
||||
print("Testing with_port()...");
|
||||
container.with_port("8080:80");
|
||||
// Test volumes setter and getter
|
||||
print("Testing volumes setter and getter...");
|
||||
let volumes_list = ["/tmp:/tmp"];
|
||||
container.volumes = volumes_list;
|
||||
assert_eq(container.volumes, volumes_list, "Container volumes should match");
|
||||
|
||||
// Test setting multiple ports
|
||||
print("Testing with_ports()...");
|
||||
container.with_ports(["9090:90", "7070:70"]);
|
||||
// Test env_vars setter and getter
|
||||
print("Testing env_vars setter and getter...");
|
||||
let env_vars_map = #{"VAR1": "value1", "VAR2": "value2"};
|
||||
container.env_vars = env_vars_map;
|
||||
assert_eq(container.env_vars, env_vars_map, "Container env_vars should match");
|
||||
|
||||
// Test setting volumes
|
||||
print("Testing with_volume()...");
|
||||
// Create a test directory for volume mounting
|
||||
let test_dir = "rhai_test_nerdctl_volume";
|
||||
mkdir(test_dir);
|
||||
container.with_volume(`${test_dir}:/data`);
|
||||
// Test network setter and getter
|
||||
print("Testing network setter and getter...");
|
||||
container.network = "test-net";
|
||||
assert_eq(container.network, "test-net", "Container network should match");
|
||||
|
||||
// Test setting resource limits
|
||||
print("Testing with_cpu_limit() and with_memory_limit()...");
|
||||
container.with_cpu_limit("0.5");
|
||||
container.with_memory_limit("256m");
|
||||
// Test network_aliases setter and getter
|
||||
print("Testing network_aliases setter and getter...");
|
||||
let aliases = ["alias1", "alias2"];
|
||||
container.network_aliases = aliases;
|
||||
assert_eq(container.network_aliases, aliases, "Container network_aliases should match");
|
||||
|
||||
// Test running the container
|
||||
print("Testing run()...");
|
||||
let run_result = container.run();
|
||||
assert_true(run_result.success, "Container run should succeed");
|
||||
assert_true(container.container_id != "", "Container ID should not be empty after run");
|
||||
print(`✓ run(): Container started with ID: ${container.container_id}`);
|
||||
// Test cpu_limit setter and getter
|
||||
print("Testing cpu_limit setter and getter...");
|
||||
container.cpu_limit = "0.5";
|
||||
assert_eq(container.cpu_limit, "0.5", "Container cpu_limit should match");
|
||||
|
||||
// Test executing a command in the container
|
||||
print("Testing exec()...");
|
||||
let exec_result = container.exec("echo 'Hello from container'");
|
||||
assert_true(exec_result.success, "Container exec should succeed");
|
||||
assert_true(exec_result.stdout.contains("Hello from container"), "Exec output should contain expected text");
|
||||
print("✓ exec(): Command executed successfully");
|
||||
// Test memory_limit setter and getter
|
||||
print("Testing memory_limit setter and getter...");
|
||||
container.memory_limit = "512m";
|
||||
assert_eq(container.memory_limit, "512m", "Container memory_limit should match");
|
||||
|
||||
// Test getting container logs
|
||||
print("Testing logs()...");
|
||||
let logs_result = container.logs();
|
||||
assert_true(logs_result.success, "Container logs should succeed");
|
||||
print("✓ logs(): Logs retrieved successfully");
|
||||
// Test memory_swap_limit setter and getter
|
||||
print("Testing memory_swap_limit setter and getter...");
|
||||
container.memory_swap_limit = "1g";
|
||||
assert_eq(container.memory_swap_limit, "1g", "Container memory_swap_limit should match");
|
||||
|
||||
// Test stopping the container
|
||||
print("Testing stop()...");
|
||||
let stop_result = container.stop();
|
||||
assert_true(stop_result.success, "Container stop should succeed");
|
||||
print("✓ stop(): Container stopped successfully");
|
||||
// Test cpu_shares setter and getter
|
||||
print("Testing cpu_shares setter and getter...");
|
||||
container.cpu_shares = "1024";
|
||||
assert_eq(container.cpu_shares, "1024", "Container cpu_shares should match");
|
||||
|
||||
// Test removing the container
|
||||
print("Testing remove()...");
|
||||
let remove_result = container.remove();
|
||||
assert_true(remove_result.success, "Container remove should succeed");
|
||||
print("✓ remove(): Container removed successfully");
|
||||
// Test restart_policy setter and getter
|
||||
print("Testing restart_policy setter and getter...");
|
||||
container.restart_policy = "always";
|
||||
assert_eq(container.restart_policy, "always", "Container restart_policy should match");
|
||||
|
||||
// Clean up test directory
|
||||
delete(test_dir);
|
||||
print("✓ Cleanup: Test directory removed");
|
||||
// Test detach setter and getter
|
||||
print("Testing detach setter and getter...");
|
||||
container.detach = false;
|
||||
assert_eq(container.detach, false, "Container detach should be false");
|
||||
container.detach = true;
|
||||
assert_eq(container.detach, true, "Container detach should be true");
|
||||
|
||||
print("All container operations tests completed successfully!");
|
||||
// Test health_check setter and getter
|
||||
print("Testing health_check setter and getter...");
|
||||
let health_check_new = health_check_new("example_cmd");
|
||||
container.health_check = health_check_new;
|
||||
container.health_check.interval = "example_interval";
|
||||
assert_eq(container.health_check.cmd, "example_cmd", "Container health check cmd should match");
|
||||
assert_eq(container.health_check.interval, "example_interval", "Container health check interval should match");
|
||||
|
||||
// Test snapshotter setter and getter
|
||||
print("Testing snapshotter setter and getter...");
|
||||
container.snapshotter = "stargz";
|
||||
assert_eq(container.snapshotter, "stargz", "Container snapshotter should match");
|
||||
|
||||
// // Test running the container
|
||||
// print("Testing run()...");
|
||||
// let run_result = container.run();
|
||||
// assert_true(run_result.success, "Container run should succeed");
|
||||
// assert_true(container.container_id != "", "Container ID should not be empty after run");
|
||||
// print(`✓ run(): Container started with ID: ${container.container_id}`);
|
||||
|
||||
// // Test executing a command in the container
|
||||
// print("Testing exec()...");
|
||||
// let exec_result = container.exec("echo 'Hello from container'");
|
||||
// assert_true(exec_result.success, "Container exec should succeed");
|
||||
// assert_true(exec_result.stdout.contains("Hello from container"), "Exec output should contain expected text");
|
||||
// print("✓ exec(): Command executed successfully");
|
||||
|
||||
// // Test getting container logs
|
||||
// print("Testing logs()...");
|
||||
// let logs_result = container.logs();
|
||||
// assert_true(logs_result.success, "Container logs should succeed");
|
||||
// print("✓ logs(): Logs retrieved successfully");
|
||||
|
||||
// // Test stopping the container
|
||||
// print("Testing stop()...");
|
||||
// let stop_result = container.stop();
|
||||
// assert_true(stop_result.success, "Container stop should succeed");
|
||||
// print("✓ stop(): Container stopped successfully");
|
||||
|
||||
// // Test removing the container
|
||||
// print("Testing remove()...");
|
||||
// let remove_result = container.remove();
|
||||
// assert_true(remove_result.success, "Container remove should succeed");
|
||||
// print("✓ remove(): Container removed successfully");
|
||||
|
||||
// // Clean up test directory
|
||||
// delete(test_dir);
|
||||
// print("✓ Cleanup: Test directory removed");
|
||||
|
||||
// print("All container operations tests completed successfully!");
|
||||
} catch(err) {
|
||||
print(`Error: ${err}`);
|
||||
|
||||
|
@@ -1,218 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# SAL Publishing Script
|
||||
# This script publishes all SAL crates to crates.io in the correct dependency order
|
||||
# Handles path dependencies, version updates, and rate limiting
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
DRY_RUN=false
|
||||
WAIT_TIME=15 # Seconds to wait between publishes
|
||||
VERSION=""
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
--wait)
|
||||
WAIT_TIME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --dry-run Show what would be published without actually publishing"
|
||||
echo " --wait SECONDS Time to wait between publishes (default: 15)"
|
||||
echo " --version VER Set version for all crates"
|
||||
echo " -h, --help Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Crates to publish in dependency order
|
||||
CRATES=(
|
||||
"os"
|
||||
"process"
|
||||
"text"
|
||||
"net"
|
||||
"git"
|
||||
"vault"
|
||||
"kubernetes"
|
||||
"virt"
|
||||
"redisclient"
|
||||
"postgresclient"
|
||||
"zinit_client"
|
||||
"mycelium"
|
||||
"rhai"
|
||||
)
|
||||
|
||||
echo -e "${BLUE}===============================================${NC}"
|
||||
echo -e "${BLUE} SAL Publishing Script${NC}"
|
||||
echo -e "${BLUE}===============================================${NC}"
|
||||
echo ""
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo -e "${YELLOW}🔍 DRY RUN MODE - No actual publishing will occur${NC}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check if we're in the right directory
|
||||
if [ ! -f "Cargo.toml" ] || [ ! -d "os" ] || [ ! -d "git" ]; then
|
||||
echo -e "${RED}❌ Error: This script must be run from the SAL repository root${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if cargo is available
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
echo -e "${RED}❌ Error: cargo is not installed or not in PATH${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if user is logged in to crates.io
|
||||
if [ "$DRY_RUN" = false ]; then
|
||||
if ! cargo login --help &> /dev/null; then
|
||||
echo -e "${RED}❌ Error: Please run 'cargo login' first${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Update version if specified
|
||||
if [ -n "$VERSION" ]; then
|
||||
echo -e "${YELLOW}📝 Updating version to $VERSION...${NC}"
|
||||
|
||||
# Update root Cargo.toml
|
||||
sed -i.bak "s/^version = \".*\"/version = \"$VERSION\"/" Cargo.toml
|
||||
|
||||
# Update each crate's Cargo.toml
|
||||
for crate in "${CRATES[@]}"; do
|
||||
if [ -f "$crate/Cargo.toml" ]; then
|
||||
sed -i.bak "s/^version = \".*\"/version = \"$VERSION\"/" "$crate/Cargo.toml"
|
||||
echo " ✅ Updated $crate to version $VERSION"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Run tests before publishing
|
||||
echo -e "${YELLOW}🧪 Running tests...${NC}"
|
||||
if [ "$DRY_RUN" = false ]; then
|
||||
if ! cargo test --workspace; then
|
||||
echo -e "${RED}❌ Tests failed! Aborting publish.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✅ All tests passed${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} (Skipped in dry-run mode)${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check for uncommitted changes
|
||||
if [ "$DRY_RUN" = false ]; then
|
||||
if ! git diff --quiet; then
|
||||
echo -e "${YELLOW}⚠️ Warning: You have uncommitted changes${NC}"
|
||||
read -p "Continue anyway? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo -e "${RED}❌ Aborted by user${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Publish individual crates
|
||||
echo -e "${BLUE}📦 Publishing individual crates...${NC}"
|
||||
echo ""
|
||||
|
||||
for crate in "${CRATES[@]}"; do
|
||||
echo -e "${YELLOW}Publishing sal-$crate...${NC}"
|
||||
|
||||
if [ ! -d "$crate" ]; then
|
||||
echo -e "${RED} ❌ Directory $crate not found${NC}"
|
||||
continue
|
||||
fi
|
||||
|
||||
cd "$crate"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo -e "${BLUE} 🔍 Would run: cargo publish --allow-dirty${NC}"
|
||||
else
|
||||
if cargo publish --allow-dirty; then
|
||||
echo -e "${GREEN} ✅ sal-$crate published successfully${NC}"
|
||||
else
|
||||
echo -e "${RED} ❌ Failed to publish sal-$crate${NC}"
|
||||
cd ..
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
cd ..
|
||||
|
||||
if [ "$DRY_RUN" = false ] && [ "$crate" != "${CRATES[-1]}" ]; then
|
||||
echo -e "${BLUE} ⏳ Waiting $WAIT_TIME seconds for crates.io to process...${NC}"
|
||||
sleep "$WAIT_TIME"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
done
|
||||
|
||||
# Publish main crate
|
||||
echo -e "${BLUE}📦 Publishing main sal crate...${NC}"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo -e "${BLUE}🔍 Would run: cargo publish --allow-dirty${NC}"
|
||||
else
|
||||
if cargo publish --allow-dirty; then
|
||||
echo -e "${GREEN}✅ Main sal crate published successfully${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Failed to publish main sal crate${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}===============================================${NC}"
|
||||
echo -e "${GREEN} Publishing Complete!${NC}"
|
||||
echo -e "${GREEN}===============================================${NC}"
|
||||
echo ""
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo -e "${YELLOW}🔍 This was a dry run. No crates were actually published.${NC}"
|
||||
echo -e "${YELLOW} Run without --dry-run to publish for real.${NC}"
|
||||
else
|
||||
echo -e "${GREEN}🎉 All SAL crates have been published to crates.io!${NC}"
|
||||
echo ""
|
||||
echo "Users can now install SAL modules with:"
|
||||
echo ""
|
||||
echo -e "${BLUE}# Individual crates${NC}"
|
||||
echo "cargo add sal-os sal-process sal-text"
|
||||
echo ""
|
||||
echo -e "${BLUE}# Meta-crate with features${NC}"
|
||||
echo "cargo add sal --features core"
|
||||
echo "cargo add sal --features all"
|
||||
echo ""
|
||||
echo "📚 See PUBLISHING.md for complete usage documentation."
|
||||
fi
|
||||
|
||||
echo ""
|
22
service_manager/Cargo.toml
Normal file
22
service_manager/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "sal-service-manager"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
thiserror = "1.0"
|
||||
tokio = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
|
||||
zinit_client = { package = "sal-zinit-client", path = "../zinit_client", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
# macOS-specific dependencies for launchctl
|
||||
plist = "1.6"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
zinit = ["dep:zinit_client", "dep:serde_json"]
|
54
service_manager/README.md
Normal file
54
service_manager/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Service Manager
|
||||
|
||||
This crate provides a unified interface for managing system services across different platforms.
|
||||
It abstracts the underlying service management system (like `launchctl` on macOS or `systemd` on Linux),
|
||||
allowing you to start, stop, and monitor services with a consistent API.
|
||||
|
||||
## Features
|
||||
|
||||
- A `ServiceManager` trait defining a common interface for service operations.
|
||||
- Platform-specific implementations for:
|
||||
- macOS (`launchctl`)
|
||||
- Linux (`systemd`)
|
||||
- A factory function `create_service_manager` that returns the appropriate manager for the current platform.
|
||||
|
||||
## Usage
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
service_manager = { path = "../service_manager" }
|
||||
```
|
||||
|
||||
Here is an example of how to use the `ServiceManager`:
|
||||
|
||||
```rust,no_run
|
||||
use service_manager::{create_service_manager, ServiceConfig};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let service_manager = create_service_manager();
|
||||
|
||||
let config = ServiceConfig {
|
||||
name: "my-service".to_string(),
|
||||
binary_path: "/usr/local/bin/my-service-executable".to_string(),
|
||||
args: vec!["--config".to_string(), "/etc/my-service.conf".to_string()],
|
||||
working_directory: Some("/var/tmp".to_string()),
|
||||
environment: HashMap::new(),
|
||||
auto_restart: true,
|
||||
};
|
||||
|
||||
// Start a new service
|
||||
service_manager.start(&config)?;
|
||||
|
||||
// Get the status of the service
|
||||
let status = service_manager.status("my-service")?;
|
||||
println!("Service status: {:?}", status);
|
||||
|
||||
// Stop the service
|
||||
service_manager.stop("my-service")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
399
service_manager/src/launchctl.rs
Normal file
399
service_manager/src/launchctl.rs
Normal file
@@ -0,0 +1,399 @@
|
||||
use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus};
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tokio::process::Command;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LaunchctlServiceManager {
|
||||
service_prefix: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct LaunchDaemon {
|
||||
#[serde(rename = "Label")]
|
||||
label: String,
|
||||
#[serde(rename = "ProgramArguments")]
|
||||
program_arguments: Vec<String>,
|
||||
#[serde(rename = "WorkingDirectory", skip_serializing_if = "Option::is_none")]
|
||||
working_directory: Option<String>,
|
||||
#[serde(rename = "EnvironmentVariables", skip_serializing_if = "Option::is_none")]
|
||||
environment_variables: Option<HashMap<String, String>>,
|
||||
#[serde(rename = "KeepAlive", skip_serializing_if = "Option::is_none")]
|
||||
keep_alive: Option<bool>,
|
||||
#[serde(rename = "RunAtLoad")]
|
||||
run_at_load: bool,
|
||||
#[serde(rename = "StandardOutPath", skip_serializing_if = "Option::is_none")]
|
||||
standard_out_path: Option<String>,
|
||||
#[serde(rename = "StandardErrorPath", skip_serializing_if = "Option::is_none")]
|
||||
standard_error_path: Option<String>,
|
||||
}
|
||||
|
||||
impl LaunchctlServiceManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
service_prefix: "tf.ourworld.circles".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_service_label(&self, service_name: &str) -> String {
|
||||
format!("{}.{}", self.service_prefix, service_name)
|
||||
}
|
||||
|
||||
fn get_plist_path(&self, service_name: &str) -> PathBuf {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
PathBuf::from(home)
|
||||
.join("Library")
|
||||
.join("LaunchAgents")
|
||||
.join(format!("{}.plist", self.get_service_label(service_name)))
|
||||
}
|
||||
|
||||
fn get_log_path(&self, service_name: &str) -> PathBuf {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
PathBuf::from(home)
|
||||
.join("Library")
|
||||
.join("Logs")
|
||||
.join("circles")
|
||||
.join(format!("{}.log", service_name))
|
||||
}
|
||||
|
||||
async fn create_plist(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
|
||||
let label = self.get_service_label(&config.name);
|
||||
let plist_path = self.get_plist_path(&config.name);
|
||||
let log_path = self.get_log_path(&config.name);
|
||||
|
||||
// Ensure the LaunchAgents directory exists
|
||||
if let Some(parent) = plist_path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
// Ensure the logs directory exists
|
||||
if let Some(parent) = log_path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let mut program_arguments = vec![config.binary_path.clone()];
|
||||
program_arguments.extend(config.args.clone());
|
||||
|
||||
let launch_daemon = LaunchDaemon {
|
||||
label: label.clone(),
|
||||
program_arguments,
|
||||
working_directory: config.working_directory.clone(),
|
||||
environment_variables: if config.environment.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(config.environment.clone())
|
||||
},
|
||||
keep_alive: if config.auto_restart { Some(true) } else { None },
|
||||
run_at_load: true,
|
||||
standard_out_path: Some(log_path.to_string_lossy().to_string()),
|
||||
standard_error_path: Some(log_path.to_string_lossy().to_string()),
|
||||
};
|
||||
|
||||
let mut plist_content = Vec::new();
|
||||
plist::to_writer_xml(&mut plist_content, &launch_daemon)
|
||||
.map_err(|e| ServiceManagerError::Other(format!("Failed to serialize plist: {}", e)))?;
|
||||
let plist_content = String::from_utf8(plist_content)
|
||||
.map_err(|e| ServiceManagerError::Other(format!("Failed to convert plist to string: {}", e)))?;
|
||||
|
||||
tokio::fs::write(&plist_path, plist_content).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_launchctl(&self, args: &[&str]) -> Result<String, ServiceManagerError> {
|
||||
let output = Command::new("launchctl")
|
||||
.args(args)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(ServiceManagerError::Other(format!(
|
||||
"launchctl command failed: {}",
|
||||
stderr
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
async fn wait_for_service_status(&self, service_name: &str, timeout_secs: u64) -> Result<(), ServiceManagerError> {
|
||||
use tokio::time::{sleep, Duration, timeout};
|
||||
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
let poll_interval = Duration::from_millis(500);
|
||||
|
||||
let result = timeout(timeout_duration, async {
|
||||
loop {
|
||||
match self.status(service_name) {
|
||||
Ok(ServiceStatus::Running) => {
|
||||
return Ok(());
|
||||
}
|
||||
Ok(ServiceStatus::Failed) => {
|
||||
// Service failed, get error details from logs
|
||||
let logs = self.logs(service_name, Some(20)).unwrap_or_default();
|
||||
let error_msg = if logs.is_empty() {
|
||||
"Service failed to start (no logs available)".to_string()
|
||||
} else {
|
||||
// Extract error lines from logs
|
||||
let error_lines: Vec<&str> = logs
|
||||
.lines()
|
||||
.filter(|line| line.to_lowercase().contains("error") || line.to_lowercase().contains("failed"))
|
||||
.take(3)
|
||||
.collect();
|
||||
|
||||
if error_lines.is_empty() {
|
||||
format!("Service failed to start. Recent logs:\n{}",
|
||||
logs.lines().rev().take(5).collect::<Vec<_>>().into_iter().rev().collect::<Vec<_>>().join("\n"))
|
||||
} else {
|
||||
format!("Service failed to start. Errors:\n{}", error_lines.join("\n"))
|
||||
}
|
||||
};
|
||||
return Err(ServiceManagerError::StartFailed(service_name.to_string(), error_msg));
|
||||
}
|
||||
Ok(ServiceStatus::Stopped) | Ok(ServiceStatus::Unknown) => {
|
||||
// Still starting, continue polling
|
||||
sleep(poll_interval).await;
|
||||
}
|
||||
Err(ServiceManagerError::ServiceNotFound(_)) => {
|
||||
return Err(ServiceManagerError::ServiceNotFound(service_name.to_string()));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => Ok(()),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(_) => Err(ServiceManagerError::StartFailed(
|
||||
service_name.to_string(),
|
||||
format!("Service did not start within {} seconds", timeout_secs)
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ServiceManager for LaunchctlServiceManager {
|
||||
fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> {
|
||||
let plist_path = self.get_plist_path(service_name);
|
||||
Ok(plist_path.exists())
|
||||
}
|
||||
|
||||
fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
|
||||
// For synchronous version, we'll use blocking operations
|
||||
let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?;
|
||||
rt.block_on(async {
|
||||
let label = self.get_service_label(&config.name);
|
||||
|
||||
// Check if service is already loaded
|
||||
let list_output = self.run_launchctl(&["list"]).await?;
|
||||
if list_output.contains(&label) {
|
||||
return Err(ServiceManagerError::ServiceAlreadyExists(config.name.clone()));
|
||||
}
|
||||
|
||||
// Create the plist file
|
||||
self.create_plist(config).await?;
|
||||
|
||||
// Load the service
|
||||
let plist_path = self.get_plist_path(&config.name);
|
||||
self.run_launchctl(&["load", &plist_path.to_string_lossy()])
|
||||
.await
|
||||
.map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?;
|
||||
rt.block_on(async {
|
||||
let label = self.get_service_label(service_name);
|
||||
let plist_path = self.get_plist_path(service_name);
|
||||
|
||||
// Check if plist file exists
|
||||
if !plist_path.exists() {
|
||||
return Err(ServiceManagerError::ServiceNotFound(service_name.to_string()));
|
||||
}
|
||||
|
||||
// Check if service is already loaded and running
|
||||
let list_output = self.run_launchctl(&["list"]).await?;
|
||||
if list_output.contains(&label) {
|
||||
// Service is loaded, check if it's running
|
||||
match self.status(service_name)? {
|
||||
ServiceStatus::Running => {
|
||||
return Ok(()); // Already running, nothing to do
|
||||
}
|
||||
_ => {
|
||||
// Service is loaded but not running, try to start it
|
||||
self.run_launchctl(&["start", &label])
|
||||
.await
|
||||
.map_err(|e| ServiceManagerError::StartFailed(service_name.to_string(), e.to_string()))?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Service is not loaded, load it
|
||||
self.run_launchctl(&["load", &plist_path.to_string_lossy()])
|
||||
.await
|
||||
.map_err(|e| ServiceManagerError::StartFailed(service_name.to_string(), e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
async fn start_and_confirm(&self, config: &ServiceConfig, timeout_secs: u64) -> Result<(), ServiceManagerError> {
|
||||
// First start the service
|
||||
self.start(config)?;
|
||||
|
||||
// Then wait for confirmation
|
||||
self.wait_for_service_status(&config.name, timeout_secs).await
|
||||
}
|
||||
|
||||
async fn run(&self, config: &ServiceConfig, timeout_secs: u64) -> Result<(), ServiceManagerError> {
|
||||
self.start_and_confirm(config, timeout_secs).await
|
||||
}
|
||||
|
||||
async fn start_existing_and_confirm(&self, service_name: &str, timeout_secs: u64) -> Result<(), ServiceManagerError> {
|
||||
// First start the existing service
|
||||
self.start_existing(service_name)?;
|
||||
|
||||
// Then wait for confirmation
|
||||
self.wait_for_service_status(service_name, timeout_secs).await
|
||||
}
|
||||
|
||||
fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?;
|
||||
rt.block_on(async {
|
||||
let _label = self.get_service_label(service_name);
|
||||
let plist_path = self.get_plist_path(service_name);
|
||||
|
||||
// Unload the service
|
||||
self.run_launchctl(&["unload", &plist_path.to_string_lossy()])
|
||||
.await
|
||||
.map_err(|e| ServiceManagerError::StopFailed(service_name.to_string(), e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
// For launchctl, we stop and start
|
||||
if let Err(e) = self.stop(service_name) {
|
||||
// If stop fails because service doesn't exist, that's ok for restart
|
||||
if !matches!(e, ServiceManagerError::ServiceNotFound(_)) {
|
||||
return Err(ServiceManagerError::RestartFailed(service_name.to_string(), e.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// We need the config to restart, but we don't have it stored
|
||||
// For now, return an error - in a real implementation we might store configs
|
||||
Err(ServiceManagerError::RestartFailed(
|
||||
service_name.to_string(),
|
||||
"Restart requires re-providing service configuration".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> {
|
||||
let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?;
|
||||
rt.block_on(async {
|
||||
let label = self.get_service_label(service_name);
|
||||
let plist_path = self.get_plist_path(service_name);
|
||||
|
||||
// First check if the plist file exists
|
||||
if !plist_path.exists() {
|
||||
return Err(ServiceManagerError::ServiceNotFound(service_name.to_string()));
|
||||
}
|
||||
|
||||
let list_output = self.run_launchctl(&["list"]).await?;
|
||||
|
||||
if !list_output.contains(&label) {
|
||||
return Ok(ServiceStatus::Stopped);
|
||||
}
|
||||
|
||||
// Get detailed status
|
||||
match self.run_launchctl(&["list", &label]).await {
|
||||
Ok(output) => {
|
||||
if output.contains("\"PID\" = ") {
|
||||
Ok(ServiceStatus::Running)
|
||||
} else if output.contains("\"LastExitStatus\" = ") {
|
||||
Ok(ServiceStatus::Failed)
|
||||
} else {
|
||||
Ok(ServiceStatus::Unknown)
|
||||
}
|
||||
}
|
||||
Err(_) => Ok(ServiceStatus::Stopped),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn logs(&self, service_name: &str, lines: Option<usize>) -> Result<String, ServiceManagerError> {
|
||||
let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?;
|
||||
rt.block_on(async {
|
||||
let log_path = self.get_log_path(service_name);
|
||||
|
||||
if !log_path.exists() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
match lines {
|
||||
Some(n) => {
|
||||
let output = Command::new("tail")
|
||||
.args(&["-n", &n.to_string(), &log_path.to_string_lossy()])
|
||||
.output()
|
||||
.await?;
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
None => {
|
||||
let content = tokio::fs::read_to_string(&log_path).await?;
|
||||
Ok(content)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn list(&self) -> Result<Vec<String>, ServiceManagerError> {
|
||||
let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?;
|
||||
rt.block_on(async {
|
||||
let list_output = self.run_launchctl(&["list"]).await?;
|
||||
|
||||
let services: Vec<String> = list_output
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
if line.contains(&self.service_prefix) {
|
||||
// Extract service name from label
|
||||
line.split_whitespace()
|
||||
.last()
|
||||
.and_then(|label| label.strip_prefix(&format!("{}.", self.service_prefix)))
|
||||
.map(|s| s.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(services)
|
||||
})
|
||||
}
|
||||
|
||||
fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
// Stop the service first
|
||||
let _ = self.stop(service_name);
|
||||
|
||||
// Remove the plist file
|
||||
let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?;
|
||||
rt.block_on(async {
|
||||
let plist_path = self.get_plist_path(service_name);
|
||||
if plist_path.exists() {
|
||||
tokio::fs::remove_file(&plist_path).await?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
112
service_manager/src/lib.rs
Normal file
112
service_manager/src/lib.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use async_trait::async_trait;
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ServiceManagerError {
|
||||
#[error("Service '{0}' not found")]
|
||||
ServiceNotFound(String),
|
||||
#[error("Service '{0}' already exists")]
|
||||
ServiceAlreadyExists(String),
|
||||
#[error("Failed to start service '{0}': {1}")]
|
||||
StartFailed(String, String),
|
||||
#[error("Failed to stop service '{0}': {1}")]
|
||||
StopFailed(String, String),
|
||||
#[error("Failed to restart service '{0}': {1}")]
|
||||
RestartFailed(String, String),
|
||||
#[error("Failed to get logs for service '{0}': {1}")]
|
||||
LogsFailed(String, String),
|
||||
#[error("IO error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("Service manager error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServiceConfig {
|
||||
pub name: String,
|
||||
pub binary_path: String,
|
||||
pub args: Vec<String>,
|
||||
pub working_directory: Option<String>,
|
||||
pub environment: HashMap<String, String>,
|
||||
pub auto_restart: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ServiceStatus {
|
||||
Running,
|
||||
Stopped,
|
||||
Failed,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ServiceManager: Send + Sync {
|
||||
/// Check if a service exists
|
||||
fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError>;
|
||||
|
||||
/// Start a service with the given configuration
|
||||
fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError>;
|
||||
|
||||
/// Start an existing service by name (load existing plist/config)
|
||||
fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError>;
|
||||
|
||||
/// Start a service and wait for confirmation that it's running or failed
|
||||
async fn start_and_confirm(&self, config: &ServiceConfig, timeout_secs: u64) -> Result<(), ServiceManagerError>;
|
||||
|
||||
/// Start a service and wait for confirmation that it's running or failed
|
||||
async fn run(&self, config: &ServiceConfig, timeout_secs: u64) -> Result<(), ServiceManagerError>;
|
||||
|
||||
/// Start an existing service and wait for confirmation that it's running or failed
|
||||
async fn start_existing_and_confirm(&self, service_name: &str, timeout_secs: u64) -> Result<(), ServiceManagerError>;
|
||||
|
||||
/// Stop a service by name
|
||||
fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError>;
|
||||
|
||||
/// Restart a service by name
|
||||
fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError>;
|
||||
|
||||
/// Get the status of a service
|
||||
fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError>;
|
||||
|
||||
/// Get logs for a service
|
||||
fn logs(&self, service_name: &str, lines: Option<usize>) -> Result<String, ServiceManagerError>;
|
||||
|
||||
/// List all managed services
|
||||
fn list(&self) -> Result<Vec<String>, ServiceManagerError>;
|
||||
|
||||
/// Remove a service configuration (stop if running)
|
||||
fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError>;
|
||||
}
|
||||
|
||||
// Platform-specific implementations
|
||||
#[cfg(target_os = "macos")]
|
||||
mod launchctl;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use launchctl::LaunchctlServiceManager;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod systemd;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use systemd::SystemdServiceManager;
|
||||
|
||||
#[cfg(feature = "zinit")]
|
||||
mod zinit;
|
||||
#[cfg(feature = "zinit")]
|
||||
pub use zinit::ZinitServiceManager;
|
||||
|
||||
// Factory function to create the appropriate service manager for the platform
|
||||
pub fn create_service_manager() -> Box<dyn ServiceManager> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Box::new(LaunchctlServiceManager::new())
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Box::new(SystemdServiceManager::new())
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
compile_error!("Service manager not implemented for this platform")
|
||||
}
|
||||
}
|
42
service_manager/src/systemd.rs
Normal file
42
service_manager/src/systemd.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus};
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SystemdServiceManager;
|
||||
|
||||
impl SystemdServiceManager {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ServiceManager for SystemdServiceManager {
|
||||
async fn start(&self, _config: &ServiceConfig) -> Result<(), ServiceManagerError> {
|
||||
Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string()))
|
||||
}
|
||||
|
||||
async fn stop(&self, _service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string()))
|
||||
}
|
||||
|
||||
async fn restart(&self, _service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string()))
|
||||
}
|
||||
|
||||
async fn status(&self, _service_name: &str) -> Result<ServiceStatus, ServiceManagerError> {
|
||||
Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string()))
|
||||
}
|
||||
|
||||
async fn logs(&self, _service_name: &str, _lines: Option<usize>) -> Result<String, ServiceManagerError> {
|
||||
Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string()))
|
||||
}
|
||||
|
||||
async fn list(&self) -> Result<Vec<String>, ServiceManagerError> {
|
||||
Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string()))
|
||||
}
|
||||
|
||||
async fn remove(&self, _service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string()))
|
||||
}
|
||||
}
|
122
service_manager/src/zinit.rs
Normal file
122
service_manager/src/zinit.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus};
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use zinit_client::{get_zinit_client, ServiceStatus as ZinitServiceStatus, ZinitClientWrapper};
|
||||
|
||||
pub struct ZinitServiceManager {
|
||||
client: Arc<ZinitClientWrapper>,
|
||||
}
|
||||
|
||||
impl ZinitServiceManager {
|
||||
pub fn new(socket_path: &str) -> Result<Self, ServiceManagerError> {
|
||||
// This is a blocking call to get the async client.
|
||||
// We might want to make this async in the future if the constructor can be async.
|
||||
let client = tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(get_zinit_client(socket_path))
|
||||
.map_err(|e| ServiceManagerError::Other(e.to_string()))?;
|
||||
Ok(ZinitServiceManager { client })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ServiceManager for ZinitServiceManager {
|
||||
fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> {
|
||||
let status_res = self.status(service_name);
|
||||
match status_res {
|
||||
Ok(_) => Ok(true),
|
||||
Err(ServiceManagerError::ServiceNotFound(_)) => Ok(false),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
|
||||
let service_config = json!({
|
||||
"exec": config.binary_path,
|
||||
"args": config.args,
|
||||
"working_directory": config.working_directory,
|
||||
"env": config.environment,
|
||||
"restart": config.auto_restart,
|
||||
});
|
||||
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(self.client.create_service(&config.name, service_config))
|
||||
.map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?;
|
||||
|
||||
self.start_existing(&config.name)
|
||||
}
|
||||
|
||||
fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(self.client.start(service_name))
|
||||
.map_err(|e| ServiceManagerError::StartFailed(service_name.to_string(), e.to_string()))
|
||||
}
|
||||
|
||||
async fn start_and_confirm(&self, config: &ServiceConfig, _timeout_secs: u64) -> Result<(), ServiceManagerError> {
|
||||
self.start(config)
|
||||
}
|
||||
|
||||
async fn run(&self, config: &ServiceConfig, _timeout_secs: u64) -> Result<(), ServiceManagerError> {
|
||||
self.start(config)
|
||||
}
|
||||
|
||||
async fn start_existing_and_confirm(&self, service_name: &str, _timeout_secs: u64) -> Result<(), ServiceManagerError> {
|
||||
self.start_existing(service_name)
|
||||
}
|
||||
|
||||
fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(self.client.stop(service_name))
|
||||
.map_err(|e| ServiceManagerError::StopFailed(service_name.to_string(), e.to_string()))
|
||||
}
|
||||
|
||||
fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(self.client.restart(service_name))
|
||||
.map_err(|e| ServiceManagerError::RestartFailed(service_name.to_string(), e.to_string()))
|
||||
}
|
||||
|
||||
fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> {
|
||||
let status: ZinitServiceStatus = tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(self.client.status(service_name))
|
||||
.map_err(|e| ServiceManagerError::Other(e.to_string()))?;
|
||||
|
||||
let service_status = match status {
|
||||
ZinitServiceStatus::Running(_) => crate::ServiceStatus::Running,
|
||||
ZinitServiceStatus::Stopped => crate::ServiceStatus::Stopped,
|
||||
ZinitServiceStatus::Failed(_) => crate::ServiceStatus::Failed,
|
||||
ZinitServiceStatus::Waiting(_) => crate::ServiceStatus::Unknown,
|
||||
};
|
||||
Ok(service_status)
|
||||
}
|
||||
|
||||
fn logs(&self, service_name: &str, _lines: Option<usize>) -> Result<String, ServiceManagerError> {
|
||||
let logs = tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(self.client.logs(Some(service_name.to_string())))
|
||||
.map_err(|e| ServiceManagerError::LogsFailed(service_name.to_string(), e.to_string()))?;
|
||||
Ok(logs.join("\n"))
|
||||
}
|
||||
|
||||
fn list(&self) -> Result<Vec<String>, ServiceManagerError> {
|
||||
let services = tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(self.client.list())
|
||||
.map_err(|e| ServiceManagerError::Other(e.to_string()))?;
|
||||
Ok(services.keys().cloned().collect())
|
||||
}
|
||||
|
||||
fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
let _ = self.stop(service_name); // Best effort to stop before removing
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(self.client.delete_service(service_name))
|
||||
.map_err(|e| ServiceManagerError::Other(e.to_string()))
|
||||
}
|
||||
}
|
29
src/lib.rs
29
src/lib.rs
@@ -36,44 +36,17 @@ pub enum Error {
|
||||
/// Result type for SAL operations
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
// Re-export modules conditionally based on features
|
||||
#[cfg(feature = "git")]
|
||||
pub use sal_git as git;
|
||||
|
||||
#[cfg(feature = "kubernetes")]
|
||||
pub use sal_kubernetes as kubernetes;
|
||||
|
||||
#[cfg(feature = "mycelium")]
|
||||
// Re-export modules
|
||||
pub use sal_mycelium as mycelium;
|
||||
|
||||
#[cfg(feature = "net")]
|
||||
pub use sal_net as net;
|
||||
|
||||
#[cfg(feature = "os")]
|
||||
pub use sal_os as os;
|
||||
|
||||
#[cfg(feature = "postgresclient")]
|
||||
pub use sal_postgresclient as postgresclient;
|
||||
|
||||
#[cfg(feature = "process")]
|
||||
pub use sal_process as process;
|
||||
|
||||
#[cfg(feature = "redisclient")]
|
||||
pub use sal_redisclient as redisclient;
|
||||
|
||||
#[cfg(feature = "rhai")]
|
||||
pub use sal_rhai as rhai;
|
||||
|
||||
#[cfg(feature = "text")]
|
||||
pub use sal_text as text;
|
||||
|
||||
#[cfg(feature = "vault")]
|
||||
pub use sal_vault as vault;
|
||||
|
||||
#[cfg(feature = "virt")]
|
||||
pub use sal_virt as virt;
|
||||
|
||||
#[cfg(feature = "zinit_client")]
|
||||
pub use sal_zinit_client as zinit_client;
|
||||
|
||||
// Version information
|
||||
|
@@ -1,16 +1,7 @@
|
||||
# SAL Text - Text Processing and Manipulation Utilities (`sal-text`)
|
||||
# SAL Text - Text Processing and Manipulation Utilities
|
||||
|
||||
SAL Text provides a comprehensive collection of text processing utilities for both Rust applications and Rhai scripting environments.
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sal-text = "0.1.0"
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Text Indentation**: Remove common leading whitespace (`dedent`) and add prefixes (`prefix`)
|
||||
|
@@ -1,16 +1,7 @@
|
||||
# SAL Vault (`sal-vault`)
|
||||
# SAL Vault
|
||||
|
||||
SAL Vault is a comprehensive cryptographic library that provides secure key management, digital signatures, symmetric encryption, Ethereum wallet functionality, and encrypted key-value storage.
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sal-vault = "0.1.0"
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Core Cryptographic Operations
|
||||
|
@@ -1,16 +1,7 @@
|
||||
# SAL Virt Package (`sal-virt`)
|
||||
# SAL Virt Package
|
||||
|
||||
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
|
||||
|
@@ -2,7 +2,6 @@ use crate::buildah::{
|
||||
execute_buildah_command, set_thread_local_debug, thread_local_debug, BuildahError, Image,
|
||||
};
|
||||
use sal_process::CommandResult;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Builder struct for buildah operations
|
||||
#[derive(Clone)]
|
||||
@@ -249,8 +248,32 @@ impl Builder {
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn commit(&self, image_name: &str) -> Result<CommandResult, BuildahError> {
|
||||
pub fn commit(&self, image_name: &str, options: Option<Vec<(String, String)>>) -> Result<CommandResult, BuildahError> {
|
||||
if let Some(container_id) = &self.container_id {
|
||||
let mut args_owned: Vec<String> = Vec::new();
|
||||
args_owned.push("commit".to_string());
|
||||
|
||||
// Process options
|
||||
if let Some(options_vec) = options {
|
||||
for (key, value) in options_vec.iter() {
|
||||
let option_name = if key.len() == 1 {
|
||||
format!("-{}", key)
|
||||
} else {
|
||||
format!("--{}", key)
|
||||
};
|
||||
args_owned.push(option_name);
|
||||
if !value.is_empty() {
|
||||
args_owned.push(value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
args_owned.push(container_id.clone());
|
||||
args_owned.push(image_name.to_string());
|
||||
|
||||
// Convert Vec<String> to Vec<&str> for execute_buildah_command
|
||||
let args: Vec<&str> = args_owned.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
@@ -258,7 +281,7 @@ impl Builder {
|
||||
set_thread_local_debug(self.debug);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&["commit", container_id, image_name]);
|
||||
let result = execute_buildah_command(&args);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
@@ -336,16 +359,22 @@ impl Builder {
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn config(&self, options: HashMap<String, String>) -> Result<CommandResult, BuildahError> {
|
||||
pub fn config(&self, options: Vec<(String, String)>) -> Result<CommandResult, BuildahError> {
|
||||
if let Some(container_id) = &self.container_id {
|
||||
let mut args_owned: Vec<String> = Vec::new();
|
||||
args_owned.push("config".to_string());
|
||||
|
||||
// Process options map
|
||||
// Process options
|
||||
for (key, value) in options.iter() {
|
||||
let option_name = format!("--{}", key);
|
||||
let option_name = if key.len() == 1 {
|
||||
format!("-{}", key)
|
||||
} else {
|
||||
format!("--{}", key)
|
||||
};
|
||||
args_owned.push(option_name);
|
||||
args_owned.push(value.clone());
|
||||
if !value.is_empty() {
|
||||
args_owned.push(value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
args_owned.push(container_id.clone());
|
||||
@@ -380,8 +409,19 @@ impl Builder {
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn set_entrypoint(&self, entrypoint: &str) -> Result<CommandResult, BuildahError> {
|
||||
pub fn set_entrypoint(&self, entrypoint: Vec<String>) -> Result<CommandResult, BuildahError> {
|
||||
if let Some(container_id) = &self.container_id {
|
||||
// Serialize the entrypoint vector to a JSON string
|
||||
let entrypoint_json = match serde_json::to_string(&entrypoint) {
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
return Err(BuildahError::JsonParseError(format!(
|
||||
"Failed to serialize entrypoint to JSON: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
@@ -390,7 +430,7 @@ impl Builder {
|
||||
|
||||
// Execute the command
|
||||
let result =
|
||||
execute_buildah_command(&["config", "--entrypoint", entrypoint, container_id]);
|
||||
execute_buildah_command(&["config", "--entrypoint", &entrypoint_json, container_id]);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
@@ -410,8 +450,19 @@ impl Builder {
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn set_cmd(&self, cmd: &str) -> Result<CommandResult, BuildahError> {
|
||||
pub fn set_cmd(&self, cmd: Vec<String>) -> Result<CommandResult, BuildahError> {
|
||||
if let Some(container_id) = &self.container_id {
|
||||
// Serialize the cmd vector to a JSON string
|
||||
let cmd_json = match serde_json::to_string(&cmd) {
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
return Err(BuildahError::JsonParseError(format!(
|
||||
"Failed to serialize cmd to JSON: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
@@ -419,7 +470,7 @@ impl Builder {
|
||||
set_thread_local_debug(self.debug);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&["config", "--cmd", cmd, container_id]);
|
||||
let result = execute_buildah_command(&["config", "--cmd", &cmd_json, container_id]);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
@@ -55,34 +55,44 @@ impl ContentOperations {
|
||||
///
|
||||
/// * `Result<String, BuildahError>` - File content or error
|
||||
pub fn read_content(container_id: &str, source_path: &str) -> Result<String, BuildahError> {
|
||||
// Create a temporary file
|
||||
// Create a temporary file to store the content from the container.
|
||||
let temp_file = NamedTempFile::new()
|
||||
.map_err(|e| BuildahError::Other(format!("Failed to create temporary file: {}", e)))?;
|
||||
|
||||
let temp_path = temp_file.path().to_string_lossy().to_string();
|
||||
|
||||
// Copy the file from the container to the temporary file
|
||||
// Use mount to access the container's filesystem
|
||||
let mount_result = execute_buildah_command(&["mount", container_id])?;
|
||||
let mount_point = mount_result.stdout.trim();
|
||||
// In rootless mode, `buildah mount` must run inside `buildah unshare`.
|
||||
// We create a script to mount, copy the file, and unmount, all within the unshare session.
|
||||
let script = format!(
|
||||
r#"
|
||||
set -e
|
||||
mount_point=$(buildah mount '{container_id}')
|
||||
if [ -z "$mount_point" ]; then
|
||||
echo "Error: Failed to mount container '{container_id}'." >&2
|
||||
exit 1
|
||||
fi
|
||||
trap 'buildah umount '{container_id}'' EXIT
|
||||
cp "${{mount_point}}{source_path}" '{temp_path}'
|
||||
"#,
|
||||
container_id = container_id,
|
||||
source_path = source_path,
|
||||
temp_path = &temp_path
|
||||
);
|
||||
|
||||
// Construct the full path to the file in the container
|
||||
let full_source_path = format!("{}{}", mount_point, source_path);
|
||||
let result = execute_buildah_command(&["unshare", "sh", "-c", &script])?;
|
||||
|
||||
// Copy the file from the mounted container to the temporary file
|
||||
execute_buildah_command(&["copy", container_id, &full_source_path, &temp_path])?;
|
||||
|
||||
// Unmount the container
|
||||
execute_buildah_command(&["umount", container_id])?;
|
||||
|
||||
// Read the content from the temporary file
|
||||
let mut file = File::open(temp_file.path())
|
||||
.map_err(|e| BuildahError::Other(format!("Failed to open temporary file: {}", e)))?;
|
||||
if !result.success {
|
||||
return Err(BuildahError::Other(format!(
|
||||
"Failed to execute read_content script in unshare session: {}",
|
||||
result.stderr
|
||||
)));
|
||||
}
|
||||
|
||||
// The script has copied the file content to our temporary file.
|
||||
// Now, we read it.
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content).map_err(|e| {
|
||||
BuildahError::Other(format!("Failed to read from temporary file: {}", e))
|
||||
})?;
|
||||
File::open(&temp_path)
|
||||
.and_then(|mut f| f.read_to_string(&mut content))
|
||||
.map_err(|e| BuildahError::Other(format!("Failed to read from temporary file: {}", e)))?;
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
@@ -52,6 +52,20 @@ impl Container {
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an image
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `image` - Image to create the container from
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Self` - The container instance for method chaining
|
||||
pub fn with_image(mut self, image: &str) -> Self {
|
||||
self.image = Some(image.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a port mapping
|
||||
///
|
||||
/// # Arguments
|
||||
|
@@ -42,7 +42,7 @@ pub struct Container {
|
||||
}
|
||||
|
||||
/// Health check configuration for a container
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct HealthCheck {
|
||||
/// Command to run for health check
|
||||
pub cmd: String,
|
||||
|
@@ -3,9 +3,8 @@
|
||||
//! This module provides Rhai wrappers for the functions in the Buildah module.
|
||||
|
||||
use crate::buildah::{BuildahError, Builder, ContentOperations, Image};
|
||||
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
|
||||
use rhai::{Array, Dynamic, Engine, EvalAltResult};
|
||||
use sal_process::CommandResult;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Register Buildah module functions with the Rhai engine
|
||||
///
|
||||
@@ -45,7 +44,6 @@ pub fn register_bah_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>
|
||||
engine.register_fn("image_push", builder_image_push);
|
||||
engine.register_fn("image_tag", builder_image_tag);
|
||||
engine.register_fn("build", builder_build);
|
||||
engine.register_fn("read_content", builder_read_content);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -89,29 +87,51 @@ fn register_bah_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
}
|
||||
|
||||
// Helper functions for error conversion
|
||||
fn bah_error_to_rhai_error<T>(result: Result<T, BuildahError>) -> Result<T, Box<EvalAltResult>> {
|
||||
result.map_err(|e| {
|
||||
impl From<BuildahError> for Box<EvalAltResult> {
|
||||
fn from(err: BuildahError) -> Self {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Buildah error: {}", e).into(),
|
||||
format!("Buildah error: {}", err).into(),
|
||||
rhai::Position::NONE,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert Rhai Map to Rust HashMap
|
||||
fn convert_map_to_hashmap(options: Map) -> Result<HashMap<String, String>, Box<EvalAltResult>> {
|
||||
let mut config_options = HashMap::<String, String>::new();
|
||||
// Helper function to convert Rhai Array of pairs to a Vec of tuples
|
||||
fn convert_array_to_vec(
|
||||
options: Array,
|
||||
) -> Result<Vec<(String, String)>, Box<EvalAltResult>> {
|
||||
let mut config_options: Vec<(String, String)> = Vec::new();
|
||||
|
||||
for (key, value) in options.iter() {
|
||||
if let Ok(value_str) = value.clone().into_string() {
|
||||
// Convert SmartString to String
|
||||
config_options.insert(key.to_string(), value_str);
|
||||
} else {
|
||||
for option_pair_dynamic in options {
|
||||
let pair = option_pair_dynamic.into_array().map_err(|_e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
"Each option must be an array of [key, value]".into(),
|
||||
rhai::Position::NONE,
|
||||
))
|
||||
})?;
|
||||
|
||||
if pair.len() != 2 {
|
||||
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Option '{}' must be a string", key).into(),
|
||||
"Each option must be an array of [key, value] with 2 elements".into(),
|
||||
rhai::Position::NONE,
|
||||
)));
|
||||
}
|
||||
|
||||
let key = pair[0].clone().into_string().map_err(|_e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
"Option key must be a string".into(),
|
||||
rhai::Position::NONE,
|
||||
))
|
||||
})?;
|
||||
|
||||
let value = pair[1].clone().into_string().map_err(|_e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
"Option value must be a string".into(),
|
||||
rhai::Position::NONE,
|
||||
))
|
||||
})?;
|
||||
|
||||
config_options.push((key, value));
|
||||
}
|
||||
|
||||
Ok(config_options)
|
||||
@@ -119,7 +139,7 @@ fn convert_map_to_hashmap(options: Map) -> Result<HashMap<String, String>, Box<E
|
||||
|
||||
/// Create a new Builder
|
||||
pub fn bah_new(name: &str, image: &str) -> Result<Builder, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(Builder::new(name, image))
|
||||
Builder::new(name, image).map_err(Into::into)
|
||||
}
|
||||
|
||||
// Builder instance methods
|
||||
@@ -127,7 +147,7 @@ pub fn builder_run(
|
||||
builder: &mut Builder,
|
||||
command: &str,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.run(command))
|
||||
builder.run(command).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn builder_run_with_isolation(
|
||||
@@ -135,7 +155,9 @@ pub fn builder_run_with_isolation(
|
||||
command: &str,
|
||||
isolation: &str,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.run_with_isolation(command, isolation))
|
||||
builder
|
||||
.run_with_isolation(command, isolation)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn builder_copy(
|
||||
@@ -143,7 +165,7 @@ pub fn builder_copy(
|
||||
source: &str,
|
||||
dest: &str,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.copy(source, dest))
|
||||
builder.copy(source, dest).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn builder_add(
|
||||
@@ -151,43 +173,54 @@ pub fn builder_add(
|
||||
source: &str,
|
||||
dest: &str,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.add(source, dest))
|
||||
builder.add(source, dest).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn builder_commit(
|
||||
builder: &mut Builder,
|
||||
image_name: &str,
|
||||
options: Array,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.commit(image_name))
|
||||
let commit_options = convert_array_to_vec(options)?;
|
||||
builder
|
||||
.commit(image_name, Some(commit_options))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn builder_remove(builder: &mut Builder) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.remove())
|
||||
builder.remove().map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn builder_config(
|
||||
builder: &mut Builder,
|
||||
options: Map,
|
||||
options: Array,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
// Convert Rhai Map to Rust HashMap
|
||||
let config_options = convert_map_to_hashmap(options)?;
|
||||
bah_error_to_rhai_error(builder.config(config_options))
|
||||
let config_options = convert_array_to_vec(options)?;
|
||||
builder.config(config_options).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Set the entrypoint for the container
|
||||
pub fn builder_set_entrypoint(
|
||||
builder: &mut Builder,
|
||||
entrypoint: &str,
|
||||
entrypoint: Array,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.set_entrypoint(entrypoint))
|
||||
let entrypoint_vec: Vec<String> = entrypoint
|
||||
.into_iter()
|
||||
.map(|item| item.into_string().unwrap_or_default())
|
||||
.collect();
|
||||
builder.set_entrypoint(entrypoint_vec).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Set the default command for the container
|
||||
pub fn builder_set_cmd(
|
||||
builder: &mut Builder,
|
||||
cmd: &str,
|
||||
cmd: Array,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.set_cmd(cmd))
|
||||
let cmd_vec: Vec<String> = cmd
|
||||
.into_iter()
|
||||
.map(|item| item.into_string().unwrap_or_default())
|
||||
.collect();
|
||||
builder.set_cmd(cmd_vec).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Write content to a file in the container
|
||||
@@ -197,11 +230,7 @@ pub fn builder_write_content(
|
||||
dest_path: &str,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
if let Some(container_id) = builder.container_id() {
|
||||
bah_error_to_rhai_error(ContentOperations::write_content(
|
||||
container_id,
|
||||
content,
|
||||
dest_path,
|
||||
))
|
||||
ContentOperations::write_content(container_id, content, dest_path).map_err(Into::into)
|
||||
} else {
|
||||
Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"No container ID available".into(),
|
||||
@@ -216,7 +245,7 @@ pub fn builder_read_content(
|
||||
source_path: &str,
|
||||
) -> Result<String, Box<EvalAltResult>> {
|
||||
if let Some(container_id) = builder.container_id() {
|
||||
bah_error_to_rhai_error(ContentOperations::read_content(container_id, source_path))
|
||||
ContentOperations::read_content(container_id, source_path).map_err(Into::into)
|
||||
} else {
|
||||
Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"No container ID available".into(),
|
||||
@@ -227,7 +256,7 @@ pub fn builder_read_content(
|
||||
|
||||
// Builder static methods
|
||||
pub fn builder_images(_builder: &mut Builder) -> Result<Array, Box<EvalAltResult>> {
|
||||
let images = bah_error_to_rhai_error(Builder::images())?;
|
||||
let images = Builder::images()?;
|
||||
|
||||
// Convert Vec<Image> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
@@ -242,7 +271,7 @@ pub fn builder_image_remove(
|
||||
_builder: &mut Builder,
|
||||
image: &str,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(Builder::image_remove(image))
|
||||
Builder::image_remove(image).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn builder_image_pull(
|
||||
@@ -250,7 +279,7 @@ pub fn builder_image_pull(
|
||||
image: &str,
|
||||
tls_verify: bool,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(Builder::image_pull(image, tls_verify))
|
||||
Builder::image_pull(image, tls_verify).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn builder_image_push(
|
||||
@@ -259,7 +288,7 @@ pub fn builder_image_push(
|
||||
destination: &str,
|
||||
tls_verify: bool,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(Builder::image_push(image, destination, tls_verify))
|
||||
Builder::image_push(image, destination, tls_verify).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn builder_image_tag(
|
||||
@@ -267,7 +296,7 @@ pub fn builder_image_tag(
|
||||
image: &str,
|
||||
new_name: &str,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(Builder::image_tag(image, new_name))
|
||||
Builder::image_tag(image, new_name).map_err(Into::into)
|
||||
}
|
||||
|
||||
// Getter functions for Builder properties
|
||||
@@ -298,7 +327,7 @@ pub fn set_builder_debug(builder: &mut Builder, debug: bool) {
|
||||
|
||||
// Reset function for Builder
|
||||
pub fn builder_reset(builder: &mut Builder) -> Result<(), Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.reset())
|
||||
builder.reset().map_err(Into::into)
|
||||
}
|
||||
|
||||
// Build function for Builder
|
||||
@@ -309,10 +338,5 @@ pub fn builder_build(
|
||||
file: &str,
|
||||
isolation: &str,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(Builder::build(
|
||||
Some(tag),
|
||||
context_dir,
|
||||
file,
|
||||
Some(isolation),
|
||||
))
|
||||
Builder::build(Some(tag), context_dir, file, Some(isolation)).map_err(Into::into)
|
||||
}
|
||||
|
@@ -2,38 +2,37 @@
|
||||
//!
|
||||
//! This module provides Rhai wrappers for the functions in the Nerdctl module.
|
||||
|
||||
use crate::nerdctl::{self, Container, Image, NerdctlError};
|
||||
use crate::nerdctl::{self, Container, HealthCheck, Image, NerdctlError};
|
||||
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
|
||||
use sal_process::CommandResult;
|
||||
|
||||
// Helper functions for error conversion with improved context
|
||||
fn nerdctl_error_to_rhai_error<T>(
|
||||
result: Result<T, NerdctlError>,
|
||||
) -> Result<T, Box<EvalAltResult>> {
|
||||
result.map_err(|e| {
|
||||
// Create a more detailed error message based on the error type
|
||||
let error_message = match &e {
|
||||
NerdctlError::CommandExecutionFailed(io_err) => {
|
||||
format!("Failed to execute nerdctl command: {}. This may indicate nerdctl is not installed or not in PATH.", io_err)
|
||||
},
|
||||
NerdctlError::CommandFailed(msg) => {
|
||||
format!("Nerdctl command failed: {}. Check container status and logs for more details.", msg)
|
||||
},
|
||||
NerdctlError::JsonParseError(msg) => {
|
||||
format!("Failed to parse nerdctl JSON output: {}. This may indicate an incompatible nerdctl version.", msg)
|
||||
},
|
||||
NerdctlError::ConversionError(msg) => {
|
||||
format!("Data conversion error: {}. This may indicate unexpected output format from nerdctl.", msg)
|
||||
},
|
||||
NerdctlError::Other(msg) => {
|
||||
format!("Nerdctl error: {}. This is an unexpected error.", msg)
|
||||
},
|
||||
impl From<NerdctlError> for Box<EvalAltResult> {
|
||||
fn from(err: NerdctlError) -> Self {
|
||||
let error_message = match err {
|
||||
NerdctlError::CommandExecutionFailed(io_err) => format!(
|
||||
"Failed to execute nerdctl command: {}. This may indicate nerdctl is not installed or not in PATH.",
|
||||
io_err
|
||||
),
|
||||
NerdctlError::CommandFailed(msg) => format!(
|
||||
"Nerdctl command failed: {}. Check container status and logs for more details.",
|
||||
msg
|
||||
),
|
||||
NerdctlError::JsonParseError(msg) => format!(
|
||||
"Failed to parse nerdctl JSON output: {}. This may indicate an incompatible nerdctl version.",
|
||||
msg
|
||||
),
|
||||
NerdctlError::ConversionError(msg) => format!(
|
||||
"Data conversion error: {}. This may indicate unexpected output format from nerdctl.",
|
||||
msg
|
||||
),
|
||||
NerdctlError::Other(msg) => format!("Nerdctl error: {}. This is an unexpected error.", msg),
|
||||
};
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
error_message.into(),
|
||||
rhai::Position::NONE
|
||||
rhai::Position::NONE,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -42,12 +41,7 @@ fn nerdctl_error_to_rhai_error<T>(
|
||||
|
||||
/// Create a new Container
|
||||
pub fn container_new(name: &str) -> Result<Container, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(Container::new(name))
|
||||
}
|
||||
|
||||
/// Create a Container from an image
|
||||
pub fn container_from_image(name: &str, image: &str) -> Result<Container, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(Container::from_image(name, image))
|
||||
Container::new(name).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Reset the container configuration to defaults while keeping the name and image
|
||||
@@ -55,127 +49,10 @@ pub fn container_reset(container: Container) -> Container {
|
||||
container.reset()
|
||||
}
|
||||
|
||||
/// Add a port mapping to a Container
|
||||
pub fn container_with_port(container: Container, port: &str) -> Container {
|
||||
container.with_port(port)
|
||||
}
|
||||
pub fn health_check_new(cmd: &str) -> Result<HealthCheck, Box<EvalAltResult>> {
|
||||
let health_check = HealthCheck::new(cmd);
|
||||
|
||||
/// Add a volume mount to a Container
|
||||
pub fn container_with_volume(container: Container, volume: &str) -> Container {
|
||||
container.with_volume(volume)
|
||||
}
|
||||
|
||||
/// Add an environment variable to a Container
|
||||
pub fn container_with_env(container: Container, key: &str, value: &str) -> Container {
|
||||
container.with_env(key, value)
|
||||
}
|
||||
|
||||
/// Set the network for a Container
|
||||
pub fn container_with_network(container: Container, network: &str) -> Container {
|
||||
container.with_network(network)
|
||||
}
|
||||
|
||||
/// Add a network alias to a Container
|
||||
pub fn container_with_network_alias(container: Container, alias: &str) -> Container {
|
||||
container.with_network_alias(alias)
|
||||
}
|
||||
|
||||
/// Set CPU limit for a Container
|
||||
pub fn container_with_cpu_limit(container: Container, cpus: &str) -> Container {
|
||||
container.with_cpu_limit(cpus)
|
||||
}
|
||||
|
||||
/// Set memory limit for a Container
|
||||
pub fn container_with_memory_limit(container: Container, memory: &str) -> Container {
|
||||
container.with_memory_limit(memory)
|
||||
}
|
||||
|
||||
/// Set restart policy for a Container
|
||||
pub fn container_with_restart_policy(container: Container, policy: &str) -> Container {
|
||||
container.with_restart_policy(policy)
|
||||
}
|
||||
|
||||
/// Set health check for a Container
|
||||
pub fn container_with_health_check(container: Container, cmd: &str) -> Container {
|
||||
container.with_health_check(cmd)
|
||||
}
|
||||
|
||||
/// Add multiple port mappings to a Container
|
||||
pub fn container_with_ports(mut container: Container, ports: Array) -> Container {
|
||||
for port in ports.iter() {
|
||||
if port.is_string() {
|
||||
let port_str = port.clone().cast::<String>();
|
||||
container = container.with_port(&port_str);
|
||||
}
|
||||
}
|
||||
container
|
||||
}
|
||||
|
||||
/// Add multiple volume mounts to a Container
|
||||
pub fn container_with_volumes(mut container: Container, volumes: Array) -> Container {
|
||||
for volume in volumes.iter() {
|
||||
if volume.is_string() {
|
||||
let volume_str = volume.clone().cast::<String>();
|
||||
container = container.with_volume(&volume_str);
|
||||
}
|
||||
}
|
||||
container
|
||||
}
|
||||
|
||||
/// Add multiple environment variables to a Container
|
||||
pub fn container_with_envs(mut container: Container, env_map: Map) -> Container {
|
||||
for (key, value) in env_map.iter() {
|
||||
if value.is_string() {
|
||||
let value_str = value.clone().cast::<String>();
|
||||
container = container.with_env(&key, &value_str);
|
||||
}
|
||||
}
|
||||
container
|
||||
}
|
||||
|
||||
/// Add multiple network aliases to a Container
|
||||
pub fn container_with_network_aliases(mut container: Container, aliases: Array) -> Container {
|
||||
for alias in aliases.iter() {
|
||||
if alias.is_string() {
|
||||
let alias_str = alias.clone().cast::<String>();
|
||||
container = container.with_network_alias(&alias_str);
|
||||
}
|
||||
}
|
||||
container
|
||||
}
|
||||
|
||||
/// Set memory swap limit for a Container
|
||||
pub fn container_with_memory_swap_limit(container: Container, memory_swap: &str) -> Container {
|
||||
container.with_memory_swap_limit(memory_swap)
|
||||
}
|
||||
|
||||
/// Set CPU shares for a Container
|
||||
pub fn container_with_cpu_shares(container: Container, shares: &str) -> Container {
|
||||
container.with_cpu_shares(shares)
|
||||
}
|
||||
|
||||
/// Set health check with options for a Container
|
||||
pub fn container_with_health_check_options(
|
||||
container: Container,
|
||||
cmd: &str,
|
||||
interval: Option<&str>,
|
||||
timeout: Option<&str>,
|
||||
retries: Option<i64>,
|
||||
start_period: Option<&str>,
|
||||
) -> Container {
|
||||
// Convert i64 to u32 for retries
|
||||
let retries_u32 = retries.map(|r| r as u32);
|
||||
container.with_health_check_options(cmd, interval, timeout, retries_u32, start_period)
|
||||
}
|
||||
|
||||
/// Set snapshotter for a Container
|
||||
pub fn container_with_snapshotter(container: Container, snapshotter: &str) -> Container {
|
||||
container.with_snapshotter(snapshotter)
|
||||
}
|
||||
|
||||
/// Set detach mode for a Container
|
||||
pub fn container_with_detach(container: Container, detach: bool) -> Container {
|
||||
container.with_detach(detach)
|
||||
Ok(health_check)
|
||||
}
|
||||
|
||||
/// Build and run the Container
|
||||
@@ -242,7 +119,7 @@ pub fn container_build(container: Container) -> Result<Container, Box<EvalAltRes
|
||||
_ => err,
|
||||
};
|
||||
|
||||
nerdctl_error_to_rhai_error(Err(enhanced_error))
|
||||
Err(enhanced_error.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,19 +176,19 @@ pub fn container_start(container: &mut Container) -> Result<CommandResult, Box<E
|
||||
_ => err,
|
||||
};
|
||||
|
||||
nerdctl_error_to_rhai_error(Err(enhanced_error))
|
||||
Err(enhanced_error.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop the Container
|
||||
pub fn container_stop(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(container.stop())
|
||||
container.stop().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Remove the Container
|
||||
pub fn container_remove(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(container.remove())
|
||||
container.remove().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Execute a command in the Container
|
||||
@@ -319,7 +196,7 @@ pub fn container_exec(
|
||||
container: &mut Container,
|
||||
command: &str,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(container.exec(command))
|
||||
container.exec(command).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Get container logs
|
||||
@@ -343,7 +220,7 @@ pub fn container_logs(container: &mut Container) -> Result<CommandResult, Box<Ev
|
||||
container_name, container_id, err
|
||||
));
|
||||
|
||||
nerdctl_error_to_rhai_error(Err(enhanced_error))
|
||||
Err(enhanced_error.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,7 +231,7 @@ pub fn container_copy(
|
||||
source: &str,
|
||||
dest: &str,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(container.copy(source, dest))
|
||||
container.copy(source, dest).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Create a new Map with default run options
|
||||
@@ -375,12 +252,12 @@ pub fn new_run_options() -> Map {
|
||||
///
|
||||
/// Run a container from an image.
|
||||
pub fn nerdctl_run(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::run(image, None, true, None, None))
|
||||
nerdctl::run(image, None, true, None, None).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Run a container with a name
|
||||
pub fn nerdctl_run_with_name(image: &str, name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::run(image, Some(name), true, None, None))
|
||||
nerdctl::run(image, Some(name), true, None, None).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Run a container with a port mapping
|
||||
@@ -390,49 +267,49 @@ pub fn nerdctl_run_with_port(
|
||||
port: &str,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
let ports = vec![port];
|
||||
nerdctl_error_to_rhai_error(nerdctl::run(image, Some(name), true, Some(&ports), None))
|
||||
nerdctl::run(image, Some(name), true, Some(&ports), None).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::exec
|
||||
///
|
||||
/// Execute a command in a container.
|
||||
pub fn nerdctl_exec(container: &str, command: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::exec(container, command))
|
||||
nerdctl::exec(container, command).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::copy
|
||||
///
|
||||
/// Copy files between container and local filesystem.
|
||||
pub fn nerdctl_copy(source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::copy(source, dest))
|
||||
nerdctl::copy(source, dest).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::stop
|
||||
///
|
||||
/// Stop a container.
|
||||
pub fn nerdctl_stop(container: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::stop(container))
|
||||
nerdctl::stop(container).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::remove
|
||||
///
|
||||
/// Remove a container.
|
||||
pub fn nerdctl_remove(container: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::remove(container))
|
||||
nerdctl::remove(container).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::list
|
||||
///
|
||||
/// List containers.
|
||||
pub fn nerdctl_list(all: bool) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::list(all))
|
||||
nerdctl::list(all).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::logs
|
||||
///
|
||||
/// Get container logs.
|
||||
pub fn nerdctl_logs(container: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::logs(container))
|
||||
nerdctl::logs(container).map_err(Into::into)
|
||||
}
|
||||
|
||||
//
|
||||
@@ -443,14 +320,14 @@ pub fn nerdctl_logs(container: &str) -> Result<CommandResult, Box<EvalAltResult>
|
||||
///
|
||||
/// List images in local storage.
|
||||
pub fn nerdctl_images() -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::images())
|
||||
nerdctl::images().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::image_remove
|
||||
///
|
||||
/// Remove one or more images.
|
||||
pub fn nerdctl_image_remove(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::image_remove(image))
|
||||
nerdctl::image_remove(image).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::image_push
|
||||
@@ -460,21 +337,21 @@ pub fn nerdctl_image_push(
|
||||
image: &str,
|
||||
destination: &str,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::image_push(image, destination))
|
||||
nerdctl::image_push(image, destination).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::image_tag
|
||||
///
|
||||
/// Add an additional name to a local image.
|
||||
pub fn nerdctl_image_tag(image: &str, new_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::image_tag(image, new_name))
|
||||
nerdctl::image_tag(image, new_name).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::image_pull
|
||||
///
|
||||
/// Pull an image from a registry.
|
||||
pub fn nerdctl_image_pull(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::image_pull(image))
|
||||
nerdctl::image_pull(image).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::image_commit
|
||||
@@ -484,7 +361,7 @@ pub fn nerdctl_image_commit(
|
||||
container: &str,
|
||||
image_name: &str,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::image_commit(container, image_name))
|
||||
nerdctl::image_commit(container, image_name).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::image_build
|
||||
@@ -494,7 +371,7 @@ pub fn nerdctl_image_build(
|
||||
tag: &str,
|
||||
context_path: &str,
|
||||
) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::image_build(tag, context_path))
|
||||
nerdctl::image_build(tag, context_path).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Register Nerdctl module functions with the Rhai engine
|
||||
@@ -512,31 +389,13 @@ pub fn register_nerdctl_module(engine: &mut Engine) -> Result<(), Box<EvalAltRes
|
||||
|
||||
// Register Container constructor
|
||||
engine.register_fn("nerdctl_container_new", container_new);
|
||||
engine.register_fn("nerdctl_container_from_image", container_from_image);
|
||||
|
||||
// Register HealthCheck constructor
|
||||
engine.register_fn("health_check_new", health_check_new);
|
||||
|
||||
// Register Container instance methods
|
||||
engine.register_fn("reset", container_reset);
|
||||
engine.register_fn("with_port", container_with_port);
|
||||
engine.register_fn("with_volume", container_with_volume);
|
||||
engine.register_fn("with_env", container_with_env);
|
||||
engine.register_fn("with_network", container_with_network);
|
||||
engine.register_fn("with_network_alias", container_with_network_alias);
|
||||
engine.register_fn("with_cpu_limit", container_with_cpu_limit);
|
||||
engine.register_fn("with_memory_limit", container_with_memory_limit);
|
||||
engine.register_fn("with_restart_policy", container_with_restart_policy);
|
||||
engine.register_fn("with_health_check", container_with_health_check);
|
||||
engine.register_fn("with_ports", container_with_ports);
|
||||
engine.register_fn("with_volumes", container_with_volumes);
|
||||
engine.register_fn("with_envs", container_with_envs);
|
||||
engine.register_fn("with_network_aliases", container_with_network_aliases);
|
||||
engine.register_fn("with_memory_swap_limit", container_with_memory_swap_limit);
|
||||
engine.register_fn("with_cpu_shares", container_with_cpu_shares);
|
||||
engine.register_fn(
|
||||
"with_health_check_options",
|
||||
container_with_health_check_options,
|
||||
);
|
||||
engine.register_fn("with_snapshotter", container_with_snapshotter);
|
||||
engine.register_fn("with_detach", container_with_detach);
|
||||
|
||||
engine.register_fn("build", container_build);
|
||||
engine.register_fn("start", container_start);
|
||||
engine.register_fn("stop", container_stop);
|
||||
@@ -571,11 +430,18 @@ pub fn register_nerdctl_module(engine: &mut Engine) -> Result<(), Box<EvalAltRes
|
||||
|
||||
/// Register Nerdctl module types with the Rhai engine
|
||||
fn register_nerdctl_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
|
||||
// Register Container type
|
||||
engine.register_type_with_name::<Container>("NerdctlContainer");
|
||||
|
||||
// Register getters for Container properties
|
||||
// Register getters & setters for Container properties
|
||||
// -- name
|
||||
engine.register_get("name", |container: &mut Container| container.name.clone());
|
||||
engine.register_set("image", |container: &mut Container, image: &str| {
|
||||
container.image = Some(image.to_string());
|
||||
});
|
||||
|
||||
// -- container_id
|
||||
engine.register_get(
|
||||
"container_id",
|
||||
|container: &mut Container| match &container.container_id {
|
||||
@@ -583,12 +449,37 @@ fn register_nerdctl_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>>
|
||||
None => "".to_string(),
|
||||
},
|
||||
);
|
||||
engine.register_set("container_id", |container: &mut Container, container_id: &str| {
|
||||
container.container_id = Some(container_id.to_string());
|
||||
});
|
||||
|
||||
// -- image
|
||||
engine.register_get("image", |container: &mut Container| {
|
||||
match &container.image {
|
||||
Some(img) => img.clone(),
|
||||
None => "".to_string(),
|
||||
}
|
||||
});
|
||||
engine.register_set("image", |container: &mut Container, image: &str| {
|
||||
container.image = Some(image.to_string());
|
||||
});
|
||||
|
||||
// -- config
|
||||
engine.register_get("config", |container: &mut Container| {
|
||||
container
|
||||
.config
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone().into(), v.clone().into()))
|
||||
.collect::<Map>()
|
||||
});
|
||||
engine.register_set("config", |container: &mut Container, config: Map| {
|
||||
container.config = config
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_string(), v.into_string().unwrap_or_default()))
|
||||
.collect();
|
||||
});
|
||||
|
||||
// -- ports
|
||||
engine.register_get("ports", |container: &mut Container| {
|
||||
let mut array = Array::new();
|
||||
for port in &container.ports {
|
||||
@@ -596,6 +487,14 @@ fn register_nerdctl_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>>
|
||||
}
|
||||
array
|
||||
});
|
||||
engine.register_set("ports", |container: &mut Container, ports: Array| {
|
||||
container.ports = ports
|
||||
.into_iter()
|
||||
.map(|v| v.into_string().unwrap_or_default())
|
||||
.collect();
|
||||
});
|
||||
|
||||
// -- volumes
|
||||
engine.register_get("volumes", |container: &mut Container| {
|
||||
let mut array = Array::new();
|
||||
for volume in &container.volumes {
|
||||
@@ -603,11 +502,161 @@ fn register_nerdctl_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>>
|
||||
}
|
||||
array
|
||||
});
|
||||
engine.register_set("volumes", |container: &mut Container, volumes: Array| {
|
||||
container.volumes = volumes
|
||||
.into_iter()
|
||||
.map(|v| v.into_string().unwrap_or_default())
|
||||
.collect();
|
||||
});
|
||||
|
||||
// -- env_vars
|
||||
engine.register_get("env_vars", |container: &mut Container| {
|
||||
container
|
||||
.env_vars
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone().into(), v.clone().into()))
|
||||
.collect::<Map>()
|
||||
});
|
||||
engine.register_set("env_vars", |container: &mut Container, env_vars: Map| {
|
||||
container.env_vars = env_vars
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_string(), v.into_string().unwrap_or_default()))
|
||||
.collect();
|
||||
});
|
||||
|
||||
// -- network
|
||||
engine.register_get("network", |container: &mut Container| {
|
||||
container.network.clone().unwrap_or_default()
|
||||
});
|
||||
engine.register_set("network", |container: &mut Container, network: &str| {
|
||||
container.network = Some(network.to_string());
|
||||
});
|
||||
|
||||
// -- network_aliases
|
||||
engine.register_get("network_aliases", |container: &mut Container| {
|
||||
container
|
||||
.network_aliases
|
||||
.iter()
|
||||
.map(|alias| Dynamic::from(alias.clone()))
|
||||
.collect::<Array>()
|
||||
});
|
||||
engine.register_set(
|
||||
"network_aliases",
|
||||
|container: &mut Container, aliases: Array| {
|
||||
container.network_aliases = aliases
|
||||
.into_iter()
|
||||
.map(|a| a.into_string().unwrap_or_default())
|
||||
.collect();
|
||||
},
|
||||
);
|
||||
|
||||
// -- cpu_limit
|
||||
engine.register_get("cpu_limit", |container: &mut Container| {
|
||||
container.cpu_limit.clone().unwrap_or_default()
|
||||
});
|
||||
engine.register_set("cpu_limit", |container: &mut Container, limit: &str| {
|
||||
container.cpu_limit = Some(limit.to_string());
|
||||
});
|
||||
|
||||
// -- memory_limit
|
||||
engine.register_get("memory_limit", |container: &mut Container| {
|
||||
container.memory_limit.clone().unwrap_or_default()
|
||||
});
|
||||
engine.register_set("memory_limit", |container: &mut Container, limit: &str| {
|
||||
container.memory_limit = Some(limit.to_string());
|
||||
});
|
||||
|
||||
// -- memory_swap_limit
|
||||
engine.register_get("memory_swap_limit", |container: &mut Container| {
|
||||
container.memory_swap_limit.clone().unwrap_or_default()
|
||||
});
|
||||
engine.register_set(
|
||||
"memory_swap_limit",
|
||||
|container: &mut Container, limit: &str| {
|
||||
container.memory_swap_limit = Some(limit.to_string());
|
||||
},
|
||||
);
|
||||
|
||||
// -- cpu_shares
|
||||
engine.register_get("cpu_shares", |container: &mut Container| {
|
||||
container.cpu_shares.clone().unwrap_or_default()
|
||||
});
|
||||
engine.register_set("cpu_shares", |container: &mut Container, shares: &str| {
|
||||
container.cpu_shares = Some(shares.to_string());
|
||||
});
|
||||
|
||||
// -- restart_policy
|
||||
engine.register_get("restart_policy", |container: &mut Container| {
|
||||
container.restart_policy.clone().unwrap_or_default()
|
||||
});
|
||||
engine.register_set(
|
||||
"restart_policy",
|
||||
|container: &mut Container, policy: &str| {
|
||||
container.restart_policy = Some(policy.to_string());
|
||||
},
|
||||
);
|
||||
|
||||
// -- health_check
|
||||
engine.register_type_with_name::<HealthCheck>("NerdctlHealthCheck");
|
||||
engine.register_get("health_check", |container: &mut Container| {
|
||||
if let Some(health_check) = container.health_check.clone() {
|
||||
Dynamic::from(health_check)
|
||||
} else {
|
||||
Dynamic::UNIT
|
||||
}
|
||||
});
|
||||
engine.register_set(
|
||||
"health_check",
|
||||
|container: &mut Container, health_check: HealthCheck| {
|
||||
container.health_check = Some(health_check);
|
||||
},
|
||||
);
|
||||
// Register getters & setters for HealthCheck properties
|
||||
engine.register_get("cmd", |hc: &mut HealthCheck| hc.cmd.clone());
|
||||
engine.register_set("cmd", |hc: &mut HealthCheck, cmd: &str| {
|
||||
hc.cmd = cmd.to_string();
|
||||
});
|
||||
engine.register_get("interval", |hc: &mut HealthCheck| {
|
||||
hc.interval.clone().unwrap_or_default()
|
||||
});
|
||||
engine.register_set("interval", |hc: &mut HealthCheck, interval: &str| {
|
||||
hc.interval = Some(interval.to_string());
|
||||
});
|
||||
engine.register_get("timeout", |hc: &mut HealthCheck| {
|
||||
hc.timeout.clone().unwrap_or_default()
|
||||
});
|
||||
engine.register_set("timeout", |hc: &mut HealthCheck, timeout: &str| {
|
||||
hc.timeout = Some(timeout.to_string());
|
||||
});
|
||||
engine.register_get("retries", |hc: &mut HealthCheck| {
|
||||
hc.retries.map_or(0, |r| r as i64)
|
||||
});
|
||||
engine.register_set("retries", |hc: &mut HealthCheck, retries: i64| {
|
||||
hc.retries = Some(retries as u32);
|
||||
});
|
||||
engine.register_get("start_period", |hc: &mut HealthCheck| {
|
||||
hc.start_period.clone().unwrap_or_default()
|
||||
});
|
||||
engine.register_set("start_period", |hc: &mut HealthCheck, start_period: &str| {
|
||||
hc.start_period = Some(start_period.to_string());
|
||||
});
|
||||
|
||||
// -- detach
|
||||
engine.register_get("detach", |container: &mut Container| container.detach);
|
||||
engine.register_set("detach", |container: &mut Container, detach: bool| {
|
||||
container.detach = detach;
|
||||
});
|
||||
|
||||
// -- snapshotter
|
||||
engine.register_get("snapshotter", |container: &mut Container| {
|
||||
container.snapshotter.clone().unwrap_or_default()
|
||||
});
|
||||
engine.register_set("snapshotter", |container: &mut Container, snapshotter: &str| {
|
||||
container.snapshotter = Some(snapshotter.to_string());
|
||||
});
|
||||
|
||||
// Register Image type and methods
|
||||
engine.register_type_with_name::<Image>("NerdctlImage");
|
||||
|
||||
// Register getters for Image properties
|
||||
engine.register_get("id", |img: &mut Image| img.id.clone());
|
||||
engine.register_get("repository", |img: &mut Image| img.repository.clone());
|
||||
|
Reference in New Issue
Block a user