Compare commits
186 Commits
9f33c94020
...
network_se
Author | SHA1 | Date | |
---|---|---|---|
|
f8436a726e | ||
|
182b0edeb7 | ||
|
f5670f20be | ||
|
0f4ed1d64d | ||
|
f4512b66cf | ||
|
da3da0ae30 | ||
|
784f87db97 | ||
|
773db2238d | ||
|
e8a369e3a2 | ||
|
4b4f3371b0 | ||
|
1bb731711b | ||
|
af89ef0149 | ||
|
768e3e176d | ||
|
aa0248ef17 | ||
|
aab2b6f128 | ||
|
d735316b7f | ||
|
d1c80863b8 | ||
|
169c62da47 | ||
|
33a5f24981 | ||
|
d7562ce466 | ||
ca736d62f3 | |||
|
078c6f723b | ||
|
9fdb8d8845 | ||
8203a3b1ff | |||
1770ac561e | |||
|
eed6dbf8dc | ||
4cd4e04028 | |||
8cc828fc0e | |||
56af312aad | |||
dfd6931c5b | |||
6e01f99958 | |||
0c02d0e99f | |||
7856fc0a4e | |||
|
758e59e921 | ||
f1806eb788 | |||
|
6e5d9b35e8 | ||
61f5331804 | |||
|
423b7bfa7e | ||
fc2830da31 | |||
|
6b12001ca2 | ||
|
99e121b0d8 | ||
|
502e345f91 | ||
|
352e846410 | ||
|
b72c50bed9 | ||
|
95122dffee | ||
|
a63cbe2bd9 | ||
|
1e4c0ac41a | ||
|
0e49be8d71 | ||
|
32339e6063 | ||
|
131d978450 | ||
|
46ad848e7e | ||
|
b4e370b668 | ||
ef8cc74d2b | |||
|
23db07b0bd | ||
b4dfa7733d | |||
|
e01b83f12a | ||
|
52f2f7e3c4 | ||
717cd7b16f | |||
|
e125bb6511 | ||
|
8012a66250 | ||
|
6dead402a2 | ||
|
c94467c205 | ||
|
b737cd6337 | ||
|
455f84528b | ||
|
3e3d0a1d45 | ||
|
511729c477 | ||
|
74217364fa | ||
|
d22fd686b7 | ||
|
c4cdb8126c | ||
|
a35edc2030 | ||
|
a7a7353aa1 | ||
|
4a8d3bfd24 | ||
|
3e617c2489 | ||
|
4d51518f31 | ||
|
e031b03e04 | ||
ba9103685f | |||
dee38eb6c2 | |||
49c879359b | |||
c0df07d6df | |||
6a1e70c484 | |||
e7e8e7daf8 | |||
8a8ead17cb | |||
0e7dba9466 | |||
f0d7636cda | |||
3a6bde02d5 | |||
3a7b323f9a | |||
66d5c8588a | |||
29a06d2bb4 | |||
|
bb39f3e3f2 | ||
|
5194f5245d | ||
a9255de679 | |||
7d05567ad2 | |||
|
c0e11c6510 | ||
|
fedf957079 | ||
|
65e404e517 | ||
944f22be23 | |||
887e66bb17 | |||
|
e5a4a1b634
|
||
|
7f55cf4fba
|
||
|
c26e0e5ad8 | ||
|
365814b424
|
||
|
cc4e087f1d | ||
|
229fef217f | ||
|
dd84ce3f48 | ||
|
7b8b8c662e | ||
|
d29a8fbb67
|
||
|
771df07c25 | ||
|
9a23c4cc09 | ||
|
2014c63b78
|
||
|
2adda10664
|
||
|
7b1908b676
|
||
|
e9b867a36e
|
||
|
78c0fd7871
|
||
|
e44ee83e74
|
||
0c425470a5 | |||
|
3e64a53a83 | ||
|
3225b3f029 | ||
|
3417e2c1ff | ||
|
7add64562e | ||
|
809599d60c | ||
|
25f2ae6fa9 | ||
|
dfe6c91273
|
||
a4438d63e0 | |||
393c4270d4 | |||
495fe92321 | |||
577d80b282 | |||
3f8aecb786 | |||
|
c7a5699798 | ||
|
3a0900fc15 | ||
|
916eabfa42 | ||
a8ed0900fd | |||
e47e163285 | |||
8aa2b2da26 | |||
992481ce1b | |||
516d0177e7 | |||
|
8285fdb7b9 | ||
|
1ebd591f19 | ||
7298645368 | |||
f669bdb84f | |||
654f91b849 | |||
619ce57776 | |||
2695b5f5f7 | |||
7828f82f58 | |||
7a346a1dd1 | |||
07390c3cae | |||
|
138dce66fa | ||
|
49e85ff8e6 | ||
|
f386890a8a | ||
663367ea57 | |||
|
114d63e590 | ||
|
22f87b320e | ||
|
f002445c9e | ||
|
61bd58498a | ||
|
d3c645e8e6 | ||
98ab2e1536 | |||
|
83662736c0 | ||
|
4897eb9133 | ||
|
1286939608 | ||
|
32217b6545 | ||
|
4578b10acb | ||
|
4e166f7750 | ||
|
d6905916ee | ||
|
76582706ab | ||
2cd9faf4fa | |||
104097ee4b | |||
21893ce225 | |||
0fa9eddd1c | |||
7bf2ffe47d | |||
768542afd3 | |||
e9a73327e3 | |||
88e4a2a4b1 | |||
8605e08a65 | |||
749c60e131 | |||
fe7a676cac | |||
4be9445702 | |||
78db13d738 | |||
4c50d4b62c | |||
c177ac5efb | |||
6d4c1742e7 | |||
5fec3b967b | |||
6de7bf9b56 | |||
245aee12bf | |||
d336153247 | |||
3803a54529 | |||
7cdd9f5559 | |||
e48063a79c |
227
.github/workflows/publish.yml
vendored
Normal file
227
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,227 @@
|
||||
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
|
73
.github/workflows/rhai-tests.yml
vendored
Normal file
73
.github/workflows/rhai-tests.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: Rhai Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ '*' ]
|
||||
paths:
|
||||
- 'src/rhai_tests/**'
|
||||
- 'src/rhai/**'
|
||||
- 'src/git/**'
|
||||
- 'src/os/**'
|
||||
- 'run_rhai_tests.sh'
|
||||
- '.github/workflows/rhai-tests.yml'
|
||||
pull_request:
|
||||
branches: [ '*' ]
|
||||
paths:
|
||||
- 'src/rhai_tests/**'
|
||||
- 'src/rhai/**'
|
||||
- 'src/git/**'
|
||||
- 'src/os/**'
|
||||
- 'run_rhai_tests.sh'
|
||||
- '.github/workflows/rhai-tests.yml'
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
|
||||
jobs:
|
||||
rhai-tests:
|
||||
name: Run Rhai Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- name: Build herodo
|
||||
run: |
|
||||
cargo build --bin herodo
|
||||
echo "${{ github.workspace }}/target/debug" >> $GITHUB_PATH
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git curl
|
||||
|
||||
- name: Run Rhai tests
|
||||
run: |
|
||||
chmod +x run_rhai_tests.sh
|
||||
./run_rhai_tests.sh
|
||||
|
||||
- name: Check for test failures
|
||||
run: |
|
||||
if grep -q "Some tests failed" run_rhai_tests.log; then
|
||||
echo "::error::Some Rhai tests failed. Check the logs for details."
|
||||
exit 1
|
||||
else
|
||||
echo "All Rhai tests passed!"
|
||||
fi
|
||||
if: always()
|
233
.github/workflows/test-publish.yml
vendored
Normal file
233
.github/workflows/test-publish.yml
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
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
|
45
.gitignore
vendored
45
.gitignore
vendored
@@ -19,3 +19,48 @@ Cargo.lock
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
/rhai_test_template
|
||||
/rhai_test_download
|
||||
/rhai_test_fs
|
||||
run_rhai_tests.log
|
||||
new_location
|
||||
log.txt
|
||||
file.txt
|
||||
fix_doc*
|
||||
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
bun.lockb
|
||||
bun.lock
|
||||
|
||||
yarn.lock
|
||||
|
||||
build.sh
|
||||
build_dev.sh
|
||||
develop.sh
|
||||
|
||||
docusaurus.config.ts
|
||||
|
||||
sidebars.ts
|
||||
|
||||
tsconfig.json
|
||||
Cargo.toml.bak
|
||||
for_augment
|
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"gitea": {
|
||||
"command": "/Users/despiegk/hero/bin/mcpgitea",
|
||||
"args": [
|
||||
"-t", "stdio",
|
||||
"--host", "https://gitea.com",
|
||||
"--token", "5bd13c898368a2edbfcef43f898a34857b51b37a"
|
||||
],
|
||||
"env": {
|
||||
"GITEA_HOST": "https://git.ourworld.tf/",
|
||||
"GITEA_ACCESS_TOKEN": "5bd13c898368a2edbfcef43f898a34857b51b37a"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
208
Cargo.toml
208
Cargo.toml
@@ -4,38 +4,196 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["PlanetFirst <info@incubaid.com>"]
|
||||
description = "System Abstraction Layer - A library for easy interaction with operating system features"
|
||||
repository = "https://git.ourworld.tf/herocode/sal"
|
||||
repository = "https://git.threefold.info/herocode/sal"
|
||||
license = "Apache-2.0"
|
||||
keywords = ["system", "os", "abstraction", "platform", "filesystem"]
|
||||
categories = ["os", "filesystem", "api-bindings"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
# Cross-platform functionality
|
||||
[workspace]
|
||||
members = [
|
||||
"packages/clients/myceliumclient",
|
||||
"packages/clients/postgresclient",
|
||||
"packages/clients/redisclient",
|
||||
"packages/clients/zinitclient",
|
||||
"packages/core/net",
|
||||
"packages/core/text",
|
||||
"packages/crypt/vault",
|
||||
"packages/data/ourdb",
|
||||
"packages/data/radixtree",
|
||||
"packages/data/tst",
|
||||
"packages/system/git",
|
||||
"packages/system/kubernetes",
|
||||
"packages/system/os",
|
||||
"packages/system/process",
|
||||
"packages/system/virt",
|
||||
"rhai",
|
||||
"rhailib",
|
||||
"herodo",
|
||||
"packages/clients/hetznerclient",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.metadata]
|
||||
# Workspace-level metadata
|
||||
rust-version = "1.70.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
# Core shared dependencies with consistent versions
|
||||
anyhow = "1.0.98"
|
||||
base64 = "0.22.1"
|
||||
dirs = "6.0.0"
|
||||
env_logger = "0.11.8"
|
||||
futures = "0.3.30"
|
||||
glob = "0.3.1"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2"
|
||||
cfg-if = "1.0"
|
||||
thiserror = "1.0" # For error handling
|
||||
redis = "0.22.0" # Redis client
|
||||
lazy_static = "1.4.0" # For lazy initialization of static variables
|
||||
regex = "1.8.1" # For regex pattern matching
|
||||
serde = { version = "1.0", features = ["derive"] } # For serialization/deserialization
|
||||
serde_json = "1.0" # For JSON handling
|
||||
glob = "0.3.1" # For file pattern matching
|
||||
tempfile = "3.5" # For temporary file operations
|
||||
log = "0.4" # Logging facade
|
||||
rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language
|
||||
clap = "2.33" # Command-line argument parsing
|
||||
log = "0.4"
|
||||
once_cell = "1.18.0"
|
||||
rand = "0.8.5"
|
||||
regex = "1.8.1"
|
||||
reqwest = { version = "0.12.15", features = ["json", "blocking"] }
|
||||
rhai = { version = "1.12.0", features = ["sync"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tempfile = "3.5"
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.45.0", features = ["full"] }
|
||||
url = "2.4"
|
||||
uuid = { version = "1.16.0", features = ["v4"] }
|
||||
|
||||
# Optional features for specific OS functionality
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = "0.26" # Unix-specific functionality
|
||||
# Database dependencies
|
||||
postgres = "0.19.10"
|
||||
r2d2_postgres = "0.18.2"
|
||||
redis = "0.31.0"
|
||||
tokio-postgres = "0.7.13"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.48", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_Storage_FileSystem"] }
|
||||
# Crypto dependencies
|
||||
chacha20poly1305 = "0.10.1"
|
||||
k256 = { version = "0.13.4", features = ["ecdsa", "ecdh"] }
|
||||
sha2 = "0.10.7"
|
||||
hex = "0.4"
|
||||
bincode = { version = "2.0.1", features = ["serde"] }
|
||||
pbkdf2 = "0.12.2"
|
||||
getrandom = { version = "0.3.3", features = ["wasm_js"] }
|
||||
tera = "1.19.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.5" # For tests that need temporary files/directories
|
||||
# Ethereum dependencies
|
||||
ethers = { version = "2.0.7", features = ["legacy"] }
|
||||
|
||||
[[bin]]
|
||||
name = "herodo"
|
||||
path = "src/bin/herodo.rs"
|
||||
# Platform-specific dependencies
|
||||
nix = "0.30.1"
|
||||
windows = { version = "0.61.1", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Threading",
|
||||
"Win32_Storage_FileSystem",
|
||||
] }
|
||||
|
||||
# Specialized dependencies
|
||||
zinit-client = "0.4.0"
|
||||
urlencoding = "2.1.3"
|
||||
tokio-test = "0.4.4"
|
||||
kube = { version = "0.95.0", features = ["client", "config", "derive"] }
|
||||
k8s-openapi = { version = "0.23.0", features = ["latest"] }
|
||||
tokio-retry = "0.3.0"
|
||||
governor = "0.6.3"
|
||||
tower = { version = "0.5.2", features = ["timeout", "limit"] }
|
||||
serde_yaml = "0.9"
|
||||
postgres-types = "0.2.5"
|
||||
r2d2 = "0.8.10"
|
||||
|
||||
# SAL dependencies
|
||||
sal-git = { path = "packages/system/git" }
|
||||
sal-kubernetes = { path = "packages/system/kubernetes" }
|
||||
sal-redisclient = { path = "packages/clients/redisclient" }
|
||||
sal-mycelium = { path = "packages/clients/myceliumclient" }
|
||||
sal-hetzner = { path = "packages/clients/hetznerclient" }
|
||||
sal-text = { path = "packages/core/text" }
|
||||
sal-os = { path = "packages/system/os" }
|
||||
sal-net = { path = "packages/core/net" }
|
||||
sal-zinit-client = { path = "packages/clients/zinitclient" }
|
||||
sal-process = { path = "packages/system/process" }
|
||||
sal-virt = { path = "packages/system/virt" }
|
||||
sal-postgresclient = { path = "packages/clients/postgresclient" }
|
||||
sal-vault = { path = "packages/crypt/vault" }
|
||||
sal-rhai = { path = "rhai" }
|
||||
sal-service-manager = { path = "_archive/service_manager" }
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
# Optional dependencies - users can choose which modules to include
|
||||
sal-git = { workspace = true, optional = true }
|
||||
sal-kubernetes = { workspace = true, optional = true }
|
||||
sal-redisclient = { workspace = true, optional = true }
|
||||
sal-mycelium = { workspace = true, optional = true }
|
||||
sal-hetzner = { workspace = true, optional = true }
|
||||
sal-text = { workspace = true, optional = true }
|
||||
sal-os = { workspace = true, optional = true }
|
||||
sal-net = { workspace = true, optional = true }
|
||||
sal-zinit-client = { workspace = true, optional = true }
|
||||
sal-process = { workspace = true, optional = true }
|
||||
sal-virt = { workspace = true, optional = true }
|
||||
sal-postgresclient = { workspace = true, optional = true }
|
||||
sal-vault = { workspace = true, optional = true }
|
||||
sal-rhai = { workspace = true, optional = true }
|
||||
sal-service-manager = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
# Individual module features
|
||||
git = ["dep:sal-git"]
|
||||
kubernetes = ["dep:sal-kubernetes"]
|
||||
redisclient = ["dep:sal-redisclient"]
|
||||
mycelium = ["dep:sal-mycelium"]
|
||||
hetzner = ["dep:sal-hetzner"]
|
||||
text = ["dep:sal-text"]
|
||||
os = ["dep:sal-os"]
|
||||
net = ["dep:sal-net"]
|
||||
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"]
|
||||
# service_manager is removed as it's not a direct member anymore
|
||||
|
||||
# Convenience feature groups
|
||||
core = ["os", "process", "text", "net"]
|
||||
clients = ["redisclient", "postgresclient", "zinit_client", "mycelium", "hetzner"]
|
||||
infrastructure = ["git", "vault", "kubernetes", "virt"]
|
||||
scripting = ["rhai"]
|
||||
all = [
|
||||
"git",
|
||||
"kubernetes",
|
||||
"redisclient",
|
||||
"mycelium",
|
||||
"hetzner",
|
||||
"text",
|
||||
"os",
|
||||
"net",
|
||||
"zinit_client",
|
||||
"process",
|
||||
"virt",
|
||||
"postgresclient",
|
||||
"vault",
|
||||
"rhai",
|
||||
]
|
||||
|
||||
# Examples
|
||||
[[example]]
|
||||
name = "postgres_cluster"
|
||||
path = "examples/kubernetes/clusters/postgres.rs"
|
||||
required-features = ["kubernetes"]
|
||||
|
||||
[[example]]
|
||||
name = "redis_cluster"
|
||||
path = "examples/kubernetes/clusters/redis.rs"
|
||||
required-features = ["kubernetes"]
|
||||
|
||||
[[example]]
|
||||
name = "generic_cluster"
|
||||
path = "examples/kubernetes/clusters/generic.rs"
|
||||
required-features = ["kubernetes"]
|
||||
|
239
PUBLISHING.md
Normal file
239
PUBLISHING.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# 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.
|
161
README.md
161
README.md
@@ -1,73 +1,136 @@
|
||||
# SAL (System Abstraction Layer)
|
||||
# Herocode Herolib Rust Repository
|
||||
|
||||
A Rust library that provides a unified interface for interacting with operating system features across different platforms. It abstracts away platform-specific details, allowing developers to write cross-platform code with ease.
|
||||
## Overview
|
||||
|
||||
## Features
|
||||
This repository contains the **Herocode Herolib** Rust library and a collection of scripts, examples, and utilities for building, testing, and publishing the SAL (System Abstraction Layer) crates. The repository includes:
|
||||
|
||||
- **File System Operations**: Simplified file and directory management
|
||||
- **Process Management**: Create, monitor, and control processes
|
||||
- **System Information**: Access system details and metrics
|
||||
- **Git Integration**: Interface with Git repositories
|
||||
- **Redis Client**: Robust Redis connection management and command execution
|
||||
- **Text Processing**: Utilities for text manipulation and formatting
|
||||
- **Rust crates** for various system components (e.g., `os`, `process`, `text`, `git`, `vault`, `kubernetes`, etc.).
|
||||
- **Rhai scripts** and test suites for each crate.
|
||||
- **Utility scripts** to automate common development tasks.
|
||||
|
||||
## Modules
|
||||
## Scripts
|
||||
|
||||
### Redis Client
|
||||
The repository provides three primary helper scripts located in the repository root:
|
||||
|
||||
The Redis client module provides a robust wrapper around the Redis client library for Rust, offering:
|
||||
| Script | Description | Typical Usage |
|
||||
|--------|-------------|--------------|
|
||||
| `scripts/publish-all.sh` | Publishes all SAL crates to **crates.io** in the correct dependency order. Handles version bumping, dependency updates, dry‑run mode, and rate‑limiting. | `./scripts/publish-all.sh [--dry-run] [--wait <seconds>] [--version <ver>]` |
|
||||
| `build_herodo.sh` | Builds the `herodo` binary from the `herodo` package and optionally runs a specified Rhai script. | `./build_herodo.sh [script_name]` |
|
||||
| `run_rhai_tests.sh` | Executes all Rhai test suites across the repository, logging results and providing a summary. | `./run_rhai_tests.sh` |
|
||||
|
||||
- Automatic connection management and reconnection
|
||||
- Support for both Unix socket and TCP connections
|
||||
- Database selection via environment variables
|
||||
- Thread-safe global client instance
|
||||
- Simple command execution interface
|
||||
Below are detailed usage instructions for each script.
|
||||
|
||||
[View Redis Client Documentation](src/redisclient/README.md)
|
||||
---
|
||||
|
||||
### OS Module
|
||||
## 1. `scripts/publish-all.sh`
|
||||
|
||||
Provides platform-independent interfaces for operating system functionality.
|
||||
### Purpose
|
||||
|
||||
### Git Module
|
||||
- Publishes each SAL crate in the correct dependency order.
|
||||
- Updates crate versions (if `--version` is supplied).
|
||||
- Updates path dependencies to version dependencies before publishing.
|
||||
- Supports **dry‑run** mode to preview actions without publishing.
|
||||
- Handles rate‑limiting between crate publishes.
|
||||
|
||||
Tools for interacting with Git repositories programmatically.
|
||||
### Options
|
||||
|
||||
### Process Module
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--dry-run` | Shows what would be published without actually publishing. |
|
||||
| `--wait <seconds>` | Wait time between publishes (default: 15 s). |
|
||||
| `--version <ver>` | Set a new version for all crates (updates `Cargo.toml` files). |
|
||||
| `-h, --help` | Show help message. |
|
||||
|
||||
Utilities for process creation, monitoring, and management.
|
||||
### Example Usage
|
||||
|
||||
### Text Module
|
||||
```bash
|
||||
# Dry run – no crates will be published
|
||||
./scripts/publish-all.sh --dry-run
|
||||
|
||||
Text processing utilities for common operations.
|
||||
# Publish with a custom wait time and version bump
|
||||
./scripts/publish-all.sh --wait 30 --version 1.2.3
|
||||
|
||||
## Usage
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sal = "0.1.0"
|
||||
# Normal publish (no dry‑run)
|
||||
./scripts/publish-all.sh
|
||||
```
|
||||
|
||||
Basic example:
|
||||
### Notes
|
||||
|
||||
```rust
|
||||
use sal::redisclient::execute;
|
||||
use redis::cmd;
|
||||
- Must be run from the repository root (where `Cargo.toml` lives).
|
||||
- Requires `cargo` and a logged‑in `cargo` session (`cargo login`).
|
||||
- The script automatically updates dependencies in each crate’s `Cargo.toml` to use the new version before publishing.
|
||||
|
||||
fn main() -> redis::RedisResult<()> {
|
||||
// Execute a Redis command
|
||||
let mut cmd = redis::cmd("SET");
|
||||
cmd.arg("example_key").arg("example_value");
|
||||
execute(&mut cmd)?;
|
||||
---
|
||||
|
||||
// Retrieve the value
|
||||
let mut get_cmd = redis::cmd("GET");
|
||||
get_cmd.arg("example_key");
|
||||
let value: String = execute(&mut get_cmd)?;
|
||||
println!("Value: {}", value);
|
||||
## 2. `build_herodo.sh`
|
||||
|
||||
Ok(())
|
||||
}
|
||||
### Purpose
|
||||
|
||||
- Builds the `herodo` binary from the `herodo` package.
|
||||
- Copies the binary to a system‑wide location (`/usr/local/bin`) if run as root, otherwise to `~/hero/bin`.
|
||||
- Optionally runs a specified Rhai script after building.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Build only
|
||||
./build_herodo.sh
|
||||
|
||||
# Build and run a specific Rhai script (e.g., `example`):
|
||||
./build_herodo.sh example
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
- The script changes to its own directory, builds the `herodo` crate (`cargo build`), and copies the binary.
|
||||
- If a script name is provided, it looks for the script in:
|
||||
- `src/rhaiexamples/<name>.rhai`
|
||||
- `src/herodo/scripts/<name>.rhai`
|
||||
- If the script is not found, the script exits with an error.
|
||||
|
||||
---
|
||||
|
||||
## 3. `run_rhai_tests.sh`
|
||||
|
||||
### Purpose
|
||||
|
||||
- Runs **all** Rhai test suites across the repository.
|
||||
- Supports both the legacy `rhai_tests` directory and the newer `*/tests/rhai` layout.
|
||||
- Logs output to `run_rhai_tests.log` and prints a summary.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
./run_rhai_tests.sh
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
- Colored console output for readability.
|
||||
- Log file (`run_rhai_tests.log`) contains full output for later review.
|
||||
- Summary includes total modules, passed, and failed counts.
|
||||
- Exit code `0` if all tests pass, `1` otherwise.
|
||||
|
||||
---
|
||||
|
||||
## General Development Workflow
|
||||
|
||||
1. **Build**: Use `build_herodo.sh` to compile the `herodo` binary.
|
||||
2. **Test**: Run `run_rhai_tests.sh` to ensure all Rhai scripts pass.
|
||||
3. **Publish**: When ready to release, use `scripts/publish-all.sh` (with `--dry-run` first to verify).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Rust toolchain** (`cargo`, `rustc`) installed.
|
||||
- **Rhai** interpreter (`herodo`) built and available.
|
||||
- **Git** for version control.
|
||||
- **Cargo login** for publishing to crates.io.
|
||||
|
||||
## License
|
||||
|
||||
See `LICENSE` for details.
|
||||
|
||||
---
|
||||
|
||||
**Happy coding!**
|
||||
|
43
_archive/service_manager/Cargo.toml
Normal file
43
_archive/service_manager/Cargo.toml
Normal file
@@ -0,0 +1,43 @@
|
||||
[package]
|
||||
name = "sal-service-manager"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["PlanetFirst <info@incubaid.com>"]
|
||||
description = "SAL Service Manager - Cross-platform service management for dynamic worker deployment"
|
||||
repository = "https://git.threefold.info/herocode/sal"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
# Use workspace dependencies for consistency
|
||||
thiserror = "1.0"
|
||||
tokio = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
# Use base zinit-client instead of SAL wrapper
|
||||
zinit-client = { version = "0.4.0" }
|
||||
# Optional Rhai integration
|
||||
rhai = { workspace = true, optional = true }
|
||||
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
# macOS-specific dependencies for launchctl
|
||||
plist = "1.6"
|
||||
|
||||
[features]
|
||||
default = ["zinit"]
|
||||
zinit = []
|
||||
rhai = ["dep:rhai"]
|
||||
|
||||
# Enable zinit feature for tests
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
rhai = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
env_logger = "0.10"
|
||||
|
||||
[[test]]
|
||||
name = "zinit_integration_tests"
|
||||
required-features = ["zinit"]
|
198
_archive/service_manager/README.md
Normal file
198
_archive/service_manager/README.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# SAL Service Manager
|
||||
|
||||
[](https://crates.io/crates/sal-service-manager)
|
||||
[](https://docs.rs/sal-service-manager)
|
||||
|
||||
A cross-platform service management library for the System Abstraction Layer (SAL). This crate provides a unified interface for managing system services across different platforms, enabling dynamic deployment of workers and services.
|
||||
|
||||
## Features
|
||||
|
||||
- **Cross-platform service management** - Unified API across macOS and Linux
|
||||
- **Dynamic worker deployment** - Perfect for circle workers and on-demand services
|
||||
- **Platform-specific implementations**:
|
||||
- **macOS**: Uses `launchctl` with plist management
|
||||
- **Linux**: Uses `zinit` for lightweight service management (systemd also available)
|
||||
- **Complete lifecycle management** - Start, stop, restart, status monitoring, and log retrieval
|
||||
- **Service configuration** - Environment variables, working directories, auto-restart
|
||||
- **Production-ready** - Comprehensive error handling and resource management
|
||||
|
||||
## Usage
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sal-service-manager = "0.1.0"
|
||||
```
|
||||
|
||||
Or use it as part of the SAL ecosystem:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sal = { version = "0.1.0", features = ["service_manager"] }
|
||||
```
|
||||
|
||||
## Primary Use Case: Dynamic Circle Worker Management
|
||||
|
||||
This service manager was designed specifically for dynamic deployment of circle workers in freezone environments. When a new resident registers, you can instantly launch a dedicated circle worker:
|
||||
|
||||
```rust,no_run
|
||||
use sal_service_manager::{create_service_manager, ServiceConfig};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// New resident registration triggers worker creation
|
||||
fn deploy_circle_worker(resident_id: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let manager = create_service_manager();
|
||||
|
||||
let mut env = HashMap::new();
|
||||
env.insert("RESIDENT_ID".to_string(), resident_id.to_string());
|
||||
env.insert("WORKER_TYPE".to_string(), "circle".to_string());
|
||||
|
||||
let config = ServiceConfig {
|
||||
name: format!("circle-worker-{}", resident_id),
|
||||
binary_path: "/usr/bin/circle-worker".to_string(),
|
||||
args: vec!["--resident".to_string(), resident_id.to_string()],
|
||||
working_directory: Some("/var/lib/circle-workers".to_string()),
|
||||
environment: env,
|
||||
auto_restart: true,
|
||||
};
|
||||
|
||||
// Deploy the worker
|
||||
manager.start(&config)?;
|
||||
println!("✅ Circle worker deployed for resident: {}", resident_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Basic Usage Example
|
||||
|
||||
Here is an example of the core service management API:
|
||||
|
||||
```rust,no_run
|
||||
use sal_service_manager::{create_service_manager, ServiceConfig};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let service_manager = create_service_manager();
|
||||
|
||||
let config = ServiceConfig {
|
||||
name: "my-service".to_string(),
|
||||
binary_path: "/usr/local/bin/my-service-executable".to_string(),
|
||||
args: vec!["--config".to_string(), "/etc/my-service.conf".to_string()],
|
||||
working_directory: Some("/var/tmp".to_string()),
|
||||
environment: HashMap::new(),
|
||||
auto_restart: true,
|
||||
};
|
||||
|
||||
// Start a new service
|
||||
service_manager.start(&config)?;
|
||||
|
||||
// Get the status of the service
|
||||
let status = service_manager.status("my-service")?;
|
||||
println!("Service status: {:?}", status);
|
||||
|
||||
// Stop the service
|
||||
service_manager.stop("my-service")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Comprehensive examples are available in the SAL examples directory:
|
||||
|
||||
### Circle Worker Manager Example
|
||||
|
||||
The primary use case - dynamically launching circle workers for new freezone residents:
|
||||
|
||||
```bash
|
||||
# Run the circle worker management example
|
||||
herodo examples/service_manager/circle_worker_manager.rhai
|
||||
```
|
||||
|
||||
This example demonstrates:
|
||||
- Creating service configurations for circle workers
|
||||
- Complete service lifecycle management
|
||||
- Error handling and status monitoring
|
||||
- Service cleanup and removal
|
||||
|
||||
### Basic Usage Example
|
||||
|
||||
A simpler example showing the core API:
|
||||
|
||||
```bash
|
||||
# Run the basic usage example
|
||||
herodo examples/service_manager/basic_usage.rhai
|
||||
```
|
||||
|
||||
See `examples/service_manager/README.md` for detailed documentation.
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
cargo test -p sal-service-manager
|
||||
```
|
||||
|
||||
For Rhai integration tests:
|
||||
|
||||
```bash
|
||||
cargo test -p sal-service-manager --features rhai
|
||||
```
|
||||
|
||||
### Testing with Herodo
|
||||
|
||||
To test the service manager with real Rhai scripts using herodo, first build herodo:
|
||||
|
||||
```bash
|
||||
./build_herodo.sh
|
||||
```
|
||||
|
||||
Then run Rhai scripts that use the service manager:
|
||||
|
||||
```bash
|
||||
herodo your_service_script.rhai
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Linux (zinit/systemd)
|
||||
|
||||
The service manager automatically discovers running zinit servers and falls back to systemd if none are found.
|
||||
|
||||
**For zinit (recommended):**
|
||||
|
||||
```bash
|
||||
# Start zinit with default socket
|
||||
zinit -s /tmp/zinit.sock init
|
||||
|
||||
# Or with a custom socket path
|
||||
zinit -s /var/run/zinit.sock init
|
||||
```
|
||||
|
||||
**Socket Discovery:**
|
||||
The service manager will automatically find running zinit servers by checking:
|
||||
1. `ZINIT_SOCKET_PATH` environment variable (if set)
|
||||
2. Common socket locations: `/var/run/zinit.sock`, `/tmp/zinit.sock`, `/run/zinit.sock`, `./zinit.sock`
|
||||
|
||||
**Custom socket path:**
|
||||
```bash
|
||||
# Set custom socket path
|
||||
export ZINIT_SOCKET_PATH=/your/custom/path/zinit.sock
|
||||
```
|
||||
|
||||
**Systemd fallback:**
|
||||
If no zinit server is detected, the service manager automatically falls back to systemd.
|
||||
|
||||
### macOS (launchctl)
|
||||
|
||||
No additional setup required - uses the built-in launchctl system.
|
||||
|
||||
## Platform Support
|
||||
|
||||
- **macOS**: Full support using `launchctl` for service management
|
||||
- **Linux**: Full support using `zinit` for service management (systemd also available as alternative)
|
||||
- **Windows**: Not currently supported
|
47
_archive/service_manager/examples/README.md
Normal file
47
_archive/service_manager/examples/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Service Manager Examples
|
||||
|
||||
This directory contains examples demonstrating the usage of the `sal-service-manager` crate.
|
||||
|
||||
## Running Examples
|
||||
|
||||
To run any example, use the following command structure from the `service_manager` crate's root directory:
|
||||
|
||||
```sh
|
||||
cargo run --example <EXAMPLE_NAME>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1. `simple_service`
|
||||
|
||||
This example demonstrates the ideal, clean lifecycle of a service using the separated `create` and `start` steps.
|
||||
|
||||
**Behavior:**
|
||||
1. Creates a new service definition.
|
||||
2. Starts the newly created service.
|
||||
3. Checks its status to confirm it's running.
|
||||
4. Stops the service.
|
||||
5. Checks its status again to confirm it's stopped.
|
||||
6. Removes the service definition.
|
||||
|
||||
**Run it:**
|
||||
```sh
|
||||
cargo run --example simple_service
|
||||
```
|
||||
|
||||
### 2. `service_spaghetti`
|
||||
|
||||
This example demonstrates how the service manager handles "messy" or improper sequences of operations, showcasing its error handling and robustness.
|
||||
|
||||
**Behavior:**
|
||||
1. Creates a service.
|
||||
2. Starts the service.
|
||||
3. Tries to start the **same service again** (which should fail as it's already running).
|
||||
4. Removes the service **without stopping it first** (the manager should handle this gracefully).
|
||||
5. Tries to stop the **already removed** service (which should fail).
|
||||
6. Tries to remove the service **again** (which should also fail).
|
||||
|
||||
**Run it:**
|
||||
```sh
|
||||
cargo run --example service_spaghetti
|
||||
```
|
109
_archive/service_manager/examples/service_spaghetti.rs
Normal file
109
_archive/service_manager/examples/service_spaghetti.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
//! service_spaghetti - An example of messy service management.
|
||||
//!
|
||||
//! This example demonstrates how the service manager behaves when commands
|
||||
//! are issued in a less-than-ideal order, such as starting a service that's
|
||||
//! already running or removing a service that hasn't been stopped.
|
||||
|
||||
use sal_service_manager::{create_service_manager, ServiceConfig};
|
||||
use std::collections::HashMap;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
// Initialize logging to see socket discovery in action
|
||||
env_logger::init();
|
||||
|
||||
let manager = match create_service_manager() {
|
||||
Ok(manager) => manager,
|
||||
Err(e) => {
|
||||
eprintln!("Error: Failed to create service manager: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let service_name = "com.herocode.examples.spaghetti";
|
||||
|
||||
let service_config = ServiceConfig {
|
||||
name: service_name.to_string(),
|
||||
binary_path: "/bin/sh".to_string(),
|
||||
args: vec![
|
||||
"-c".to_string(),
|
||||
"while true; do echo 'Spaghetti service is running...'; sleep 5; done".to_string(),
|
||||
],
|
||||
working_directory: None,
|
||||
environment: HashMap::new(),
|
||||
auto_restart: false,
|
||||
};
|
||||
|
||||
println!("--- Service Spaghetti Example ---");
|
||||
println!("This example demonstrates messy, error-prone service management.");
|
||||
|
||||
// Cleanup from previous runs to ensure a clean slate
|
||||
if let Ok(true) = manager.exists(service_name) {
|
||||
println!(
|
||||
"\nService '{}' found from a previous run. Cleaning up first.",
|
||||
service_name
|
||||
);
|
||||
let _ = manager.stop(service_name);
|
||||
let _ = manager.remove(service_name);
|
||||
println!("Cleanup complete.");
|
||||
}
|
||||
|
||||
// 1. Start the service (creates and starts in one step)
|
||||
println!("\n1. Starting the service for the first time...");
|
||||
match manager.start(&service_config) {
|
||||
Ok(()) => println!(" -> Success: Service '{}' started.", service_name),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
" -> Error: Failed to start service: {}. Halting example.",
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_secs(2));
|
||||
|
||||
// 2. Try to start the service again while it's already running
|
||||
println!("\n2. Trying to start the *same service* again...");
|
||||
match manager.start(&service_config) {
|
||||
Ok(()) => println!(" -> Unexpected Success: Service started again."),
|
||||
Err(e) => eprintln!(
|
||||
" -> Expected Error: {}. The manager should detect it is already running.",
|
||||
e
|
||||
),
|
||||
}
|
||||
|
||||
// 3. Let it run for a bit
|
||||
println!("\n3. Letting the service run for 5 seconds...");
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
|
||||
// 4. Remove the service without stopping it first
|
||||
// The `remove` function is designed to stop the service if it's running.
|
||||
println!("\n4. Removing the service without explicitly stopping it first...");
|
||||
match manager.remove(service_name) {
|
||||
Ok(()) => println!(" -> Success: Service was stopped and removed."),
|
||||
Err(e) => eprintln!(" -> Error: Failed to remove service: {}", e),
|
||||
}
|
||||
|
||||
// 5. Try to stop the service after it has been removed
|
||||
println!("\n5. Trying to stop the service that was just removed...");
|
||||
match manager.stop(service_name) {
|
||||
Ok(()) => println!(" -> Unexpected Success: Stopped a removed service."),
|
||||
Err(e) => eprintln!(
|
||||
" -> Expected Error: {}. The manager knows the service is gone.",
|
||||
e
|
||||
),
|
||||
}
|
||||
|
||||
// 6. Try to remove the service again
|
||||
println!("\n6. Trying to remove the service again...");
|
||||
match manager.remove(service_name) {
|
||||
Ok(()) => println!(" -> Unexpected Success: Removed a non-existent service."),
|
||||
Err(e) => eprintln!(
|
||||
" -> Expected Error: {}. The manager correctly reports it's not found.",
|
||||
e
|
||||
),
|
||||
}
|
||||
|
||||
println!("\n--- Spaghetti Example Finished ---");
|
||||
}
|
110
_archive/service_manager/examples/simple_service.rs
Normal file
110
_archive/service_manager/examples/simple_service.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use sal_service_manager::{create_service_manager, ServiceConfig};
|
||||
use std::collections::HashMap;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
// Initialize logging to see socket discovery in action
|
||||
env_logger::init();
|
||||
|
||||
// 1. Create a service manager for the current platform
|
||||
let manager = match create_service_manager() {
|
||||
Ok(manager) => manager,
|
||||
Err(e) => {
|
||||
eprintln!("Error: Failed to create service manager: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Define the configuration for our new service
|
||||
let service_name = "com.herocode.examples.simpleservice";
|
||||
let service_config = ServiceConfig {
|
||||
name: service_name.to_string(),
|
||||
// A simple command that runs in a loop
|
||||
binary_path: "/bin/sh".to_string(),
|
||||
args: vec![
|
||||
"-c".to_string(),
|
||||
"while true; do echo 'Simple service is running...'; date; sleep 5; done".to_string(),
|
||||
],
|
||||
working_directory: None,
|
||||
environment: HashMap::new(),
|
||||
auto_restart: false,
|
||||
};
|
||||
|
||||
println!("--- Service Manager Example ---");
|
||||
|
||||
// Cleanup from previous runs, if necessary
|
||||
if let Ok(true) = manager.exists(service_name) {
|
||||
println!(
|
||||
"Service '{}' already exists. Cleaning up before starting.",
|
||||
service_name
|
||||
);
|
||||
if let Err(e) = manager.stop(service_name) {
|
||||
println!(
|
||||
"Note: could not stop existing service (it might not be running): {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
if let Err(e) = manager.remove(service_name) {
|
||||
eprintln!("Error: failed to remove existing service: {}", e);
|
||||
return;
|
||||
}
|
||||
println!("Cleanup complete.");
|
||||
}
|
||||
|
||||
// 3. Start the service (creates and starts in one step)
|
||||
println!("\n1. Starting service: '{}'", service_name);
|
||||
match manager.start(&service_config) {
|
||||
Ok(()) => println!("Service '{}' started successfully.", service_name),
|
||||
Err(e) => {
|
||||
eprintln!("Error: Failed to start service '{}': {}", service_name, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Give it a moment to run
|
||||
println!("\nWaiting for 2 seconds for the service to initialize...");
|
||||
thread::sleep(Duration::from_secs(2));
|
||||
|
||||
// 4. Check the status of the service
|
||||
println!("\n2. Checking service status...");
|
||||
match manager.status(service_name) {
|
||||
Ok(status) => println!("Service status: {:?}", status),
|
||||
Err(e) => eprintln!(
|
||||
"Error: Failed to get status for service '{}': {}",
|
||||
service_name, e
|
||||
),
|
||||
}
|
||||
|
||||
println!("\nLetting the service run for 10 seconds. Check logs if you can.");
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
|
||||
// 5. Stop the service
|
||||
println!("\n3. Stopping service: '{}'", service_name);
|
||||
match manager.stop(service_name) {
|
||||
Ok(()) => println!("Service '{}' stopped successfully.", service_name),
|
||||
Err(e) => eprintln!("Error: Failed to stop service '{}': {}", service_name, e),
|
||||
}
|
||||
|
||||
println!("\nWaiting for 2 seconds for the service to stop...");
|
||||
thread::sleep(Duration::from_secs(2));
|
||||
|
||||
// Check status again
|
||||
println!("\n4. Checking status after stopping...");
|
||||
match manager.status(service_name) {
|
||||
Ok(status) => println!("Service status: {:?}", status),
|
||||
Err(e) => eprintln!(
|
||||
"Error: Failed to get status for service '{}': {}",
|
||||
service_name, e
|
||||
),
|
||||
}
|
||||
|
||||
// 6. Remove the service
|
||||
println!("\n5. Removing service: '{}'", service_name);
|
||||
match manager.remove(service_name) {
|
||||
Ok(()) => println!("Service '{}' removed successfully.", service_name),
|
||||
Err(e) => eprintln!("Error: Failed to remove service '{}': {}", service_name, e),
|
||||
}
|
||||
|
||||
println!("\n--- Example Finished ---");
|
||||
}
|
47
_archive/service_manager/examples/socket_discovery_test.rs
Normal file
47
_archive/service_manager/examples/socket_discovery_test.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! Socket Discovery Test
|
||||
//!
|
||||
//! This example demonstrates the zinit socket discovery functionality.
|
||||
//! It shows how the service manager finds available zinit sockets.
|
||||
|
||||
use sal_service_manager::create_service_manager;
|
||||
|
||||
fn main() {
|
||||
// Initialize logging to see socket discovery in action
|
||||
env_logger::init();
|
||||
|
||||
println!("=== Zinit Socket Discovery Test ===");
|
||||
println!("This test demonstrates how the service manager discovers zinit sockets.");
|
||||
println!();
|
||||
|
||||
// Test environment variable
|
||||
if let Ok(socket_path) = std::env::var("ZINIT_SOCKET_PATH") {
|
||||
println!("🔍 ZINIT_SOCKET_PATH environment variable set to: {}", socket_path);
|
||||
} else {
|
||||
println!("🔍 ZINIT_SOCKET_PATH environment variable not set");
|
||||
}
|
||||
println!();
|
||||
|
||||
println!("🚀 Creating service manager...");
|
||||
match create_service_manager() {
|
||||
Ok(_manager) => {
|
||||
println!("✅ Service manager created successfully!");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
println!("📱 Platform: macOS - Using launchctl");
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
println!("🐧 Platform: Linux - Check logs above for socket discovery details");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ Failed to create service manager: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("=== Test Complete ===");
|
||||
println!();
|
||||
println!("To test zinit socket discovery on Linux:");
|
||||
println!("1. Start zinit: zinit -s /tmp/zinit.sock init");
|
||||
println!("2. Run with logging: RUST_LOG=debug cargo run --example socket_discovery_test -p sal-service-manager");
|
||||
println!("3. Or set custom path: ZINIT_SOCKET_PATH=/custom/path.sock RUST_LOG=debug cargo run --example socket_discovery_test -p sal-service-manager");
|
||||
}
|
492
_archive/service_manager/src/launchctl.rs
Normal file
492
_archive/service_manager/src/launchctl.rs
Normal file
@@ -0,0 +1,492 @@
|
||||
use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tokio::process::Command;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
// Shared runtime for async operations - production-safe initialization
|
||||
static ASYNC_RUNTIME: Lazy<Option<Runtime>> = Lazy::new(|| Runtime::new().ok());
|
||||
|
||||
/// Get the async runtime, creating a temporary one if the static runtime failed
|
||||
fn get_runtime() -> Result<Runtime, ServiceManagerError> {
|
||||
// Try to use the static runtime first
|
||||
if let Some(_runtime) = ASYNC_RUNTIME.as_ref() {
|
||||
// We can't return a reference to the static runtime because we need ownership
|
||||
// for block_on, so we create a new one. This is a reasonable trade-off for safety.
|
||||
Runtime::new().map_err(|e| {
|
||||
ServiceManagerError::Other(format!("Failed to create async runtime: {}", e))
|
||||
})
|
||||
} else {
|
||||
// Static runtime failed, try to create a new one
|
||||
Runtime::new().map_err(|e| {
|
||||
ServiceManagerError::Other(format!("Failed to create async runtime: {}", e))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LaunchctlServiceManager {
|
||||
service_prefix: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct LaunchDaemon {
|
||||
#[serde(rename = "Label")]
|
||||
label: String,
|
||||
#[serde(rename = "ProgramArguments")]
|
||||
program_arguments: Vec<String>,
|
||||
#[serde(rename = "WorkingDirectory", skip_serializing_if = "Option::is_none")]
|
||||
working_directory: Option<String>,
|
||||
#[serde(
|
||||
rename = "EnvironmentVariables",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
environment_variables: Option<HashMap<String, String>>,
|
||||
#[serde(rename = "KeepAlive", skip_serializing_if = "Option::is_none")]
|
||||
keep_alive: Option<bool>,
|
||||
#[serde(rename = "RunAtLoad")]
|
||||
run_at_load: bool,
|
||||
#[serde(rename = "StandardOutPath", skip_serializing_if = "Option::is_none")]
|
||||
standard_out_path: Option<String>,
|
||||
#[serde(rename = "StandardErrorPath", skip_serializing_if = "Option::is_none")]
|
||||
standard_error_path: Option<String>,
|
||||
}
|
||||
|
||||
impl LaunchctlServiceManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
service_prefix: "tf.ourworld.circles".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_service_label(&self, service_name: &str) -> String {
|
||||
format!("{}.{}", self.service_prefix, service_name)
|
||||
}
|
||||
|
||||
fn get_plist_path(&self, service_name: &str) -> PathBuf {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
PathBuf::from(home)
|
||||
.join("Library")
|
||||
.join("LaunchAgents")
|
||||
.join(format!("{}.plist", self.get_service_label(service_name)))
|
||||
}
|
||||
|
||||
fn get_log_path(&self, service_name: &str) -> PathBuf {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
PathBuf::from(home)
|
||||
.join("Library")
|
||||
.join("Logs")
|
||||
.join("circles")
|
||||
.join(format!("{}.log", service_name))
|
||||
}
|
||||
|
||||
async fn create_plist(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
|
||||
let label = self.get_service_label(&config.name);
|
||||
let plist_path = self.get_plist_path(&config.name);
|
||||
let log_path = self.get_log_path(&config.name);
|
||||
|
||||
// Ensure the LaunchAgents directory exists
|
||||
if let Some(parent) = plist_path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
// Ensure the logs directory exists
|
||||
if let Some(parent) = log_path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let mut program_arguments = vec![config.binary_path.clone()];
|
||||
program_arguments.extend(config.args.clone());
|
||||
|
||||
let launch_daemon = LaunchDaemon {
|
||||
label: label.clone(),
|
||||
program_arguments,
|
||||
working_directory: config.working_directory.clone(),
|
||||
environment_variables: if config.environment.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(config.environment.clone())
|
||||
},
|
||||
keep_alive: if config.auto_restart {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
run_at_load: true,
|
||||
standard_out_path: Some(log_path.to_string_lossy().to_string()),
|
||||
standard_error_path: Some(log_path.to_string_lossy().to_string()),
|
||||
};
|
||||
|
||||
let mut plist_content = Vec::new();
|
||||
plist::to_writer_xml(&mut plist_content, &launch_daemon)
|
||||
.map_err(|e| ServiceManagerError::Other(format!("Failed to serialize plist: {}", e)))?;
|
||||
let plist_content = String::from_utf8(plist_content).map_err(|e| {
|
||||
ServiceManagerError::Other(format!("Failed to convert plist to string: {}", e))
|
||||
})?;
|
||||
|
||||
tokio::fs::write(&plist_path, plist_content).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_launchctl(&self, args: &[&str]) -> Result<String, ServiceManagerError> {
|
||||
let output = Command::new("launchctl").args(args).output().await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(ServiceManagerError::Other(format!(
|
||||
"launchctl command failed: {}",
|
||||
stderr
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
async fn wait_for_service_status(
|
||||
&self,
|
||||
service_name: &str,
|
||||
timeout_secs: u64,
|
||||
) -> Result<(), ServiceManagerError> {
|
||||
use tokio::time::{sleep, timeout, Duration};
|
||||
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
let poll_interval = Duration::from_millis(500);
|
||||
|
||||
let result = timeout(timeout_duration, async {
|
||||
loop {
|
||||
match self.status(service_name) {
|
||||
Ok(ServiceStatus::Running) => {
|
||||
return Ok(());
|
||||
}
|
||||
Ok(ServiceStatus::Failed) => {
|
||||
// Service failed, get error details from logs
|
||||
let logs = self.logs(service_name, Some(20)).unwrap_or_default();
|
||||
let error_msg = if logs.is_empty() {
|
||||
"Service failed to start (no logs available)".to_string()
|
||||
} else {
|
||||
// Extract error lines from logs
|
||||
let error_lines: Vec<&str> = logs
|
||||
.lines()
|
||||
.filter(|line| {
|
||||
line.to_lowercase().contains("error")
|
||||
|| line.to_lowercase().contains("failed")
|
||||
})
|
||||
.take(3)
|
||||
.collect();
|
||||
|
||||
if error_lines.is_empty() {
|
||||
format!(
|
||||
"Service failed to start. Recent logs:\n{}",
|
||||
logs.lines()
|
||||
.rev()
|
||||
.take(5)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Service failed to start. Errors:\n{}",
|
||||
error_lines.join("\n")
|
||||
)
|
||||
}
|
||||
};
|
||||
return Err(ServiceManagerError::StartFailed(
|
||||
service_name.to_string(),
|
||||
error_msg,
|
||||
));
|
||||
}
|
||||
Ok(ServiceStatus::Stopped) | Ok(ServiceStatus::Unknown) => {
|
||||
// Still starting, continue polling
|
||||
sleep(poll_interval).await;
|
||||
}
|
||||
Err(ServiceManagerError::ServiceNotFound(_)) => {
|
||||
return Err(ServiceManagerError::ServiceNotFound(
|
||||
service_name.to_string(),
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => Ok(()),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(_) => Err(ServiceManagerError::StartFailed(
|
||||
service_name.to_string(),
|
||||
format!("Service did not start within {} seconds", timeout_secs),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceManager for LaunchctlServiceManager {
|
||||
fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> {
|
||||
let plist_path = self.get_plist_path(service_name);
|
||||
Ok(plist_path.exists())
|
||||
}
|
||||
|
||||
fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
|
||||
// Use production-safe runtime for async operations
|
||||
let runtime = get_runtime()?;
|
||||
runtime.block_on(async {
|
||||
let label = self.get_service_label(&config.name);
|
||||
|
||||
// Check if service is already loaded
|
||||
let list_output = self.run_launchctl(&["list"]).await?;
|
||||
if list_output.contains(&label) {
|
||||
return Err(ServiceManagerError::ServiceAlreadyExists(
|
||||
config.name.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
// Create the plist file
|
||||
self.create_plist(config).await?;
|
||||
|
||||
// Load the service
|
||||
let plist_path = self.get_plist_path(&config.name);
|
||||
self.run_launchctl(&["load", &plist_path.to_string_lossy()])
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServiceManagerError::StartFailed(config.name.clone(), e.to_string())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
let runtime = get_runtime()?;
|
||||
runtime.block_on(async {
|
||||
let label = self.get_service_label(service_name);
|
||||
let plist_path = self.get_plist_path(service_name);
|
||||
|
||||
// Check if plist file exists
|
||||
if !plist_path.exists() {
|
||||
return Err(ServiceManagerError::ServiceNotFound(
|
||||
service_name.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check if service is already loaded and running
|
||||
let list_output = self.run_launchctl(&["list"]).await?;
|
||||
if list_output.contains(&label) {
|
||||
// Service is loaded, check if it's running
|
||||
match self.status(service_name)? {
|
||||
ServiceStatus::Running => {
|
||||
return Ok(()); // Already running, nothing to do
|
||||
}
|
||||
_ => {
|
||||
// Service is loaded but not running, try to start it
|
||||
self.run_launchctl(&["start", &label]).await.map_err(|e| {
|
||||
ServiceManagerError::StartFailed(
|
||||
service_name.to_string(),
|
||||
e.to_string(),
|
||||
)
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Service is not loaded, load it
|
||||
self.run_launchctl(&["load", &plist_path.to_string_lossy()])
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServiceManagerError::StartFailed(service_name.to_string(), e.to_string())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn start_and_confirm(
|
||||
&self,
|
||||
config: &ServiceConfig,
|
||||
timeout_secs: u64,
|
||||
) -> Result<(), ServiceManagerError> {
|
||||
// First start the service
|
||||
self.start(config)?;
|
||||
|
||||
// Then wait for confirmation using production-safe runtime
|
||||
let runtime = get_runtime()?;
|
||||
runtime.block_on(async {
|
||||
self.wait_for_service_status(&config.name, timeout_secs)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
fn start_existing_and_confirm(
|
||||
&self,
|
||||
service_name: &str,
|
||||
timeout_secs: u64,
|
||||
) -> Result<(), ServiceManagerError> {
|
||||
// First start the existing service
|
||||
self.start_existing(service_name)?;
|
||||
|
||||
// Then wait for confirmation using production-safe runtime
|
||||
let runtime = get_runtime()?;
|
||||
runtime.block_on(async {
|
||||
self.wait_for_service_status(service_name, timeout_secs)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
let runtime = get_runtime()?;
|
||||
runtime.block_on(async {
|
||||
let _label = self.get_service_label(service_name);
|
||||
let plist_path = self.get_plist_path(service_name);
|
||||
|
||||
// Unload the service
|
||||
self.run_launchctl(&["unload", &plist_path.to_string_lossy()])
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServiceManagerError::StopFailed(service_name.to_string(), e.to_string())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
// For launchctl, we stop and start
|
||||
if let Err(e) = self.stop(service_name) {
|
||||
// If stop fails because service doesn't exist, that's ok for restart
|
||||
if !matches!(e, ServiceManagerError::ServiceNotFound(_)) {
|
||||
return Err(ServiceManagerError::RestartFailed(
|
||||
service_name.to_string(),
|
||||
e.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// We need the config to restart, but we don't have it stored
|
||||
// For now, return an error - in a real implementation we might store configs
|
||||
Err(ServiceManagerError::RestartFailed(
|
||||
service_name.to_string(),
|
||||
"Restart requires re-providing service configuration".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> {
|
||||
let runtime = get_runtime()?;
|
||||
runtime.block_on(async {
|
||||
let label = self.get_service_label(service_name);
|
||||
let plist_path = self.get_plist_path(service_name);
|
||||
|
||||
// First check if the plist file exists
|
||||
if !plist_path.exists() {
|
||||
return Err(ServiceManagerError::ServiceNotFound(
|
||||
service_name.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let list_output = self.run_launchctl(&["list"]).await?;
|
||||
|
||||
if !list_output.contains(&label) {
|
||||
return Ok(ServiceStatus::Stopped);
|
||||
}
|
||||
|
||||
// Get detailed status
|
||||
match self.run_launchctl(&["list", &label]).await {
|
||||
Ok(output) => {
|
||||
if output.contains("\"PID\" = ") {
|
||||
Ok(ServiceStatus::Running)
|
||||
} else if output.contains("\"LastExitStatus\" = ") {
|
||||
Ok(ServiceStatus::Failed)
|
||||
} else {
|
||||
Ok(ServiceStatus::Unknown)
|
||||
}
|
||||
}
|
||||
Err(_) => Ok(ServiceStatus::Stopped),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn logs(
|
||||
&self,
|
||||
service_name: &str,
|
||||
lines: Option<usize>,
|
||||
) -> Result<String, ServiceManagerError> {
|
||||
let runtime = get_runtime()?;
|
||||
runtime.block_on(async {
|
||||
let log_path = self.get_log_path(service_name);
|
||||
|
||||
if !log_path.exists() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
match lines {
|
||||
Some(n) => {
|
||||
let output = Command::new("tail")
|
||||
.args(&["-n", &n.to_string(), &log_path.to_string_lossy()])
|
||||
.output()
|
||||
.await?;
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
None => {
|
||||
let content = tokio::fs::read_to_string(&log_path).await?;
|
||||
Ok(content)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn list(&self) -> Result<Vec<String>, ServiceManagerError> {
|
||||
let runtime = get_runtime()?;
|
||||
runtime.block_on(async {
|
||||
let list_output = self.run_launchctl(&["list"]).await?;
|
||||
|
||||
let services: Vec<String> = list_output
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
if line.contains(&self.service_prefix) {
|
||||
// Extract service name from label
|
||||
line.split_whitespace()
|
||||
.last()
|
||||
.and_then(|label| {
|
||||
label.strip_prefix(&format!("{}.", self.service_prefix))
|
||||
})
|
||||
.map(|s| s.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(services)
|
||||
})
|
||||
}
|
||||
|
||||
fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
// Try to stop the service first, but don't fail if it's already stopped or doesn't exist
|
||||
if let Err(e) = self.stop(service_name) {
|
||||
// Log the error but continue with removal
|
||||
log::warn!(
|
||||
"Failed to stop service '{}' before removal: {}",
|
||||
service_name,
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
// Remove the plist file using production-safe runtime
|
||||
let runtime = get_runtime()?;
|
||||
runtime.block_on(async {
|
||||
let plist_path = self.get_plist_path(service_name);
|
||||
if plist_path.exists() {
|
||||
tokio::fs::remove_file(&plist_path).await?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
301
_archive/service_manager/src/lib.rs
Normal file
301
_archive/service_manager/src/lib.rs
Normal file
@@ -0,0 +1,301 @@
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ServiceManagerError {
|
||||
#[error("Service '{0}' not found")]
|
||||
ServiceNotFound(String),
|
||||
#[error("Service '{0}' already exists")]
|
||||
ServiceAlreadyExists(String),
|
||||
#[error("Failed to start service '{0}': {1}")]
|
||||
StartFailed(String, String),
|
||||
#[error("Failed to stop service '{0}': {1}")]
|
||||
StopFailed(String, String),
|
||||
#[error("Failed to restart service '{0}': {1}")]
|
||||
RestartFailed(String, String),
|
||||
#[error("Failed to get logs for service '{0}': {1}")]
|
||||
LogsFailed(String, String),
|
||||
#[error("IO error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("Service manager error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServiceConfig {
|
||||
pub name: String,
|
||||
pub binary_path: String,
|
||||
pub args: Vec<String>,
|
||||
pub working_directory: Option<String>,
|
||||
pub environment: HashMap<String, String>,
|
||||
pub auto_restart: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ServiceStatus {
|
||||
Running,
|
||||
Stopped,
|
||||
Failed,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
pub trait ServiceManager: Send + Sync {
|
||||
/// Check if a service exists
|
||||
fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError>;
|
||||
|
||||
/// Start a service with the given configuration
|
||||
fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError>;
|
||||
|
||||
/// Start an existing service by name (load existing plist/config)
|
||||
fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError>;
|
||||
|
||||
/// Start a service and wait for confirmation that it's running or failed
|
||||
fn start_and_confirm(
|
||||
&self,
|
||||
config: &ServiceConfig,
|
||||
timeout_secs: u64,
|
||||
) -> Result<(), ServiceManagerError>;
|
||||
|
||||
/// Start an existing service and wait for confirmation that it's running or failed
|
||||
fn start_existing_and_confirm(
|
||||
&self,
|
||||
service_name: &str,
|
||||
timeout_secs: u64,
|
||||
) -> Result<(), ServiceManagerError>;
|
||||
|
||||
/// Stop a service by name
|
||||
fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError>;
|
||||
|
||||
/// Restart a service by name
|
||||
fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError>;
|
||||
|
||||
/// Get the status of a service
|
||||
fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError>;
|
||||
|
||||
/// Get logs for a service
|
||||
fn logs(&self, service_name: &str, lines: Option<usize>)
|
||||
-> Result<String, ServiceManagerError>;
|
||||
|
||||
/// List all managed services
|
||||
fn list(&self) -> Result<Vec<String>, ServiceManagerError>;
|
||||
|
||||
/// Remove a service configuration (stop if running)
|
||||
fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError>;
|
||||
}
|
||||
|
||||
// Platform-specific implementations
|
||||
#[cfg(target_os = "macos")]
|
||||
mod launchctl;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use launchctl::LaunchctlServiceManager;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod systemd;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use systemd::SystemdServiceManager;
|
||||
|
||||
mod zinit;
|
||||
pub use zinit::ZinitServiceManager;
|
||||
|
||||
#[cfg(feature = "rhai")]
|
||||
pub mod rhai;
|
||||
|
||||
/// Discover available zinit socket paths
|
||||
///
|
||||
/// This function checks for zinit sockets in the following order:
|
||||
/// 1. Environment variable ZINIT_SOCKET_PATH (if set)
|
||||
/// 2. Common socket locations with connectivity testing
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns the first working socket path found, or None if no working zinit server is detected.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn discover_zinit_socket() -> Option<String> {
|
||||
// First check environment variable
|
||||
if let Ok(env_socket_path) = std::env::var("ZINIT_SOCKET_PATH") {
|
||||
log::debug!("Checking ZINIT_SOCKET_PATH: {}", env_socket_path);
|
||||
if test_zinit_socket(&env_socket_path) {
|
||||
log::info!(
|
||||
"Using zinit socket from ZINIT_SOCKET_PATH: {}",
|
||||
env_socket_path
|
||||
);
|
||||
return Some(env_socket_path);
|
||||
} else {
|
||||
log::warn!(
|
||||
"ZINIT_SOCKET_PATH specified but socket is not accessible: {}",
|
||||
env_socket_path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Try common socket locations
|
||||
let common_paths = [
|
||||
"/var/run/zinit.sock",
|
||||
"/tmp/zinit.sock",
|
||||
"/run/zinit.sock",
|
||||
"./zinit.sock",
|
||||
];
|
||||
|
||||
log::debug!("Discovering zinit socket from common locations...");
|
||||
for path in &common_paths {
|
||||
log::debug!("Testing socket path: {}", path);
|
||||
if test_zinit_socket(path) {
|
||||
log::info!("Found working zinit socket at: {}", path);
|
||||
return Some(path.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("No working zinit socket found");
|
||||
None
|
||||
}
|
||||
|
||||
/// Test if a zinit socket is accessible and responsive
|
||||
///
|
||||
/// This function attempts to create a ZinitServiceManager and perform a basic
|
||||
/// connectivity test by listing services.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_zinit_socket(socket_path: &str) -> bool {
|
||||
// Check if socket file exists first
|
||||
if !std::path::Path::new(socket_path).exists() {
|
||||
log::debug!("Socket file does not exist: {}", socket_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to create a manager and test basic connectivity
|
||||
match ZinitServiceManager::new(socket_path) {
|
||||
Ok(manager) => {
|
||||
// Test basic connectivity by trying to list services
|
||||
match manager.list() {
|
||||
Ok(_) => {
|
||||
log::debug!("Socket {} is responsive", socket_path);
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("Socket {} exists but not responsive: {}", socket_path, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("Failed to create manager for socket {}: {}", socket_path, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a service manager appropriate for the current platform
|
||||
///
|
||||
/// - On macOS: Uses launchctl for service management
|
||||
/// - On Linux: Uses zinit for service management with systemd fallback
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns a Result containing the service manager or an error if initialization fails.
|
||||
/// On Linux, it first tries to discover a working zinit socket. If no zinit server is found,
|
||||
/// it will fall back to systemd.
|
||||
///
|
||||
/// # Environment Variables
|
||||
///
|
||||
/// - `ZINIT_SOCKET_PATH`: Specifies the zinit socket path (Linux only)
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `ServiceManagerError` if:
|
||||
/// - The platform is not supported (Windows, etc.)
|
||||
/// - Service manager initialization fails on all available backends
|
||||
pub fn create_service_manager() -> Result<Box<dyn ServiceManager>, ServiceManagerError> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Ok(Box::new(LaunchctlServiceManager::new()))
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Try to discover a working zinit socket
|
||||
if let Some(socket_path) = discover_zinit_socket() {
|
||||
match ZinitServiceManager::new(&socket_path) {
|
||||
Ok(zinit_manager) => {
|
||||
log::info!("Using zinit service manager with socket: {}", socket_path);
|
||||
return Ok(Box::new(zinit_manager));
|
||||
}
|
||||
Err(zinit_error) => {
|
||||
log::warn!(
|
||||
"Failed to create zinit manager for discovered socket {}: {}",
|
||||
socket_path,
|
||||
zinit_error
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::info!("No running zinit server detected. To use zinit, start it with: zinit -s /tmp/zinit.sock init");
|
||||
}
|
||||
|
||||
// Fallback to systemd
|
||||
log::info!("Falling back to systemd service manager");
|
||||
Ok(Box::new(SystemdServiceManager::new()))
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
Err(ServiceManagerError::Other(
|
||||
"Service manager not implemented for this platform".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a service manager for zinit with a custom socket path
|
||||
///
|
||||
/// This is useful when zinit is running with a non-default socket path
|
||||
pub fn create_zinit_service_manager(
|
||||
socket_path: &str,
|
||||
) -> Result<Box<dyn ServiceManager>, ServiceManagerError> {
|
||||
Ok(Box::new(ZinitServiceManager::new(socket_path)?))
|
||||
}
|
||||
|
||||
/// Create a service manager for systemd (Linux alternative)
|
||||
///
|
||||
/// This creates a systemd-based service manager as an alternative to zinit on Linux
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn create_systemd_service_manager() -> Box<dyn ServiceManager> {
|
||||
Box::new(SystemdServiceManager::new())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_service_manager() {
|
||||
// This test ensures the service manager can be created without panicking
|
||||
let result = create_service_manager();
|
||||
assert!(result.is_ok(), "Service manager creation should succeed");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn test_socket_discovery_with_env_var() {
|
||||
// Test that environment variable is respected
|
||||
std::env::set_var("ZINIT_SOCKET_PATH", "/test/path.sock");
|
||||
|
||||
// The discover function should check the env var first
|
||||
// Since the socket doesn't exist, it should return None, but we can't test
|
||||
// the actual discovery logic without a real socket
|
||||
|
||||
std::env::remove_var("ZINIT_SOCKET_PATH");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn test_socket_discovery_without_env_var() {
|
||||
// Ensure env var is not set
|
||||
std::env::remove_var("ZINIT_SOCKET_PATH");
|
||||
|
||||
// The discover function should try common paths
|
||||
// Since no zinit is running, it should return None
|
||||
let result = discover_zinit_socket();
|
||||
|
||||
// This is expected to be None in test environment
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"Should return None when no zinit server is running"
|
||||
);
|
||||
}
|
||||
}
|
256
_archive/service_manager/src/rhai.rs
Normal file
256
_archive/service_manager/src/rhai.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
//! Rhai integration for the service manager module
|
||||
//!
|
||||
//! This module provides Rhai scripting support for service management operations.
|
||||
|
||||
use crate::{create_service_manager, ServiceConfig, ServiceManager};
|
||||
use rhai::{Engine, EvalAltResult, Map};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A wrapper around ServiceManager that can be used in Rhai
|
||||
#[derive(Clone)]
|
||||
pub struct RhaiServiceManager {
|
||||
inner: Arc<Box<dyn ServiceManager>>,
|
||||
}
|
||||
|
||||
impl RhaiServiceManager {
|
||||
pub fn new() -> Result<Self, Box<EvalAltResult>> {
|
||||
let manager = create_service_manager()
|
||||
.map_err(|e| format!("Failed to create service manager: {}", e))?;
|
||||
Ok(Self {
|
||||
inner: Arc::new(manager),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Register the service manager module with a Rhai engine
|
||||
pub fn register_service_manager_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Factory function to create service manager
|
||||
engine.register_type::<RhaiServiceManager>();
|
||||
engine.register_fn(
|
||||
"create_service_manager",
|
||||
|| -> Result<RhaiServiceManager, Box<EvalAltResult>> { RhaiServiceManager::new() },
|
||||
);
|
||||
|
||||
// Service management functions
|
||||
engine.register_fn(
|
||||
"start",
|
||||
|manager: &mut RhaiServiceManager, config: Map| -> Result<(), Box<EvalAltResult>> {
|
||||
let service_config = map_to_service_config(config)?;
|
||||
manager
|
||||
.inner
|
||||
.start(&service_config)
|
||||
.map_err(|e| format!("Failed to start service: {}", e).into())
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"stop",
|
||||
|manager: &mut RhaiServiceManager,
|
||||
service_name: String|
|
||||
-> Result<(), Box<EvalAltResult>> {
|
||||
manager
|
||||
.inner
|
||||
.stop(&service_name)
|
||||
.map_err(|e| format!("Failed to stop service: {}", e).into())
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"restart",
|
||||
|manager: &mut RhaiServiceManager,
|
||||
service_name: String|
|
||||
-> Result<(), Box<EvalAltResult>> {
|
||||
manager
|
||||
.inner
|
||||
.restart(&service_name)
|
||||
.map_err(|e| format!("Failed to restart service: {}", e).into())
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"status",
|
||||
|manager: &mut RhaiServiceManager,
|
||||
service_name: String|
|
||||
-> Result<String, Box<EvalAltResult>> {
|
||||
let status = manager
|
||||
.inner
|
||||
.status(&service_name)
|
||||
.map_err(|e| format!("Failed to get service status: {}", e))?;
|
||||
Ok(format!("{:?}", status))
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"logs",
|
||||
|manager: &mut RhaiServiceManager,
|
||||
service_name: String,
|
||||
lines: i64|
|
||||
-> Result<String, Box<EvalAltResult>> {
|
||||
let lines_opt = if lines > 0 {
|
||||
Some(lines as usize)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
manager
|
||||
.inner
|
||||
.logs(&service_name, lines_opt)
|
||||
.map_err(|e| format!("Failed to get service logs: {}", e).into())
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"list",
|
||||
|manager: &mut RhaiServiceManager| -> Result<Vec<String>, Box<EvalAltResult>> {
|
||||
manager
|
||||
.inner
|
||||
.list()
|
||||
.map_err(|e| format!("Failed to list services: {}", e).into())
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"remove",
|
||||
|manager: &mut RhaiServiceManager,
|
||||
service_name: String|
|
||||
-> Result<(), Box<EvalAltResult>> {
|
||||
manager
|
||||
.inner
|
||||
.remove(&service_name)
|
||||
.map_err(|e| format!("Failed to remove service: {}", e).into())
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"exists",
|
||||
|manager: &mut RhaiServiceManager,
|
||||
service_name: String|
|
||||
-> Result<bool, Box<EvalAltResult>> {
|
||||
manager
|
||||
.inner
|
||||
.exists(&service_name)
|
||||
.map_err(|e| format!("Failed to check if service exists: {}", e).into())
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"start_and_confirm",
|
||||
|manager: &mut RhaiServiceManager,
|
||||
config: Map,
|
||||
timeout_secs: i64|
|
||||
-> Result<(), Box<EvalAltResult>> {
|
||||
let service_config = map_to_service_config(config)?;
|
||||
let timeout = if timeout_secs > 0 {
|
||||
timeout_secs as u64
|
||||
} else {
|
||||
30
|
||||
};
|
||||
manager
|
||||
.inner
|
||||
.start_and_confirm(&service_config, timeout)
|
||||
.map_err(|e| format!("Failed to start and confirm service: {}", e).into())
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"start_existing_and_confirm",
|
||||
|manager: &mut RhaiServiceManager,
|
||||
service_name: String,
|
||||
timeout_secs: i64|
|
||||
-> Result<(), Box<EvalAltResult>> {
|
||||
let timeout = if timeout_secs > 0 {
|
||||
timeout_secs as u64
|
||||
} else {
|
||||
30
|
||||
};
|
||||
manager
|
||||
.inner
|
||||
.start_existing_and_confirm(&service_name, timeout)
|
||||
.map_err(|e| format!("Failed to start existing service and confirm: {}", e).into())
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert a Rhai Map to a ServiceConfig
|
||||
fn map_to_service_config(map: Map) -> Result<ServiceConfig, Box<EvalAltResult>> {
|
||||
let name = map
|
||||
.get("name")
|
||||
.and_then(|v| v.clone().into_string().ok())
|
||||
.ok_or("Service config must have a 'name' field")?;
|
||||
|
||||
let binary_path = map
|
||||
.get("binary_path")
|
||||
.and_then(|v| v.clone().into_string().ok())
|
||||
.ok_or("Service config must have a 'binary_path' field")?;
|
||||
|
||||
let args = map
|
||||
.get("args")
|
||||
.and_then(|v| v.clone().try_cast::<rhai::Array>())
|
||||
.map(|arr| {
|
||||
arr.into_iter()
|
||||
.filter_map(|v| v.into_string().ok())
|
||||
.collect::<Vec<String>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let working_directory = map
|
||||
.get("working_directory")
|
||||
.and_then(|v| v.clone().into_string().ok());
|
||||
|
||||
let environment = map
|
||||
.get("environment")
|
||||
.and_then(|v| v.clone().try_cast::<Map>())
|
||||
.map(|env_map| {
|
||||
env_map
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| v.into_string().ok().map(|val| (k.to_string(), val)))
|
||||
.collect::<HashMap<String, String>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let auto_restart = map
|
||||
.get("auto_restart")
|
||||
.and_then(|v| v.as_bool().ok())
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(ServiceConfig {
|
||||
name,
|
||||
binary_path,
|
||||
args,
|
||||
working_directory,
|
||||
environment,
|
||||
auto_restart,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rhai::{Engine, Map};
|
||||
|
||||
#[test]
|
||||
fn test_register_service_manager_module() {
|
||||
let mut engine = Engine::new();
|
||||
register_service_manager_module(&mut engine).unwrap();
|
||||
|
||||
// Test that the functions are registered
|
||||
// Note: Rhai doesn't expose a public API to check if functions are registered
|
||||
// So we'll just verify the module registration doesn't panic
|
||||
assert!(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_map_to_service_config() {
|
||||
let mut map = Map::new();
|
||||
map.insert("name".into(), "test-service".into());
|
||||
map.insert("binary_path".into(), "/bin/echo".into());
|
||||
map.insert("auto_restart".into(), true.into());
|
||||
|
||||
let config = map_to_service_config(map).unwrap();
|
||||
assert_eq!(config.name, "test-service");
|
||||
assert_eq!(config.binary_path, "/bin/echo");
|
||||
assert_eq!(config.auto_restart, true);
|
||||
}
|
||||
}
|
434
_archive/service_manager/src/systemd.rs
Normal file
434
_archive/service_manager/src/systemd.rs
Normal file
@@ -0,0 +1,434 @@
|
||||
use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SystemdServiceManager {
|
||||
service_prefix: String,
|
||||
user_mode: bool,
|
||||
}
|
||||
|
||||
impl SystemdServiceManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
service_prefix: "sal".to_string(),
|
||||
user_mode: true, // Default to user services for safety
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_system() -> Self {
|
||||
Self {
|
||||
service_prefix: "sal".to_string(),
|
||||
user_mode: false, // System-wide services (requires root)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_service_name(&self, service_name: &str) -> String {
|
||||
format!("{}-{}.service", self.service_prefix, service_name)
|
||||
}
|
||||
|
||||
fn get_unit_file_path(&self, service_name: &str) -> PathBuf {
|
||||
let service_file = self.get_service_name(service_name);
|
||||
if self.user_mode {
|
||||
// User service directory
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
PathBuf::from(home)
|
||||
.join(".config")
|
||||
.join("systemd")
|
||||
.join("user")
|
||||
.join(service_file)
|
||||
} else {
|
||||
// System service directory
|
||||
PathBuf::from("/etc/systemd/system").join(service_file)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_systemctl(&self, args: &[&str]) -> Result<String, ServiceManagerError> {
|
||||
let mut cmd = Command::new("systemctl");
|
||||
|
||||
if self.user_mode {
|
||||
cmd.arg("--user");
|
||||
}
|
||||
|
||||
cmd.args(args);
|
||||
|
||||
let output = cmd
|
||||
.output()
|
||||
.map_err(|e| ServiceManagerError::Other(format!("Failed to run systemctl: {}", e)))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(ServiceManagerError::Other(format!(
|
||||
"systemctl command failed: {}",
|
||||
stderr
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
fn create_unit_file(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
|
||||
let unit_path = self.get_unit_file_path(&config.name);
|
||||
|
||||
// Ensure the directory exists
|
||||
if let Some(parent) = unit_path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| {
|
||||
ServiceManagerError::Other(format!("Failed to create unit directory: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Create the unit file content
|
||||
let mut unit_content = String::new();
|
||||
unit_content.push_str("[Unit]\n");
|
||||
unit_content.push_str(&format!("Description={} service\n", config.name));
|
||||
unit_content.push_str("After=network.target\n\n");
|
||||
|
||||
unit_content.push_str("[Service]\n");
|
||||
unit_content.push_str("Type=simple\n");
|
||||
|
||||
// Build the ExecStart command
|
||||
let mut exec_start = config.binary_path.clone();
|
||||
for arg in &config.args {
|
||||
exec_start.push(' ');
|
||||
exec_start.push_str(arg);
|
||||
}
|
||||
unit_content.push_str(&format!("ExecStart={}\n", exec_start));
|
||||
|
||||
if let Some(working_dir) = &config.working_directory {
|
||||
unit_content.push_str(&format!("WorkingDirectory={}\n", working_dir));
|
||||
}
|
||||
|
||||
// Add environment variables
|
||||
for (key, value) in &config.environment {
|
||||
unit_content.push_str(&format!("Environment=\"{}={}\"\n", key, value));
|
||||
}
|
||||
|
||||
if config.auto_restart {
|
||||
unit_content.push_str("Restart=always\n");
|
||||
unit_content.push_str("RestartSec=5\n");
|
||||
}
|
||||
|
||||
unit_content.push_str("\n[Install]\n");
|
||||
unit_content.push_str("WantedBy=default.target\n");
|
||||
|
||||
// Write the unit file
|
||||
fs::write(&unit_path, unit_content)
|
||||
.map_err(|e| ServiceManagerError::Other(format!("Failed to write unit file: {}", e)))?;
|
||||
|
||||
// Reload systemd to pick up the new unit file
|
||||
self.run_systemctl(&["daemon-reload"])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceManager for SystemdServiceManager {
|
||||
fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> {
|
||||
let unit_path = self.get_unit_file_path(service_name);
|
||||
Ok(unit_path.exists())
|
||||
}
|
||||
|
||||
fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
|
||||
let service_name = self.get_service_name(&config.name);
|
||||
|
||||
// Check if service already exists and is running
|
||||
if self.exists(&config.name)? {
|
||||
match self.status(&config.name)? {
|
||||
ServiceStatus::Running => {
|
||||
return Err(ServiceManagerError::ServiceAlreadyExists(
|
||||
config.name.clone(),
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
// Service exists but not running, we can start it
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Create the unit file
|
||||
self.create_unit_file(config)?;
|
||||
}
|
||||
|
||||
// Enable and start the service
|
||||
self.run_systemctl(&["enable", &service_name])
|
||||
.map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?;
|
||||
|
||||
self.run_systemctl(&["start", &service_name])
|
||||
.map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
let service_unit = self.get_service_name(service_name);
|
||||
|
||||
// Check if unit file exists
|
||||
if !self.exists(service_name)? {
|
||||
return Err(ServiceManagerError::ServiceNotFound(
|
||||
service_name.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check if already running
|
||||
match self.status(service_name)? {
|
||||
ServiceStatus::Running => {
|
||||
return Ok(()); // Already running, nothing to do
|
||||
}
|
||||
_ => {
|
||||
// Start the service
|
||||
self.run_systemctl(&["start", &service_unit]).map_err(|e| {
|
||||
ServiceManagerError::StartFailed(service_name.to_string(), e.to_string())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_and_confirm(
|
||||
&self,
|
||||
config: &ServiceConfig,
|
||||
timeout_secs: u64,
|
||||
) -> Result<(), ServiceManagerError> {
|
||||
// Start the service first
|
||||
self.start(config)?;
|
||||
|
||||
// Wait for confirmation with timeout
|
||||
let start_time = std::time::Instant::now();
|
||||
let timeout_duration = std::time::Duration::from_secs(timeout_secs);
|
||||
|
||||
while start_time.elapsed() < timeout_duration {
|
||||
match self.status(&config.name) {
|
||||
Ok(ServiceStatus::Running) => return Ok(()),
|
||||
Ok(ServiceStatus::Failed) => {
|
||||
return Err(ServiceManagerError::StartFailed(
|
||||
config.name.clone(),
|
||||
"Service failed to start".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(_) => {
|
||||
// Still starting, wait a bit
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
Err(_) => {
|
||||
// Service might not exist yet, wait a bit
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(ServiceManagerError::StartFailed(
|
||||
config.name.clone(),
|
||||
format!("Service did not start within {} seconds", timeout_secs),
|
||||
))
|
||||
}
|
||||
|
||||
fn start_existing_and_confirm(
|
||||
&self,
|
||||
service_name: &str,
|
||||
timeout_secs: u64,
|
||||
) -> Result<(), ServiceManagerError> {
|
||||
// Start the existing service first
|
||||
self.start_existing(service_name)?;
|
||||
|
||||
// Wait for confirmation with timeout
|
||||
let start_time = std::time::Instant::now();
|
||||
let timeout_duration = std::time::Duration::from_secs(timeout_secs);
|
||||
|
||||
while start_time.elapsed() < timeout_duration {
|
||||
match self.status(service_name) {
|
||||
Ok(ServiceStatus::Running) => return Ok(()),
|
||||
Ok(ServiceStatus::Failed) => {
|
||||
return Err(ServiceManagerError::StartFailed(
|
||||
service_name.to_string(),
|
||||
"Service failed to start".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(_) => {
|
||||
// Still starting, wait a bit
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
Err(_) => {
|
||||
// Service might not exist yet, wait a bit
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(ServiceManagerError::StartFailed(
|
||||
service_name.to_string(),
|
||||
format!("Service did not start within {} seconds", timeout_secs),
|
||||
))
|
||||
}
|
||||
|
||||
fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
let service_unit = self.get_service_name(service_name);
|
||||
|
||||
// Check if service exists
|
||||
if !self.exists(service_name)? {
|
||||
return Err(ServiceManagerError::ServiceNotFound(
|
||||
service_name.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Stop the service
|
||||
self.run_systemctl(&["stop", &service_unit]).map_err(|e| {
|
||||
ServiceManagerError::StopFailed(service_name.to_string(), e.to_string())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
let service_unit = self.get_service_name(service_name);
|
||||
|
||||
// Check if service exists
|
||||
if !self.exists(service_name)? {
|
||||
return Err(ServiceManagerError::ServiceNotFound(
|
||||
service_name.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Restart the service
|
||||
self.run_systemctl(&["restart", &service_unit])
|
||||
.map_err(|e| {
|
||||
ServiceManagerError::RestartFailed(service_name.to_string(), e.to_string())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> {
|
||||
let service_unit = self.get_service_name(service_name);
|
||||
|
||||
// Check if service exists
|
||||
if !self.exists(service_name)? {
|
||||
return Err(ServiceManagerError::ServiceNotFound(
|
||||
service_name.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Get service status
|
||||
let output = self
|
||||
.run_systemctl(&["is-active", &service_unit])
|
||||
.unwrap_or_else(|_| "unknown".to_string());
|
||||
|
||||
let status = match output.trim() {
|
||||
"active" => ServiceStatus::Running,
|
||||
"inactive" => ServiceStatus::Stopped,
|
||||
"failed" => ServiceStatus::Failed,
|
||||
_ => ServiceStatus::Unknown,
|
||||
};
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
fn logs(
|
||||
&self,
|
||||
service_name: &str,
|
||||
lines: Option<usize>,
|
||||
) -> Result<String, ServiceManagerError> {
|
||||
let service_unit = self.get_service_name(service_name);
|
||||
|
||||
// Check if service exists
|
||||
if !self.exists(service_name)? {
|
||||
return Err(ServiceManagerError::ServiceNotFound(
|
||||
service_name.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Build journalctl command
|
||||
let mut args = vec!["--unit", &service_unit, "--no-pager"];
|
||||
let lines_arg;
|
||||
if let Some(n) = lines {
|
||||
lines_arg = format!("--lines={}", n);
|
||||
args.push(&lines_arg);
|
||||
}
|
||||
|
||||
// Use journalctl to get logs
|
||||
let mut cmd = std::process::Command::new("journalctl");
|
||||
if self.user_mode {
|
||||
cmd.arg("--user");
|
||||
}
|
||||
cmd.args(&args);
|
||||
|
||||
let output = cmd.output().map_err(|e| {
|
||||
ServiceManagerError::LogsFailed(
|
||||
service_name.to_string(),
|
||||
format!("Failed to run journalctl: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(ServiceManagerError::LogsFailed(
|
||||
service_name.to_string(),
|
||||
format!("journalctl command failed: {}", stderr),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
fn list(&self) -> Result<Vec<String>, ServiceManagerError> {
|
||||
// List all services with our prefix
|
||||
let output =
|
||||
self.run_systemctl(&["list-units", "--type=service", "--all", "--no-pager"])?;
|
||||
|
||||
let mut services = Vec::new();
|
||||
for line in output.lines() {
|
||||
if line.contains(&format!("{}-", self.service_prefix)) {
|
||||
// Extract service name from the line
|
||||
if let Some(unit_name) = line.split_whitespace().next() {
|
||||
if let Some(service_name) = unit_name.strip_suffix(".service") {
|
||||
if let Some(name) =
|
||||
service_name.strip_prefix(&format!("{}-", self.service_prefix))
|
||||
{
|
||||
services.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(services)
|
||||
}
|
||||
|
||||
fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
let service_unit = self.get_service_name(service_name);
|
||||
|
||||
// Check if service exists
|
||||
if !self.exists(service_name)? {
|
||||
return Err(ServiceManagerError::ServiceNotFound(
|
||||
service_name.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Try to stop the service first, but don't fail if it's already stopped
|
||||
if let Err(e) = self.stop(service_name) {
|
||||
log::warn!(
|
||||
"Failed to stop service '{}' before removal: {}",
|
||||
service_name,
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
// Disable the service
|
||||
if let Err(e) = self.run_systemctl(&["disable", &service_unit]) {
|
||||
log::warn!("Failed to disable service '{}': {}", service_name, e);
|
||||
}
|
||||
|
||||
// Remove the unit file
|
||||
let unit_path = self.get_unit_file_path(service_name);
|
||||
if unit_path.exists() {
|
||||
std::fs::remove_file(&unit_path).map_err(|e| {
|
||||
ServiceManagerError::Other(format!("Failed to remove unit file: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Reload systemd to pick up the changes
|
||||
self.run_systemctl(&["daemon-reload"])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
379
_archive/service_manager/src/zinit.rs
Normal file
379
_archive/service_manager/src/zinit.rs
Normal file
@@ -0,0 +1,379 @@
|
||||
use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::time::timeout;
|
||||
use zinit_client::{ServiceStatus as ZinitServiceStatus, ZinitClient, ZinitError};
|
||||
|
||||
// Shared runtime for async operations - production-safe initialization
|
||||
static ASYNC_RUNTIME: Lazy<Option<Runtime>> = Lazy::new(|| Runtime::new().ok());
|
||||
|
||||
/// Get the async runtime, creating a temporary one if the static runtime failed
|
||||
fn get_runtime() -> Result<Runtime, ServiceManagerError> {
|
||||
// Try to use the static runtime first
|
||||
if let Some(_runtime) = ASYNC_RUNTIME.as_ref() {
|
||||
// We can't return a reference to the static runtime because we need ownership
|
||||
// for block_on, so we create a new one. This is a reasonable trade-off for safety.
|
||||
Runtime::new().map_err(|e| {
|
||||
ServiceManagerError::Other(format!("Failed to create async runtime: {}", e))
|
||||
})
|
||||
} else {
|
||||
// Static runtime failed, try to create a new one
|
||||
Runtime::new().map_err(|e| {
|
||||
ServiceManagerError::Other(format!("Failed to create async runtime: {}", e))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ZinitServiceManager {
|
||||
client: Arc<ZinitClient>,
|
||||
}
|
||||
|
||||
impl ZinitServiceManager {
|
||||
pub fn new(socket_path: &str) -> Result<Self, ServiceManagerError> {
|
||||
// Create the base zinit client directly
|
||||
let client = Arc::new(ZinitClient::new(socket_path));
|
||||
|
||||
Ok(ZinitServiceManager { client })
|
||||
}
|
||||
|
||||
/// Execute an async operation using the shared runtime or current context
|
||||
fn execute_async<F, T>(&self, operation: F) -> Result<T, ServiceManagerError>
|
||||
where
|
||||
F: std::future::Future<Output = Result<T, ZinitError>> + Send + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
// Check if we're already in a tokio runtime context
|
||||
if let Ok(_handle) = tokio::runtime::Handle::try_current() {
|
||||
// We're in an async context, use spawn_blocking to avoid nested runtime
|
||||
let result = std::thread::spawn(
|
||||
move || -> Result<Result<T, ZinitError>, ServiceManagerError> {
|
||||
let rt = Runtime::new().map_err(|e| {
|
||||
ServiceManagerError::Other(format!("Failed to create runtime: {}", e))
|
||||
})?;
|
||||
Ok(rt.block_on(operation))
|
||||
},
|
||||
)
|
||||
.join()
|
||||
.map_err(|_| ServiceManagerError::Other("Thread join failed".to_string()))?;
|
||||
result?.map_err(|e| ServiceManagerError::Other(e.to_string()))
|
||||
} else {
|
||||
// No current runtime, use production-safe runtime
|
||||
let runtime = get_runtime()?;
|
||||
runtime
|
||||
.block_on(operation)
|
||||
.map_err(|e| ServiceManagerError::Other(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute an async operation with timeout using the shared runtime or current context
|
||||
fn execute_async_with_timeout<F, T>(
|
||||
&self,
|
||||
operation: F,
|
||||
timeout_secs: u64,
|
||||
) -> Result<T, ServiceManagerError>
|
||||
where
|
||||
F: std::future::Future<Output = Result<T, ZinitError>> + Send + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
let timeout_op = timeout(timeout_duration, operation);
|
||||
|
||||
// Check if we're already in a tokio runtime context
|
||||
if let Ok(_handle) = tokio::runtime::Handle::try_current() {
|
||||
// We're in an async context, use spawn_blocking to avoid nested runtime
|
||||
let result = std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(timeout_op)
|
||||
})
|
||||
.join()
|
||||
.map_err(|_| ServiceManagerError::Other("Thread join failed".to_string()))?;
|
||||
|
||||
result
|
||||
.map_err(|_| {
|
||||
ServiceManagerError::Other(format!(
|
||||
"Operation timed out after {} seconds",
|
||||
timeout_secs
|
||||
))
|
||||
})?
|
||||
.map_err(|e| ServiceManagerError::Other(e.to_string()))
|
||||
} else {
|
||||
// No current runtime, use production-safe runtime
|
||||
let runtime = get_runtime()?;
|
||||
runtime
|
||||
.block_on(timeout_op)
|
||||
.map_err(|_| {
|
||||
ServiceManagerError::Other(format!(
|
||||
"Operation timed out after {} seconds",
|
||||
timeout_secs
|
||||
))
|
||||
})?
|
||||
.map_err(|e| ServiceManagerError::Other(e.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceManager for ZinitServiceManager {
|
||||
fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> {
|
||||
let status_res = self.status(service_name);
|
||||
match status_res {
|
||||
Ok(_) => Ok(true),
|
||||
Err(ServiceManagerError::ServiceNotFound(_)) => Ok(false),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
|
||||
// Build the exec command with args
|
||||
let mut exec_command = config.binary_path.clone();
|
||||
if !config.args.is_empty() {
|
||||
exec_command.push(' ');
|
||||
exec_command.push_str(&config.args.join(" "));
|
||||
}
|
||||
|
||||
// Create zinit-compatible service configuration
|
||||
let mut service_config = json!({
|
||||
"exec": exec_command,
|
||||
"oneshot": !config.auto_restart, // zinit uses oneshot, not restart
|
||||
"env": config.environment,
|
||||
});
|
||||
|
||||
// Add optional fields if present
|
||||
if let Some(ref working_dir) = config.working_directory {
|
||||
// Zinit doesn't support working_directory directly, so we need to modify the exec command
|
||||
let cd_command = format!("cd {} && {}", working_dir, exec_command);
|
||||
service_config["exec"] = json!(cd_command);
|
||||
}
|
||||
|
||||
let client = Arc::clone(&self.client);
|
||||
let service_name = config.name.clone();
|
||||
self.execute_async(
|
||||
async move { client.create_service(&service_name, service_config).await },
|
||||
)
|
||||
.map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?;
|
||||
|
||||
self.start_existing(&config.name)
|
||||
}
|
||||
|
||||
fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
let client = Arc::clone(&self.client);
|
||||
let service_name_owned = service_name.to_string();
|
||||
let service_name_for_error = service_name.to_string();
|
||||
self.execute_async(async move { client.start(&service_name_owned).await })
|
||||
.map_err(|e| ServiceManagerError::StartFailed(service_name_for_error, e.to_string()))
|
||||
}
|
||||
|
||||
fn start_and_confirm(
|
||||
&self,
|
||||
config: &ServiceConfig,
|
||||
timeout_secs: u64,
|
||||
) -> Result<(), ServiceManagerError> {
|
||||
// Start the service first
|
||||
self.start(config)?;
|
||||
|
||||
// Wait for confirmation with timeout using the shared runtime
|
||||
self.execute_async_with_timeout(
|
||||
async move {
|
||||
let start_time = std::time::Instant::now();
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
|
||||
while start_time.elapsed() < timeout_duration {
|
||||
// We need to call status in a blocking way from within the async context
|
||||
// For now, we'll use a simple polling approach
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
// Return a timeout error that will be handled by execute_async_with_timeout
|
||||
// Use a generic error since we don't know the exact ZinitError variants
|
||||
Err(ZinitError::from(std::io::Error::new(
|
||||
std::io::ErrorKind::TimedOut,
|
||||
"Timeout waiting for service confirmation",
|
||||
)))
|
||||
},
|
||||
timeout_secs,
|
||||
)?;
|
||||
|
||||
// Check final status
|
||||
match self.status(&config.name)? {
|
||||
ServiceStatus::Running => Ok(()),
|
||||
ServiceStatus::Failed => Err(ServiceManagerError::StartFailed(
|
||||
config.name.clone(),
|
||||
"Service failed to start".to_string(),
|
||||
)),
|
||||
_ => Err(ServiceManagerError::StartFailed(
|
||||
config.name.clone(),
|
||||
format!("Service did not start within {} seconds", timeout_secs),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn start_existing_and_confirm(
|
||||
&self,
|
||||
service_name: &str,
|
||||
timeout_secs: u64,
|
||||
) -> Result<(), ServiceManagerError> {
|
||||
// Start the existing service first
|
||||
self.start_existing(service_name)?;
|
||||
|
||||
// Wait for confirmation with timeout using the shared runtime
|
||||
self.execute_async_with_timeout(
|
||||
async move {
|
||||
let start_time = std::time::Instant::now();
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
|
||||
while start_time.elapsed() < timeout_duration {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
// Return a timeout error that will be handled by execute_async_with_timeout
|
||||
// Use a generic error since we don't know the exact ZinitError variants
|
||||
Err(ZinitError::from(std::io::Error::new(
|
||||
std::io::ErrorKind::TimedOut,
|
||||
"Timeout waiting for service confirmation",
|
||||
)))
|
||||
},
|
||||
timeout_secs,
|
||||
)?;
|
||||
|
||||
// Check final status
|
||||
match self.status(service_name)? {
|
||||
ServiceStatus::Running => Ok(()),
|
||||
ServiceStatus::Failed => Err(ServiceManagerError::StartFailed(
|
||||
service_name.to_string(),
|
||||
"Service failed to start".to_string(),
|
||||
)),
|
||||
_ => Err(ServiceManagerError::StartFailed(
|
||||
service_name.to_string(),
|
||||
format!("Service did not start within {} seconds", timeout_secs),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
let client = Arc::clone(&self.client);
|
||||
let service_name_owned = service_name.to_string();
|
||||
let service_name_for_error = service_name.to_string();
|
||||
self.execute_async(async move { client.stop(&service_name_owned).await })
|
||||
.map_err(|e| ServiceManagerError::StopFailed(service_name_for_error, e.to_string()))
|
||||
}
|
||||
|
||||
fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
let client = Arc::clone(&self.client);
|
||||
let service_name_owned = service_name.to_string();
|
||||
let service_name_for_error = service_name.to_string();
|
||||
self.execute_async(async move { client.restart(&service_name_owned).await })
|
||||
.map_err(|e| ServiceManagerError::RestartFailed(service_name_for_error, e.to_string()))
|
||||
}
|
||||
|
||||
fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> {
|
||||
let client = Arc::clone(&self.client);
|
||||
let service_name_owned = service_name.to_string();
|
||||
let service_name_for_error = service_name.to_string();
|
||||
let status: ZinitServiceStatus = self
|
||||
.execute_async(async move { client.status(&service_name_owned).await })
|
||||
.map_err(|e| {
|
||||
// Check if this is a "service not found" error
|
||||
if e.to_string().contains("not found") || e.to_string().contains("does not exist") {
|
||||
ServiceManagerError::ServiceNotFound(service_name_for_error)
|
||||
} else {
|
||||
ServiceManagerError::Other(e.to_string())
|
||||
}
|
||||
})?;
|
||||
|
||||
// ServiceStatus is a struct with fields, not an enum
|
||||
// We need to check the state field to determine the status
|
||||
// Convert ServiceState to string and match on that
|
||||
let state_str = format!("{:?}", status.state).to_lowercase();
|
||||
let service_status = match state_str.as_str() {
|
||||
s if s.contains("running") => crate::ServiceStatus::Running,
|
||||
s if s.contains("stopped") => crate::ServiceStatus::Stopped,
|
||||
s if s.contains("failed") => crate::ServiceStatus::Failed,
|
||||
_ => crate::ServiceStatus::Unknown,
|
||||
};
|
||||
Ok(service_status)
|
||||
}
|
||||
|
||||
fn logs(
|
||||
&self,
|
||||
service_name: &str,
|
||||
_lines: Option<usize>,
|
||||
) -> Result<String, ServiceManagerError> {
|
||||
// The logs method takes (follow: bool, filter: Option<impl AsRef<str>>)
|
||||
let client = Arc::clone(&self.client);
|
||||
let service_name_owned = service_name.to_string();
|
||||
let logs = self
|
||||
.execute_async(async move {
|
||||
use futures::StreamExt;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
let mut log_stream = client
|
||||
.logs(false, Some(service_name_owned.as_str()))
|
||||
.await?;
|
||||
let mut logs = Vec::new();
|
||||
|
||||
// Collect logs from the stream with a reasonable limit
|
||||
let mut count = 0;
|
||||
const MAX_LOGS: usize = 100;
|
||||
const LOG_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
// Use timeout to prevent hanging
|
||||
let result = timeout(LOG_TIMEOUT, async {
|
||||
while let Some(log_result) = log_stream.next().await {
|
||||
match log_result {
|
||||
Ok(log_entry) => {
|
||||
logs.push(format!("{:?}", log_entry));
|
||||
count += 1;
|
||||
if count >= MAX_LOGS {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
// Handle timeout - this is not an error, just means no more logs available
|
||||
if result.is_err() {
|
||||
log::debug!(
|
||||
"Log reading timed out after {} seconds, returning {} logs",
|
||||
LOG_TIMEOUT.as_secs(),
|
||||
logs.len()
|
||||
);
|
||||
}
|
||||
|
||||
Ok::<Vec<String>, ZinitError>(logs)
|
||||
})
|
||||
.map_err(|e| {
|
||||
ServiceManagerError::LogsFailed(service_name.to_string(), e.to_string())
|
||||
})?;
|
||||
Ok(logs.join("\n"))
|
||||
}
|
||||
|
||||
fn list(&self) -> Result<Vec<String>, ServiceManagerError> {
|
||||
let client = Arc::clone(&self.client);
|
||||
let services = self
|
||||
.execute_async(async move { client.list().await })
|
||||
.map_err(|e| ServiceManagerError::Other(e.to_string()))?;
|
||||
Ok(services.keys().cloned().collect())
|
||||
}
|
||||
|
||||
fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> {
|
||||
// Try to stop the service first, but don't fail if it's already stopped or doesn't exist
|
||||
if let Err(e) = self.stop(service_name) {
|
||||
// Log the error but continue with removal
|
||||
log::warn!(
|
||||
"Failed to stop service '{}' before removal: {}",
|
||||
service_name,
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
let client = Arc::clone(&self.client);
|
||||
let service_name = service_name.to_string();
|
||||
self.execute_async(async move { client.delete_service(&service_name).await })
|
||||
.map_err(|e| ServiceManagerError::Other(e.to_string()))
|
||||
}
|
||||
}
|
243
_archive/service_manager/tests/factory_tests.rs
Normal file
243
_archive/service_manager/tests/factory_tests.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
use sal_service_manager::{create_service_manager, ServiceConfig, ServiceManager};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn test_create_service_manager() {
|
||||
// Test that the factory function creates the appropriate service manager for the platform
|
||||
let manager = create_service_manager().expect("Failed to create service manager");
|
||||
|
||||
// Test basic functionality - should be able to call methods without panicking
|
||||
let list_result = manager.list();
|
||||
|
||||
// The result might be an error (if no service system is available), but it shouldn't panic
|
||||
match list_result {
|
||||
Ok(services) => {
|
||||
println!(
|
||||
"✓ Service manager created successfully, found {} services",
|
||||
services.len()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✓ Service manager created, but got expected error: {}", e);
|
||||
// This is expected on systems without the appropriate service manager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_service_config_creation() {
|
||||
// Test creating various service configurations
|
||||
let basic_config = ServiceConfig {
|
||||
name: "test-service".to_string(),
|
||||
binary_path: "/usr/bin/echo".to_string(),
|
||||
args: vec!["hello".to_string(), "world".to_string()],
|
||||
working_directory: None,
|
||||
environment: HashMap::new(),
|
||||
auto_restart: false,
|
||||
};
|
||||
|
||||
assert_eq!(basic_config.name, "test-service");
|
||||
assert_eq!(basic_config.binary_path, "/usr/bin/echo");
|
||||
assert_eq!(basic_config.args.len(), 2);
|
||||
assert_eq!(basic_config.args[0], "hello");
|
||||
assert_eq!(basic_config.args[1], "world");
|
||||
assert!(basic_config.working_directory.is_none());
|
||||
assert!(basic_config.environment.is_empty());
|
||||
assert!(!basic_config.auto_restart);
|
||||
|
||||
println!("✓ Basic service config created successfully");
|
||||
|
||||
// Test config with environment variables
|
||||
let mut env = HashMap::new();
|
||||
env.insert("PATH".to_string(), "/usr/bin:/bin".to_string());
|
||||
env.insert("HOME".to_string(), "/tmp".to_string());
|
||||
|
||||
let env_config = ServiceConfig {
|
||||
name: "env-service".to_string(),
|
||||
binary_path: "/usr/bin/env".to_string(),
|
||||
args: vec![],
|
||||
working_directory: Some("/tmp".to_string()),
|
||||
environment: env.clone(),
|
||||
auto_restart: true,
|
||||
};
|
||||
|
||||
assert_eq!(env_config.name, "env-service");
|
||||
assert_eq!(env_config.binary_path, "/usr/bin/env");
|
||||
assert!(env_config.args.is_empty());
|
||||
assert_eq!(env_config.working_directory, Some("/tmp".to_string()));
|
||||
assert_eq!(env_config.environment.len(), 2);
|
||||
assert_eq!(
|
||||
env_config.environment.get("PATH"),
|
||||
Some(&"/usr/bin:/bin".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
env_config.environment.get("HOME"),
|
||||
Some(&"/tmp".to_string())
|
||||
);
|
||||
assert!(env_config.auto_restart);
|
||||
|
||||
println!("✓ Environment service config created successfully");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_service_config_clone() {
|
||||
// Test that ServiceConfig can be cloned
|
||||
let original_config = ServiceConfig {
|
||||
name: "original".to_string(),
|
||||
binary_path: "/bin/sh".to_string(),
|
||||
args: vec!["-c".to_string(), "echo test".to_string()],
|
||||
working_directory: Some("/home".to_string()),
|
||||
environment: {
|
||||
let mut env = HashMap::new();
|
||||
env.insert("TEST".to_string(), "value".to_string());
|
||||
env
|
||||
},
|
||||
auto_restart: true,
|
||||
};
|
||||
|
||||
let cloned_config = original_config.clone();
|
||||
|
||||
assert_eq!(original_config.name, cloned_config.name);
|
||||
assert_eq!(original_config.binary_path, cloned_config.binary_path);
|
||||
assert_eq!(original_config.args, cloned_config.args);
|
||||
assert_eq!(
|
||||
original_config.working_directory,
|
||||
cloned_config.working_directory
|
||||
);
|
||||
assert_eq!(original_config.environment, cloned_config.environment);
|
||||
assert_eq!(original_config.auto_restart, cloned_config.auto_restart);
|
||||
|
||||
println!("✓ Service config cloning works correctly");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn test_macos_service_manager() {
|
||||
use sal_service_manager::LaunchctlServiceManager;
|
||||
|
||||
// Test creating macOS-specific service manager
|
||||
let manager = LaunchctlServiceManager::new();
|
||||
|
||||
// Test basic functionality
|
||||
let list_result = manager.list();
|
||||
match list_result {
|
||||
Ok(services) => {
|
||||
println!(
|
||||
"✓ macOS LaunchctlServiceManager created successfully, found {} services",
|
||||
services.len()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!(
|
||||
"✓ macOS LaunchctlServiceManager created, but got expected error: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn test_linux_service_manager() {
|
||||
use sal_service_manager::SystemdServiceManager;
|
||||
|
||||
// Test creating Linux-specific service manager
|
||||
let manager = SystemdServiceManager::new();
|
||||
|
||||
// Test basic functionality
|
||||
let list_result = manager.list();
|
||||
match list_result {
|
||||
Ok(services) => {
|
||||
println!(
|
||||
"✓ Linux SystemdServiceManager created successfully, found {} services",
|
||||
services.len()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!(
|
||||
"✓ Linux SystemdServiceManager created, but got expected error: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_service_status_debug() {
|
||||
use sal_service_manager::ServiceStatus;
|
||||
|
||||
// Test that ServiceStatus can be debugged and cloned
|
||||
let statuses = vec![
|
||||
ServiceStatus::Running,
|
||||
ServiceStatus::Stopped,
|
||||
ServiceStatus::Failed,
|
||||
ServiceStatus::Unknown,
|
||||
];
|
||||
|
||||
for status in &statuses {
|
||||
let cloned = status.clone();
|
||||
let debug_str = format!("{:?}", status);
|
||||
|
||||
assert!(!debug_str.is_empty());
|
||||
assert_eq!(status, &cloned);
|
||||
|
||||
println!(
|
||||
"✓ ServiceStatus::{:?} debug and clone work correctly",
|
||||
status
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_service_manager_error_debug() {
|
||||
use sal_service_manager::ServiceManagerError;
|
||||
|
||||
// Test that ServiceManagerError can be debugged and displayed
|
||||
let errors = vec![
|
||||
ServiceManagerError::ServiceNotFound("test".to_string()),
|
||||
ServiceManagerError::ServiceAlreadyExists("test".to_string()),
|
||||
ServiceManagerError::StartFailed("test".to_string(), "reason".to_string()),
|
||||
ServiceManagerError::StopFailed("test".to_string(), "reason".to_string()),
|
||||
ServiceManagerError::RestartFailed("test".to_string(), "reason".to_string()),
|
||||
ServiceManagerError::LogsFailed("test".to_string(), "reason".to_string()),
|
||||
ServiceManagerError::Other("generic error".to_string()),
|
||||
];
|
||||
|
||||
for error in &errors {
|
||||
let debug_str = format!("{:?}", error);
|
||||
let display_str = format!("{}", error);
|
||||
|
||||
assert!(!debug_str.is_empty());
|
||||
assert!(!display_str.is_empty());
|
||||
|
||||
println!("✓ Error debug: {:?}", error);
|
||||
println!("✓ Error display: {}", error);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_service_manager_trait_object() {
|
||||
// Test that we can use ServiceManager as a trait object
|
||||
let manager: Box<dyn ServiceManager> =
|
||||
create_service_manager().expect("Failed to create service manager");
|
||||
|
||||
// Test that we can call methods through the trait object
|
||||
let list_result = manager.list();
|
||||
|
||||
match list_result {
|
||||
Ok(services) => {
|
||||
println!("✓ Trait object works, found {} services", services.len());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✓ Trait object works, got expected error: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Test exists method
|
||||
let exists_result = manager.exists("non-existent-service");
|
||||
match exists_result {
|
||||
Ok(false) => println!("✓ Trait object exists method works correctly"),
|
||||
Ok(true) => println!("⚠ Unexpectedly found non-existent service"),
|
||||
Err(_) => println!("✓ Trait object exists method works (with error)"),
|
||||
}
|
||||
}
|
177
_archive/service_manager/tests/rhai/service_lifecycle.rhai
Normal file
177
_archive/service_manager/tests/rhai/service_lifecycle.rhai
Normal file
@@ -0,0 +1,177 @@
|
||||
// Service lifecycle management test script
|
||||
// This script tests REAL complete service lifecycle scenarios
|
||||
|
||||
print("=== Service Lifecycle Management Test ===");
|
||||
|
||||
// Create service manager
|
||||
let manager = create_service_manager();
|
||||
print("✓ Service manager created");
|
||||
|
||||
// Test configuration - real services for testing
|
||||
let test_services = [
|
||||
#{
|
||||
name: "lifecycle-test-1",
|
||||
binary_path: "/bin/echo",
|
||||
args: ["Lifecycle test 1"],
|
||||
working_directory: "/tmp",
|
||||
environment: #{},
|
||||
auto_restart: false
|
||||
},
|
||||
#{
|
||||
name: "lifecycle-test-2",
|
||||
binary_path: "/bin/echo",
|
||||
args: ["Lifecycle test 2"],
|
||||
working_directory: "/tmp",
|
||||
environment: #{ "TEST_VAR": "test_value" },
|
||||
auto_restart: false
|
||||
}
|
||||
];
|
||||
|
||||
let total_tests = 0;
|
||||
let passed_tests = 0;
|
||||
|
||||
// Test 1: Service Creation and Start
|
||||
print("\n1. Testing service creation and start...");
|
||||
for service_config in test_services {
|
||||
print(`\nStarting service: ${service_config.name}`);
|
||||
try {
|
||||
start(manager, service_config);
|
||||
print(` ✓ Service ${service_config.name} started successfully`);
|
||||
passed_tests += 1;
|
||||
} catch(e) {
|
||||
print(` ✗ Service ${service_config.name} start failed: ${e}`);
|
||||
}
|
||||
total_tests += 1;
|
||||
}
|
||||
|
||||
// Test 2: Service Existence Check
|
||||
print("\n2. Testing service existence checks...");
|
||||
for service_config in test_services {
|
||||
print(`\nChecking existence of: ${service_config.name}`);
|
||||
try {
|
||||
let service_exists = exists(manager, service_config.name);
|
||||
if service_exists {
|
||||
print(` ✓ Service ${service_config.name} exists: ${service_exists}`);
|
||||
passed_tests += 1;
|
||||
} else {
|
||||
print(` ✗ Service ${service_config.name} doesn't exist after start`);
|
||||
}
|
||||
} catch(e) {
|
||||
print(` ✗ Existence check failed for ${service_config.name}: ${e}`);
|
||||
}
|
||||
total_tests += 1;
|
||||
}
|
||||
|
||||
// Test 3: Status Check
|
||||
print("\n3. Testing status checks...");
|
||||
for service_config in test_services {
|
||||
print(`\nChecking status of: ${service_config.name}`);
|
||||
try {
|
||||
let service_status = status(manager, service_config.name);
|
||||
print(` ✓ Service ${service_config.name} status: ${service_status}`);
|
||||
passed_tests += 1;
|
||||
} catch(e) {
|
||||
print(` ✗ Status check failed for ${service_config.name}: ${e}`);
|
||||
}
|
||||
total_tests += 1;
|
||||
}
|
||||
|
||||
// Test 4: Service List Check
|
||||
print("\n4. Testing service list...");
|
||||
try {
|
||||
let services = list(manager);
|
||||
print(` ✓ Service list retrieved (${services.len()} services)`);
|
||||
|
||||
// Check if our test services are in the list
|
||||
for service_config in test_services {
|
||||
let found = false;
|
||||
for service in services {
|
||||
if service.contains(service_config.name) {
|
||||
found = true;
|
||||
print(` ✓ Found ${service_config.name} in list`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
print(` ⚠ ${service_config.name} not found in service list`);
|
||||
}
|
||||
}
|
||||
passed_tests += 1;
|
||||
} catch(e) {
|
||||
print(` ✗ Service list failed: ${e}`);
|
||||
}
|
||||
total_tests += 1;
|
||||
|
||||
// Test 5: Service Stop
|
||||
print("\n5. Testing service stop...");
|
||||
for service_config in test_services {
|
||||
print(`\nStopping service: ${service_config.name}`);
|
||||
try {
|
||||
stop(manager, service_config.name);
|
||||
print(` ✓ Service ${service_config.name} stopped successfully`);
|
||||
passed_tests += 1;
|
||||
} catch(e) {
|
||||
print(` ✗ Service ${service_config.name} stop failed: ${e}`);
|
||||
}
|
||||
total_tests += 1;
|
||||
}
|
||||
|
||||
// Test 6: Service Removal
|
||||
print("\n6. Testing service removal...");
|
||||
for service_config in test_services {
|
||||
print(`\nRemoving service: ${service_config.name}`);
|
||||
try {
|
||||
remove(manager, service_config.name);
|
||||
print(` ✓ Service ${service_config.name} removed successfully`);
|
||||
passed_tests += 1;
|
||||
} catch(e) {
|
||||
print(` ✗ Service ${service_config.name} removal failed: ${e}`);
|
||||
}
|
||||
total_tests += 1;
|
||||
}
|
||||
|
||||
// Test 7: Cleanup Verification
|
||||
print("\n7. Testing cleanup verification...");
|
||||
for service_config in test_services {
|
||||
print(`\nVerifying removal of: ${service_config.name}`);
|
||||
try {
|
||||
let exists_after_remove = exists(manager, service_config.name);
|
||||
if !exists_after_remove {
|
||||
print(` ✓ Service ${service_config.name} correctly doesn't exist after removal`);
|
||||
passed_tests += 1;
|
||||
} else {
|
||||
print(` ✗ Service ${service_config.name} still exists after removal`);
|
||||
}
|
||||
} catch(e) {
|
||||
print(` ✗ Cleanup verification failed for ${service_config.name}: ${e}`);
|
||||
}
|
||||
total_tests += 1;
|
||||
}
|
||||
|
||||
// Test Summary
|
||||
print("\n=== Lifecycle Test Summary ===");
|
||||
print(`Services tested: ${test_services.len()}`);
|
||||
print(`Total operations: ${total_tests}`);
|
||||
print(`Successful operations: ${passed_tests}`);
|
||||
print(`Failed operations: ${total_tests - passed_tests}`);
|
||||
print(`Success rate: ${(passed_tests * 100) / total_tests}%`);
|
||||
|
||||
if passed_tests == total_tests {
|
||||
print("\n🎉 All lifecycle tests passed!");
|
||||
print("Service manager is working correctly across all scenarios.");
|
||||
} else {
|
||||
print(`\n⚠ ${total_tests - passed_tests} test(s) failed`);
|
||||
print("Some service manager operations need attention.");
|
||||
}
|
||||
|
||||
print("\n=== Service Lifecycle Test Complete ===");
|
||||
|
||||
// Return test results
|
||||
#{
|
||||
summary: #{
|
||||
total_tests: total_tests,
|
||||
passed_tests: passed_tests,
|
||||
success_rate: (passed_tests * 100) / total_tests,
|
||||
services_tested: test_services.len()
|
||||
}
|
||||
}
|
218
_archive/service_manager/tests/rhai/service_manager_basic.rhai
Normal file
218
_archive/service_manager/tests/rhai/service_manager_basic.rhai
Normal file
@@ -0,0 +1,218 @@
|
||||
// Basic service manager functionality test script
|
||||
// This script tests the REAL service manager through Rhai integration
|
||||
|
||||
print("=== Service Manager Basic Functionality Test ===");
|
||||
|
||||
// Test configuration
|
||||
let test_service_name = "rhai-test-service";
|
||||
let test_binary = "/bin/echo";
|
||||
let test_args = ["Hello from Rhai service manager test"];
|
||||
|
||||
print(`Testing service: ${test_service_name}`);
|
||||
print(`Binary: ${test_binary}`);
|
||||
print(`Args: ${test_args}`);
|
||||
|
||||
// Test results tracking
|
||||
let test_results = #{
|
||||
creation: "NOT_RUN",
|
||||
exists_before: "NOT_RUN",
|
||||
start: "NOT_RUN",
|
||||
exists_after: "NOT_RUN",
|
||||
status: "NOT_RUN",
|
||||
list: "NOT_RUN",
|
||||
stop: "NOT_RUN",
|
||||
remove: "NOT_RUN",
|
||||
cleanup: "NOT_RUN"
|
||||
};
|
||||
|
||||
let passed_tests = 0;
|
||||
let total_tests = 0;
|
||||
|
||||
// Test 1: Service Manager Creation
|
||||
print("\n1. Testing service manager creation...");
|
||||
try {
|
||||
let manager = create_service_manager();
|
||||
print("✓ Service manager created successfully");
|
||||
test_results["creation"] = "PASS";
|
||||
passed_tests += 1;
|
||||
total_tests += 1;
|
||||
} catch(e) {
|
||||
print(`✗ Service manager creation failed: ${e}`);
|
||||
test_results["creation"] = "FAIL";
|
||||
total_tests += 1;
|
||||
// Return early if we can't create the manager
|
||||
return test_results;
|
||||
}
|
||||
|
||||
// Create the service manager for all subsequent tests
|
||||
let manager = create_service_manager();
|
||||
|
||||
// Test 2: Check if service exists before creation
|
||||
print("\n2. Testing service existence check (before creation)...");
|
||||
try {
|
||||
let exists_before = exists(manager, test_service_name);
|
||||
print(`✓ Service existence check: ${exists_before}`);
|
||||
|
||||
if !exists_before {
|
||||
print("✓ Service correctly doesn't exist before creation");
|
||||
test_results["exists_before"] = "PASS";
|
||||
passed_tests += 1;
|
||||
} else {
|
||||
print("⚠ Service unexpectedly exists before creation");
|
||||
test_results["exists_before"] = "WARN";
|
||||
}
|
||||
total_tests += 1;
|
||||
} catch(e) {
|
||||
print(`✗ Service existence check failed: ${e}`);
|
||||
test_results["exists_before"] = "FAIL";
|
||||
total_tests += 1;
|
||||
}
|
||||
|
||||
// Test 3: Start the service
|
||||
print("\n3. Testing service start...");
|
||||
try {
|
||||
// Create a service configuration object
|
||||
let service_config = #{
|
||||
name: test_service_name,
|
||||
binary_path: test_binary,
|
||||
args: test_args,
|
||||
working_directory: "/tmp",
|
||||
environment: #{},
|
||||
auto_restart: false
|
||||
};
|
||||
|
||||
start(manager, service_config);
|
||||
print("✓ Service started successfully");
|
||||
test_results["start"] = "PASS";
|
||||
passed_tests += 1;
|
||||
total_tests += 1;
|
||||
} catch(e) {
|
||||
print(`✗ Service start failed: ${e}`);
|
||||
test_results["start"] = "FAIL";
|
||||
total_tests += 1;
|
||||
}
|
||||
|
||||
// Test 4: Check if service exists after creation
|
||||
print("\n4. Testing service existence check (after creation)...");
|
||||
try {
|
||||
let exists_after = exists(manager, test_service_name);
|
||||
print(`✓ Service existence check: ${exists_after}`);
|
||||
|
||||
if exists_after {
|
||||
print("✓ Service correctly exists after creation");
|
||||
test_results["exists_after"] = "PASS";
|
||||
passed_tests += 1;
|
||||
} else {
|
||||
print("✗ Service doesn't exist after creation");
|
||||
test_results["exists_after"] = "FAIL";
|
||||
}
|
||||
total_tests += 1;
|
||||
} catch(e) {
|
||||
print(`✗ Service existence check failed: ${e}`);
|
||||
test_results["exists_after"] = "FAIL";
|
||||
total_tests += 1;
|
||||
}
|
||||
|
||||
// Test 5: Check service status
|
||||
print("\n5. Testing service status...");
|
||||
try {
|
||||
let service_status = status(manager, test_service_name);
|
||||
print(`✓ Service status: ${service_status}`);
|
||||
test_results["status"] = "PASS";
|
||||
passed_tests += 1;
|
||||
total_tests += 1;
|
||||
} catch(e) {
|
||||
print(`✗ Service status check failed: ${e}`);
|
||||
test_results["status"] = "FAIL";
|
||||
total_tests += 1;
|
||||
}
|
||||
|
||||
// Test 6: List services
|
||||
print("\n6. Testing service list...");
|
||||
try {
|
||||
let services = list(manager);
|
||||
print("✓ Service list retrieved");
|
||||
|
||||
// Skip service search due to Rhai type constraints with Vec iteration
|
||||
print(" ⚠️ Skipping service search due to Rhai type constraints");
|
||||
|
||||
test_results["list"] = "PASS";
|
||||
passed_tests += 1;
|
||||
total_tests += 1;
|
||||
} catch(e) {
|
||||
print(`✗ Service list failed: ${e}`);
|
||||
test_results["list"] = "FAIL";
|
||||
total_tests += 1;
|
||||
}
|
||||
|
||||
// Test 7: Stop the service
|
||||
print("\n7. Testing service stop...");
|
||||
try {
|
||||
stop(manager, test_service_name);
|
||||
print(`✓ Service stopped: ${test_service_name}`);
|
||||
test_results["stop"] = "PASS";
|
||||
passed_tests += 1;
|
||||
total_tests += 1;
|
||||
} catch(e) {
|
||||
print(`✗ Service stop failed: ${e}`);
|
||||
test_results["stop"] = "FAIL";
|
||||
total_tests += 1;
|
||||
}
|
||||
|
||||
// Test 8: Remove the service
|
||||
print("\n8. Testing service remove...");
|
||||
try {
|
||||
remove(manager, test_service_name);
|
||||
print(`✓ Service removed: ${test_service_name}`);
|
||||
test_results["remove"] = "PASS";
|
||||
passed_tests += 1;
|
||||
total_tests += 1;
|
||||
} catch(e) {
|
||||
print(`✗ Service remove failed: ${e}`);
|
||||
test_results["remove"] = "FAIL";
|
||||
total_tests += 1;
|
||||
}
|
||||
|
||||
// Test 9: Verify cleanup
|
||||
print("\n9. Testing cleanup verification...");
|
||||
try {
|
||||
let exists_after_remove = exists(manager, test_service_name);
|
||||
if !exists_after_remove {
|
||||
print("✓ Service correctly doesn't exist after removal");
|
||||
test_results["cleanup"] = "PASS";
|
||||
passed_tests += 1;
|
||||
} else {
|
||||
print("✗ Service still exists after removal");
|
||||
test_results["cleanup"] = "FAIL";
|
||||
}
|
||||
total_tests += 1;
|
||||
} catch(e) {
|
||||
print(`✗ Cleanup verification failed: ${e}`);
|
||||
test_results["cleanup"] = "FAIL";
|
||||
total_tests += 1;
|
||||
}
|
||||
|
||||
// Test Summary
|
||||
print("\n=== Test Summary ===");
|
||||
print(`Total tests: ${total_tests}`);
|
||||
print(`Passed: ${passed_tests}`);
|
||||
print(`Failed: ${total_tests - passed_tests}`);
|
||||
print(`Success rate: ${(passed_tests * 100) / total_tests}%`);
|
||||
|
||||
print("\nDetailed Results:");
|
||||
for test_name in test_results.keys() {
|
||||
let result = test_results[test_name];
|
||||
let status_icon = if result == "PASS" { "✓" } else if result == "FAIL" { "✗" } else { "⚠" };
|
||||
print(` ${status_icon} ${test_name}: ${result}`);
|
||||
}
|
||||
|
||||
if passed_tests == total_tests {
|
||||
print("\n🎉 All tests passed!");
|
||||
} else {
|
||||
print(`\n⚠ ${total_tests - passed_tests} test(s) failed`);
|
||||
}
|
||||
|
||||
print("\n=== Service Manager Basic Test Complete ===");
|
||||
|
||||
// Return test results for potential use by calling code
|
||||
test_results
|
252
_archive/service_manager/tests/rhai_integration_tests.rs
Normal file
252
_archive/service_manager/tests/rhai_integration_tests.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
use rhai::{Engine, EvalAltResult};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
/// Helper function to create a Rhai engine for service manager testing
|
||||
fn create_service_manager_engine() -> Result<Engine, Box<EvalAltResult>> {
|
||||
#[cfg(feature = "rhai")]
|
||||
{
|
||||
let mut engine = Engine::new();
|
||||
// Register the service manager module for real testing
|
||||
sal_service_manager::rhai::register_service_manager_module(&mut engine)?;
|
||||
Ok(engine)
|
||||
}
|
||||
#[cfg(not(feature = "rhai"))]
|
||||
{
|
||||
Ok(Engine::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to run a Rhai script file
|
||||
fn run_rhai_script(script_path: &str) -> Result<rhai::Dynamic, Box<EvalAltResult>> {
|
||||
let engine = create_service_manager_engine()?;
|
||||
|
||||
// Read the script file
|
||||
let script_content = fs::read_to_string(script_path)
|
||||
.map_err(|e| format!("Failed to read script file {}: {}", script_path, e))?;
|
||||
|
||||
// Execute the script
|
||||
engine.eval::<rhai::Dynamic>(&script_content)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rhai_service_manager_basic() {
|
||||
let script_path = "tests/rhai/service_manager_basic.rhai";
|
||||
|
||||
if !Path::new(script_path).exists() {
|
||||
println!("⚠ Skipping test: Rhai script not found at {}", script_path);
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Running Rhai service manager basic test...");
|
||||
|
||||
match run_rhai_script(script_path) {
|
||||
Ok(result) => {
|
||||
println!("✓ Rhai basic test completed successfully");
|
||||
|
||||
// Try to extract test results if the script returns them
|
||||
if let Some(map) = result.try_cast::<rhai::Map>() {
|
||||
println!("Test results received from Rhai script:");
|
||||
for (key, value) in map.iter() {
|
||||
println!(" {}: {:?}", key, value);
|
||||
}
|
||||
|
||||
// Check if all tests passed
|
||||
let all_passed = map.values().all(|v| {
|
||||
if let Some(s) = v.clone().try_cast::<String>() {
|
||||
s == "PASS"
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if all_passed {
|
||||
println!("✓ All Rhai tests reported as PASS");
|
||||
} else {
|
||||
println!("⚠ Some Rhai tests did not pass");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Rhai basic test failed: {}", e);
|
||||
assert!(false, "Rhai script execution failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rhai_service_lifecycle() {
|
||||
let script_path = "tests/rhai/service_lifecycle.rhai";
|
||||
|
||||
if !Path::new(script_path).exists() {
|
||||
println!("⚠ Skipping test: Rhai script not found at {}", script_path);
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Running Rhai service lifecycle test...");
|
||||
|
||||
match run_rhai_script(script_path) {
|
||||
Ok(result) => {
|
||||
println!("✓ Rhai lifecycle test completed successfully");
|
||||
|
||||
// Try to extract test results if the script returns them
|
||||
if let Some(map) = result.try_cast::<rhai::Map>() {
|
||||
println!("Lifecycle test results received from Rhai script:");
|
||||
|
||||
// Extract summary if available
|
||||
if let Some(summary) = map.get("summary") {
|
||||
if let Some(summary_map) = summary.clone().try_cast::<rhai::Map>() {
|
||||
println!("Summary:");
|
||||
for (key, value) in summary_map.iter() {
|
||||
println!(" {}: {:?}", key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract performance metrics if available
|
||||
if let Some(performance) = map.get("performance") {
|
||||
if let Some(perf_map) = performance.clone().try_cast::<rhai::Map>() {
|
||||
println!("Performance:");
|
||||
for (key, value) in perf_map.iter() {
|
||||
println!(" {}: {:?}", key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Rhai lifecycle test failed: {}", e);
|
||||
assert!(false, "Rhai script execution failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rhai_engine_functionality() {
|
||||
println!("Testing basic Rhai engine functionality...");
|
||||
|
||||
let engine = create_service_manager_engine().expect("Failed to create Rhai engine");
|
||||
|
||||
// Test basic Rhai functionality
|
||||
let test_script = r#"
|
||||
let test_results = #{
|
||||
basic_math: 2 + 2 == 4,
|
||||
string_ops: "hello".len() == 5,
|
||||
array_ops: [1, 2, 3].len() == 3,
|
||||
map_ops: #{ a: 1, b: 2 }.len() == 2
|
||||
};
|
||||
|
||||
let all_passed = true;
|
||||
for result in test_results.values() {
|
||||
if !result {
|
||||
all_passed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#{
|
||||
results: test_results,
|
||||
all_passed: all_passed
|
||||
}
|
||||
"#;
|
||||
|
||||
match engine.eval::<rhai::Dynamic>(test_script) {
|
||||
Ok(result) => {
|
||||
if let Some(map) = result.try_cast::<rhai::Map>() {
|
||||
if let Some(all_passed) = map.get("all_passed") {
|
||||
if let Some(passed) = all_passed.clone().try_cast::<bool>() {
|
||||
if passed {
|
||||
println!("✓ All basic Rhai functionality tests passed");
|
||||
} else {
|
||||
println!("✗ Some basic Rhai functionality tests failed");
|
||||
assert!(false, "Basic Rhai tests failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(results) = map.get("results") {
|
||||
if let Some(results_map) = results.clone().try_cast::<rhai::Map>() {
|
||||
println!("Detailed results:");
|
||||
for (test_name, result) in results_map.iter() {
|
||||
let status = if let Some(passed) = result.clone().try_cast::<bool>() {
|
||||
if passed {
|
||||
"✓"
|
||||
} else {
|
||||
"✗"
|
||||
}
|
||||
} else {
|
||||
"?"
|
||||
};
|
||||
println!(" {} {}: {:?}", status, test_name, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Basic Rhai functionality test failed: {}", e);
|
||||
assert!(false, "Basic Rhai test failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rhai_script_error_handling() {
|
||||
println!("Testing Rhai error handling...");
|
||||
|
||||
let engine = create_service_manager_engine().expect("Failed to create Rhai engine");
|
||||
|
||||
// Test script with intentional error
|
||||
let error_script = r#"
|
||||
let result = "test";
|
||||
result.non_existent_method(); // This should cause an error
|
||||
"#;
|
||||
|
||||
match engine.eval::<rhai::Dynamic>(error_script) {
|
||||
Ok(_) => {
|
||||
println!("⚠ Expected error but script succeeded");
|
||||
assert!(
|
||||
false,
|
||||
"Error handling test failed - expected error but got success"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✓ Error correctly caught: {}", e);
|
||||
// Verify it's the expected type of error
|
||||
assert!(e.to_string().contains("method") || e.to_string().contains("function"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rhai_script_files_exist() {
|
||||
println!("Checking that Rhai test scripts exist...");
|
||||
|
||||
let script_files = [
|
||||
"tests/rhai/service_manager_basic.rhai",
|
||||
"tests/rhai/service_lifecycle.rhai",
|
||||
];
|
||||
|
||||
for script_file in &script_files {
|
||||
if Path::new(script_file).exists() {
|
||||
println!("✓ Found script: {}", script_file);
|
||||
|
||||
// Verify the file is readable and not empty
|
||||
match fs::read_to_string(script_file) {
|
||||
Ok(content) => {
|
||||
if content.trim().is_empty() {
|
||||
assert!(false, "Script file {} is empty", script_file);
|
||||
}
|
||||
println!(" Content length: {} characters", content.len());
|
||||
}
|
||||
Err(e) => {
|
||||
assert!(false, "Failed to read script file {}: {}", script_file, e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert!(false, "Required script file not found: {}", script_file);
|
||||
}
|
||||
}
|
||||
|
||||
println!("✓ All required Rhai script files exist and are readable");
|
||||
}
|
317
_archive/service_manager/tests/zinit_integration_tests.rs
Normal file
317
_archive/service_manager/tests/zinit_integration_tests.rs
Normal file
@@ -0,0 +1,317 @@
|
||||
use sal_service_manager::{
|
||||
ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus, ZinitServiceManager,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
/// Helper function to find an available Zinit socket path
|
||||
async fn get_available_socket_path() -> Option<String> {
|
||||
let socket_paths = [
|
||||
"/var/run/zinit.sock",
|
||||
"/tmp/zinit.sock",
|
||||
"/run/zinit.sock",
|
||||
"./zinit.sock",
|
||||
];
|
||||
|
||||
for path in &socket_paths {
|
||||
// Try to create a ZinitServiceManager to test connectivity
|
||||
if let Ok(manager) = ZinitServiceManager::new(path) {
|
||||
// Test if we can list services (basic connectivity test)
|
||||
if manager.list().is_ok() {
|
||||
println!("✓ Found working Zinit socket at: {}", path);
|
||||
return Some(path.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Helper function to clean up test services
|
||||
async fn cleanup_test_service(manager: &dyn ServiceManager, service_name: &str) {
|
||||
let _ = manager.stop(service_name);
|
||||
let _ = manager.remove(service_name);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_zinit_service_manager_creation() {
|
||||
if let Some(socket_path) = get_available_socket_path().await {
|
||||
let manager = ZinitServiceManager::new(&socket_path);
|
||||
assert!(
|
||||
manager.is_ok(),
|
||||
"Should be able to create ZinitServiceManager"
|
||||
);
|
||||
|
||||
let manager = manager.unwrap();
|
||||
|
||||
// Test basic connectivity by listing services
|
||||
let list_result = manager.list();
|
||||
assert!(list_result.is_ok(), "Should be able to list services");
|
||||
|
||||
println!("✓ ZinitServiceManager created successfully");
|
||||
} else {
|
||||
println!("⚠ Skipping test_zinit_service_manager_creation: No Zinit socket available");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_service_lifecycle() {
|
||||
if let Some(socket_path) = get_available_socket_path().await {
|
||||
let manager = ZinitServiceManager::new(&socket_path).expect("Failed to create manager");
|
||||
let service_name = "test-lifecycle-service";
|
||||
|
||||
// Clean up any existing service first
|
||||
cleanup_test_service(&manager, service_name).await;
|
||||
|
||||
let config = ServiceConfig {
|
||||
name: service_name.to_string(),
|
||||
binary_path: "echo".to_string(),
|
||||
args: vec!["Hello from lifecycle test".to_string()],
|
||||
working_directory: Some("/tmp".to_string()),
|
||||
environment: HashMap::new(),
|
||||
auto_restart: false,
|
||||
};
|
||||
|
||||
// Test service creation and start
|
||||
println!("Testing service creation and start...");
|
||||
let start_result = manager.start(&config);
|
||||
match start_result {
|
||||
Ok(_) => {
|
||||
println!("✓ Service started successfully");
|
||||
|
||||
// Wait a bit for the service to run
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
|
||||
// Test service exists
|
||||
let exists = manager.exists(service_name);
|
||||
assert!(exists.is_ok(), "Should be able to check if service exists");
|
||||
|
||||
if let Ok(true) = exists {
|
||||
println!("✓ Service exists check passed");
|
||||
|
||||
// Test service status
|
||||
let status_result = manager.status(service_name);
|
||||
match status_result {
|
||||
Ok(status) => {
|
||||
println!("✓ Service status: {:?}", status);
|
||||
assert!(
|
||||
matches!(status, ServiceStatus::Running | ServiceStatus::Stopped),
|
||||
"Service should be running or stopped (for oneshot)"
|
||||
);
|
||||
}
|
||||
Err(e) => println!("⚠ Status check failed: {}", e),
|
||||
}
|
||||
|
||||
// Test service logs
|
||||
let logs_result = manager.logs(service_name, None);
|
||||
match logs_result {
|
||||
Ok(logs) => {
|
||||
println!("✓ Retrieved logs: {}", logs.len());
|
||||
// For echo command, we should have some output
|
||||
assert!(
|
||||
!logs.is_empty() || logs.contains("Hello"),
|
||||
"Should have log output"
|
||||
);
|
||||
}
|
||||
Err(e) => println!("⚠ Logs retrieval failed: {}", e),
|
||||
}
|
||||
|
||||
// Test service list
|
||||
let list_result = manager.list();
|
||||
match list_result {
|
||||
Ok(services) => {
|
||||
println!("✓ Listed {} services", services.len());
|
||||
assert!(
|
||||
services.contains(&service_name.to_string()),
|
||||
"Service should appear in list"
|
||||
);
|
||||
}
|
||||
Err(e) => println!("⚠ List services failed: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// Test service stop
|
||||
println!("Testing service stop...");
|
||||
let stop_result = manager.stop(service_name);
|
||||
match stop_result {
|
||||
Ok(_) => println!("✓ Service stopped successfully"),
|
||||
Err(e) => println!("⚠ Stop failed: {}", e),
|
||||
}
|
||||
|
||||
// Test service removal
|
||||
println!("Testing service removal...");
|
||||
let remove_result = manager.remove(service_name);
|
||||
match remove_result {
|
||||
Ok(_) => println!("✓ Service removed successfully"),
|
||||
Err(e) => println!("⚠ Remove failed: {}", e),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("⚠ Service creation/start failed: {}", e);
|
||||
// This might be expected if zinit doesn't allow service creation
|
||||
}
|
||||
}
|
||||
|
||||
// Final cleanup
|
||||
cleanup_test_service(&manager, service_name).await;
|
||||
} else {
|
||||
println!("⚠ Skipping test_service_lifecycle: No Zinit socket available");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_service_start_and_confirm() {
|
||||
if let Some(socket_path) = get_available_socket_path().await {
|
||||
let manager = ZinitServiceManager::new(&socket_path).expect("Failed to create manager");
|
||||
let service_name = "test-start-confirm-service";
|
||||
|
||||
// Clean up any existing service first
|
||||
cleanup_test_service(&manager, service_name).await;
|
||||
|
||||
let config = ServiceConfig {
|
||||
name: service_name.to_string(),
|
||||
binary_path: "sleep".to_string(),
|
||||
args: vec!["5".to_string()], // Sleep for 5 seconds
|
||||
working_directory: Some("/tmp".to_string()),
|
||||
environment: HashMap::new(),
|
||||
auto_restart: false,
|
||||
};
|
||||
|
||||
// Test start_and_confirm with timeout
|
||||
println!("Testing start_and_confirm with timeout...");
|
||||
let start_result = manager.start_and_confirm(&config, 10);
|
||||
match start_result {
|
||||
Ok(_) => {
|
||||
println!("✓ Service started and confirmed successfully");
|
||||
|
||||
// Verify it's actually running
|
||||
let status = manager.status(service_name);
|
||||
match status {
|
||||
Ok(ServiceStatus::Running) => println!("✓ Service confirmed running"),
|
||||
Ok(other_status) => {
|
||||
println!("⚠ Service in unexpected state: {:?}", other_status)
|
||||
}
|
||||
Err(e) => println!("⚠ Status check failed: {}", e),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("⚠ start_and_confirm failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Test start_existing_and_confirm
|
||||
println!("Testing start_existing_and_confirm...");
|
||||
let start_existing_result = manager.start_existing_and_confirm(service_name, 5);
|
||||
match start_existing_result {
|
||||
Ok(_) => println!("✓ start_existing_and_confirm succeeded"),
|
||||
Err(e) => println!("⚠ start_existing_and_confirm failed: {}", e),
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
cleanup_test_service(&manager, service_name).await;
|
||||
} else {
|
||||
println!("⚠ Skipping test_service_start_and_confirm: No Zinit socket available");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_service_restart() {
|
||||
if let Some(socket_path) = get_available_socket_path().await {
|
||||
let manager = ZinitServiceManager::new(&socket_path).expect("Failed to create manager");
|
||||
let service_name = "test-restart-service";
|
||||
|
||||
// Clean up any existing service first
|
||||
cleanup_test_service(&manager, service_name).await;
|
||||
|
||||
let config = ServiceConfig {
|
||||
name: service_name.to_string(),
|
||||
binary_path: "echo".to_string(),
|
||||
args: vec!["Restart test".to_string()],
|
||||
working_directory: Some("/tmp".to_string()),
|
||||
environment: HashMap::new(),
|
||||
auto_restart: true, // Enable auto-restart for this test
|
||||
};
|
||||
|
||||
// Start the service first
|
||||
let start_result = manager.start(&config);
|
||||
if start_result.is_ok() {
|
||||
// Wait for service to be established
|
||||
sleep(Duration::from_millis(1000)).await;
|
||||
|
||||
// Test restart
|
||||
println!("Testing service restart...");
|
||||
let restart_result = manager.restart(service_name);
|
||||
match restart_result {
|
||||
Ok(_) => {
|
||||
println!("✓ Service restarted successfully");
|
||||
|
||||
// Wait and check status
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
|
||||
let status_result = manager.status(service_name);
|
||||
match status_result {
|
||||
Ok(status) => {
|
||||
println!("✓ Service state after restart: {:?}", status);
|
||||
}
|
||||
Err(e) => println!("⚠ Status check after restart failed: {}", e),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("⚠ Restart failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
cleanup_test_service(&manager, service_name).await;
|
||||
} else {
|
||||
println!("⚠ Skipping test_service_restart: No Zinit socket available");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_handling() {
|
||||
if let Some(socket_path) = get_available_socket_path().await {
|
||||
let manager = ZinitServiceManager::new(&socket_path).expect("Failed to create manager");
|
||||
|
||||
// Test operations on non-existent service
|
||||
let non_existent_service = "non-existent-service-12345";
|
||||
|
||||
// Test status of non-existent service
|
||||
let status_result = manager.status(non_existent_service);
|
||||
match status_result {
|
||||
Err(ServiceManagerError::ServiceNotFound(_)) => {
|
||||
println!("✓ Correctly returned ServiceNotFound for non-existent service");
|
||||
}
|
||||
Err(other_error) => {
|
||||
println!(
|
||||
"⚠ Got different error for non-existent service: {}",
|
||||
other_error
|
||||
);
|
||||
}
|
||||
Ok(_) => {
|
||||
println!("⚠ Unexpectedly found non-existent service");
|
||||
}
|
||||
}
|
||||
|
||||
// Test exists for non-existent service
|
||||
let exists_result = manager.exists(non_existent_service);
|
||||
match exists_result {
|
||||
Ok(false) => println!("✓ Correctly reported non-existent service as not existing"),
|
||||
Ok(true) => println!("⚠ Incorrectly reported non-existent service as existing"),
|
||||
Err(e) => println!("⚠ Error checking existence: {}", e),
|
||||
}
|
||||
|
||||
// Test stop of non-existent service
|
||||
let stop_result = manager.stop(non_existent_service);
|
||||
match stop_result {
|
||||
Err(_) => println!("✓ Correctly failed to stop non-existent service"),
|
||||
Ok(_) => println!("⚠ Unexpectedly succeeded in stopping non-existent service"),
|
||||
}
|
||||
|
||||
println!("✓ Error handling tests completed");
|
||||
} else {
|
||||
println!("⚠ Skipping test_error_handling: No Zinit socket available");
|
||||
}
|
||||
}
|
@@ -6,10 +6,12 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
rm -f ./target/debug/herodo
|
||||
|
||||
# Build the herodo project
|
||||
echo "Building herodo..."
|
||||
cargo build --bin herodo
|
||||
# cargo build --release --bin herodo
|
||||
# Build the herodo project from the herodo package
|
||||
echo "Building herodo from herodo package..."
|
||||
cd herodo
|
||||
cargo build
|
||||
# cargo build --release
|
||||
cd ..
|
||||
|
||||
# Check if the build was successful
|
||||
if [ $? -ne 0 ]; then
|
||||
@@ -20,12 +22,29 @@ fi
|
||||
# Echo a success message
|
||||
echo "Build successful!"
|
||||
|
||||
mkdir -p ~/hero/bin/
|
||||
cp target/debug/herodo ~/hero/bin/herodo
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "Running as root, copying to /usr/local/bin/"
|
||||
cp target/debug/herodo /usr/local/bin/herodo
|
||||
else
|
||||
echo "Running as non-root user, copying to ~/hero/bin/"
|
||||
mkdir -p ~/hero/bin/
|
||||
cp target/debug/herodo ~/hero/bin/herodo
|
||||
fi
|
||||
|
||||
# Check if a script name was provided
|
||||
if [ $# -eq 1 ]; then
|
||||
echo "Running specified test: $1"
|
||||
herodo "src/herodo/scripts/$1.rhai"
|
||||
|
||||
# Check if the script exists in src/rhaiexamples/
|
||||
if [ -f "src/rhaiexamples/$1.rhai" ]; then
|
||||
herodo "src/rhaiexamples/$1.rhai"
|
||||
# Check if the script exists in src/herodo/scripts/
|
||||
elif [ -f "src/herodo/scripts/$1.rhai" ]; then
|
||||
herodo "src/herodo/scripts/$1.rhai"
|
||||
else
|
||||
echo "Error: Script $1.rhai not found in src/rhaiexamples/ or src/herodo/scripts/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
22
docs/cfg/footer.json
Normal file
22
docs/cfg/footer.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"style": "dark",
|
||||
"links": [
|
||||
{
|
||||
"title": "Web",
|
||||
"items": [
|
||||
{
|
||||
"label": "ThreeFold.io",
|
||||
"href": "https://threefold.io"
|
||||
},
|
||||
{
|
||||
"href": "https://mycelium.threefold.io/",
|
||||
"label": "Mycelium Network"
|
||||
},
|
||||
{
|
||||
"href": "https://aibox.threefold.io/",
|
||||
"label": "AI Box"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
17
docs/cfg/main.json
Normal file
17
docs/cfg/main.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"title": "ThreeFold HeroScript",
|
||||
"tagline": "ThreeFold HeroScript",
|
||||
"favicon": "img/favicon.png",
|
||||
"url": "https://threefold.info",
|
||||
"url_home": "docs/intro",
|
||||
"baseUrl": "/heroscript/",
|
||||
"image": "img/tf_graph.png",
|
||||
"metadata": {
|
||||
"description": "Internet Infrastructur for Everyone by Everyone, Everywhere.",
|
||||
"image": "https://threefold.info/tfgrid4/img/tf_graph.png",
|
||||
"title": "ThreeFold"
|
||||
},
|
||||
"buildDest":["root@info.ourworld.tf:/root/hero/www/info/heroscript"],
|
||||
"buildDestDev":["root@info.ourworld.tf:/root/hero/www/infodev/heroscript"],
|
||||
"copyright": "ThreeFold"
|
||||
}
|
25
docs/cfg/navbar.json
Normal file
25
docs/cfg/navbar.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"title": "",
|
||||
"logo": {
|
||||
"alt": "ThreeFold Logo",
|
||||
"src": "img/logo.svg",
|
||||
"srcDark": "img/new_logo_tft.png"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"href": "https://threefold.io",
|
||||
"label": "ThreeFold.io",
|
||||
"position": "right"
|
||||
},
|
||||
{
|
||||
"href": "https://mycelium.threefold.io/",
|
||||
"label": "Mycelium Network",
|
||||
"position": "right"
|
||||
},
|
||||
{
|
||||
"href": "https://aibox.threefold.io/",
|
||||
"label": "AI Box",
|
||||
"position": "right"
|
||||
}
|
||||
]
|
||||
}
|
7
docs/docs/intro.md
Normal file
7
docs/docs/intro.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: "intro"
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# HeroScript
|
||||
|
105
docs/docs/rhai/buildah_module_tests.md
Normal file
105
docs/docs/rhai/buildah_module_tests.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Buildah Module Tests
|
||||
|
||||
This document describes the test scripts for the Buildah module in the SAL library. These tests verify the functionality of the Buildah module's container and image operations.
|
||||
|
||||
## Test Structure
|
||||
|
||||
The tests are organized into three main scripts:
|
||||
|
||||
1. **Builder Pattern** (`01_builder_pattern.rhai`): Tests for the Builder pattern, including creating containers, running commands, and working with container content.
|
||||
2. **Image Operations** (`02_image_operations.rhai`): Tests for image-related operations like pulling, tagging, listing, and removing images.
|
||||
3. **Container Operations** (`03_container_operations.rhai`): Tests for container-related operations like configuration, isolation, and content management.
|
||||
|
||||
Additionally, there's a runner script (`run_all_tests.rhai`) that executes all tests and reports results. The runner script contains simplified versions of the individual tests to avoid dependency issues.
|
||||
|
||||
## Running the Tests
|
||||
|
||||
To run all tests, execute the following command from the project root:
|
||||
|
||||
```bash
|
||||
herodo --path src/rhai_tests/buildah/run_all_tests.rhai
|
||||
```
|
||||
|
||||
To run individual test scripts:
|
||||
|
||||
```bash
|
||||
herodo --path src/rhai_tests/buildah/01_builder_pattern.rhai
|
||||
```
|
||||
|
||||
## Test Details
|
||||
|
||||
### Builder Pattern Test
|
||||
|
||||
The Builder Pattern test (`01_builder_pattern.rhai`) verifies the following functions:
|
||||
|
||||
- `bah_new`: Creating a new Builder with a container from a specified image
|
||||
- Builder properties: `container_id`, `name`, `image`, `debug_mode`
|
||||
- `run`: Running commands in the container
|
||||
- `write_content`: Writing content to files in the container
|
||||
- `read_content`: Reading content from files in the container
|
||||
- `set_entrypoint`: Setting the container's entrypoint
|
||||
- `set_cmd`: Setting the container's command
|
||||
- `add`: Adding files to the container
|
||||
- `copy`: Copying files to the container
|
||||
- `commit`: Committing the container to an image
|
||||
- `remove`: Removing the container
|
||||
- `images`: Listing images
|
||||
- `image_remove`: Removing images
|
||||
|
||||
### Image Operations Test
|
||||
|
||||
The Image Operations test (`02_image_operations.rhai`) verifies the following functions:
|
||||
|
||||
- `image_pull`: Pulling images from registries
|
||||
- `image_tag`: Tagging images
|
||||
- `images`: Listing images
|
||||
- `build`: Building images from Dockerfiles
|
||||
- `image_remove`: Removing images
|
||||
|
||||
The test creates a temporary directory with a Dockerfile for testing the build functionality.
|
||||
|
||||
### Container Operations Test
|
||||
|
||||
The Container Operations test (`03_container_operations.rhai`) verifies the following functions:
|
||||
|
||||
- `reset`: Resetting a Builder by removing its container
|
||||
- `config`: Configuring container properties
|
||||
- `run_with_isolation`: Running commands with isolation
|
||||
- Content operations: Creating and executing scripts in the container
|
||||
- `commit` with options: Committing a container with additional configuration
|
||||
|
||||
## Test Runner
|
||||
|
||||
The test runner script (`run_all_tests.rhai`) provides a framework for executing all tests and reporting results. It:
|
||||
|
||||
1. Checks if Buildah is available before running tests
|
||||
2. Skips tests if Buildah is not available
|
||||
3. Contains simplified versions of each test
|
||||
4. Runs each test in a try/catch block to handle errors
|
||||
5. Catches and reports any errors
|
||||
6. Provides a summary of passed, failed, and skipped tests
|
||||
|
||||
## Buildah Requirements
|
||||
|
||||
These tests require the Buildah tool to be installed and available in the system's PATH. The tests will check for Buildah's availability and skip the tests if it's not found, rather than failing.
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
To add a new test:
|
||||
|
||||
1. Create a new Rhai script in the `src/rhai_tests/buildah` directory
|
||||
2. Add a new test section to the `run_all_tests.rhai` script
|
||||
3. Update this documentation to include information about the new test
|
||||
|
||||
## Best Practices for Writing Tests
|
||||
|
||||
When writing tests for the Buildah module:
|
||||
|
||||
1. Always check if Buildah is available before running tests
|
||||
2. Use unique names for containers and images to avoid conflicts
|
||||
3. Clean up any containers, images, or files created during testing
|
||||
4. Use assertions to verify expected behavior
|
||||
5. Print clear messages about what's being tested
|
||||
6. Handle errors gracefully
|
||||
7. Make tests independent of each other
|
||||
8. Keep tests focused on specific functionality
|
71
docs/docs/rhai/ci_workflow.md
Normal file
71
docs/docs/rhai/ci_workflow.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Continuous Integration for Rhai Tests
|
||||
|
||||
This document describes the continuous integration (CI) workflow for running Rhai tests in the SAL library.
|
||||
|
||||
## GitHub Actions Workflow
|
||||
|
||||
The SAL project includes a GitHub Actions workflow that automatically runs all Rhai tests whenever changes are made to relevant files. This ensures that the Rhai integration continues to work correctly as the codebase evolves.
|
||||
|
||||
### Workflow File
|
||||
|
||||
The workflow is defined in `.github/workflows/rhai-tests.yml`.
|
||||
|
||||
### Trigger Events
|
||||
|
||||
The workflow runs automatically when:
|
||||
|
||||
1. Changes are pushed to the `main` or `master` branch that affect:
|
||||
- Rhai test scripts (`src/rhai_tests/**`)
|
||||
- Rhai module code (`src/rhai/**`)
|
||||
- Git module code (`src/git/**`)
|
||||
- OS module code (`src/os/**`)
|
||||
- The test runner script (`run_rhai_tests.sh`)
|
||||
- The workflow file itself (`.github/workflows/rhai-tests.yml`)
|
||||
|
||||
2. A pull request is opened or updated that affects the same files.
|
||||
|
||||
3. The workflow is manually triggered using the GitHub Actions interface.
|
||||
|
||||
### Workflow Steps
|
||||
|
||||
The workflow performs the following steps:
|
||||
|
||||
1. **Checkout Code**: Checks out the repository code.
|
||||
2. **Set up Rust**: Installs the Rust toolchain.
|
||||
3. **Cache Dependencies**: Caches Rust dependencies to speed up builds.
|
||||
4. **Build herodo**: Builds the `herodo` binary used to run Rhai scripts.
|
||||
5. **Install Dependencies**: Installs system dependencies like Git and curl.
|
||||
6. **Run Rhai Tests**: Runs the `run_rhai_tests.sh` script to execute all Rhai tests.
|
||||
7. **Check for Failures**: Verifies that all tests passed.
|
||||
|
||||
### Test Results
|
||||
|
||||
The workflow will fail if any Rhai test fails. This prevents changes that break the Rhai integration from being merged.
|
||||
|
||||
## Local Testing
|
||||
|
||||
Before pushing changes, you can run the same tests locally using the `run_rhai_tests.sh` script:
|
||||
|
||||
```bash
|
||||
./run_rhai_tests.sh
|
||||
```
|
||||
|
||||
This will produce the same test results as the CI workflow, allowing you to catch and fix issues before pushing your changes.
|
||||
|
||||
## Logs
|
||||
|
||||
The test runner script creates a log file (`run_rhai_tests.log`) that contains the output of all tests. This log is used by the CI workflow to check for test failures.
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
When adding new tests, make sure they are included in the appropriate module's test runner script (`run_all_tests.rhai`). The CI workflow will automatically run the new tests.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the CI workflow fails, check the GitHub Actions logs for details. Common issues include:
|
||||
|
||||
1. **Missing Dependencies**: Ensure all required dependencies are installed.
|
||||
2. **Test Failures**: Fix any failing tests.
|
||||
3. **Build Errors**: Fix any errors in the Rust code.
|
||||
|
||||
If you need to modify the workflow, edit the `.github/workflows/rhai-tests.yml` file.
|
81
docs/docs/rhai/git_module_tests.md
Normal file
81
docs/docs/rhai/git_module_tests.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Git Module Tests
|
||||
|
||||
This document describes the test scripts for the Git module in the SAL library. These tests verify the functionality of the Git module's repository management and Git operations.
|
||||
|
||||
## Test Structure
|
||||
|
||||
The tests are organized into two main scripts:
|
||||
|
||||
1. **Basic Git Operations** (`01_git_basic.rhai`): Tests basic Git functionality like creating a GitTree, listing repositories, finding repositories, and cloning repositories.
|
||||
2. **Git Repository Operations** (`02_git_operations.rhai`): Tests Git operations like pull, reset, commit, and push.
|
||||
|
||||
Additionally, there's a runner script (`run_all_tests.rhai`) that executes all tests and reports results. The runner script contains simplified versions of the individual tests to avoid dependency issues.
|
||||
|
||||
## Running the Tests
|
||||
|
||||
To run all tests, execute the following command from the project root:
|
||||
|
||||
```bash
|
||||
herodo --path git/tests/rhai/run_all_tests.rhai
|
||||
```
|
||||
|
||||
To run individual test scripts:
|
||||
|
||||
```bash
|
||||
herodo --path git/tests/rhai/01_git_basic.rhai
|
||||
```
|
||||
|
||||
## Test Details
|
||||
|
||||
### Basic Git Operations Test
|
||||
|
||||
The basic Git operations test (`01_git_basic.rhai`) verifies the following functions:
|
||||
|
||||
- `git_tree_new`: Creating a GitTree
|
||||
- `list`: Listing repositories in a GitTree
|
||||
- `find`: Finding repositories matching a pattern
|
||||
- `get`: Getting or cloning a repository
|
||||
- `path`: Getting the path of a repository
|
||||
- `has_changes`: Checking if a repository has changes
|
||||
|
||||
The test creates a temporary directory, performs operations on it, and then cleans up after itself.
|
||||
|
||||
### Git Repository Operations Test
|
||||
|
||||
The Git repository operations test (`02_git_operations.rhai`) verifies the following functions:
|
||||
|
||||
- `pull`: Pulling changes from a remote repository
|
||||
- `reset`: Resetting local changes
|
||||
- `commit`: Committing changes (method existence only)
|
||||
- `push`: Pushing changes to a remote repository (method existence only)
|
||||
|
||||
Note: The test does not actually commit or push changes to avoid modifying remote repositories. It only verifies that the methods exist and can be called.
|
||||
|
||||
## Test Runner
|
||||
|
||||
The test runner script (`run_all_tests.rhai`) provides a framework for executing all tests and reporting results. It:
|
||||
|
||||
1. Contains simplified versions of each test
|
||||
2. Runs each test in a try/catch block to handle errors
|
||||
3. Catches and reports any errors
|
||||
4. Provides a summary of passed and failed tests
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
To add a new test:
|
||||
|
||||
1. Create a new Rhai script in the `src/rhai_tests/git` directory
|
||||
2. Add a new test section to the `run_all_tests.rhai` script
|
||||
3. Update this documentation to include information about the new test
|
||||
|
||||
## Best Practices for Writing Tests
|
||||
|
||||
When writing tests for the Git module:
|
||||
|
||||
1. Always clean up temporary files and directories
|
||||
2. Use assertions to verify expected behavior
|
||||
3. Print clear messages about what's being tested
|
||||
4. Handle errors gracefully
|
||||
5. Make tests independent of each other
|
||||
6. Avoid tests that modify remote repositories
|
||||
7. Keep tests focused on specific functionality
|
85
docs/docs/rhai/index.md
Normal file
85
docs/docs/rhai/index.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Rhai Scripting in SAL
|
||||
|
||||
This documentation covers the Rhai scripting integration in the SAL (System Abstraction Layer) library.
|
||||
|
||||
## Overview
|
||||
|
||||
SAL provides integration with the [Rhai scripting language](https://rhai.rs/), allowing you to use SAL's functionality in scripts. This enables automation of system tasks, testing, and more complex operations without having to write Rust code.
|
||||
|
||||
## Modules
|
||||
|
||||
SAL exposes the following modules to Rhai scripts:
|
||||
|
||||
- [OS Module](os_module_tests.md): File system operations, downloads, and package management
|
||||
- Process Module: Process management and command execution
|
||||
- Git Module: Git repository operations
|
||||
- Text Module: Text processing utilities
|
||||
- Buildah Module: Container image building
|
||||
- Nerdctl Module: Container runtime operations
|
||||
- RFS Module: Remote file system operations
|
||||
- Redis Client Module: Redis database connection and operations
|
||||
- PostgreSQL Client Module: PostgreSQL database connection and operations
|
||||
|
||||
## Running Rhai Scripts
|
||||
|
||||
You can run Rhai scripts using the `herodo` binary:
|
||||
|
||||
```bash
|
||||
herodo --path path/to/script.rhai
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
SAL includes test scripts for verifying the functionality of its Rhai integration. These tests are located in the `src/rhai_tests` directory and are organized by module.
|
||||
|
||||
- [OS Module Tests](os_module_tests.md): Tests for file system, download, and package management operations
|
||||
- [Git Module Tests](git_module_tests.md): Tests for Git repository management and operations
|
||||
- [Process Module Tests](process_module_tests.md): Tests for command execution and process management
|
||||
- [Redis Client Module Tests](redisclient_module_tests.md): Tests for Redis connection and operations
|
||||
- [PostgreSQL Client Module Tests](postgresclient_module_tests.md): Tests for PostgreSQL connection and operations
|
||||
- [Text Module Tests](text_module_tests.md): Tests for text manipulation, normalization, replacement, and template rendering
|
||||
- [Buildah Module Tests](buildah_module_tests.md): Tests for container and image operations
|
||||
- [Nerdctl Module Tests](nerdctl_module_tests.md): Tests for container and image operations using nerdctl
|
||||
- [RFS Module Tests](rfs_module_tests.md): Tests for remote filesystem operations and filesystem layers
|
||||
- [Running Tests](running_tests.md): Instructions for running all Rhai tests
|
||||
- [CI Workflow](ci_workflow.md): Continuous integration workflow for Rhai tests
|
||||
|
||||
## Examples
|
||||
|
||||
For examples of how to use SAL's Rhai integration, see the `examples` directory in the project root. These examples demonstrate various features and use cases.
|
||||
|
||||
## Writing Your Own Scripts
|
||||
|
||||
When writing Rhai scripts that use SAL:
|
||||
|
||||
1. Import the necessary modules (they're automatically registered)
|
||||
2. Use the functions provided by each module
|
||||
3. Handle errors appropriately
|
||||
4. Clean up resources when done
|
||||
|
||||
Example:
|
||||
|
||||
```rhai
|
||||
// Simple example of using the OS module
|
||||
let test_dir = "my_test_dir";
|
||||
mkdir(test_dir);
|
||||
|
||||
if exist(test_dir) {
|
||||
print(`Directory ${test_dir} created successfully`);
|
||||
|
||||
// Create a file
|
||||
let test_file = test_dir + "/test.txt";
|
||||
file_write(test_file, "Hello, world!");
|
||||
|
||||
// Read the file
|
||||
let content = file_read(test_file);
|
||||
print(`File content: ${content}`);
|
||||
|
||||
// Clean up
|
||||
delete(test_dir);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
For detailed information about the functions available in each module, refer to the module-specific documentation.
|
386
docs/docs/rhai/mycelium_tutorial.md
Normal file
386
docs/docs/rhai/mycelium_tutorial.md
Normal file
@@ -0,0 +1,386 @@
|
||||
# Mycelium Tutorial for Rhai
|
||||
|
||||
This tutorial explains how to use the Mycelium networking functionality in Rhai scripts. Mycelium is a peer-to-peer networking system that allows nodes to communicate with each other, and the Rhai bindings provide an easy way to interact with Mycelium from your scripts.
|
||||
|
||||
## Introduction
|
||||
|
||||
The Mycelium module for Rhai provides the following capabilities:
|
||||
|
||||
- Getting node information
|
||||
- Managing peers (listing, adding, removing)
|
||||
- Viewing routing information
|
||||
- Sending and receiving messages between nodes
|
||||
|
||||
This tutorial will walk you through using these features with example scripts.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using the Mycelium functionality in Rhai, you need:
|
||||
|
||||
1. A running Mycelium node accessible via HTTP
|
||||
> See https://github.com/threefoldtech/mycelium
|
||||
2. The Rhai runtime with Mycelium module enabled
|
||||
|
||||
## Basic Mycelium Operations
|
||||
|
||||
Let's start by exploring the basic operations available in Mycelium using the `mycelium_basic.rhai` example.
|
||||
|
||||
### Getting Node Information
|
||||
|
||||
To get information about your Mycelium node:
|
||||
|
||||
```rhai
|
||||
// API URL for Mycelium
|
||||
let api_url = "http://localhost:8989";
|
||||
|
||||
// Get node information
|
||||
print("Getting node information:");
|
||||
try {
|
||||
let node_info = mycelium_get_node_info(api_url);
|
||||
print(`Node subnet: ${node_info.nodeSubnet}`);
|
||||
print(`Node public key: ${node_info.nodePubkey}`);
|
||||
} catch(err) {
|
||||
print(`Error getting node info: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
This code:
|
||||
1. Sets the API URL for your Mycelium node
|
||||
2. Calls `mycelium_get_node_info()` to retrieve information about the node
|
||||
3. Prints the node's subnet and public key
|
||||
|
||||
### Managing Peers
|
||||
|
||||
#### Listing Peers
|
||||
|
||||
To list all peers connected to your Mycelium node:
|
||||
|
||||
```rhai
|
||||
// List all peers
|
||||
print("\nListing all peers:");
|
||||
try {
|
||||
let peers = mycelium_list_peers(api_url);
|
||||
|
||||
if peers.is_empty() {
|
||||
print("No peers connected.");
|
||||
} else {
|
||||
for peer in peers {
|
||||
print(`Peer Endpoint: ${peer.endpoint.proto}://${peer.endpoint.socketAddr}`);
|
||||
print(` Type: ${peer.type}`);
|
||||
print(` Connection State: ${peer.connectionState}`);
|
||||
print(` Bytes sent: ${peer.txBytes}`);
|
||||
print(` Bytes received: ${peer.rxBytes}`);
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error listing peers: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
This code:
|
||||
1. Calls `mycelium_list_peers()` to get all connected peers
|
||||
2. Iterates through the peers and prints their details
|
||||
|
||||
#### Adding a Peer
|
||||
|
||||
To add a new peer to your Mycelium node:
|
||||
|
||||
```rhai
|
||||
// Add a new peer
|
||||
print("\nAdding a new peer:");
|
||||
let new_peer_address = "tcp://65.21.231.58:9651";
|
||||
try {
|
||||
let result = mycelium_add_peer(api_url, new_peer_address);
|
||||
print(`Peer added: ${result.success}`);
|
||||
} catch(err) {
|
||||
print(`Error adding peer: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
This code:
|
||||
1. Specifies a peer address to add
|
||||
2. Calls `mycelium_add_peer()` to add the peer to your node
|
||||
3. Prints whether the operation was successful
|
||||
|
||||
#### Removing a Peer
|
||||
|
||||
To remove a peer from your Mycelium node:
|
||||
|
||||
```rhai
|
||||
// Remove a peer
|
||||
print("\nRemoving a peer:");
|
||||
let peer_id = "tcp://65.21.231.58:9651"; // This is the peer we added earlier
|
||||
try {
|
||||
let result = mycelium_remove_peer(api_url, peer_id);
|
||||
print(`Peer removed: ${result.success}`);
|
||||
} catch(err) {
|
||||
print(`Error removing peer: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
This code:
|
||||
1. Specifies the peer ID to remove
|
||||
2. Calls `mycelium_remove_peer()` to remove the peer
|
||||
3. Prints whether the operation was successful
|
||||
|
||||
### Viewing Routing Information
|
||||
|
||||
#### Listing Selected Routes
|
||||
|
||||
To list the selected routes in your Mycelium node:
|
||||
|
||||
```rhai
|
||||
// List selected routes
|
||||
print("\nListing selected routes:");
|
||||
try {
|
||||
let routes = mycelium_list_selected_routes(api_url);
|
||||
|
||||
if routes.is_empty() {
|
||||
print("No selected routes.");
|
||||
} else {
|
||||
for route in routes {
|
||||
print(`Subnet: ${route.subnet}`);
|
||||
print(` Next hop: ${route.nextHop}`);
|
||||
print(` Metric: ${route.metric}`);
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error listing routes: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
This code:
|
||||
1. Calls `mycelium_list_selected_routes()` to get all selected routes
|
||||
2. Iterates through the routes and prints their details
|
||||
|
||||
#### Listing Fallback Routes
|
||||
|
||||
To list the fallback routes in your Mycelium node:
|
||||
|
||||
```rhai
|
||||
// List fallback routes
|
||||
print("\nListing fallback routes:");
|
||||
try {
|
||||
let routes = mycelium_list_fallback_routes(api_url);
|
||||
|
||||
if routes.is_empty() {
|
||||
print("No fallback routes.");
|
||||
} else {
|
||||
for route in routes {
|
||||
print(`Subnet: ${route.subnet}`);
|
||||
print(` Next hop: ${route.nextHop}`);
|
||||
print(` Metric: ${route.metric}`);
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error listing fallback routes: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
This code:
|
||||
1. Calls `mycelium_list_fallback_routes()` to get all fallback routes
|
||||
2. Iterates through the routes and prints their details
|
||||
|
||||
## Sending Messages
|
||||
|
||||
Now let's look at how to send messages using the `mycelium_send_message.rhai` example.
|
||||
|
||||
```rhai
|
||||
// API URL for Mycelium
|
||||
let api_url = "http://localhost:1111";
|
||||
|
||||
// Send a message
|
||||
print("\nSending a message:");
|
||||
let destination = "5af:ae6b:dcd8:ffdb:b71:7dde:d3:1033"; // Replace with the actual destination IP address
|
||||
let topic = "test_topic";
|
||||
let message = "Hello from Rhai sender!";
|
||||
let deadline_secs = -10; // Seconds we wait for a reply
|
||||
|
||||
try {
|
||||
print(`Attempting to send message to ${destination} on topic '${topic}'`);
|
||||
let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs);
|
||||
print(`result: ${result}`);
|
||||
print(`Message sent: ${result.success}`);
|
||||
if result.id != "" {
|
||||
print(`Message ID: ${result.id}`);
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error sending message: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
This code:
|
||||
1. Sets the API URL for your Mycelium node
|
||||
2. Specifies the destination IP address, topic, message content, and deadline
|
||||
3. Calls `mycelium_send_message()` to send the message
|
||||
4. Prints the result, including the message ID if successful
|
||||
|
||||
### Important Parameters for Sending Messages
|
||||
|
||||
- `api_url`: The URL of your Mycelium node's API
|
||||
- `destination`: The IP address of the destination node
|
||||
- `topic`: The topic to send the message on (must match what the receiver is listening for)
|
||||
- `message`: The content of the message
|
||||
- `deadline_secs`: Time in seconds to wait for a reply. Use a negative value if you don't want to wait for a reply.
|
||||
|
||||
## Receiving Messages
|
||||
|
||||
Now let's look at how to receive messages using the `mycelium_receive_message.rhai` example.
|
||||
|
||||
```rhai
|
||||
// API URL for Mycelium
|
||||
let api_url = "http://localhost:2222";
|
||||
|
||||
// Receive messages
|
||||
print("\nReceiving messages:");
|
||||
let receive_topic = "test_topic";
|
||||
let wait_deadline_secs = 100;
|
||||
|
||||
print(`Listening for messages on topic '${receive_topic}'...`);
|
||||
try {
|
||||
let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs);
|
||||
|
||||
if messages.is_empty() {
|
||||
// print("No new messages received in this poll.");
|
||||
} else {
|
||||
print("Received a message:");
|
||||
print(` Message id: ${messages.id}`);
|
||||
print(` Message from: ${messages.srcIp}`);
|
||||
print(` Topic: ${messages.topic}`);
|
||||
print(` Payload: ${messages.payload}`);
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error receiving messages: ${err}`);
|
||||
}
|
||||
|
||||
print("Finished attempting to receive messages.");
|
||||
```
|
||||
|
||||
This code:
|
||||
1. Sets the API URL for your Mycelium node
|
||||
2. Specifies the topic to listen on and how long to wait for messages
|
||||
3. Calls `mycelium_receive_messages()` to receive messages
|
||||
4. Processes and prints any received messages
|
||||
|
||||
### Important Parameters for Receiving Messages
|
||||
|
||||
- `api_url`: The URL of your Mycelium node's API
|
||||
- `receive_topic`: The topic to listen for messages on (must match what the sender is using)
|
||||
- `wait_deadline_secs`: Time in seconds to wait for messages to arrive. The function will block for this duration if no messages are immediately available.
|
||||
|
||||
## Complete Messaging Example
|
||||
|
||||
To set up a complete messaging system, you would typically run two instances of Mycelium (node A sender, node B receiver).
|
||||
|
||||
1. Run the `mycelium_receive_message.rhai` script to listen for messages. **Fill in the API address of node B**.
|
||||
2. Run the `mycelium_send_message.rhai` script to send messages. **Fill in the API address of node A, and fill in the overlay address of node B as destination**.
|
||||
|
||||
### Setting Up the Receiver
|
||||
|
||||
First, start a Mycelium node and run the receiver script:
|
||||
|
||||
```rhai
|
||||
// API URL for Mycelium
|
||||
let api_url = "http://localhost:2222"; // Your receiver node's API URL
|
||||
|
||||
// Receive messages
|
||||
let receive_topic = "test_topic";
|
||||
let wait_deadline_secs = 100; // Wait up to 100 seconds for messages
|
||||
|
||||
print(`Listening for messages on topic '${receive_topic}'...`);
|
||||
try {
|
||||
let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs);
|
||||
|
||||
if messages.is_empty() {
|
||||
print("No new messages received in this poll.");
|
||||
} else {
|
||||
print("Received a message:");
|
||||
print(` Message id: ${messages.id}`);
|
||||
print(` Message from: ${messages.srcIp}`);
|
||||
print(` Topic: ${messages.topic}`);
|
||||
print(` Payload: ${messages.payload}`);
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error receiving messages: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Setting Up the Sender
|
||||
|
||||
Then, on another Mycelium node, run the sender script:
|
||||
|
||||
```rhai
|
||||
// API URL for Mycelium
|
||||
let api_url = "http://localhost:1111"; // Your sender node's API URL
|
||||
|
||||
// Send a message
|
||||
let destination = "5af:ae6b:dcd8:ffdb:b71:7dde:d3:1033"; // The receiver node's IP address
|
||||
let topic = "test_topic"; // Must match the receiver's topic
|
||||
let message = "Hello from Rhai sender!";
|
||||
let deadline_secs = -10; // Don't wait for a reply
|
||||
|
||||
try {
|
||||
print(`Attempting to send message to ${destination} on topic '${topic}'`);
|
||||
let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs);
|
||||
print(`Message sent: ${result.success}`);
|
||||
if result.id != "" {
|
||||
print(`Message ID: ${result.id}`);
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error sending message: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Example: setting up 2 different Mycelium peers on same the host and sending/receiving a message
|
||||
|
||||
#### Obtain Mycelium
|
||||
|
||||
- Download the latest Mycelium binary from https://github.com/threefoldtech/mycelium/releases/
|
||||
- Or compile from source
|
||||
|
||||
#### Setup
|
||||
- Create two different private key files. Each key file should contain exactely 32 bytes. In this example we'll save these files as `sender.bin` and `receiver.bin`. Note: generate your own 32-byte key files, the values below are just used as examples.
|
||||
> `echo '9f3d72c1a84be6f027bba94cde015ee839cedb2ac4f2822bfc94449e3e2a1c6a' > sender.bin`
|
||||
|
||||
> `echo 'e81c5a76f42bd9a3c73fe0bb2196acdfb6348e99d0b01763a2e57ce3a4e8f5dd' > receiver.bin`
|
||||
|
||||
#### Start the nodes
|
||||
- **Sender**: this node will have the API server hosted on `127.0.0.1:1111` and the JSON-RPC server on `127.0.0.1:8991`.
|
||||
> `sudo ./mycelium --key-file sender.bin --disable-peer-discovery --disable-quic --no-tun --api-addr 127.0.0.1:1111 --jsonrpc-addr 127.0.0.1:8991`
|
||||
|
||||
- **Receiver**: this node will have the API server hosted on `127.0.0.1:2222` and the JSON-RPC server on `127.0.0.1:8992`.
|
||||
> `sudo ./mycelium --key-file receiver.bin --disable-peer-discovery --disable-quic --no-tun --api-addr 127.0.0.1:2222 --jsonrpc-addr 127.0.0.1:8992 --peers tcp://<UNDERLAY_IP_SENDER>:9651`
|
||||
- Obtain the Mycelium overlay IP by running `./mycelium --key-file receiver.bin --api-addr 127.0.0.1:2222 inspect`. **Replace this IP as destination in the [mycelium_send_message.rhai](../../../examples/mycelium/mycelium_send_message.rhai) example**.
|
||||
|
||||
#### Execute the examples
|
||||
- First build by executing `./build_herdo.sh` from the SAL root directory
|
||||
- `cd target/debug`
|
||||
|
||||
- Run the sender script: `sudo ./herodo --path ../../examples/mycelium/mycelium_send_message.rhai`
|
||||
```
|
||||
Executing: ../../examples/mycelium/mycelium_send_message.rhai
|
||||
|
||||
Sending a message:
|
||||
Attempting to send message to 50e:6d75:4568:366e:f75:2ac3:bbb1:3fdd on topic 'test_topic'
|
||||
result: #{"id": "bfd47dc689a7b826"}
|
||||
Message sent:
|
||||
Message ID: bfd47dc689a7b826
|
||||
Script executed successfull
|
||||
```
|
||||
|
||||
- Run the receiver script: `sudo ./herodo --path ../../examples/mycelium/mycelium_receive_message.rhai`
|
||||
```
|
||||
Executing: ../../examples/mycelium/mycelium_receive_message.rhai
|
||||
|
||||
Receiving messages:
|
||||
Listening for messages on topic 'test_topic'...
|
||||
Received a message:
|
||||
Message id: bfd47dc689a7b826
|
||||
Message from: 45d:26e1:a413:9d08:80ce:71c6:a931:4315
|
||||
Topic: dGVzdF90b3BpYw==
|
||||
Payload: SGVsbG8gZnJvbSBSaGFpIHNlbmRlciE=
|
||||
Finished attempting to receive messages.
|
||||
Script executed successfully
|
||||
```
|
||||
> Decoding the payload `SGVsbG8gZnJvbSBSaGFpIHNlbmRlciE=` results in the expected `Hello from Rhai sender!` message. Mission succesful!
|
||||
|
116
docs/docs/rhai/nerdctl_module_tests.md
Normal file
116
docs/docs/rhai/nerdctl_module_tests.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Nerdctl Module Tests
|
||||
|
||||
This document describes the test scripts for the Nerdctl module in the SAL library. These tests verify the functionality of the Nerdctl module's container and image operations.
|
||||
|
||||
## Test Structure
|
||||
|
||||
The tests are organized into three main scripts:
|
||||
|
||||
1. **Container Operations** (`01_container_operations.rhai`): Tests for basic container operations like creating, running, executing commands, and removing containers.
|
||||
2. **Image Operations** (`02_image_operations.rhai`): Tests for image-related operations like pulling, tagging, listing, building, and removing images.
|
||||
3. **Container Builder Pattern** (`03_container_builder.rhai`): Tests for the Container Builder pattern, which provides a fluent interface for configuring and running containers.
|
||||
|
||||
Additionally, there's a runner script (`run_all_tests.rhai`) that executes all tests and reports results. The runner script contains simplified versions of the individual tests to avoid dependency issues.
|
||||
|
||||
## Running the Tests
|
||||
|
||||
To run all tests, execute the following command from the project root:
|
||||
|
||||
```bash
|
||||
herodo --path src/rhai_tests/nerdctl/run_all_tests.rhai
|
||||
```
|
||||
|
||||
To run individual test scripts:
|
||||
|
||||
```bash
|
||||
herodo --path src/rhai_tests/nerdctl/01_container_operations.rhai
|
||||
```
|
||||
|
||||
## Test Details
|
||||
|
||||
### Container Operations Test
|
||||
|
||||
The Container Operations test (`01_container_operations.rhai`) verifies the following functions:
|
||||
|
||||
- `nerdctl_container_new`: Creating a new Container
|
||||
- Container properties: `name`, `container_id`, `image`, `detach`
|
||||
- `with_image`: Setting the container image
|
||||
- `with_detach`: Setting detach mode
|
||||
- `with_env` and `with_envs`: Setting environment variables
|
||||
- `with_port` and `with_ports`: Setting port mappings
|
||||
- `with_volume`: Setting volume mounts
|
||||
- `with_cpu_limit` and `with_memory_limit`: Setting resource limits
|
||||
- `run`: Running the container
|
||||
- `exec`: Executing commands in the container
|
||||
- `logs`: Getting container logs
|
||||
- `stop`: Stopping the container
|
||||
- `remove`: Removing the container
|
||||
|
||||
### Image Operations Test
|
||||
|
||||
The Image Operations test (`02_image_operations.rhai`) verifies the following functions:
|
||||
|
||||
- `nerdctl_image_pull`: Pulling images from registries
|
||||
- `nerdctl_images`: Listing images
|
||||
- `nerdctl_image_tag`: Tagging images
|
||||
- `nerdctl_image_build`: Building images from Dockerfiles
|
||||
- `nerdctl_run_with_name`: Running containers from images
|
||||
- `nerdctl_stop` and `nerdctl_remove`: Stopping and removing containers
|
||||
- `nerdctl_image_remove`: Removing images
|
||||
|
||||
The test creates a temporary directory with a Dockerfile for testing the build functionality.
|
||||
|
||||
### Container Builder Pattern Test
|
||||
|
||||
The Container Builder Pattern test (`03_container_builder.rhai`) verifies the following functions:
|
||||
|
||||
- `nerdctl_container_from_image`: Creating a container from an image
|
||||
- `reset`: Resetting container configuration
|
||||
- `with_detach`: Setting detach mode
|
||||
- `with_ports`: Setting multiple port mappings
|
||||
- `with_volumes`: Setting multiple volume mounts
|
||||
- `with_envs`: Setting multiple environment variables
|
||||
- `with_network`: Setting network
|
||||
- `with_cpu_limit` and `with_memory_limit`: Setting resource limits
|
||||
- `run`: Running the container
|
||||
- `exec`: Executing commands in the container
|
||||
- `stop`: Stopping the container
|
||||
- `remove`: Removing the container
|
||||
|
||||
The test also verifies that environment variables and volume mounts work correctly by writing and reading files between the container and the host.
|
||||
|
||||
## Test Runner
|
||||
|
||||
The test runner script (`run_all_tests.rhai`) provides a framework for executing all tests and reporting results. It:
|
||||
|
||||
1. Checks if nerdctl is available before running tests
|
||||
2. Skips tests if nerdctl is not available
|
||||
3. Contains simplified versions of each test
|
||||
4. Runs each test in a try/catch block to handle errors
|
||||
5. Catches and reports any errors
|
||||
6. Provides a summary of passed, failed, and skipped tests
|
||||
|
||||
## Nerdctl Requirements
|
||||
|
||||
These tests require the nerdctl tool to be installed and available in the system's PATH. The tests will check for nerdctl's availability and skip the tests if it's not found, rather than failing.
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
To add a new test:
|
||||
|
||||
1. Create a new Rhai script in the `src/rhai_tests/nerdctl` directory
|
||||
2. Add a new test section to the `run_all_tests.rhai` script
|
||||
3. Update this documentation to include information about the new test
|
||||
|
||||
## Best Practices for Writing Tests
|
||||
|
||||
When writing tests for the Nerdctl module:
|
||||
|
||||
1. Always check if nerdctl is available before running tests
|
||||
2. Use unique names for containers and images to avoid conflicts
|
||||
3. Clean up any containers, images, or files created during testing
|
||||
4. Use assertions to verify expected behavior
|
||||
5. Print clear messages about what's being tested
|
||||
6. Handle errors gracefully
|
||||
7. Make tests independent of each other
|
||||
8. Keep tests focused on specific functionality
|
105
docs/docs/rhai/os_module_tests.md
Normal file
105
docs/docs/rhai/os_module_tests.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# OS Module Tests
|
||||
|
||||
This document describes the test scripts for the OS module in the SAL library. These tests verify the functionality of the OS module's file system operations, download capabilities, and package management features.
|
||||
|
||||
## Test Structure
|
||||
|
||||
The tests are organized into three main scripts:
|
||||
|
||||
1. **File Operations** (`01_file_operations.rhai`): Tests file system operations like creating, reading, writing, and manipulating files and directories.
|
||||
2. **Download Operations** (`02_download_operations.rhai`): Tests downloading files from the internet and related operations.
|
||||
3. **Package Operations** (`03_package_operations.rhai`): Tests package management functionality.
|
||||
|
||||
Additionally, there's a runner script (`run_all_tests.rhai`) that executes all tests and reports results. The runner script contains simplified versions of the individual tests to avoid dependency on the `run_script` function.
|
||||
|
||||
## Running the Tests
|
||||
|
||||
To run all tests, execute the following command from the project root:
|
||||
|
||||
```bash
|
||||
# Assume that you have the herodo binary/built into your system
|
||||
herodo --path src/rhai_tests/os/run_all_tests.rhai
|
||||
```
|
||||
|
||||
To run individual test scripts:
|
||||
|
||||
```bash
|
||||
# Assume that you have the herodo binary/built into your system
|
||||
herodo --path src/rhai_tests/os/01_file_operations.rhai
|
||||
```
|
||||
|
||||
## Test Details
|
||||
|
||||
### File Operations Test
|
||||
|
||||
The file operations test (`01_file_operations.rhai`) verifies the following functions:
|
||||
|
||||
- `mkdir`: Creating directories
|
||||
- `file_write`: Writing content to files
|
||||
- `file_read`: Reading content from files
|
||||
- `file_size`: Getting file size
|
||||
- `file_write_append`: Appending content to files
|
||||
- `copy`: Copying files
|
||||
- `mv`: Moving files
|
||||
- `find_file`: Finding a single file matching a pattern
|
||||
- `find_files`: Finding multiple files matching a pattern
|
||||
- `find_dir`: Finding a single directory matching a pattern
|
||||
- `find_dirs`: Finding multiple directories matching a pattern
|
||||
- `chdir`: Changing the current working directory
|
||||
- `rsync`: Synchronizing directories
|
||||
- `delete`: Deleting files and directories
|
||||
- `exist`: Checking if files or directories exist
|
||||
|
||||
The test creates a temporary directory structure, performs operations on it, and then cleans up after itself.
|
||||
|
||||
### Download Operations Test
|
||||
|
||||
The download operations test (`02_download_operations.rhai`) verifies the following functions:
|
||||
|
||||
- `which`: Checking if a command exists in the system PATH
|
||||
- `cmd_ensure_exists`: Ensuring commands exist
|
||||
- `download_file`: Downloading a file from a URL
|
||||
- `chmod_exec`: Making a file executable
|
||||
|
||||
The test downloads a small file from GitHub, verifies its content, and then cleans up.
|
||||
|
||||
### Package Operations Test
|
||||
|
||||
The package operations test (`03_package_operations.rhai`) verifies the following functions:
|
||||
|
||||
- `package_platform`: Getting the current platform
|
||||
- `package_set_debug`: Setting debug mode for package operations
|
||||
- `package_is_installed`: Checking if a package is installed
|
||||
- `package_search`: Searching for packages
|
||||
- `package_list`: Listing installed packages
|
||||
|
||||
Note: The test does not verify `package_install`, `package_remove`, `package_update`, or `package_upgrade` as these require root privileges and could modify the system state.
|
||||
|
||||
## Test Runner
|
||||
|
||||
The test runner script (`run_all_tests.rhai`) provides a framework for executing all tests and reporting results. It:
|
||||
|
||||
1. Contains simplified versions of each test
|
||||
2. Runs each test in a try/catch block to handle errors
|
||||
3. Catches and reports any errors
|
||||
4. Provides a summary of passed and failed tests
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
To add a new test:
|
||||
|
||||
1. Create a new Rhai script in the `src/rhai_tests/os` directory
|
||||
2. Add a new test section to the `run_all_tests.rhai` script
|
||||
3. Update this documentation to include information about the new test
|
||||
|
||||
## Best Practices for Writing Tests
|
||||
|
||||
When writing tests for the OS module:
|
||||
|
||||
1. Always clean up temporary files and directories
|
||||
2. Use assertions to verify expected behavior
|
||||
3. Print clear messages about what's being tested
|
||||
4. Handle errors gracefully
|
||||
5. Make tests independent of each other
|
||||
6. Avoid tests that require root privileges when possible
|
||||
7. Keep tests focused on specific functionality
|
188
docs/docs/rhai/postgresclient_module_tests.md
Normal file
188
docs/docs/rhai/postgresclient_module_tests.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# PostgreSQL Client Module Tests
|
||||
|
||||
The PostgreSQL client module provides functions for connecting to and interacting with PostgreSQL databases. These tests verify the functionality of the module.
|
||||
|
||||
## PostgreSQL Client Features
|
||||
|
||||
The PostgreSQL client module provides the following features:
|
||||
|
||||
1. **Basic PostgreSQL Operations**: Execute queries, fetch results, etc.
|
||||
2. **Connection Management**: Automatic connection handling and reconnection
|
||||
3. **Builder Pattern for Configuration**: Flexible configuration with authentication support
|
||||
4. **PostgreSQL Installer**: Install and configure PostgreSQL using nerdctl
|
||||
5. **Database Management**: Create databases and execute SQL scripts
|
||||
|
||||
## Prerequisites
|
||||
|
||||
For basic PostgreSQL operations:
|
||||
- PostgreSQL server must be running and accessible
|
||||
- Environment variables should be set for connection details:
|
||||
- `POSTGRES_HOST`: PostgreSQL server host (default: localhost)
|
||||
- `POSTGRES_PORT`: PostgreSQL server port (default: 5432)
|
||||
- `POSTGRES_USER`: PostgreSQL username (default: postgres)
|
||||
- `POSTGRES_PASSWORD`: PostgreSQL password
|
||||
- `POSTGRES_DB`: PostgreSQL database name (default: postgres)
|
||||
|
||||
For PostgreSQL installer:
|
||||
- nerdctl must be installed and working
|
||||
- Docker images must be accessible
|
||||
- Sufficient permissions to create and manage containers
|
||||
|
||||
## Test Files
|
||||
|
||||
### 01_postgres_connection.rhai
|
||||
|
||||
Tests basic PostgreSQL connection and operations:
|
||||
|
||||
- Connecting to PostgreSQL
|
||||
- Pinging the server
|
||||
- Creating a table
|
||||
- Inserting data
|
||||
- Querying data
|
||||
- Dropping a table
|
||||
- Resetting the connection
|
||||
|
||||
### 02_postgres_installer.rhai
|
||||
|
||||
Tests PostgreSQL installer functionality:
|
||||
|
||||
- Installing PostgreSQL using nerdctl
|
||||
- Creating a database
|
||||
- Executing SQL scripts
|
||||
- Checking if PostgreSQL is running
|
||||
|
||||
### run_all_tests.rhai
|
||||
|
||||
Runs all PostgreSQL client module tests and provides a summary of the results.
|
||||
|
||||
## Running the Tests
|
||||
|
||||
You can run the tests using the `herodo` command:
|
||||
|
||||
```bash
|
||||
herodo --path src/rhai_tests/postgresclient/run_all_tests.rhai
|
||||
```
|
||||
|
||||
Or run individual tests:
|
||||
|
||||
```bash
|
||||
herodo --path src/rhai_tests/postgresclient/01_postgres_connection.rhai
|
||||
```
|
||||
|
||||
## Available Functions
|
||||
|
||||
### Connection Functions
|
||||
|
||||
- `pg_connect()`: Connect to PostgreSQL using environment variables
|
||||
- `pg_ping()`: Ping the PostgreSQL server to check if it's available
|
||||
- `pg_reset()`: Reset the PostgreSQL client connection
|
||||
|
||||
### Query Functions
|
||||
|
||||
- `pg_execute(query)`: Execute a query and return the number of affected rows
|
||||
- `pg_query(query)`: Execute a query and return the results as an array of maps
|
||||
- `pg_query_one(query)`: Execute a query and return a single row as a map
|
||||
|
||||
### Installer Functions
|
||||
|
||||
- `pg_install(container_name, version, port, username, password)`: Install PostgreSQL using nerdctl
|
||||
- `pg_create_database(container_name, db_name)`: Create a new database in PostgreSQL
|
||||
- `pg_execute_sql(container_name, db_name, sql)`: Execute a SQL script in PostgreSQL
|
||||
- `pg_is_running(container_name)`: Check if PostgreSQL is running
|
||||
|
||||
## Authentication Support
|
||||
|
||||
The PostgreSQL client module will support authentication using the builder pattern in a future update.
|
||||
|
||||
The backend implementation is ready, but the Rhai bindings are still in development.
|
||||
|
||||
When implemented, the builder pattern will support the following configuration options:
|
||||
|
||||
- Host: Set the PostgreSQL host
|
||||
- Port: Set the PostgreSQL port
|
||||
- User: Set the PostgreSQL username
|
||||
- Password: Set the PostgreSQL password
|
||||
- Database: Set the PostgreSQL database name
|
||||
- Application name: Set the application name
|
||||
- Connection timeout: Set the connection timeout in seconds
|
||||
- SSL mode: Set the SSL mode
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Basic PostgreSQL Operations
|
||||
|
||||
```rust
|
||||
// Connect to PostgreSQL
|
||||
if (pg_connect()) {
|
||||
print("Connected to PostgreSQL!");
|
||||
|
||||
// Create a table
|
||||
let create_table_query = "CREATE TABLE IF NOT EXISTS test_table (id SERIAL PRIMARY KEY, name TEXT)";
|
||||
pg_execute(create_table_query);
|
||||
|
||||
// Insert data
|
||||
let insert_query = "INSERT INTO test_table (name) VALUES ('test')";
|
||||
pg_execute(insert_query);
|
||||
|
||||
// Query data
|
||||
let select_query = "SELECT * FROM test_table";
|
||||
let results = pg_query(select_query);
|
||||
|
||||
// Process results
|
||||
for (result in results) {
|
||||
print(`ID: ${result.id}, Name: ${result.name}`);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
let drop_query = "DROP TABLE test_table";
|
||||
pg_execute(drop_query);
|
||||
}
|
||||
```
|
||||
|
||||
### PostgreSQL Installer
|
||||
|
||||
```rust
|
||||
// Install PostgreSQL
|
||||
let container_name = "my-postgres";
|
||||
let postgres_version = "15";
|
||||
let postgres_port = 5432;
|
||||
let postgres_user = "myuser";
|
||||
let postgres_password = "mypassword";
|
||||
|
||||
if (pg_install(container_name, postgres_version, postgres_port, postgres_user, postgres_password)) {
|
||||
print("PostgreSQL installed successfully!");
|
||||
|
||||
// Create a database
|
||||
let db_name = "mydb";
|
||||
if (pg_create_database(container_name, db_name)) {
|
||||
print(`Database '${db_name}' created successfully!`);
|
||||
|
||||
// Execute a SQL script
|
||||
let create_table_sql = `
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL
|
||||
);
|
||||
`;
|
||||
|
||||
let result = pg_execute_sql(container_name, db_name, create_table_sql);
|
||||
print("Table created successfully!");
|
||||
|
||||
// Insert data
|
||||
let insert_sql = "#
|
||||
INSERT INTO users (name, email) VALUES
|
||||
('John Doe', 'john@example.com'),
|
||||
('Jane Smith', 'jane@example.com');
|
||||
#";
|
||||
|
||||
result = pg_execute_sql(container_name, db_name, insert_sql);
|
||||
print("Data inserted successfully!");
|
||||
|
||||
// Query data
|
||||
let query_sql = "SELECT * FROM users;";
|
||||
result = pg_execute_sql(container_name, db_name, query_sql);
|
||||
print(`Query result: ${result}`);
|
||||
}
|
||||
}
|
||||
```
|
79
docs/docs/rhai/process_module_tests.md
Normal file
79
docs/docs/rhai/process_module_tests.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Process Module Tests
|
||||
|
||||
This document describes the test scripts for the Process module in the SAL library. These tests verify the functionality of the Process module's command execution and process management features.
|
||||
|
||||
## Test Structure
|
||||
|
||||
The tests are organized into two main scripts:
|
||||
|
||||
1. **Command Execution** (`01_command_execution.rhai`): Tests command execution functions like `run()` and `which()`.
|
||||
2. **Process Management** (`02_process_management.rhai`): Tests process management functions like `process_list()` and `process_get()`.
|
||||
|
||||
Additionally, there's a runner script (`run_all_tests.rhai`) that executes all tests and reports results. The runner script contains simplified versions of the individual tests to avoid dependency issues.
|
||||
|
||||
## Running the Tests
|
||||
|
||||
To run all tests, execute the following command from the project root:
|
||||
|
||||
```bash
|
||||
herodo --path src/rhai_tests/process/run_all_tests.rhai
|
||||
```
|
||||
|
||||
To run individual test scripts:
|
||||
|
||||
```bash
|
||||
herodo --path src/rhai_tests/process/01_command_execution.rhai
|
||||
```
|
||||
|
||||
## Test Details
|
||||
|
||||
### Command Execution Test
|
||||
|
||||
The command execution test (`01_command_execution.rhai`) verifies the following functions:
|
||||
|
||||
- `run()`: Running shell commands
|
||||
- `run().do()`: Executing commands and capturing output
|
||||
- `run().silent()`: Running commands without displaying output
|
||||
- `run().ignore_error()`: Running commands that might fail without throwing errors
|
||||
- `which()`: Finding the path of an executable
|
||||
|
||||
The test runs various commands and verifies their output and exit status.
|
||||
|
||||
### Process Management Test
|
||||
|
||||
The process management test (`02_process_management.rhai`) verifies the following functions:
|
||||
|
||||
- `process_list()`: Listing running processes
|
||||
- `process_get()`: Getting information about a specific process
|
||||
- Process properties: Accessing process information like PID, name, CPU usage, and memory usage
|
||||
|
||||
The test lists running processes and verifies that their properties are accessible.
|
||||
|
||||
## Test Runner
|
||||
|
||||
The test runner script (`run_all_tests.rhai`) provides a framework for executing all tests and reporting results. It:
|
||||
|
||||
1. Contains simplified versions of each test
|
||||
2. Runs each test in a try/catch block to handle errors
|
||||
3. Catches and reports any errors
|
||||
4. Provides a summary of passed and failed tests
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
To add a new test:
|
||||
|
||||
1. Create a new Rhai script in the `src/rhai_tests/process` directory
|
||||
2. Add a new test section to the `run_all_tests.rhai` script
|
||||
3. Update this documentation to include information about the new test
|
||||
|
||||
## Best Practices for Writing Tests
|
||||
|
||||
When writing tests for the Process module:
|
||||
|
||||
1. Use assertions to verify expected behavior
|
||||
2. Print clear messages about what's being tested
|
||||
3. Handle errors gracefully
|
||||
4. Make tests independent of each other
|
||||
5. Avoid tests that could disrupt the system (e.g., killing important processes)
|
||||
6. Keep tests focused on specific functionality
|
||||
7. Clean up any resources created during testing
|
125
docs/docs/rhai/redisclient_module_tests.md
Normal file
125
docs/docs/rhai/redisclient_module_tests.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Redis Client Module Tests
|
||||
|
||||
This document describes the test scripts for the Redis client module in the SAL library. These tests verify the functionality of the Redis client module's connection management and Redis operations.
|
||||
|
||||
## Redis Client Features
|
||||
|
||||
The Redis client module provides the following features:
|
||||
|
||||
1. **Basic Redis Operations**: SET, GET, DEL, etc.
|
||||
2. **Hash Operations**: HSET, HGET, HGETALL, HDEL
|
||||
3. **List Operations**: RPUSH, LPUSH, LLEN, LRANGE
|
||||
4. **Connection Management**: Automatic connection handling and reconnection
|
||||
5. **Builder Pattern for Configuration**: Flexible configuration with authentication support
|
||||
|
||||
## Test Structure
|
||||
|
||||
The tests are organized into two main scripts:
|
||||
|
||||
1. **Redis Connection** (`01_redis_connection.rhai`): Tests basic Redis connection and simple operations like PING, SET, GET, and DEL.
|
||||
2. **Redis Operations** (`02_redis_operations.rhai`): Tests more advanced Redis operations like hash operations (HSET, HGET, HGETALL, HDEL) and list operations (RPUSH, LLEN, LRANGE).
|
||||
|
||||
Additionally, there's a runner script (`run_all_tests.rhai`) that executes all tests and reports results. The runner script contains simplified versions of the individual tests to avoid dependency issues.
|
||||
|
||||
## Running the Tests
|
||||
|
||||
To run all tests, execute the following command from the project root:
|
||||
|
||||
```bash
|
||||
herodo --path src/rhai_tests/redisclient/run_all_tests.rhai
|
||||
```
|
||||
|
||||
To run individual test scripts:
|
||||
|
||||
```bash
|
||||
herodo --path src/rhai_tests/redisclient/01_redis_connection.rhai
|
||||
```
|
||||
|
||||
## Test Details
|
||||
|
||||
### Redis Connection Test
|
||||
|
||||
The Redis connection test (`01_redis_connection.rhai`) verifies the following functions:
|
||||
|
||||
- `redis_ping`: Checking if the Redis server is available
|
||||
- `redis_set`: Setting a key-value pair
|
||||
- `redis_get`: Getting a value by key
|
||||
- `redis_del`: Deleting a key
|
||||
|
||||
The test creates a temporary key, performs operations on it, and then cleans up after itself.
|
||||
|
||||
### Redis Operations Test
|
||||
|
||||
The Redis operations test (`02_redis_operations.rhai`) verifies the following functions:
|
||||
|
||||
- Hash operations:
|
||||
- `redis_hset`: Setting a field in a hash
|
||||
- `redis_hget`: Getting a field from a hash
|
||||
- `redis_hgetall`: Getting all fields and values from a hash
|
||||
- `redis_hdel`: Deleting a field from a hash
|
||||
|
||||
- List operations:
|
||||
- `redis_rpush`: Adding elements to a list
|
||||
- `redis_llen`: Getting the length of a list
|
||||
- `redis_lrange`: Getting a range of elements from a list
|
||||
|
||||
The test creates temporary keys with a unique prefix, performs operations on them, and then cleans up after itself.
|
||||
|
||||
## Test Runner
|
||||
|
||||
The test runner script (`run_all_tests.rhai`) provides a framework for executing all tests and reporting results. It:
|
||||
|
||||
1. Checks if Redis is available before running tests
|
||||
2. Skips tests if Redis is not available
|
||||
3. Contains simplified versions of each test
|
||||
4. Runs each test in a try/catch block to handle errors
|
||||
5. Catches and reports any errors
|
||||
6. Provides a summary of passed, failed, and skipped tests
|
||||
|
||||
## Redis Server Requirements
|
||||
|
||||
These tests require a Redis server to be running and accessible. The tests will attempt to connect to Redis using the following strategy:
|
||||
|
||||
1. First, try to connect via Unix socket at `$HOME/hero/var/myredis.sock`
|
||||
2. If that fails, try to connect via TCP to `127.0.0.1` on the default Redis port (6379)
|
||||
|
||||
If no Redis server is available, the tests will be skipped rather than failing.
|
||||
|
||||
## Authentication Support
|
||||
|
||||
The Redis client module will support authentication using the builder pattern in a future update.
|
||||
|
||||
The backend implementation is ready, but the Rhai bindings are still in development.
|
||||
|
||||
When implemented, the builder pattern will support the following configuration options:
|
||||
|
||||
- Host: Set the Redis host
|
||||
- Port: Set the Redis port
|
||||
- Database: Set the Redis database number
|
||||
- Username: Set the Redis username (Redis 6.0+)
|
||||
- Password: Set the Redis password
|
||||
- TLS: Enable/disable TLS
|
||||
- Unix socket: Enable/disable Unix socket
|
||||
- Socket path: Set the Unix socket path
|
||||
- Connection timeout: Set the connection timeout in seconds
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
To add a new test:
|
||||
|
||||
1. Create a new Rhai script in the `src/rhai_tests/redisclient` directory
|
||||
2. Add a new test section to the `run_all_tests.rhai` script
|
||||
3. Update this documentation to include information about the new test
|
||||
|
||||
## Best Practices for Writing Tests
|
||||
|
||||
When writing tests for the Redis client module:
|
||||
|
||||
1. Always check if Redis is available before running tests
|
||||
2. Use a unique prefix for test keys to avoid conflicts
|
||||
3. Clean up any keys created during testing
|
||||
4. Use assertions to verify expected behavior
|
||||
5. Print clear messages about what's being tested
|
||||
6. Handle errors gracefully
|
||||
7. Make tests independent of each other
|
||||
8. Keep tests focused on specific functionality
|
113
docs/docs/rhai/rfs_module_tests.md
Normal file
113
docs/docs/rhai/rfs_module_tests.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# RFS Module Tests
|
||||
|
||||
This document describes the test scripts for the RFS (Remote File System) module in the SAL library. These tests verify the functionality of the RFS module's mount operations and filesystem layer management.
|
||||
|
||||
## Test Structure
|
||||
|
||||
The tests are organized into two main scripts:
|
||||
|
||||
1. **Mount Operations** (`01_mount_operations.rhai`): Tests for mounting, listing, and unmounting filesystems.
|
||||
2. **Filesystem Layer Operations** (`02_filesystem_layer_operations.rhai`): Tests for packing, unpacking, listing, and verifying filesystem layers.
|
||||
|
||||
Additionally, there's a runner script (`run_all_tests.rhai`) that executes all tests and reports results. The runner script contains simplified versions of the individual tests to avoid dependency issues.
|
||||
|
||||
## Running the Tests
|
||||
|
||||
To run all tests, execute the following command from the project root:
|
||||
|
||||
```bash
|
||||
herodo --path src/rhai_tests/rfs/run_all_tests.rhai
|
||||
```
|
||||
|
||||
To run individual test scripts:
|
||||
|
||||
```bash
|
||||
herodo --path src/rhai_tests/rfs/01_mount_operations.rhai
|
||||
```
|
||||
|
||||
## Test Details
|
||||
|
||||
### Mount Operations Test
|
||||
|
||||
The Mount Operations test (`01_mount_operations.rhai`) verifies the following functions:
|
||||
|
||||
- `rfs_mount`: Mounting a filesystem
|
||||
- Tests mounting a local directory with options
|
||||
- Verifies mount properties (ID, source, target, type)
|
||||
|
||||
- `rfs_list_mounts`: Listing mounted filesystems
|
||||
- Tests listing all mounts
|
||||
- Verifies that the mounted filesystem is in the list
|
||||
|
||||
- `rfs_get_mount_info`: Getting information about a mounted filesystem
|
||||
- Tests getting information about a specific mount
|
||||
- Verifies that the mount information is correct
|
||||
|
||||
- `rfs_unmount`: Unmounting a specific filesystem
|
||||
- Tests unmounting a specific mount
|
||||
- Verifies that the mount is no longer available
|
||||
|
||||
- `rfs_unmount_all`: Unmounting all filesystems
|
||||
- Tests unmounting all mounts
|
||||
- Verifies that no mounts remain after the operation
|
||||
|
||||
The test also verifies that files in the mounted filesystem are accessible and have the correct content.
|
||||
|
||||
### Filesystem Layer Operations Test
|
||||
|
||||
The Filesystem Layer Operations test (`02_filesystem_layer_operations.rhai`) verifies the following functions:
|
||||
|
||||
- `rfs_pack`: Packing a directory into a filesystem layer
|
||||
- Tests packing a directory with files and subdirectories
|
||||
- Verifies that the output file is created
|
||||
|
||||
- `rfs_list_contents`: Listing the contents of a filesystem layer
|
||||
- Tests listing the contents of a packed filesystem layer
|
||||
- Verifies that the list includes all expected files
|
||||
|
||||
- `rfs_verify`: Verifying a filesystem layer
|
||||
- Tests verifying a packed filesystem layer
|
||||
- Verifies that the layer is valid
|
||||
|
||||
- `rfs_unpack`: Unpacking a filesystem layer
|
||||
- Tests unpacking a filesystem layer to a directory
|
||||
- Verifies that all files are unpacked correctly with the right content
|
||||
|
||||
The test creates a directory structure with files, packs it into a filesystem layer, and then unpacks it to verify the integrity of the process.
|
||||
|
||||
## Test Runner
|
||||
|
||||
The test runner script (`run_all_tests.rhai`) provides a framework for executing all tests and reporting results. It:
|
||||
|
||||
1. Checks if RFS is available before running tests
|
||||
2. Skips tests if RFS is not available
|
||||
3. Contains simplified versions of each test
|
||||
4. Runs each test in a try/catch block to handle errors
|
||||
5. Catches and reports any errors
|
||||
6. Provides a summary of passed, failed, and skipped tests
|
||||
|
||||
## RFS Requirements
|
||||
|
||||
These tests require the RFS tool to be installed and available in the system's PATH. The tests will check for RFS's availability and skip the tests if it's not found, rather than failing.
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
To add a new test:
|
||||
|
||||
1. Create a new Rhai script in the `src/rhai_tests/rfs` directory
|
||||
2. Add a new test section to the `run_all_tests.rhai` script
|
||||
3. Update this documentation to include information about the new test
|
||||
|
||||
## Best Practices for Writing Tests
|
||||
|
||||
When writing tests for the RFS module:
|
||||
|
||||
1. Always check if RFS is available before running tests
|
||||
2. Clean up any mounts before and after testing
|
||||
3. Use unique names for test directories and files to avoid conflicts
|
||||
4. Clean up any files or directories created during testing
|
||||
5. Use assertions to verify expected behavior
|
||||
6. Print clear messages about what's being tested
|
||||
7. Handle errors gracefully
|
||||
8. Make tests independent of each other
|
||||
9. Keep tests focused on specific functionality
|
76
docs/docs/rhai/running_tests.md
Normal file
76
docs/docs/rhai/running_tests.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Running Rhai Tests
|
||||
|
||||
This document describes how to run the Rhai tests for the SAL library.
|
||||
|
||||
## Test Structure
|
||||
|
||||
The Rhai tests are organized by module in the `src/rhai_tests` directory:
|
||||
|
||||
- `src/rhai_tests/os/`: Tests for the OS module
|
||||
- `src/rhai_tests/git/`: Tests for the Git module
|
||||
|
||||
Each module directory contains:
|
||||
- Individual test scripts (e.g., `01_file_operations.rhai`)
|
||||
- A test runner script (`run_all_tests.rhai`) that runs all tests for that module
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Running All Tests
|
||||
|
||||
To run all Rhai tests across all modules, use the provided shell script:
|
||||
|
||||
```bash
|
||||
./run_rhai_tests.sh
|
||||
```
|
||||
|
||||
This script:
|
||||
1. Finds all test runner scripts in the `src/rhai_tests` directory
|
||||
2. Runs each test runner
|
||||
3. Reports the results for each module
|
||||
4. Provides a summary of all test results
|
||||
|
||||
The script will exit with code 0 if all tests pass, or code 1 if any tests fail.
|
||||
|
||||
### Running Tests for a Specific Module
|
||||
|
||||
To run tests for a specific module, use the `herodo` command with the module's test runner:
|
||||
|
||||
```bash
|
||||
herodo --path src/rhai_tests/os/run_all_tests.rhai
|
||||
```
|
||||
|
||||
### Running Individual Tests
|
||||
|
||||
To run a specific test, use the `herodo` command with the test script:
|
||||
|
||||
```bash
|
||||
herodo --path src/rhai_tests/os/01_file_operations.rhai
|
||||
```
|
||||
|
||||
## Test Output
|
||||
|
||||
The test output includes:
|
||||
- Information about what's being tested
|
||||
- Success or failure messages for each test
|
||||
- A summary of test results
|
||||
|
||||
Successful tests are indicated with a checkmark (✓), while failed tests show an error message.
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
When adding new tests:
|
||||
|
||||
1. Create a new test script in the appropriate module directory
|
||||
2. Update the module's test runner script to include the new test
|
||||
3. Update the module's documentation to describe the new test
|
||||
|
||||
The `run_rhai_tests.sh` script will automatically find and run the new tests as long as they're included in a module's test runner script.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If tests fail, check the following:
|
||||
|
||||
1. Make sure the `herodo` binary is in your PATH
|
||||
2. Verify that the test scripts have the correct permissions
|
||||
3. Check for any dependencies required by the tests (e.g., `git` for Git module tests)
|
||||
4. Look for specific error messages in the test output
|
129
docs/docs/rhai/text_module_tests.md
Normal file
129
docs/docs/rhai/text_module_tests.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Text Module Tests
|
||||
|
||||
This document describes the test scripts for the Text module in the SAL library. These tests verify the functionality of the Text module's text manipulation, normalization, replacement, and template rendering capabilities.
|
||||
|
||||
## Test Structure
|
||||
|
||||
The tests are organized into four main scripts:
|
||||
|
||||
1. **Text Indentation** (`01_text_indentation.rhai`): Tests for the `dedent` and `prefix` functions.
|
||||
2. **Filename and Path Normalization** (`02_name_path_fix.rhai`): Tests for the `name_fix` and `path_fix` functions.
|
||||
3. **Text Replacement** (`03_text_replacer.rhai`): Tests for the `TextReplacer` class and its methods.
|
||||
4. **Template Rendering** (`04_template_builder.rhai`): Tests for the `TemplateBuilder` class and its methods.
|
||||
|
||||
Additionally, there's a runner script (`run_all_tests.rhai`) that executes all tests and reports results. The runner script contains simplified versions of the individual tests to avoid dependency issues.
|
||||
|
||||
## Running the Tests
|
||||
|
||||
To run all tests, execute the following command from the project root:
|
||||
|
||||
```bash
|
||||
herodo --path src/rhai_tests/text/run_all_tests.rhai
|
||||
```
|
||||
|
||||
To run individual test scripts:
|
||||
|
||||
```bash
|
||||
herodo --path src/rhai_tests/text/01_text_indentation.rhai
|
||||
```
|
||||
|
||||
## Test Details
|
||||
|
||||
### Text Indentation Test
|
||||
|
||||
The text indentation test (`01_text_indentation.rhai`) verifies the following functions:
|
||||
|
||||
- `dedent`: Removes common leading whitespace from multiline strings
|
||||
- Tests basic indentation removal
|
||||
- Tests mixed indentation handling
|
||||
- Tests preservation of empty lines
|
||||
- Tests handling of text without indentation
|
||||
- Tests single line indentation removal
|
||||
|
||||
- `prefix`: Adds a specified prefix to each line of a multiline string
|
||||
- Tests basic prefix addition
|
||||
- Tests empty prefix handling
|
||||
- Tests prefix addition to empty lines
|
||||
- Tests prefix addition to single line
|
||||
- Tests non-space prefix addition
|
||||
|
||||
- Combination of `dedent` and `prefix` functions
|
||||
|
||||
### Filename and Path Normalization Test
|
||||
|
||||
The filename and path normalization test (`02_name_path_fix.rhai`) verifies the following functions:
|
||||
|
||||
- `name_fix`: Normalizes filenames
|
||||
- Tests basic name fixing (spaces to underscores, lowercase conversion)
|
||||
- Tests special character handling
|
||||
- Tests multiple special character handling
|
||||
- Tests non-ASCII character removal
|
||||
- Tests uppercase conversion
|
||||
|
||||
- `path_fix`: Applies `name_fix` to the filename portion of a path
|
||||
- Tests paths ending with `/` (directories)
|
||||
- Tests single filename handling
|
||||
- Tests path with filename handling
|
||||
- Tests relative path handling
|
||||
- Tests path with special characters in filename
|
||||
|
||||
### Text Replacement Test
|
||||
|
||||
The text replacement test (`03_text_replacer.rhai`) verifies the following functions:
|
||||
|
||||
- `TextReplacer` with simple replacements
|
||||
- Tests basic replacement
|
||||
- Tests multiple replacements
|
||||
|
||||
- `TextReplacer` with regex replacements
|
||||
- Tests basic regex replacement
|
||||
- Tests case-insensitive regex replacement
|
||||
|
||||
- `TextReplacer` with file operations
|
||||
- Tests `replace_file` (read file, apply replacements, return result)
|
||||
- Tests `replace_file_to` (read file, apply replacements, write to new file)
|
||||
- Tests `replace_file_in_place` (read file, apply replacements, write back to same file)
|
||||
|
||||
### Template Rendering Test
|
||||
|
||||
The template rendering test (`04_template_builder.rhai`) verifies the following functions:
|
||||
|
||||
- `TemplateBuilder` with file template
|
||||
- Tests basic template with string variable
|
||||
- Tests template with multiple variables of different types
|
||||
- Tests template with array variable
|
||||
- Tests template with map variable
|
||||
|
||||
- `TemplateBuilder` with file operations
|
||||
- Tests template from file
|
||||
- Tests `render_to_file` (render template, write to file)
|
||||
|
||||
Note: The `template_builder_open` function expects a file path, not a string template. The test creates template files on disk for testing.
|
||||
|
||||
## Test Runner
|
||||
|
||||
The test runner script (`run_all_tests.rhai`) provides a framework for executing all tests and reporting results. It:
|
||||
|
||||
1. Contains simplified versions of each test
|
||||
2. Runs each test in a try/catch block to handle errors
|
||||
3. Catches and reports any errors
|
||||
4. Provides a summary of passed and failed tests
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
To add a new test:
|
||||
|
||||
1. Create a new Rhai script in the `src/rhai_tests/text` directory
|
||||
2. Add a new test section to the `run_all_tests.rhai` script
|
||||
3. Update this documentation to include information about the new test
|
||||
|
||||
## Best Practices for Writing Tests
|
||||
|
||||
When writing tests for the Text module:
|
||||
|
||||
1. Use the `assert_true` and `assert_eq` functions to verify expected behavior
|
||||
2. Print clear messages about what's being tested
|
||||
3. Clean up any temporary files or directories created during testing
|
||||
4. Handle errors gracefully
|
||||
5. Make tests independent of each other
|
||||
6. Keep tests focused on specific functionality
|
8
docs/docs/sal/_category_.json
Normal file
8
docs/docs/sal/_category_.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"label": "SAL",
|
||||
"position": 6,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Tools to work with operating system."
|
||||
}
|
||||
}
|
239
docs/docs/sal/buildah.md
Normal file
239
docs/docs/sal/buildah.md
Normal file
@@ -0,0 +1,239 @@
|
||||
---
|
||||
title: "build containers"
|
||||
sidebar_position: 20
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
# Container Builder
|
||||
|
||||
The Buildah module provides functions for working with containers and images using the Buildah tool. Buildah helps you create and manage container images.
|
||||
|
||||
## Builder Pattern
|
||||
|
||||
The Buildah module now supports a Builder pattern, which provides a more intuitive and flexible way to work with containers and images.
|
||||
|
||||
### Creating a Builder
|
||||
|
||||
```js
|
||||
// Create a builder with a name and base image
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Access builder properties
|
||||
let container_id = builder.container_id;
|
||||
let name = builder.name;
|
||||
let image = builder.image;
|
||||
```
|
||||
|
||||
### Builder Methods
|
||||
The Builder object provides the following methods:
|
||||
|
||||
- `run(command)`: Run a command in the container
|
||||
- `run_with_isolation(command, isolation)`: Run a command with specified isolation
|
||||
- `copy(source, dest)`: Copy files into the container
|
||||
- `add(source, dest)`: Add files into the container
|
||||
- `commit(image_name)`: Commit the container to an image
|
||||
- `remove()`: Remove the container
|
||||
- `reset()`: Remove the container and clear the container_id
|
||||
- `config(options)`: Configure container metadata
|
||||
- `set_entrypoint(entrypoint)`: Set the entrypoint for the container
|
||||
- `set_cmd(cmd)`: Set the default command for the container
|
||||
- `debug_mode`: Get or set the debug flag (true/false)
|
||||
- `write_content(content, dest_path)`: Write content to a file in the container
|
||||
- `read_content(source_path)`: Read content from a file in the container
|
||||
- `images()`: List images in local storage
|
||||
- `image_remove(image)`: Remove an image
|
||||
- `image_pull(image, tls_verify)`: Pull an image from a registry
|
||||
- `image_push(image, destination, tls_verify)`: Push an image to a registry
|
||||
- `image_tag(image, new_name)`: Add a tag to an image
|
||||
- `build(tag, context_dir, file, isolation)`: Build an image from a Dockerfile
|
||||
- `build(tag, context_dir, file, isolation)`: Build an image from a Dockerfile
|
||||
|
||||
### Example
|
||||
|
||||
```js
|
||||
// Create a builder
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Enable debug mode to see command output
|
||||
builder.debug_mode = true;
|
||||
|
||||
// Reset the builder to remove any existing container
|
||||
builder.reset();
|
||||
|
||||
// Create a new container
|
||||
builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Run a command
|
||||
let result = builder.run("echo 'Hello from container'");
|
||||
println(`Command output: ${result.stdout}`);
|
||||
|
||||
// Write content directly to a file in the container
|
||||
let script_content = `#!/bin/sh
|
||||
echo "Hello from startup script"
|
||||
`;
|
||||
builder.write_content(script_content, "/start.sh");
|
||||
builder.run("chmod +x /start.sh");
|
||||
|
||||
// Set the entrypoint for the container
|
||||
builder.set_entrypoint("/start.sh");
|
||||
|
||||
// Add a file
|
||||
file_write("test_file.txt", "Test content");
|
||||
builder.add("test_file.txt", "/");
|
||||
|
||||
// Commit to an image
|
||||
builder.commit("my-custom-image:latest");
|
||||
|
||||
// Clean up
|
||||
builder.remove();
|
||||
delete("test_file.txt");
|
||||
```
|
||||
|
||||
## Image Information
|
||||
|
||||
### Image Properties
|
||||
|
||||
When working with images, you can access the following information:
|
||||
|
||||
- `id`: The unique identifier for the image
|
||||
- `names`: A list of names/tags for the image
|
||||
- `name`: The primary name of the image, or `<none>` if the image has no names
|
||||
- `size`: The size of the image
|
||||
- `created`: When the image was created
|
||||
|
||||
## Builder Methods
|
||||
|
||||
### `bah_new(name, image)`
|
||||
|
||||
Creates a new Builder object for working with a container.
|
||||
|
||||
**Parameters:**
|
||||
- `name` (string): The name to give the container
|
||||
- `image` (string): The name or ID of the image to create the container from
|
||||
|
||||
**Returns:** A Builder object if successful.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create a new Builder
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- If a container with the given name already exists, it will be reused instead of creating a new one
|
||||
- The Builder object provides methods for working with the container
|
||||
|
||||
### `reset()`
|
||||
|
||||
Resets a Builder by removing the container and clearing the container_id. This allows you to start fresh with the same Builder object.
|
||||
|
||||
**Returns:** Nothing.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create a Builder
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Reset the Builder to remove the container
|
||||
builder.reset();
|
||||
|
||||
// Create a new container with the same name
|
||||
builder = bah_new("my-container", "alpine:latest");
|
||||
```
|
||||
|
||||
### `debug_mode`
|
||||
|
||||
Get or set the debug flag for the Builder. When debug mode is enabled, all buildah commands will output their stdout/stderr, making it easier to debug issues.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create a Builder
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Enable debug mode
|
||||
builder.debug_mode = true;
|
||||
|
||||
// Run a command with debug output
|
||||
builder.run("echo 'Hello with debug'");
|
||||
|
||||
// Disable debug mode
|
||||
builder.debug_mode = false;
|
||||
```
|
||||
|
||||
### `set_entrypoint(entrypoint)`
|
||||
|
||||
Sets the entrypoint for the container. The entrypoint is the command that will be executed when the container starts.
|
||||
|
||||
**Parameters:**
|
||||
- `entrypoint` (string): The entrypoint command
|
||||
|
||||
**Returns:** Command result if successful.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create a Builder
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Set the entrypoint
|
||||
builder.set_entrypoint("/start.sh");
|
||||
```
|
||||
|
||||
### `set_cmd(cmd)`
|
||||
|
||||
Sets the default command for the container. This is used as arguments to the entrypoint.
|
||||
|
||||
**Parameters:**
|
||||
- `cmd` (string): The default command
|
||||
|
||||
**Returns:** Command result if successful.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create a Builder
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Set the default command
|
||||
builder.set_cmd("--verbose");
|
||||
```
|
||||
|
||||
### `write_content(content, dest_path)`
|
||||
|
||||
Writes content to a file in the container.
|
||||
|
||||
**Parameters:**
|
||||
- `content` (string): The content to write
|
||||
- `dest_path` (string): The destination path in the container
|
||||
|
||||
**Returns:** Command result if successful.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create a Builder
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Write content to a file
|
||||
let content = "Hello, world!";
|
||||
builder.write_content(content, "/hello.txt");
|
||||
```
|
||||
|
||||
### `read_content(source_path)`
|
||||
|
||||
Reads content from a file in the container.
|
||||
|
||||
**Parameters:**
|
||||
- `source_path` (string): The source path in the container
|
||||
|
||||
**Returns:** The file content as a string if successful.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create a Builder
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Write content to a file
|
||||
builder.write_content("Hello, world!", "/hello.txt");
|
||||
|
||||
// Read content from the file
|
||||
let content = builder.read_content("/hello.txt");
|
||||
println(content); // Outputs: Hello, world!
|
||||
```
|
210
docs/docs/sal/git.md
Normal file
210
docs/docs/sal/git.md
Normal file
@@ -0,0 +1,210 @@
|
||||
---
|
||||
title: "git"
|
||||
sidebar_position: 5
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
# Git
|
||||
|
||||
This module provides HeroScript wrappers for the Git functionality in SAL.
|
||||
|
||||
> **Note:** The constructor for GitTree has been renamed from `new()` to `gittree_new()` to avoid confusion with other constructors. This makes the interface more explicit and less likely to cause naming conflicts.
|
||||
|
||||
## Object-Oriented Design
|
||||
|
||||
The Git module follows an object-oriented design with two main classes:
|
||||
|
||||
1. **GitTree** - Represents a collection of git repositories under a base path
|
||||
- Created with `gittree_new(base_path)`
|
||||
- Methods for listing, finding, and getting repositories
|
||||
|
||||
2. **GitRepo** - Represents a single git repository
|
||||
- Obtained from GitTree's `get()` method
|
||||
- Methods for common git operations: pull, reset, push, commit
|
||||
|
||||
This design allows for a more intuitive and flexible interface, with method chaining for complex operations.
|
||||
|
||||
## Creating a GitTree
|
||||
|
||||
The GitTree object is the main entry point for git operations. It represents a collection of git repositories under a base path.
|
||||
|
||||
```js
|
||||
// Create a new GitTree with a base path
|
||||
let git_tree = gittree_new("/root/code");
|
||||
print(`Created GitTree with base path: /home/user/code`);
|
||||
```
|
||||
|
||||
## Finding Repositories
|
||||
|
||||
### List All Repositories
|
||||
|
||||
```js
|
||||
// List all git repositories under the base path
|
||||
let repos = git_tree.list();
|
||||
print(`Found ${repos.len()} repositories`);
|
||||
|
||||
// Print the repositories
|
||||
for repo in repos {
|
||||
print(` - ${repo}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Find Repositories Matching a Pattern
|
||||
|
||||
```js
|
||||
// Find repositories matching a pattern
|
||||
// Use a wildcard (*) suffix to find multiple matches
|
||||
let matching_repos = git_tree.find("my-project*");
|
||||
print("Matching repositories:");
|
||||
for repo in matching_repos {
|
||||
print(` - ${repo}`);
|
||||
}
|
||||
|
||||
// Find a specific repository (must match exactly one)
|
||||
let specific_repo = git_tree.find("unique-project")[0];
|
||||
print(`Found specific repository: ${specific_repo}`);
|
||||
```
|
||||
|
||||
## Working with Repositories
|
||||
|
||||
### Get Repository Objects
|
||||
|
||||
```js
|
||||
// Get GitRepo objects for repositories matching a pattern
|
||||
let repos = git_tree.get("my-project*");
|
||||
print(`Found ${repos.len()} repositories`);
|
||||
|
||||
// Get a specific repository
|
||||
let repo = git_tree.get("unique-project")[0];
|
||||
print(`Working with repository: ${repo.path()}`);
|
||||
```
|
||||
|
||||
### Clone a Repository
|
||||
|
||||
```js
|
||||
// Clone a repository by URL
|
||||
// This will clone the repository to the base path of the GitTree
|
||||
let repos = git_tree.get("https://github.com/username/repo.git");
|
||||
let repo = repos[0];
|
||||
print(`Repository cloned to: ${repo.path()}`);
|
||||
```
|
||||
|
||||
### Check for Changes
|
||||
|
||||
```js
|
||||
// Check if a repository has uncommitted changes
|
||||
let repo = git_tree.get("my-project")[0];
|
||||
if repo.has_changes() {
|
||||
print("Repository has uncommitted changes");
|
||||
} else {
|
||||
print("Repository is clean");
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Operations
|
||||
|
||||
### Pull Changes
|
||||
|
||||
```js
|
||||
// Pull the latest changes from the remote
|
||||
// This will fail if there are uncommitted changes
|
||||
let repo = git_tree.get("my-project")[0];
|
||||
let result = repo.pull();
|
||||
print("Repository updated successfully");
|
||||
```
|
||||
|
||||
### Reset Local Changes
|
||||
|
||||
```js
|
||||
// Reset any local changes in the repository
|
||||
let repo = git_tree.get("my-project")[0];
|
||||
let result = repo.reset();
|
||||
print("Repository reset successfully");
|
||||
```
|
||||
|
||||
### Commit Changes
|
||||
|
||||
```js
|
||||
// Commit changes in the repository
|
||||
let repo = git_tree.get("my-project")[0];
|
||||
let result = repo.commit("Fix bug in login form");
|
||||
print("Changes committed successfully");
|
||||
```
|
||||
|
||||
### Push Changes
|
||||
|
||||
```js
|
||||
// Push changes to the remote
|
||||
let repo = git_tree.get("my-project")[0];
|
||||
let result = repo.push();
|
||||
print("Changes pushed successfully");
|
||||
```
|
||||
|
||||
## Method Chaining
|
||||
|
||||
The GitRepo methods can be chained together for more complex operations:
|
||||
|
||||
```js
|
||||
// Commit changes and push them to the remote
|
||||
let repo = git_tree.get("my-project")[0];
|
||||
let result = repo.commit("Add new feature").push();
|
||||
print("Changes committed and pushed successfully");
|
||||
|
||||
// Reset local changes, pull the latest changes, and commit new changes
|
||||
let repo = git_tree.get("my-project")[0];
|
||||
let result = repo.reset().pull().commit("Update dependencies");
|
||||
print("Repository updated successfully");
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```js
|
||||
// Create a new GitTree
|
||||
let home_dir = env("HOME");
|
||||
let git_tree = gittree_new(`${home_dir}/code`);
|
||||
|
||||
// Clone a repository
|
||||
let repos = git_tree.get("https://github.com/username/example-repo.git");
|
||||
let repo = repos[0];
|
||||
print(`Cloned repository to: ${repo.path()}`);
|
||||
|
||||
// Make some changes (using OS module functions)
|
||||
let file_path = `${repo.path()}/README.md`;
|
||||
let content = "# Example Repository\n\nThis is an example repository.";
|
||||
write_file(file_path, content);
|
||||
|
||||
// Commit and push the changes
|
||||
let result = repo.commit("Update README.md").push();
|
||||
print("Changes committed and pushed successfully");
|
||||
|
||||
// List all repositories
|
||||
let all_repos = git_tree.list();
|
||||
print("All repositories:");
|
||||
for repo_path in all_repos {
|
||||
print(` - ${repo_path}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
All methods in the Git module return a Result type, which means they can either succeed or fail with an error. If an error occurs, it will be propagated to the HeroScript script as a runtime error.
|
||||
|
||||
For example, if you try to clone a repository that doesn't exist:
|
||||
|
||||
```js
|
||||
// Try to clone a non-existent repository
|
||||
try {
|
||||
let git_tree = gittree_new("/root/code");
|
||||
let repos = git_tree.get("https://github.com/nonexistent/repo.git");
|
||||
print("This will not be executed if the repository doesn't exist");
|
||||
} catch(err) {
|
||||
print(`Error: ${err}`); // Will print the error message from git
|
||||
}
|
||||
```
|
||||
|
||||
Common errors include:
|
||||
- Invalid URL
|
||||
- Repository not found
|
||||
- Authentication failure
|
||||
- Network issues
|
||||
- Local changes exist when trying to pull
|
215
docs/docs/sal/git/git.md
Normal file
215
docs/docs/sal/git/git.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# Rhai Git Module Manual
|
||||
|
||||
## Core Concepts
|
||||
|
||||
The Git module in Rhai allows interaction with Git repositories through two main objects:
|
||||
|
||||
- **`GitTree`**: Represents a collection of Git repositories under a specified base directory. Use it to manage, find, and access these repositories.
|
||||
- **`GitRepo`**: Represents a single Git repository. Use it to perform common Git operations like `pull`, `commit`, and `push`.
|
||||
|
||||
## Error Handling
|
||||
|
||||
Methods performing Git operations (e.g., `pull`, `GitTree.get` when cloning) return a `Result`. If an operation fails and the error is not handled within the Rhai script, the script execution will halt, and Rhai will report the error. The examples below show direct usage, relying on this default error-halting behavior.
|
||||
|
||||
## `GitTree` Object
|
||||
|
||||
The `GitTree` object is the entry point for working with Git repositories.
|
||||
|
||||
### `git_tree_new(base_path: String) -> GitTree`
|
||||
|
||||
Creates a `GitTree` instance.
|
||||
|
||||
- **Description**: Initializes a `GitTree` to operate within the `base_path`. This directory is where repositories are located or will be cloned. It's created if it doesn't exist.
|
||||
- **Parameters**:
|
||||
- `base_path: String` - Path to the directory for Git repositories.
|
||||
- **Returns**: `GitTree` - A new `GitTree` object. Halts on error (e.g., invalid path).
|
||||
- **Rhai Example**:
|
||||
```rhai
|
||||
let git_tree = git_tree_new("./my_projects");
|
||||
print("GitTree created.");
|
||||
// To access the base path from Rhai, a `base_path()` getter would need to be exposed.
|
||||
// // print(`GitTree base path: ${git_tree.base_path()}`);
|
||||
```
|
||||
|
||||
### `list() -> Array`
|
||||
|
||||
Lists names of all Git repositories in the `GitTree`'s `base_path`.
|
||||
|
||||
- **Description**: Scans `base_path` for immediate subdirectories that are Git repositories and returns their names.
|
||||
- **Returns**: `Array` - An array of strings (repository names). Returns an empty array if no repositories are found. Halts on other errors.
|
||||
- **Rhai Example**:
|
||||
```rhai
|
||||
let git_tree = git_tree_new("./my_projects");
|
||||
let repo_names = git_tree.list();
|
||||
print(`Found ${repo_names.len()} repositories: ${repo_names}`);
|
||||
```
|
||||
|
||||
### `find(pattern: String) -> Array`
|
||||
|
||||
Finds Git repositories matching `pattern` and returns them as `GitRepo` objects.
|
||||
|
||||
- **Description**: Searches `base_path` for Git repository subdirectories whose names match the `pattern` (e.g., `*`, `service-*`).
|
||||
- **Parameters**:
|
||||
- `pattern: String` - A pattern to match repository names.
|
||||
- **Returns**: `Array` - An array of `GitRepo` objects. Returns an empty array if no repositories match. Halts on other errors (e.g. invalid pattern).
|
||||
- **Rhai Example**:
|
||||
```rhai
|
||||
let git_tree = git_tree_new("./my_projects");
|
||||
let api_repos = git_tree.find("api-*");
|
||||
print(`Found ${api_repos.len()} API repositories.`);
|
||||
for repo in api_repos {
|
||||
print(`- Path: ${repo.path()}, Has Changes: ${repo.has_changes()}`);
|
||||
}
|
||||
```
|
||||
|
||||
### `get(name_or_url: String) -> GitRepo`
|
||||
|
||||
Retrieves a single `GitRepo` object by its exact local name or by a remote URL.
|
||||
|
||||
- **Description**:
|
||||
- **Local Name**: If `name_or_url` is an exact subdirectory name in `base_path` (e.g., `"myrepo"`), opens that repository.
|
||||
- **Remote URL**: If `name_or_url` is a Git URL (e.g., `"https://github.com/user/repo.git"`), it clones the repository (if not present) into `base_path` or opens it if it already exists.
|
||||
- **Note**: Does not support wildcards for local names. Use `find()` for pattern matching.
|
||||
- **Parameters**:
|
||||
- `name_or_url: String` - The exact local repository name or a full Git URL.
|
||||
- **Returns**: `GitRepo` - A single `GitRepo` object.
|
||||
- **Halts on error if**:
|
||||
- The local `name` is not found or is ambiguous.
|
||||
- The `url` is invalid, or the clone/access operation fails.
|
||||
- The target is not a valid Git repository.
|
||||
- **Rhai Examples**:
|
||||
|
||||
*Get specific local repository by name:*
|
||||
```rhai
|
||||
let git_tree = git_tree_new("./my_projects");
|
||||
// Assumes "my_service_a" is a git repo in "./my_projects/my_service_a"
|
||||
// Script halts if "my_service_a" is not found or not a git repo.
|
||||
let service_a_repo = git_tree.get("my_service_a");
|
||||
print(`Opened repo: ${service_a_repo.path()}`);
|
||||
service_a_repo.pull(); // Example operation
|
||||
```
|
||||
|
||||
*Clone or get repository by URL:*
|
||||
```rhai
|
||||
let git_tree = git_tree_new("./cloned_repos_dest");
|
||||
let url = "https://github.com/rhai-script/rhai.git";
|
||||
// Clones if not present, otherwise opens. Halts on error.
|
||||
let rhai_repo = git_tree.get(url);
|
||||
print(`Rhai repository path: ${rhai_repo.path()}`);
|
||||
print(`Rhai repo has changes: ${rhai_repo.has_changes()}`);
|
||||
```
|
||||
|
||||
## `GitRepo` Object
|
||||
|
||||
Represents a single Git repository. Obtained from `GitTree.get()` or `GitTree.find()`.
|
||||
|
||||
### `path() -> String`
|
||||
|
||||
Returns the full file system path of the repository.
|
||||
|
||||
- **Returns**: `String` - The absolute path to the repository's root directory.
|
||||
- **Rhai Example**:
|
||||
```rhai
|
||||
let git_tree = git_tree_new("./my_projects");
|
||||
// Assumes "my_app" exists and is a Git repository.
|
||||
// get() will halt if "my_app" is not found.
|
||||
let app_repo = git_tree.get("my_app");
|
||||
print(`App repository is at: ${app_repo.path()}`);
|
||||
```
|
||||
|
||||
### `has_changes() -> bool`
|
||||
|
||||
Checks if the repository has any uncommitted local changes.
|
||||
|
||||
- **Description**: Checks for uncommitted modifications in the working directory or staged changes.
|
||||
- **Returns**: `bool` - `true` if uncommitted changes exist, `false` otherwise. Halts on error.
|
||||
- **Rhai Example** (assuming `app_repo` is a `GitRepo` object):
|
||||
```rhai
|
||||
if app_repo.has_changes() {
|
||||
print(`Repository ${app_repo.path()} has uncommitted changes.`);
|
||||
} else {
|
||||
print(`Repository ${app_repo.path()} is clean.`);
|
||||
}
|
||||
```
|
||||
|
||||
### `pull() -> GitRepo`
|
||||
|
||||
Pulls latest changes from the remote.
|
||||
|
||||
- **Description**: Fetches changes from the default remote and merges them into the current local branch (`git pull`).
|
||||
- **Returns**: `GitRepo` - The same `GitRepo` object for chaining. Halts on error (e.g., network issues, merge conflicts).
|
||||
- **Rhai Example** (assuming `app_repo` is a `GitRepo` object):
|
||||
```rhai
|
||||
print(`Pulling latest changes for ${app_repo.path()}...`);
|
||||
app_repo.pull(); // Halts on error
|
||||
print("Pull successful.");
|
||||
```
|
||||
|
||||
### `reset() -> GitRepo`
|
||||
|
||||
Resets local changes. **Caution: Discards uncommitted work.**
|
||||
|
||||
- **Description**: Discards local modifications and staged changes, resetting the working directory to match the last commit (`git reset --hard HEAD` or similar).
|
||||
- **Returns**: `GitRepo` - The same `GitRepo` object for chaining. Halts on error.
|
||||
- **Rhai Example** (assuming `app_repo` is a `GitRepo` object):
|
||||
```rhai
|
||||
print(`Resetting local changes in ${app_repo.path()}...`);
|
||||
app_repo.reset(); // Halts on error
|
||||
print("Reset successful.");
|
||||
```
|
||||
|
||||
### `commit(message: String) -> GitRepo`
|
||||
|
||||
Commits staged changes.
|
||||
|
||||
- **Description**: Performs `git commit -m "message"`. Assumes changes are staged. Behavior regarding auto-staging of tracked files depends on the underlying Rust implementation.
|
||||
- **Parameters**:
|
||||
- `message: String` - The commit message.
|
||||
- **Returns**: `GitRepo` - The same `GitRepo` object for chaining. Halts on error (e.g., nothing to commit).
|
||||
- **Rhai Example** (assuming `app_repo` is a `GitRepo` object):
|
||||
```rhai
|
||||
// Ensure there are changes to commit.
|
||||
if app_repo.has_changes() {
|
||||
print(`Committing changes in ${app_repo.path()}...`);
|
||||
app_repo.commit("Automated commit via Rhai script"); // Halts on error
|
||||
print("Commit successful.");
|
||||
} else {
|
||||
print("No changes to commit.");
|
||||
}
|
||||
```
|
||||
|
||||
### `push() -> GitRepo`
|
||||
|
||||
Pushes committed changes to the remote.
|
||||
|
||||
- **Description**: Performs `git push` to the default remote and branch.
|
||||
- **Returns**: `GitRepo`
|
||||
|
||||
```rhai
|
||||
print(`Pushing changes for ${app_repo.path()}...`);
|
||||
app_repo.push(); // Halts on error
|
||||
print("Push successful.");
|
||||
```
|
||||
|
||||
## Chaining Operations
|
||||
|
||||
```rhai
|
||||
let git_tree = git_tree_new("./my_projects");
|
||||
// Assumes "my_writable_app" exists and you have write access.
|
||||
// get() will halt if not found.
|
||||
let app_repo = git_tree.get("my_writable_app");
|
||||
print(`Performing chained operations on ${app_repo.path()}`);
|
||||
|
||||
// This example demonstrates a common workflow.
|
||||
// Ensure the repo state is suitable (e.g., changes exist for commit/push).
|
||||
app_repo.pull()
|
||||
.commit("Rhai: Chained operations - automated update") // Commits if pull results in changes or local changes existed and were staged.
|
||||
.push();
|
||||
print("Chained pull, commit, and push reported successful.");
|
||||
|
||||
// Alternative:
|
||||
// app_repo.pull();
|
||||
// if app_repo.has_changes() {
|
||||
// app_repo.commit("Updates").push();
|
||||
// }
|
||||
```
|
10
docs/docs/sal/intro.md
Normal file
10
docs/docs/sal/intro.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "intro"
|
||||
sidebar_position: 1
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
## HeroScript Script Commands Documentation
|
||||
|
||||
|
||||
The SAL library provides integration with the HeroScript scripting language, allowing you to use powerful system functions within your HeroScript scripts. These functions are organized into modules that provide related functionality.
|
239
docs/docs/sal/nerdctl.md
Normal file
239
docs/docs/sal/nerdctl.md
Normal file
@@ -0,0 +1,239 @@
|
||||
---
|
||||
title: "run containers"
|
||||
sidebar_position: 21
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
# Container Manager
|
||||
|
||||
The Container Manager module provides a comprehensive API for working with containers using nerdctl. It offers a modern builder pattern approach for container management.
|
||||
|
||||
## Container Builder Pattern
|
||||
|
||||
The Container Builder Pattern allows for fluent, chainable configuration of containers. This pattern makes container creation more readable and maintainable by allowing you to build complex container configurations step by step.
|
||||
|
||||
### Creating a Container
|
||||
|
||||
Start by creating a new container instance:
|
||||
|
||||
```rhai
|
||||
// Create an empty container with just a name
|
||||
let container = nerdctl_container_new("my-container");
|
||||
|
||||
// Or create a container from an image
|
||||
let container = nerdctl_container_from_image("my-container", "nginx:latest");
|
||||
```
|
||||
|
||||
### Configuring the Container
|
||||
|
||||
Once you have a container instance, you can configure it using the various builder methods:
|
||||
|
||||
```rhai
|
||||
// Configure the container with various options
|
||||
let container = nerdctl_container_from_image("web-server", "nginx:latest")
|
||||
.with_port("8080:80") // Map port 8080 to container port 80
|
||||
.with_volume("/host/path:/container/path") // Mount a volume
|
||||
.with_env("NGINX_HOST", "localhost") // Set an environment variable
|
||||
.with_network("bridge") // Set the network
|
||||
.with_detach(true); // Run in detached mode
|
||||
```
|
||||
|
||||
### Resetting Container Configuration
|
||||
|
||||
If you need to reset the container configuration to its default state while keeping the name and image:
|
||||
|
||||
```rhai
|
||||
// Reset the container configuration
|
||||
let container = nerdctl_container_from_image("web-server", "nginx:latest")
|
||||
.reset() // Reset all configuration to defaults
|
||||
.with_port("8080:80") // Start configuring again
|
||||
.with_detach(true);
|
||||
```
|
||||
|
||||
### Building and Starting the Container
|
||||
|
||||
After configuring the container, you can build and start it:
|
||||
|
||||
```rhai
|
||||
// Build the container (creates it but doesn't start it)
|
||||
let built_container = container.build();
|
||||
|
||||
// Start the container
|
||||
let start_result = built_container.start();
|
||||
|
||||
// Check if the container started successfully
|
||||
if (start_result.success) {
|
||||
println("Container started successfully!");
|
||||
} else {
|
||||
println(`Failed to start container: ${start_result.stderr}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Container Lifecycle Operations
|
||||
|
||||
Once your container is running, you can perform various operations:
|
||||
|
||||
```rhai
|
||||
// Execute a command in the container
|
||||
let exec_result = container.exec("ls -la");
|
||||
|
||||
// Get container logs
|
||||
let logs = container.logs();
|
||||
|
||||
// Stop the container
|
||||
let stop_result = container.stop();
|
||||
|
||||
// Remove the container
|
||||
let remove_result = container.remove();
|
||||
```
|
||||
|
||||
## Available Builder Methods
|
||||
|
||||
The Container Builder Pattern provides the following methods for configuring containers:
|
||||
|
||||
| Method | Description | Example |
|
||||
| -------------------------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| `reset()` | Reset configuration to defaults | `.reset()` |
|
||||
| `with_port(port)` | Add a port mapping | `.with_port("8080:80")` |
|
||||
| `with_ports(ports_array)` | Add multiple port mappings | `.with_ports(["8080:80", "443:443"])` |
|
||||
| `with_volume(volume)` | Add a volume mount | `.with_volume("/host/path:/container/path")` |
|
||||
| `with_volumes(volumes_array)` | Add multiple volume mounts | `.with_volumes(["/host/path1:/container/path1", "/host/path2:/container/path2"])` |
|
||||
| `with_env(key, value)` | Add an environment variable | `.with_env("NGINX_HOST", "localhost")` |
|
||||
| `with_envs(env_map)` | Add multiple environment variables | `.with_envs(#{"KEY1": "value1", "KEY2": "value2"})` |
|
||||
| `with_network(network)` | Set the network | `.with_network("bridge")` |
|
||||
| `with_network_alias(alias)` | Add a network alias | `.with_network_alias("web-server")` |
|
||||
| `with_network_aliases(aliases_array)` | Add multiple network aliases | `.with_network_aliases(["web-server", "http-service"])` |
|
||||
| `with_cpu_limit(cpus)` | Set CPU limit | `.with_cpu_limit("1.0")` |
|
||||
| `with_cpu_shares(shares)` | Set CPU shares | `.with_cpu_shares("1024")` |
|
||||
| `with_memory_limit(memory)` | Set memory limit | `.with_memory_limit("512m")` |
|
||||
| `with_memory_swap_limit(memory_swap)` | Set memory swap limit | `.with_memory_swap_limit("1g")` |
|
||||
| `with_restart_policy(policy)` | Set restart policy | `.with_restart_policy("unless-stopped")` |
|
||||
| `with_health_check(cmd)` | Set health check command | `.with_health_check("curl -f http://localhost/ | | exit 1")` |
|
||||
| `with_health_check_options(cmd, interval, timeout, retries, start_period)` | Set health check with options | `.with_health_check_options("curl -f http://localhost/ | | exit 1", "5s", "3s", 3, "10s")` |
|
||||
| `with_snapshotter(snapshotter)` | Set snapshotter | `.with_snapshotter("native")` |
|
||||
| `with_detach(detach)` | Set detach mode | `.with_detach(true)` |
|
||||
|
||||
## Complete Example: Web Server
|
||||
|
||||
Here's a complete example that demonstrates setting up an Nginx web server using the Container Builder Pattern:
|
||||
|
||||
```rhai
|
||||
// Create a temporary directory for our files
|
||||
let work_dir = "/tmp/nerdctl";
|
||||
mkdir(work_dir);
|
||||
chdir(work_dir);
|
||||
|
||||
// Create a custom index.html file
|
||||
let html_content = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Rhai Nerdctl Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 40px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
h1 {
|
||||
color: #0066cc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello from Rhai Nerdctl!</h1>
|
||||
<p>This page is served by an Nginx container created using the Rhai nerdctl wrapper.</p>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
// Write the HTML file
|
||||
let html_file = `${work_dir}/index.html`;
|
||||
file_write(html_file, html_content);
|
||||
|
||||
// Set up environment variables
|
||||
let env_map = #{};
|
||||
env_map["NGINX_HOST"] = "localhost";
|
||||
env_map["NGINX_PORT"] = "80";
|
||||
env_map["NGINX_WORKER_PROCESSES"] = "auto";
|
||||
|
||||
// Create and configure the container
|
||||
let container_name = "rhai-nginx-demo";
|
||||
|
||||
// First, try to remove any existing container with the same name
|
||||
nerdctl_remove(container_name);
|
||||
|
||||
// Create a container with a rich set of options using the builder pattern
|
||||
let container = nerdctl_container_from_image(container_name, "nginx:latest")
|
||||
.reset() // Reset to default configuration
|
||||
.with_detach(true)
|
||||
.with_ports(["8080:80"]) // Add multiple ports at once
|
||||
.with_volumes([`${work_dir}:/usr/share/nginx/html`]) // Mount our work dir
|
||||
.with_envs(env_map) // Add multiple environment variables at once
|
||||
.with_network("bridge")
|
||||
.with_network_aliases(["web-server", "nginx-demo"]) // Add multiple network aliases
|
||||
.with_cpu_limit("1.0")
|
||||
.with_memory_limit("512m");
|
||||
|
||||
// Build and start the container
|
||||
let built_container = container.build();
|
||||
let start_result = built_container.start();
|
||||
|
||||
println("The web server is running at http://localhost:8080");
|
||||
```
|
||||
|
||||
## Using Local Images Created with Buildah
|
||||
|
||||
When working with images created by Buildah, you may need to take additional steps to ensure nerdctl can find and use these images. This is because Buildah and nerdctl may use different storage backends by default.
|
||||
|
||||
### Tagging with localhost Prefix
|
||||
|
||||
One approach is to tag the Buildah-created image with a `localhost/` prefix:
|
||||
|
||||
```rhai
|
||||
// Create and commit a container with Buildah
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
builder.run("echo 'Hello' > /hello.txt");
|
||||
builder.commit("my-custom-image:latest");
|
||||
|
||||
// Tag the image with localhost prefix for nerdctl compatibility
|
||||
let local_image_name = "localhost/my-custom-image:latest";
|
||||
bah_image_tag("my-custom-image:latest", local_image_name);
|
||||
|
||||
// Now use the image with nerdctl
|
||||
let container = nerdctl_container_from_image("my-app", local_image_name)
|
||||
.with_detach(true)
|
||||
.build();
|
||||
```
|
||||
|
||||
### Using a Local Registry
|
||||
|
||||
For more reliable interoperability, you can push the image to a local registry:
|
||||
|
||||
```rhai
|
||||
// Push the Buildah-created image to a local registry
|
||||
bah_image_push("my-custom-image:latest", "localhost:5000/my-custom-image:latest", false);
|
||||
|
||||
// Pull the image with nerdctl
|
||||
nerdctl_image_pull("localhost:5000/my-custom-image:latest");
|
||||
|
||||
// Use the image
|
||||
let container = nerdctl_container_from_image("my-app", "localhost:5000/my-custom-image:latest")
|
||||
.with_detach(true)
|
||||
.build();
|
||||
```
|
||||
|
||||
## Image Management Functions
|
||||
|
||||
The module also provides functions for managing container images:
|
||||
|
||||
| Function | Description | Example |
|
||||
| --------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| `nerdctl_images()` | List images in local storage | `nerdctl_images()` |
|
||||
| `nerdctl_image_remove(image)` | Remove an image | `nerdctl_image_remove("nginx:latest")` |
|
||||
| `nerdctl_image_push(image, destination)` | Push an image to a registry | `nerdctl_image_push("my-image:latest", "registry.example.com/my-image:latest")` |
|
||||
| `nerdctl_image_tag(image, new_name)` | Add an additional name to a local image | `nerdctl_image_tag("nginx:latest", "my-nginx:latest")` |
|
||||
| `nerdctl_image_pull(image)` | Pull an image from a registry | `nerdctl_image_pull("nginx:latest")` |
|
||||
| `nerdctl_image_commit(container, image_name)` | Commit a container to an image | `nerdctl_image_commit("web-server", "my-nginx:latest")` |
|
||||
| `nerdctl_image_build(tag, context_path)` | Build an image using a Dockerfile | `nerdctl_image_build("my-image:latest", "./")` |
|
@@ -1,4 +1,10 @@
|
||||
# OS Module
|
||||
---
|
||||
title: "os"
|
||||
sidebar_position: 2
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
# OS Tools
|
||||
|
||||
The OS module provides functions for working with files, directories, and downloading files from the internet.
|
||||
|
||||
@@ -15,7 +21,7 @@ Recursively copies a file or directory from source to destination.
|
||||
**Returns:** A message confirming the copy was successful.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Copy a file
|
||||
copy("source.txt", "destination.txt");
|
||||
|
||||
@@ -33,7 +39,7 @@ Checks if a file or directory exists.
|
||||
**Returns:** A boolean value - `true` if the file or directory exists, `false` otherwise.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
if exist("config.json") {
|
||||
// File exists, do something
|
||||
} else {
|
||||
@@ -52,7 +58,7 @@ Finds a file in a directory with support for wildcards.
|
||||
**Returns:** The path of the first matching file.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Find a specific file
|
||||
let config_file = find_file("./config", "settings.json");
|
||||
|
||||
@@ -71,7 +77,7 @@ Finds multiple files in a directory recursively with support for wildcards.
|
||||
**Returns:** A list of matching file paths.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Find all JSON files
|
||||
let json_files = find_files("./data", "*.json");
|
||||
|
||||
@@ -92,7 +98,7 @@ Finds a directory in a parent directory with support for wildcards.
|
||||
**Returns:** The path of the first matching directory.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Find a specific directory
|
||||
let config_dir = find_dir("./", "config");
|
||||
|
||||
@@ -111,7 +117,7 @@ Finds multiple directories in a parent directory recursively with support for wi
|
||||
**Returns:** A list of matching directory paths.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Find all version directories
|
||||
let version_dirs = find_dirs("./releases", "v*");
|
||||
|
||||
@@ -131,7 +137,7 @@ Deletes a file or directory. This function is defensive and doesn't error if the
|
||||
**Returns:** A message confirming the deletion was successful.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Delete a file
|
||||
delete("temp.txt");
|
||||
|
||||
@@ -139,6 +145,28 @@ delete("temp.txt");
|
||||
delete("temp_dir");
|
||||
```
|
||||
|
||||
### `mv(src, dest)`
|
||||
|
||||
Moves a file or directory from source to destination.
|
||||
|
||||
**Parameters:**
|
||||
- `src` (string): The source path
|
||||
- `dest` (string): The destination path
|
||||
|
||||
**Returns:** A message confirming the move was successful.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Move a file
|
||||
mv("file.txt", "new_location/file.txt");
|
||||
|
||||
// Move a directory
|
||||
mv("source_dir", "destination_dir");
|
||||
|
||||
// Rename a file
|
||||
mv("old_name.txt", "new_name.txt");
|
||||
```
|
||||
|
||||
### `mkdir(path)`
|
||||
|
||||
Creates a directory and all parent directories. This function is defensive and doesn't error if the directory already exists.
|
||||
@@ -149,7 +177,7 @@ Creates a directory and all parent directories. This function is defensive and d
|
||||
**Returns:** A message confirming the directory was created.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Create a directory
|
||||
mkdir("new_dir");
|
||||
|
||||
@@ -167,7 +195,7 @@ Gets the size of a file in bytes.
|
||||
**Returns:** The size of the file in bytes.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Get file size
|
||||
let size = file_size("large_file.dat");
|
||||
print(`File size: ${size} bytes`);
|
||||
@@ -185,7 +213,7 @@ Reads the contents of a file.
|
||||
**Returns:** The content of the file as a string.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Read a file
|
||||
let content = file_read("config.json");
|
||||
print(`File content: ${content}`);
|
||||
@@ -202,7 +230,7 @@ Writes content to a file. Creates the file if it doesn't exist, overwrites if it
|
||||
**Returns:** A message confirming the file was written.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Write to a file
|
||||
file_write("config.json", "{\n \"setting\": \"value\"\n}");
|
||||
```
|
||||
@@ -218,7 +246,7 @@ Appends content to a file. Creates the file if it doesn't exist.
|
||||
**Returns:** A message confirming the content was appended.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Append to a log file
|
||||
file_write_append("log.txt", "New log entry\n");
|
||||
```
|
||||
@@ -234,7 +262,7 @@ Syncs directories using rsync (or platform equivalent).
|
||||
**Returns:** A message confirming the directories were synced.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Sync directories
|
||||
rsync("source_dir", "backup_dir");
|
||||
```
|
||||
@@ -249,7 +277,7 @@ Changes the current working directory.
|
||||
**Returns:** A message confirming the directory was changed.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Change directory
|
||||
chdir("project/src");
|
||||
```
|
||||
@@ -258,7 +286,7 @@ chdir("project/src");
|
||||
|
||||
### `download(url, dest, min_size_kb)`
|
||||
|
||||
Downloads a file from a URL to a destination using the curl command. If the URL ends with a supported archive format, the file will be automatically extracted to the destination directory.
|
||||
Downloads a file from a URL to a destination directory using the curl command. If the URL ends with a supported archive format, the file will be automatically extracted to the destination directory.
|
||||
|
||||
**Supported archive formats for automatic extraction:**
|
||||
- `.tar.gz`
|
||||
@@ -268,15 +296,32 @@ Downloads a file from a URL to a destination using the curl command. If the URL
|
||||
|
||||
**Parameters:**
|
||||
- `url` (string): The URL to download from
|
||||
- `dest` (string): The destination path to save the file
|
||||
- `dest` (string): The destination directory where the file will be saved or extracted
|
||||
- `min_size_kb` (integer): The minimum expected file size in kilobytes (for validation)
|
||||
|
||||
**Returns:** The path where the file was saved or extracted.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
// Download a file
|
||||
download("https://example.com/file.zip", "downloads/file.zip", 10);
|
||||
```js
|
||||
// Download a file to a directory
|
||||
download("https://example.com/file.zip", "downloads/", 10);
|
||||
```
|
||||
|
||||
### `download_file(url, dest, min_size_kb)`
|
||||
|
||||
Downloads a file from a URL to a specific file destination using the curl command. This function is designed for downloading files to a specific path, not for extracting archives.
|
||||
|
||||
**Parameters:**
|
||||
- `url` (string): The URL to download from
|
||||
- `dest` (string): The destination file path where the file will be saved
|
||||
- `min_size_kb` (integer): The minimum expected file size in kilobytes (for validation)
|
||||
|
||||
**Returns:** The path where the file was saved.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Download a file to a specific path
|
||||
download_file("https://example.com/file.txt", "downloads/myfile.txt", 10);
|
||||
```
|
||||
|
||||
### `download_install(url, min_size_kb)`
|
||||
@@ -293,6 +338,22 @@ Downloads a file and installs it if it's a supported package format.
|
||||
**Returns:** The path where the file was saved or installed.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Download and install a package
|
||||
download_install("https://example.com/package.deb", 1000);
|
||||
```
|
||||
|
||||
### `chmod_exec(path)`
|
||||
|
||||
Makes a file executable (equivalent to `chmod +x` in Unix).
|
||||
|
||||
**Parameters:**
|
||||
- `path` (string): The path to the file to make executable
|
||||
|
||||
**Returns:** A message confirming the file was made executable.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Make a file executable
|
||||
chmod_exec("downloads/script.sh");
|
||||
```
|
60
docs/docs/sal/os/download.md
Normal file
60
docs/docs/sal/os/download.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# os.download Module
|
||||
|
||||
### `download(url, dest, min_size_kb)`
|
||||
|
||||
Download a file from URL to destination using the curl command.
|
||||
|
||||
- **Description**: Downloads a file from the given `url`. If `dest` is a directory, the filename is derived from the URL. If `dest` is a file path, it is used directly. Requires the `curl` command to be available. Halts script execution on download or file writing errors, or if the downloaded file size is less than `min_size_kb`. Returns the destination path.
|
||||
- **Returns**: `String` - The path where the file was downloaded.
|
||||
- **Arguments**:
|
||||
- `url`: `String` - The URL of the file to download.
|
||||
- `dest`: `String` - The destination path (directory or file).
|
||||
- `min_size_kb`: `Integer` - The minimum expected size of the downloaded file in kilobytes.
|
||||
|
||||
```rhai
|
||||
let download_url = "https://example.com/archive.zip";
|
||||
let download_dest_dir = "/tmp/downloads";
|
||||
print(`Downloading ${download_url} to ${download_dest_dir}...`);
|
||||
let downloaded_file_path = os::download(download_url, download_dest_dir, 50); // Halts on error
|
||||
print(`Downloaded to: ${downloaded_file_path}`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `download_file(url, dest, min_size_kb)`
|
||||
|
||||
Download a file from URL to a specific file destination using the curl command.
|
||||
|
||||
- **Description**: Downloads a file from the given `url` directly to the specified file `dest`. Requires the `curl` command. Halts script execution on download or file writing errors, or if the downloaded file size is less than `min_size_kb`. Returns the destination path.
|
||||
- **Returns**: `String` - The path where the file was downloaded.
|
||||
- **Arguments**:
|
||||
- `url`: `String` - The URL of the file to download.
|
||||
- `dest`: `String` - The full path where the file should be saved.
|
||||
- `min_size_kb`: `Integer` - The minimum expected size of the downloaded file in kilobytes.
|
||||
|
||||
```rhai
|
||||
let data_url = "https://example.com/dataset.tar.gz";
|
||||
let local_path = "/opt/data/dataset.tar.gz";
|
||||
print(`Downloading ${data_url} to ${local_path}...`);
|
||||
os::download_file(data_url, local_path, 1024); // Halts on error
|
||||
print(`Downloaded dataset to: ${local_path}`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `download_install(url, min_size_kb)`
|
||||
|
||||
Download a file and install it if it\'s a supported package format.
|
||||
|
||||
- **Description**: Downloads a file from the given `url` to a temporary location and then attempts to install it using the appropriate system package manager if the file format is supported (e.g., `.deb` on Ubuntu, `.pkg` or `.dmg` on MacOS). Requires the `curl` command and the system package manager. Halts script execution on download, installation, or file size errors. Returns a success message.
|
||||
- **Returns**: `String` - A success message upon successful download and installation attempt.
|
||||
- **Arguments**:
|
||||
- `url`: `String` - The URL of the package file to download and install.
|
||||
- `min_size_kb`: `Integer` - The minimum expected size of the downloaded file in kilobytes.
|
||||
|
||||
```rhai
|
||||
let package_url = "https://example.com/mytool.deb";
|
||||
print(`Downloading and installing ${package_url}...`);
|
||||
os::download_install(package_url, 300); // Halts on error
|
||||
print("Installation attempt finished.");
|
||||
```
|
338
docs/docs/sal/os/fs.md
Normal file
338
docs/docs/sal/os/fs.md
Normal file
@@ -0,0 +1,338 @@
|
||||
# os.fs Module
|
||||
|
||||
The `os` module provides functions for interacting with the operating system, including file system operations, command execution checks, downloads, and package management.
|
||||
|
||||
All functions that interact with the file system or external commands will halt the script execution if an error occurs, unless explicitly noted otherwise.
|
||||
|
||||
---
|
||||
|
||||
### `copy(src, dest)`
|
||||
|
||||
Recursively copy a file or directory from source to destination.
|
||||
|
||||
- **Description**: Performs a recursive copy operation. Halts script execution on failure.
|
||||
- **Returns**: `String` - The destination path.
|
||||
- **Arguments**:
|
||||
- `src`: `String` - The path to the source file or directory.
|
||||
- `dest`: `String` - The path to the destination file or directory.
|
||||
|
||||
```rhai
|
||||
print("Copying directory...");
|
||||
let source_dir = "/tmp/source_data";
|
||||
let dest_dir = "/backup/source_data";
|
||||
let copied_path = os::copy(source_dir, dest_dir); // Halts on error
|
||||
print(`Copied ${source_dir} to ${copied_path}`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `exist(path)`
|
||||
|
||||
Check if a file or directory exists.
|
||||
|
||||
- **Description**: Checks for the presence of a file or directory at the given path. This function does NOT halt on error if the path is invalid or permissions prevent checking.
|
||||
- **Returns**: `Boolean` - `true` if the path exists, `false` otherwise.
|
||||
- **Arguments**:
|
||||
- `path`: `String` - The path to check.
|
||||
|
||||
```rhai
|
||||
if os::exist("config.json") {
|
||||
print(`${file_path} exists.`);
|
||||
} else {
|
||||
print(`${file_path} does not exist.`);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `find_file(dir, filename)`
|
||||
|
||||
Find a file in a directory (with support for wildcards).
|
||||
|
||||
- **Description**: Searches for a file matching `filename` within the specified `dir`. Supports simple wildcards like `*` and `?`. Halts script execution if the directory cannot be read or if no file is found.
|
||||
- **Returns**: `String` - The path to the first file found that matches the pattern.
|
||||
- **Arguments**:
|
||||
- `dir`: `String` - The directory to search within.
|
||||
- `filename`: `String` - The filename pattern to search for (e.g., `"*.log"`).
|
||||
|
||||
```rhai
|
||||
let log_file = os::find_file("/var/log", "syslog*.log"); // Halts if not found or directory error
|
||||
print(`Found log file: ${log_file}`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `find_files(dir, filename)`
|
||||
|
||||
Find multiple files in a directory (recursive, with support for wildcards).
|
||||
|
||||
- **Description**: Recursively searches for all files matching `filename` within the specified `dir` and its subdirectories. Supports simple wildcards. Halts script execution if the directory cannot be read.
|
||||
- **Returns**: `Array` of `String` - An array containing paths to all matching files.
|
||||
- **Arguments**:
|
||||
- `dir`: `String` - The directory to start the recursive search from.
|
||||
- `filename`: `String` - The filename pattern to search for (e.g., `"*.tmp"`).
|
||||
|
||||
```rhai
|
||||
let temp_files = os::find_files("/tmp", "*.swp"); // Halts on directory error
|
||||
print("Found temporary files:");
|
||||
for file in temp_files {
|
||||
print(`- ${file}`);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `find_dir(dir, dirname)`
|
||||
|
||||
Find a directory in a parent directory (with support for wildcards).
|
||||
|
||||
- **Description**: Searches for a directory matching `dirname` within the specified `dir`. Supports simple wildcards. Halts script execution if the directory cannot be read or if no directory is found.
|
||||
- **Returns**: `String` - The path to the first directory found that matches the pattern.
|
||||
- **Arguments**:
|
||||
- `dir`: `String` - The directory to search within.
|
||||
- `dirname`: `String` - The directory name pattern to search for (e.g., `"backup_*"`).
|
||||
|
||||
```rhai
|
||||
let latest_backup_dir = os::find_dir("/mnt/backups", "backup_20*"); // Halts if not found or directory error
|
||||
print(`Found backup directory: ${latest_backup_dir}`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `find_dirs(dir, dirname)`
|
||||
|
||||
Find multiple directories in a parent directory (recursive, with support for wildcards).
|
||||
|
||||
- **Description**: Recursively searches for all directories matching `dirname` within the specified `dir` and its subdirectories. Supports simple wildcards. Halts script execution if the directory cannot be read.
|
||||
- **Returns**: `Array` of `String` - An array containing paths to all matching directories.
|
||||
- **Arguments**:
|
||||
- `dir`: `String` - The directory to start the recursive search from.
|
||||
- `dirname`: `String` - The directory name pattern to search for (e.g., `"project_*_v?"`).
|
||||
|
||||
```rhai
|
||||
let project_versions = os::find_dirs("/home/user/dev", "project_*_v?"); // Halts on directory error
|
||||
print("Found project version directories:");
|
||||
for dir in project_versions {
|
||||
print(`- ${dir}`);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `delete(path)`
|
||||
|
||||
Delete a file or directory (defensive - doesn't error if file doesn't exist).
|
||||
|
||||
- **Description**: Deletes the file or directory at the given path. If the path does not exist, the function does nothing and does not halt. Halts script execution on other errors (e.g., permission denied, directory not empty). Returns the path that was attempted to be deleted.
|
||||
- **Returns**: `String` - The path that was given as input.
|
||||
- **Arguments**:
|
||||
- `path`: `String` - The path to the file or directory to delete.
|
||||
|
||||
```rhai
|
||||
let temp_path = "/tmp/temporary_item";
|
||||
print(`Attempting to delete: ${temp_path}`);
|
||||
os::delete(temp_path); // Halts on permissions or non-empty directory error
|
||||
print("Deletion attempt finished.");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `mkdir(path)`
|
||||
|
||||
Create a directory and all parent directories (defensive - doesn't error if directory exists).
|
||||
|
||||
- **Description**: Creates the directory at the given path, including any necessary parent directories. If the directory already exists, the function does nothing and does not halt. Halts script execution on other errors (e.g., permission denied). Returns the path that was created (or already existed).
|
||||
- **Returns**: `String` - The path that was created or checked.
|
||||
- **Arguments**:
|
||||
- `path`: `String` - The path to the directory to create.
|
||||
|
||||
```rhai
|
||||
let new_dir = "/data/processed/reports";
|
||||
print(`Ensuring directory exists: ${new_dir}`);
|
||||
os::mkdir(new_dir); // Halts on permission error
|
||||
print("Directory check/creation finished.");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `file_size(path)`
|
||||
|
||||
Get the size of a file in bytes.
|
||||
|
||||
- **Description**: Returns the size of the file at the given path. Halts script execution if the file does not exist or cannot be accessed.
|
||||
- **Returns**: `Integer` - The size of the file in bytes (as i64).
|
||||
- **Arguments**:
|
||||
- `path`: `String` - The path to the file.
|
||||
|
||||
```rhai
|
||||
let file_path = "important_document.pdf";
|
||||
let size = os::file_size(file_path); // Halts if file not found or cannot read
|
||||
print(`File size: ${size} bytes`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `rsync(src, dest)`
|
||||
|
||||
Sync directories using rsync (or platform equivalent).
|
||||
|
||||
- **Description**: Synchronizes the contents of the source directory (`src`) to the destination directory (`dest`) using the system's available rsync-like command. Halts script execution on any error during the sync process. Returns a success message string.
|
||||
- **Returns**: `String` - A success message indicating the operation completed.
|
||||
- **Arguments**:
|
||||
- `src`: `String` - The source directory.
|
||||
- `dest`: `String` - The destination directory.
|
||||
|
||||
```rhai
|
||||
let source = "/local/project_files";
|
||||
let destination = "/remote/backup/project_files";
|
||||
print(`Syncing from ${source} to ${destination}...`);
|
||||
let result_message = os::rsync(source, destination); // Halts on error
|
||||
print(`Sync successful: ${result_message}`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `chdir(path)`
|
||||
|
||||
Change the current working directory.
|
||||
|
||||
- **Description**: Changes the current working directory of the script process. Halts script execution if the directory does not exist or cannot be accessed. Returns the new current working directory path.
|
||||
- **Returns**: `String` - The absolute path of the directory the process changed into.
|
||||
- **Arguments**:
|
||||
- `path`: `String` - The path to change the working directory to.
|
||||
|
||||
```rhai
|
||||
print(`Current directory: ${os::chdir(".")}`); // Use "." to get current path
|
||||
let new_cwd = "/tmp";
|
||||
os::chdir(new_cwd); // Halts if directory not found or access denied
|
||||
print(`Changed directory to: ${os::chdir(".")}`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `file_read(path)`
|
||||
|
||||
Read the contents of a file.
|
||||
|
||||
- **Description**: Reads the entire content of the file at the given path into a string. Halts script execution if the file does not exist or cannot be read.
|
||||
- **Returns**: `String` - The content of the file.
|
||||
- **Arguments**:
|
||||
- `path`: `String` - The path to the file.
|
||||
|
||||
```rhai
|
||||
let config_content = os::file_read("settings.conf"); // Halts if file not found or cannot read
|
||||
print("Config content:");
|
||||
print(config_content);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `file_write(path, content)`
|
||||
|
||||
Write content to a file (creates the file if it doesn\'t exist, overwrites if it does).
|
||||
|
||||
- **Description**: Writes the specified `content` to the file at the given `path`. If the file exists, its content is replaced. If it doesn't exist, it is created. Halts script execution on error (e.g., permission denied, invalid path). Returns the path written to.
|
||||
- **Returns**: `String` - The path of the file written to.
|
||||
- **Arguments**:
|
||||
- `path`: `String` - The path to the file.
|
||||
- `content`: `String` - The content to write to the file.
|
||||
|
||||
```rhai
|
||||
let output_path = "/tmp/hello.txt";
|
||||
let text_to_write = "Hello from Rhai!";
|
||||
os::file_write(output_path, text_to_write); // Halts on error
|
||||
print(`Wrote to ${output_path}`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `file_write_append(path, content)`
|
||||
|
||||
Append content to a file (creates the file if it doesn\'t exist).
|
||||
|
||||
- **Description**: Appends the specified `content` to the end of the file at the given `path`. If the file does not exist, it is created. Halts script execution on error (e.g., permission denied, invalid path). Returns the path written to.
|
||||
- **Returns**: `String` - The path of the file written to.
|
||||
- **Arguments**:
|
||||
- `path`: `String` - The path to the file.
|
||||
- `content`: `String` - The content to append to the file.
|
||||
|
||||
```rhai
|
||||
let log_path = "application.log";
|
||||
let log_entry = "User login failed.\n";
|
||||
os::file_write_append(log_path, log_entry); // Halts on error
|
||||
print(`Appended to ${log_path}`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `mv(src, dest)`
|
||||
|
||||
Move a file or directory from source to destination.
|
||||
|
||||
- **Description**: Moves the file or directory from `src` to `dest`. Halts script execution on error (e.g., permission denied, source not found, destination exists and cannot be overwritten). Returns the destination path.
|
||||
- **Returns**: `String` - The path of the destination.
|
||||
- **Arguments**:
|
||||
- `src`: `String` - The path to the source file or directory.
|
||||
- `dest`: `String` - The path to the destination.
|
||||
|
||||
```rhai
|
||||
let old_path = "/tmp/report.csv";
|
||||
let new_path = "/archive/reports/report_final.csv";
|
||||
os::mv(old_path, new_path); // Halts on error
|
||||
print(`Moved ${old_path} to ${new_path}`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `which(command)`
|
||||
|
||||
Check if a command exists in the system PATH.
|
||||
|
||||
- **Description**: Searches the system's PATH environment variable for the executable `command`. This function does NOT halt on error; it returns an empty string if the command is not found.
|
||||
- **Returns**: `String` - The full path to the command executable if found, otherwise an empty string (`""`).
|
||||
- **Arguments**:
|
||||
- `command`: `String` - The name of the command to search for (e.g., `"git"`).
|
||||
|
||||
```rhai
|
||||
let git_path = os::which("git");
|
||||
if git_path != "" {
|
||||
print(`Git executable found at: ${git_path}`);
|
||||
} else {
|
||||
print("Git executable not found in PATH.");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `cmd_ensure_exists(commands)`
|
||||
|
||||
Ensure that one or more commands exist in the system PATH.
|
||||
|
||||
- **Description**: Checks if all command names specified in the `commands` string (space or comma separated) exist in the system's PATH. Halts script execution if any of the commands are not found. Returns a success message if all commands are found.
|
||||
- **Returns**: `String` - A success message.
|
||||
- **Arguments**:
|
||||
- `commands`: `String` - A string containing one or more command names, separated by spaces or commas (e.g., `"curl,tar,unzip"`).
|
||||
|
||||
```rhai
|
||||
print("Ensuring required commands are available...");
|
||||
os::cmd_ensure_exists("git curl docker"); // Halts if any command is missing
|
||||
print("All required commands found.");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `chmod_exec(path)`
|
||||
|
||||
Make a file executable (equivalent to chmod +x).
|
||||
|
||||
- **Description**: Sets the executable permission for the file at the given `path` for the owner, group, and others. Halts script execution on error (e.g., file not found, permission denied). Returns the path modified.
|
||||
- **Returns**: `String` - The path of the file whose permissions were modified.
|
||||
- **Arguments**:
|
||||
- `path`: `String` - The path to the file.
|
||||
|
||||
```rhai
|
||||
let script_path = "/usr/local/bin/myscript";
|
||||
print(`Making ${script_path} executable...`);
|
||||
os::chmod_exec(script_path); // Halts on error
|
||||
print("Permissions updated.");
|
||||
```
|
157
docs/docs/sal/os/package.md
Normal file
157
docs/docs/sal/os/package.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# os.package Module
|
||||
|
||||
### `package_install(package)`
|
||||
|
||||
Install a package using the system package manager.
|
||||
|
||||
- **Description**: Installs the specified `package` using the detected system package manager (e.g., `apt` on Ubuntu, `brew` on MacOS). Halts script execution if the package manager command fails. Returns a success message.
|
||||
- **Returns**: `String` - A message indicating successful installation.
|
||||
- **Arguments**:
|
||||
- `package`: `String` - The name of the package to install (e.g., `"nano"`).
|
||||
|
||||
```rhai
|
||||
print("Installing 'nano' package...");
|
||||
os::package_install("nano"); // Halts on package manager error
|
||||
print("'nano' installed successfully.");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `package_remove(package)`
|
||||
|
||||
Remove a package using the system package manager.
|
||||
|
||||
- **Description**: Removes the specified `package` using the detected system package manager. Halts script execution if the package manager command fails. Returns a success message.
|
||||
- **Returns**: `String` - A message indicating successful removal.
|
||||
- **Arguments**:
|
||||
- `package`: `String` - The name of the package to remove (e.g., `"htop"`).
|
||||
|
||||
```rhai
|
||||
print("Removing 'htop' package...");
|
||||
os::package_remove("htop"); // Halts on package manager error
|
||||
print("'htop' removed successfully.");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `package_update()`
|
||||
|
||||
Update package lists using the system package manager.
|
||||
|
||||
- **Description**: Updates the package lists that the system package manager uses (e.g., `apt update`, `brew update`). Halts script execution if the package manager command fails. Returns a success message.
|
||||
- **Returns**: `String` - A message indicating successful update.
|
||||
- **Arguments**: None.
|
||||
|
||||
```rhai
|
||||
print("Updating package lists...");
|
||||
os::package_update(); // Halts on package manager error
|
||||
print("Package lists updated.");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `package_upgrade()`
|
||||
|
||||
Upgrade installed packages using the system package manager.
|
||||
|
||||
- **Description**: Upgrades installed packages using the detected system package manager (e.g., `apt upgrade`, `brew upgrade`). Halts script execution if the package manager command fails. Returns a success message.
|
||||
- **Returns**: `String` - A message indicating successful upgrade.
|
||||
- **Arguments**: None.
|
||||
|
||||
```rhai
|
||||
print("Upgrading installed packages...");
|
||||
os::package_upgrade(); // Halts on package manager error
|
||||
print("Packages upgraded.");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `package_list()`
|
||||
|
||||
List installed packages using the system package manager.
|
||||
|
||||
- **Description**: Lists the names of packages installed on the system using the detected package manager. Halts script execution if the package manager command fails.
|
||||
- **Returns**: `Array` of `String` - An array containing the names of installed packages.
|
||||
- **Arguments**: None.
|
||||
|
||||
```rhai
|
||||
print("Listing installed packages...");
|
||||
let installed_packages = os::package_list(); // Halts on package manager error
|
||||
for pkg in installed_packages {
|
||||
print(`- ${pkg}`);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `package_search(query)`
|
||||
|
||||
Search for packages using the system package manager.
|
||||
|
||||
- **Description**: Searches for packages matching the given `query` using the detected system package manager. Halts script execution if the package manager command fails.
|
||||
- **Returns**: `Array` of `String` - An array containing the search results (package names and/or descriptions).
|
||||
- **Arguments**:
|
||||
- `query`: `String` - The search term.
|
||||
|
||||
```rhai
|
||||
print("Searching for 'python' packages...");
|
||||
let python_packages = os::package_search("python"); // Halts on package manager error
|
||||
for pkg in python_packages {
|
||||
print(`- ${pkg}`);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `package_is_installed(package)`
|
||||
|
||||
Check if a package is installed using the system package manager.
|
||||
|
||||
- **Description**: Checks if the specified `package` is installed using the detected system package manager. Halts script execution if the package manager command itself fails (e.g., command not found), but does NOT halt if the package is simply not found.
|
||||
- **Returns**: `Boolean` - `true` if the package is installed, `false` otherwise.
|
||||
- **Arguments**:
|
||||
- `package`: `String` - The name of the package to check (e.g., `"wget"`).
|
||||
|
||||
```rhai
|
||||
let package_name = "wget";
|
||||
if os::package_is_installed(package_name) { // Halts on package manager command error
|
||||
print(`${package_name} is installed.`);
|
||||
} else {
|
||||
print(`${package_name} is not installed.`);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `package_set_debug(debug)`
|
||||
|
||||
Set the debug mode for package management operations.
|
||||
|
||||
- **Description**: Enables or disables debug output for subsequent package management operations. This function does NOT halt on error and always returns the boolean value it was set to.
|
||||
- **Returns**: `Boolean` - The boolean value that the debug flag was set to.
|
||||
- **Arguments**:
|
||||
- `debug`: `Boolean` - Set to `true` to enable debug output, `false` to disable.
|
||||
|
||||
```rhai
|
||||
print("Enabling package debug output.");
|
||||
os::package_set_debug(true);
|
||||
// Subsequent package operations will print debug info
|
||||
|
||||
print("Disabling package debug output.");
|
||||
os::package_set_debug(false);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `package_platform()`
|
||||
|
||||
Get the current platform name for package management.
|
||||
|
||||
- **Description**: Returns the name of the operating system platform as detected by the package manager logic. This function does NOT halt on error; it returns `"Unknown"` if the platform cannot be determined.
|
||||
- **Returns**: `String` - The platform name, one of `"Ubuntu"`, `"MacOS"`, or `"Unknown"`.
|
||||
- **Arguments**: None.
|
||||
|
||||
```rhai
|
||||
let platform = os::package_platform(); // Does not halt on error
|
||||
print(`Detected package platform: ${platform}`);
|
||||
```
|
@@ -1,3 +1,9 @@
|
||||
---
|
||||
title: "process"
|
||||
sidebar_position: 3
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
# Process Module
|
||||
|
||||
The Process module provides functions for running commands and managing processes on your system.
|
||||
@@ -23,18 +29,17 @@ When you get information about a running process, you can see:
|
||||
- `cpu`: How much CPU the process is using
|
||||
|
||||
## Run Functions
|
||||
|
||||
### `run(command)`
|
||||
|
||||
Runs a command or multiline script with arguments.
|
||||
|
||||
**Parameters:**
|
||||
- `command` (string): The command to run
|
||||
- `command` (string): The command to run (can be a single command or a multiline script)
|
||||
|
||||
**Returns:** The result of the command, including output and whether it succeeded.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
**Example 1: Running a simple command**
|
||||
```js
|
||||
// Run a simple command
|
||||
let result = run("ls -la");
|
||||
|
||||
@@ -46,6 +51,28 @@ if result.success {
|
||||
}
|
||||
```
|
||||
|
||||
**Example 2: Running a multiline script**
|
||||
```js
|
||||
// Create a multiline script using backtick string literals
|
||||
let setup_script = `
|
||||
# Create directories
|
||||
mkdir -p /tmp/test_project
|
||||
cd /tmp/test_project
|
||||
|
||||
# Initialize git repository
|
||||
git init
|
||||
echo 'Initial content' > README.md
|
||||
git add README.md
|
||||
git config --local user.email 'test@example.com'
|
||||
git config --local user.name 'Test User'
|
||||
git commit -m 'Initial commit'
|
||||
`;
|
||||
|
||||
// Execute the multiline script
|
||||
let result = run(setup_script);
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
### `run_silent(command)`
|
||||
@@ -59,7 +86,7 @@ Runs a command or multiline script with arguments silently (without displaying o
|
||||
|
||||
**Example:**
|
||||
|
||||
```rhai
|
||||
```js
|
||||
// Run a command silently
|
||||
let result = run_silent("git pull");
|
||||
|
||||
@@ -82,7 +109,7 @@ Creates a new map with default run options.
|
||||
- `log` (boolean): `false` - Whether to log the command execution
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Create run options
|
||||
let options = new_run_options();
|
||||
```
|
||||
@@ -98,7 +125,7 @@ Runs a command with options specified in a map.
|
||||
**Returns:** The result of the command with your custom settings applied.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Create and customize run options
|
||||
let options = new_run_options();
|
||||
options.die = false; // Don't throw an error if the command fails
|
||||
@@ -110,6 +137,24 @@ options.log = true; // Log the command execution
|
||||
let result = run_with_options("npm install", options);
|
||||
```
|
||||
|
||||
## Working with Multiline Scripts
|
||||
|
||||
The Process module allows you to execute multiline scripts, which is particularly useful for complex operations that require multiple commands to be executed in sequence.
|
||||
|
||||
### Creating Multiline Scripts
|
||||
|
||||
Multiline scripts can be created using backtick (`) string literals in HeroScript:
|
||||
|
||||
```js
|
||||
let my_script = `
|
||||
# This is a multiline bash script
|
||||
echo "Hello, World!"
|
||||
mkdir -p /tmp/my_project
|
||||
cd /tmp/my_project
|
||||
touch example.txt
|
||||
`;
|
||||
```
|
||||
|
||||
## Process Management Functions
|
||||
|
||||
### `which(cmd)`
|
||||
@@ -122,7 +167,7 @@ Checks if a command exists in the PATH.
|
||||
**Returns:** The full path to the command if found, or nothing if not found.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Check if a command exists
|
||||
let git_path = which("git");
|
||||
|
||||
@@ -143,7 +188,7 @@ Kills processes matching a pattern.
|
||||
**Returns:** A message confirming the processes were killed.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Kill all processes with "node" in their name
|
||||
kill("node");
|
||||
```
|
||||
@@ -158,7 +203,7 @@ Lists processes matching a pattern (or all processes if the pattern is empty).
|
||||
**Returns:** A list of processes matching your search.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// List all processes
|
||||
let all_processes = process_list("");
|
||||
|
||||
@@ -181,7 +226,7 @@ Gets a single process matching the pattern. Throws an error if zero or more than
|
||||
**Returns:** Information about the matching process. This will only work if exactly one process matches.
|
||||
|
||||
**Example:**
|
||||
```rhai
|
||||
```js
|
||||
// Try to get a specific process
|
||||
try {
|
||||
let process = process_get("my_app");
|
223
docs/docs/sal/process/process.md
Normal file
223
docs/docs/sal/process/process.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# Process Module
|
||||
|
||||
The `process` module provides functions for running external commands and managing system processes using a builder pattern for command execution.
|
||||
|
||||
For running commands, you start with the `run()` function which returns a `CommandBuilder` object. You can then chain configuration methods like `silent()`, `ignore_error()`, and `log()` before finally calling the `do()` method to execute the command.
|
||||
|
||||
By default, command execution using the builder (`.do()`) will halt the script execution if the command itself fails (returns a non-zero exit code) or if there's an operating system error preventing the command from running. You can change this behavior with `ignore_error()`.
|
||||
|
||||
Other process management functions (`which`, `kill`, `process_list`, `process_get`) have specific error handling behaviors described below.
|
||||
|
||||
---
|
||||
|
||||
### `CommandResult`
|
||||
|
||||
An object returned by command execution functions (`.do()`) containing the result of the command.
|
||||
|
||||
- **Properties**:
|
||||
- `stdout`: `String` - The standard output of the command.
|
||||
- `stderr`: `String` - The standard error of the command.
|
||||
- `success`: `Boolean` - `true` if the command exited with code 0, `false` otherwise.
|
||||
- `code`: `Integer` - The exit code of the command.
|
||||
|
||||
```rhai
|
||||
let result = run("echo hi").do();
|
||||
print(`Success: ${result.success}, Output: ${result.stdout}`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `ProcessInfo`
|
||||
|
||||
An object found by process listing/getting functions (`process_list`, `process_get`) containing information about a running process.
|
||||
|
||||
- **Properties**:
|
||||
- `pid`: `Integer` - The process ID.
|
||||
- `name`: `String` - The name of the process executable.
|
||||
- `memory`: `Integer` - The memory usage of the process (unit depends on the operating system, typically KB or bytes).
|
||||
- `cpu`: `Float` - The CPU usage percentage (value and meaning may vary by operating system).
|
||||
|
||||
```rhai
|
||||
let processes = process_list("my_service");
|
||||
if (processes.len() > 0) {
|
||||
let first_proc = processes[0];
|
||||
print(`Process ${first_proc.name} (PID: ${first_proc.pid}) is running.`);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `run(command)`
|
||||
|
||||
Start building a command execution.
|
||||
|
||||
- **Description**: Initializes a `CommandBuilder` for the given command string. This is the entry point to configure and run a process.
|
||||
- **Returns**: `CommandBuilder` - A builder object for configuring the command.
|
||||
- **Arguments**:
|
||||
- `command`: `String` - The command string to execute. Can include arguments and be a simple multiline script.
|
||||
|
||||
```rhai
|
||||
let cmd_builder = run("ls -l");
|
||||
// Now you can chain methods like .silent(), .ignore_error(), .log()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `CommandBuilder:silent()`
|
||||
|
||||
Configure the command to run silently.
|
||||
|
||||
- **Description**: Suppresses real-time standard output and standard error from being printed to the script's console during command execution. The output is still captured in the resulting `CommandResult`.
|
||||
- **Returns**: `CommandBuilder` - Returns `self` for chaining.
|
||||
- **Arguments**: None.
|
||||
|
||||
```rhai
|
||||
print("Running silent command...");
|
||||
run("echo This won\'t show directly").silent().do();
|
||||
print("Silent command finished.");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `CommandBuilder:ignore_error()`
|
||||
|
||||
Configure the command to ignore non-zero exit codes.
|
||||
|
||||
- **Description**: By default, the `do()` method halts script execution if the command returns a non-zero exit code. Calling `ignore_error()` prevents this. The `CommandResult` will still indicate `success: false` and contain the non-zero `code`, allowing the script to handle the command failure explicitly. OS errors preventing the command from running will still cause a halt.
|
||||
- **Returns**: `CommandBuilder` - Returns `self` for chaining.
|
||||
- **Arguments**: None.
|
||||
|
||||
```rhai
|
||||
print("Running command that will fail but not halt...");
|
||||
let result = run("exit 1").ignore_error().do(); // Will not halt
|
||||
if (!result.success) {
|
||||
print(`Command failed as expected with code: ${result.code}`);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `CommandBuilder:log()`
|
||||
|
||||
Configure the command to log the execution details.
|
||||
|
||||
- **Description**: Enables logging of the command string before execution.
|
||||
- **Returns**: `CommandBuilder` - Returns `self` for chaining.
|
||||
- **Arguments**: None.
|
||||
|
||||
```rhai
|
||||
print("Running command with logging...");
|
||||
run("ls /tmp").log().do(); // Will print the "ls /tmp" command before running
|
||||
print("Command finished.");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `CommandBuilder:do()`
|
||||
|
||||
Execute the configured command.
|
||||
|
||||
- **Description**: Runs the command with the options set by the builder methods. Waits for the command to complete and returns the `CommandResult`. This method is the final step in the command execution builder chain. Halts based on the `ignore_error()` setting and OS errors.
|
||||
- **Returns**: `CommandResult` - An object containing the output and status of the command.
|
||||
- **Arguments**: None.
|
||||
|
||||
```rhai
|
||||
print("Running command using builder...");
|
||||
let command_result = run("pwd")
|
||||
.log() // Log the command
|
||||
.silent() // Don't print output live
|
||||
.do(); // Execute and get result (halts on error by default)
|
||||
|
||||
print(`Command output: ${command_result.stdout}`);
|
||||
|
||||
// Example with multiple options
|
||||
let fail_result = run("command_that_does_not_exist")
|
||||
.ignore_error() // Don't halt on non-zero exit (though OS error might still halt)
|
||||
.silent() // Don't print error live
|
||||
.do();
|
||||
|
||||
if (!fail_result.success) {
|
||||
print(`Failed command exited with code: ${fail_result.code} and stderr: ${fail_result.stderr}`);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `which(cmd)`
|
||||
|
||||
Check if a command exists in the system PATH.
|
||||
|
||||
- **Description**: Searches the system's PATH environment variable for the executable `cmd`. This function does NOT halt if the command is not found; it returns an empty string.
|
||||
- **Returns**: `String` - The full path to the command executable if found, otherwise an empty string (`""`).
|
||||
- **Arguments**:
|
||||
- `cmd`: `String` - The name of the command to search for (e.g., `"node"`).
|
||||
|
||||
```rhai
|
||||
let node_path = which("node"); // Does not halt if node is not found
|
||||
if (node_path != "") {
|
||||
print(`Node executable found at: ${node_path}`);
|
||||
} else {
|
||||
print("Node executable not found in PATH.");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `kill(pattern)`
|
||||
|
||||
Kill processes matching a pattern.
|
||||
|
||||
- **Description**: Terminates running processes whose names match the provided `pattern`. Uses platform-specific commands (like `pkill` or equivalent). Halts script execution on error interacting with the system process list or kill command.
|
||||
- **Returns**: `String` - A success message indicating the kill attempt finished.
|
||||
- **Arguments**:
|
||||
- `pattern`: `String` - A pattern to match against process names (e.g., `"nginx"`).
|
||||
|
||||
```rhai
|
||||
print("Attempting to kill processes matching 'my_service'...");
|
||||
// Use with caution!
|
||||
kill("my_service"); // Halts on OS error during kill attempt
|
||||
print("Kill command sent.");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `process_list(pattern)`
|
||||
|
||||
List processes matching a pattern (or all if pattern is empty).
|
||||
|
||||
- **Description**: Lists information about running processes whose names match the provided `pattern`. If `pattern` is an empty string `""`, lists all processes. Halts script execution on error interacting with the system process list. Returns an empty array if no processes match the pattern.
|
||||
- **Returns**: `Array` of `ProcessInfo` - An array of objects, each containing `pid` (Integer), `name` (String), `memory` (Integer), and `cpu` (Float).
|
||||
- **Arguments**:
|
||||
- `pattern`: `String` - A pattern to match against process names, or `""` for all processes.
|
||||
|
||||
```rhai
|
||||
print("Listing processes matching 'bash'...");
|
||||
let bash_processes = process_list("bash"); // Halts on OS error
|
||||
if (bash_processes.len() > 0) {
|
||||
print("Found bash processes:");
|
||||
for proc in bash_processes {
|
||||
print(`- PID: ${proc.pid}, Name: ${proc.name}, CPU: ${proc.cpu}%, Memory: ${proc.memory}`);
|
||||
}
|
||||
} else {
|
||||
print("No bash processes found.");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `process_get(pattern)`
|
||||
|
||||
Get a single process matching the pattern (error if 0 or more than 1 match).
|
||||
|
||||
- **Description**: Finds exactly one running process whose name matches the provided `pattern`. Halts script execution if zero or more than one process matches the pattern, or on error interacting with the system process list.
|
||||
- **Returns**: `ProcessInfo` - An object containing `pid` (Integer), `name` (String), `memory` (Integer), and `cpu` (Float).
|
||||
- **Arguments**:
|
||||
- `pattern`: `String` - A pattern to match against process names, expected to match exactly one process.
|
||||
|
||||
```rhai
|
||||
let expected_service_name = "my_critical_service";
|
||||
print(`Getting process info for '${expected_service_name}'...`);
|
||||
// This will halt if the service isn't running, or if multiple services have this name
|
||||
let service_proc_info = process_get(expected_service_name);
|
||||
print(`Found process: PID ${service_proc_info.pid}, Name: ${service_proc_info.name}`);
|
||||
```
|
154
docs/docs/sal/rfs.md
Normal file
154
docs/docs/sal/rfs.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# RFS (Remote File System)
|
||||
|
||||
The RFS module provides a Rust wrapper for the RFS tool, which allows mounting remote filesystems locally and managing filesystem layers.
|
||||
|
||||
## Overview
|
||||
|
||||
RFS (Remote File System) is a tool that enables mounting various types of remote filesystems locally, as well as creating and managing filesystem layers. The SAL library provides a Rust wrapper for RFS with a fluent builder API, making it easy to use in your applications.
|
||||
|
||||
## Features
|
||||
|
||||
- Mount remote filesystems locally (SSH, S3, WebDAV, etc.)
|
||||
- List mounted filesystems
|
||||
- Unmount filesystems
|
||||
- Pack directories into filesystem layers
|
||||
- Unpack filesystem layers
|
||||
- List contents of filesystem layers
|
||||
- Verify filesystem layers
|
||||
|
||||
## Usage in Rust
|
||||
|
||||
### Mounting a Filesystem
|
||||
|
||||
```rust
|
||||
use sal::virt::rfs::{RfsBuilder, MountType};
|
||||
|
||||
// Create a new RFS builder
|
||||
let mount = RfsBuilder::new("user@example.com:/remote/path", "/local/mount/point", MountType::SSH)
|
||||
.with_option("port", "2222")
|
||||
.with_option("identity_file", "/path/to/key")
|
||||
.with_debug(true)
|
||||
.mount()?;
|
||||
|
||||
println!("Mounted filesystem with ID: {}", mount.id);
|
||||
```
|
||||
|
||||
### Listing Mounts
|
||||
|
||||
```rust
|
||||
use sal::virt::rfs::list_mounts;
|
||||
|
||||
// List all mounts
|
||||
let mounts = list_mounts()?;
|
||||
for mount in mounts {
|
||||
println!("Mount ID: {}, Source: {}, Target: {}", mount.id, mount.source, mount.target);
|
||||
}
|
||||
```
|
||||
|
||||
### Unmounting a Filesystem
|
||||
|
||||
```rust
|
||||
use sal::virt::rfs::unmount;
|
||||
|
||||
// Unmount a filesystem
|
||||
unmount("/local/mount/point")?;
|
||||
```
|
||||
|
||||
### Packing a Directory
|
||||
|
||||
```rust
|
||||
use sal::virt::rfs::{PackBuilder, StoreSpec};
|
||||
|
||||
// Create store specifications
|
||||
let store_spec = StoreSpec::new("file")
|
||||
.with_option("path", "/path/to/store");
|
||||
|
||||
// Pack a directory with builder pattern
|
||||
let result = PackBuilder::new("/path/to/directory", "output.fl")
|
||||
.with_store_spec(store_spec)
|
||||
.with_debug(true)
|
||||
.pack()?;
|
||||
```
|
||||
|
||||
### Unpacking a Filesystem Layer
|
||||
|
||||
```rust
|
||||
use sal::virt::rfs::unpack;
|
||||
|
||||
// Unpack a filesystem layer
|
||||
unpack("input.fl", "/path/to/unpack")?;
|
||||
```
|
||||
|
||||
## Usage in Rhai Scripts
|
||||
|
||||
### Mounting a Filesystem
|
||||
|
||||
```rhai
|
||||
// Create a map for mount options
|
||||
let options = #{
|
||||
"port": "22",
|
||||
"identity_file": "/path/to/key",
|
||||
"readonly": "true"
|
||||
};
|
||||
|
||||
// Mount the directory
|
||||
let mount = rfs_mount("user@example.com:/remote/path", "/local/mount/point", "ssh", options);
|
||||
|
||||
print(`Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`);
|
||||
```
|
||||
|
||||
### Listing Mounts
|
||||
|
||||
```rhai
|
||||
// List all mounts
|
||||
let mounts = rfs_list_mounts();
|
||||
print(`Number of mounts: ${mounts.len()}`);
|
||||
|
||||
for mount in mounts {
|
||||
print(`Mount ID: ${mount.id}, Source: ${mount.source}, Target: ${mount.target}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Unmounting a Filesystem
|
||||
|
||||
```rhai
|
||||
// Unmount the directory
|
||||
rfs_unmount("/local/mount/point");
|
||||
```
|
||||
|
||||
### Packing a Directory
|
||||
|
||||
```rhai
|
||||
// Pack the directory
|
||||
// Store specs format: "file:path=/path/to/store,s3:bucket=my-bucket"
|
||||
rfs_pack("/path/to/directory", "output.fl", "file:path=/path/to/store");
|
||||
```
|
||||
|
||||
### Unpacking a Filesystem Layer
|
||||
|
||||
```rhai
|
||||
// Unpack the filesystem layer
|
||||
rfs_unpack("output.fl", "/path/to/unpack");
|
||||
```
|
||||
|
||||
## Mount Types
|
||||
|
||||
The RFS module supports various mount types:
|
||||
|
||||
- **Local**: Mount a local directory
|
||||
- **SSH**: Mount a remote directory via SSH
|
||||
- **S3**: Mount an S3 bucket
|
||||
- **WebDAV**: Mount a WebDAV server
|
||||
|
||||
## Store Specifications
|
||||
|
||||
When packing a directory into a filesystem layer, you can specify one or more stores to use. Each store has a type and options:
|
||||
|
||||
- **File**: Store files on the local filesystem
|
||||
- Options: `path` (path to the store)
|
||||
- **S3**: Store files in an S3 bucket
|
||||
- Options: `bucket` (bucket name), `region` (AWS region), `access_key`, `secret_key`
|
||||
|
||||
## Examples
|
||||
|
||||
See the [RFS example script](../../rhaiexamples/rfs_example.rhai) for more examples of how to use the RFS module in Rhai scripts.
|
237
docs/docs/sal/text.md
Normal file
237
docs/docs/sal/text.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Text Manipulation Tools
|
||||
|
||||
The SAL text module provides powerful text manipulation capabilities that can be used from Rhai scripts. These include text replacement (with regex support), template rendering, string normalization, and text formatting utilities.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Text Replacement](#text-replacement)
|
||||
- [Template Rendering](#template-rendering)
|
||||
- [String Normalization](#string-normalization)
|
||||
- [Text Formatting](#text-formatting)
|
||||
|
||||
## Text Replacement
|
||||
|
||||
The text replacement tools allow you to perform simple or complex text replacements, with support for regular expressions, case-insensitive matching, and file operations.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```rhai
|
||||
// Create a new text replacer
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo") // Set the pattern to search for
|
||||
.replacement("bar") // Set the replacement text
|
||||
.build(); // Build the replacer
|
||||
|
||||
// Apply the replacer to a string
|
||||
let result = replacer.replace("foo bar foo");
|
||||
// Result: "bar bar bar"
|
||||
```
|
||||
|
||||
### Advanced Features
|
||||
|
||||
#### Regular Expressions
|
||||
|
||||
```rhai
|
||||
// Create a replacer with regex support
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("\\bfoo\\b") // Use regex pattern (word boundary)
|
||||
.replacement("bar")
|
||||
.regex(true) // Enable regex mode
|
||||
.build();
|
||||
|
||||
// Apply the replacer to a string
|
||||
let result = replacer.replace("foo foobar");
|
||||
// Result: "bar foobar" (only replaces whole "foo" words)
|
||||
```
|
||||
|
||||
#### Case-Insensitive Matching
|
||||
|
||||
```rhai
|
||||
// Create a replacer with case-insensitive matching
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo")
|
||||
.replacement("bar")
|
||||
.regex(true)
|
||||
.case_insensitive(true) // Enable case-insensitive matching
|
||||
.build();
|
||||
|
||||
// Apply the replacer to a string
|
||||
let result = replacer.replace("FOO foo Foo");
|
||||
// Result: "bar bar bar"
|
||||
```
|
||||
|
||||
#### Multiple Replacements
|
||||
|
||||
```rhai
|
||||
// Chain multiple replacements
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo")
|
||||
.replacement("bar")
|
||||
.and() // Add another replacement operation
|
||||
.pattern("baz")
|
||||
.replacement("qux")
|
||||
.build();
|
||||
|
||||
// Apply the replacer to a string
|
||||
let result = replacer.replace("foo baz");
|
||||
// Result: "bar qux"
|
||||
```
|
||||
|
||||
#### File Operations
|
||||
|
||||
```rhai
|
||||
// Create a replacer
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo")
|
||||
.replacement("bar")
|
||||
.build();
|
||||
|
||||
// Replace in a file and get the result as a string
|
||||
let result = replacer.replace_file("input.txt");
|
||||
|
||||
// Replace in a file and write back to the same file
|
||||
replacer.replace_file_in_place("input.txt");
|
||||
|
||||
// Replace in a file and write to a new file
|
||||
replacer.replace_file_to("input.txt", "output.txt");
|
||||
```
|
||||
|
||||
## Template Rendering
|
||||
|
||||
The template rendering tools allow you to create and render templates with variables, using the powerful Tera template engine.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```rhai
|
||||
// Create a template builder with a template file
|
||||
let template = template_builder_open("template.txt")
|
||||
.add_var("name", "John") // Add a string variable
|
||||
.add_var("age", 30) // Add a numeric variable
|
||||
.add_var("items", ["a", "b", "c"]); // Add an array variable
|
||||
|
||||
// Render the template
|
||||
let result = template.render();
|
||||
|
||||
// Render to a file
|
||||
template.render_to_file("output.txt");
|
||||
```
|
||||
|
||||
### Template Variables
|
||||
|
||||
You can add variables of various types:
|
||||
|
||||
```rhai
|
||||
let template = template_builder_open("template.txt")
|
||||
.add_var("name", "John") // String
|
||||
.add_var("age", 30) // Integer
|
||||
.add_var("height", 1.85) // Float
|
||||
.add_var("is_active", true) // Boolean
|
||||
.add_var("items", ["a", "b", "c"]); // Array
|
||||
```
|
||||
|
||||
### Using Map for Variables
|
||||
|
||||
```rhai
|
||||
// Create a map of variables
|
||||
let vars = #{
|
||||
name: "Alice",
|
||||
place: "Wonderland"
|
||||
};
|
||||
|
||||
// Add all variables from the map
|
||||
let template = template_builder_open("template.txt")
|
||||
.add_vars(vars);
|
||||
```
|
||||
|
||||
### Template Syntax
|
||||
|
||||
The template engine uses Tera, which supports:
|
||||
|
||||
- Variable interpolation: `{{ variable }}`
|
||||
- Conditionals: `{% if condition %}...{% endif %}`
|
||||
- Loops: `{% for item in items %}...{% endfor %}`
|
||||
- Filters: `{{ variable | filter }}`
|
||||
|
||||
Example template:
|
||||
|
||||
```
|
||||
Hello, {{ name }}!
|
||||
|
||||
{% if show_greeting %}
|
||||
Welcome to {{ place }}.
|
||||
{% endif %}
|
||||
|
||||
Your items:
|
||||
{% for item in items %}
|
||||
- {{ item }}{% if not loop.last %}{% endif %}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
## String Normalization
|
||||
|
||||
The string normalization tools help convert strings to consistent formats for use as file names or paths.
|
||||
|
||||
### name_fix
|
||||
|
||||
Converts a string to a safe, normalized name by:
|
||||
- Converting to lowercase
|
||||
- Replacing spaces and special characters with underscores
|
||||
- Removing non-alphanumeric characters
|
||||
|
||||
```rhai
|
||||
let fixed_name = name_fix("Hello World!");
|
||||
// Result: "hello_world"
|
||||
|
||||
let fixed_name = name_fix("File-Name.txt");
|
||||
// Result: "file_name.txt"
|
||||
```
|
||||
|
||||
### path_fix
|
||||
|
||||
Similar to name_fix, but preserves path separators:
|
||||
|
||||
```rhai
|
||||
let fixed_path = path_fix("/path/to/Hello World!");
|
||||
// Result: "/path/to/hello_world"
|
||||
|
||||
let fixed_path = path_fix("./relative/path/to/DOCUMENT-123.pdf");
|
||||
// Result: "./relative/path/to/document_123.pdf"
|
||||
```
|
||||
|
||||
## Text Formatting
|
||||
|
||||
Tools to help with text formatting and indentation.
|
||||
|
||||
### dedent
|
||||
|
||||
Removes common leading whitespace from multi-line strings:
|
||||
|
||||
```rhai
|
||||
let indented_text = " line 1
|
||||
line 2
|
||||
line 3";
|
||||
|
||||
let dedented = dedent(indented_text);
|
||||
// Result: "line 1
|
||||
// line 2
|
||||
// line 3"
|
||||
```
|
||||
|
||||
### prefix
|
||||
|
||||
Adds a prefix to every line in a multi-line string:
|
||||
|
||||
```rhai
|
||||
let text = "line 1
|
||||
line 2
|
||||
line 3";
|
||||
|
||||
let prefixed = prefix(text, " ");
|
||||
// Result: " line 1
|
||||
// line 2
|
||||
// line 3"
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See the [text_tools.rhai](https://github.com/ourworld-tf/herocode/blob/main/sal/src/rhaiexamples/text_tools.rhai) example script for more detailed examples of using these text manipulation tools.
|
@@ -1 +0,0 @@
|
||||
EXAMPLE FILE TO TEST
|
@@ -1,8 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:lts-alpine
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN yarn install --production
|
||||
CMD ["node", "src/index.js"]
|
||||
EXPOSE 3000
|
62
examples/_archive/container_example.rs
Normal file
62
examples/_archive/container_example.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
// File: /root/code/git.threefold.info/herocode/sal/examples/container_example.rs
|
||||
|
||||
use std::error::Error;
|
||||
use sal::virt::nerdctl::Container;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// Create a container from an image
|
||||
println!("Creating container from image...");
|
||||
let container = Container::from_image("my-nginx", "nginx:latest")?
|
||||
.with_port("8080:80")
|
||||
.with_env("NGINX_HOST", "example.com")
|
||||
.with_volume("/tmp/nginx:/usr/share/nginx/html")
|
||||
.with_health_check("curl -f http://localhost/ || exit 1")
|
||||
.with_detach(true)
|
||||
.build()?;
|
||||
|
||||
println!("Container created successfully");
|
||||
|
||||
// Execute a command in the container
|
||||
println!("Executing command in container...");
|
||||
let result = container.exec("echo 'Hello from container'")?;
|
||||
println!("Command output: {}", result.stdout);
|
||||
|
||||
// Get container status
|
||||
println!("Getting container status...");
|
||||
let status = container.status()?;
|
||||
println!("Container status: {}", status.status);
|
||||
|
||||
// Get resource usage
|
||||
println!("Getting resource usage...");
|
||||
let resources = container.resources()?;
|
||||
println!("CPU usage: {}", resources.cpu_usage);
|
||||
println!("Memory usage: {}", resources.memory_usage);
|
||||
|
||||
// Stop and remove the container
|
||||
println!("Stopping and removing container...");
|
||||
container.stop()?;
|
||||
container.remove()?;
|
||||
|
||||
println!("Container stopped and removed");
|
||||
|
||||
// Get a container by name (if it exists)
|
||||
println!("\nGetting a container by name...");
|
||||
match Container::new("existing-container") {
|
||||
Ok(container) => {
|
||||
if container.container_id.is_some() {
|
||||
println!("Found container with ID: {}", container.container_id.as_ref().unwrap());
|
||||
|
||||
// Perform operations on the existing container
|
||||
let status = container.status()?;
|
||||
println!("Container status: {}", status.status);
|
||||
} else {
|
||||
println!("Container exists but has no ID");
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error getting container: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
210
examples/_archive/containerd_grpc_setup.rhai
Normal file
210
examples/_archive/containerd_grpc_setup.rhai
Normal file
@@ -0,0 +1,210 @@
|
||||
// containerd_grpc_setup.rhai
|
||||
//
|
||||
// This script sets up a Rust project with gRPC connectivity to containerd
|
||||
// Following the steps from the instructions document
|
||||
|
||||
|
||||
run("apt-get -y protobuf-compiler ");
|
||||
|
||||
// Step 1: Set up project directory
|
||||
let project_dir = "/tmp/containerd-rust-client";
|
||||
print(`Setting up project in: ${project_dir}`);
|
||||
|
||||
// Clean up any existing directory
|
||||
if exist(project_dir) {
|
||||
print("Found existing project directory, removing it...");
|
||||
delete(project_dir);
|
||||
}
|
||||
|
||||
// Create our project directory
|
||||
mkdir(project_dir);
|
||||
|
||||
// Change to the project directory
|
||||
chdir(project_dir);
|
||||
|
||||
// Step 2: Clone containerd's gRPC proto files
|
||||
print("Cloning containerd repository to get proto files...");
|
||||
let git_tree = gittree_new(project_dir);
|
||||
let repos = git_tree.get("https://github.com/containerd/containerd.git");
|
||||
let repo = repos[0];
|
||||
print(`Cloned containerd repository to: ${repo.path()}`);
|
||||
|
||||
// Step 3: Create necessary project files
|
||||
print("Creating Cargo.toml file...");
|
||||
// Using raw string with # for multiline content
|
||||
let cargo_toml = #"
|
||||
[package]
|
||||
name = "containerd-rust-client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tonic = "0.11"
|
||||
prost = "0.12"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
hyper-unix-connector = "0.2.0"
|
||||
tower = "0.4"
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = "0.11"
|
||||
"#;
|
||||
|
||||
file_write("Cargo.toml", cargo_toml);
|
||||
print("Created Cargo.toml file");
|
||||
|
||||
// Step 4: Set up build.rs to compile protos
|
||||
print("Creating build.rs file...");
|
||||
let build_rs = #"
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=containerd/api/services/images/v1/images.proto");
|
||||
println!("cargo:rerun-if-changed=containerd/api/services/containers/v1/containers.proto");
|
||||
|
||||
tonic_build::configure()
|
||||
.build_server(false)
|
||||
.compile(
|
||||
&[
|
||||
"containerd/api/services/images/v1/images.proto",
|
||||
"containerd/api/services/containers/v1/containers.proto",
|
||||
// Add more proto files as needed
|
||||
],
|
||||
&[
|
||||
"containerd",
|
||||
"containerd/api",
|
||||
"containerd/api/types"
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
"#;
|
||||
|
||||
file_write("build.rs", build_rs);
|
||||
print("Created build.rs file");
|
||||
|
||||
// Step 5: Create src directory and main.rs file
|
||||
mkdir("src");
|
||||
|
||||
// Create a helper function for Unix socket connection
|
||||
print("Creating src/main.rs file...");
|
||||
let main_rs = #"
|
||||
use tonic::transport::{Channel, Endpoint, Uri};
|
||||
use tower::service_fn;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
// The proto-generated modules will be available after build
|
||||
// use containerd::services::images::v1::{
|
||||
// images_client::ImagesClient,
|
||||
// GetImageRequest,
|
||||
// };
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Connecting to containerd gRPC...");
|
||||
|
||||
// Path to containerd socket
|
||||
let socket_path = "/run/containerd/containerd.sock";
|
||||
|
||||
// Connect to the Unix socket
|
||||
let channel = unix_socket_channel(socket_path).await?;
|
||||
|
||||
// Now we'd create a client and use it
|
||||
// let mut client = ImagesClient::new(channel);
|
||||
// let response = client.get(GetImageRequest {
|
||||
// name: "docker.io/library/ubuntu:latest".to_string(),
|
||||
// }).await?;
|
||||
// println!("Image: {:?}", response.into_inner());
|
||||
|
||||
println!("Connection to containerd socket established successfully!");
|
||||
println!("This is a template - uncomment the client code after building.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper function to connect to Unix socket
|
||||
async fn unix_socket_channel(path: &str) -> Result<Channel, Box<dyn std::error::Error>> {
|
||||
// Use a placeholder URI since Unix sockets don't have URIs
|
||||
let endpoint = Endpoint::try_from("http://[::]:50051")?;
|
||||
|
||||
// The socket path to connect to
|
||||
let path_to_connect = path.to_string();
|
||||
|
||||
// Create a connector function that connects to the Unix socket
|
||||
let channel = endpoint
|
||||
.connect_with_connector(service_fn(move |_: Uri| {
|
||||
let path = path_to_connect.clone();
|
||||
async move {
|
||||
tokio::net::UnixStream::connect(path)
|
||||
.await
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
|
||||
}
|
||||
}))
|
||||
.await?;
|
||||
|
||||
Ok(channel)
|
||||
}
|
||||
"#;
|
||||
|
||||
file_write("src/main.rs", main_rs);
|
||||
print("Created src/main.rs file");
|
||||
|
||||
// Step 6: Create a README.md file
|
||||
print("Creating README.md file...");
|
||||
// Using raw string with # for multiline content containing markdown backticks
|
||||
let readme = #"# containerd Rust gRPC Client
|
||||
|
||||
A Rust client for interacting with containerd via gRPC.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Rust and Cargo installed
|
||||
- containerd running on your system
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cargo build
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
cargo run
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Connect to containerd via Unix socket
|
||||
- Query image information
|
||||
- Work with containers
|
||||
|
||||
## Structure
|
||||
|
||||
- `src/main.rs` - Example client code
|
||||
- `build.rs` - Proto compilation script
|
||||
"#;
|
||||
|
||||
file_write("README.md", readme);
|
||||
print("Created README.md file");
|
||||
|
||||
// Step 7: Build the project
|
||||
print("Building the project...");
|
||||
let build_result = run("cargo build");
|
||||
|
||||
if build_result.success {
|
||||
print("Project built successfully!");
|
||||
} else {
|
||||
print(`Build failed with error: ${build_result.stderr}`);
|
||||
}
|
||||
|
||||
print(`
|
||||
--------------------------------------
|
||||
🎉 Setup complete!
|
||||
|
||||
Project created at: ${project_dir}
|
||||
|
||||
To use the project:
|
||||
1. cd ${project_dir}
|
||||
2. cargo run
|
||||
|
||||
Note: Make sure containerd is running and the socket exists at /run/containerd/containerd.sock
|
||||
--------------------------------------
|
||||
`);
|
@@ -49,7 +49,7 @@ print(` Extraction successful: ${success_msg}`);
|
||||
|
||||
// Download binary file and check size
|
||||
print("\nTesting binary download:");
|
||||
download(binary_url, binary_dest, 8000);
|
||||
download_file(binary_url, binary_dest, 8000);
|
||||
|
||||
// Check file size using our new file_size function
|
||||
let size_bytes = file_size(binary_dest);
|
||||
@@ -63,6 +63,43 @@ print(` Minimum size check passed:${success_msg}`);
|
||||
// Clean up test files
|
||||
delete(download_dir);
|
||||
print("Cleaned up test directory");
|
||||
//PART 4
|
||||
|
||||
print("\nDownload Tests completed successfully!");
|
||||
// Test the new download_file function
|
||||
print("\nTesting download_file function:");
|
||||
let text_url = "https://raw.githubusercontent.com/freeflowuniverse/herolib/main/README.md";
|
||||
let text_file_dest = `${download_dir}/README.md`;
|
||||
|
||||
// Create the directory again for this test
|
||||
mkdir(download_dir);
|
||||
|
||||
// Download a text file using the new download_file function
|
||||
let file_result = download_file(text_url, text_file_dest, 0);
|
||||
print(` File downloaded to: ${file_result}`);
|
||||
|
||||
// Check if the file exists and has content
|
||||
let file_exists = exist(text_file_dest);
|
||||
print(` File exists: ${file_exists}`);
|
||||
let file_content = file_read(text_file_dest);
|
||||
let content_check = if file_content.len() > 100 { "yes" } else { "no" };
|
||||
print(` File has content: ${content_check}`);
|
||||
|
||||
//PART 5
|
||||
|
||||
// Test the new chmod_exec function
|
||||
print("\nTesting chmod_exec function:");
|
||||
// Create a simple shell script
|
||||
let script_path = `${download_dir}/test_script.sh`;
|
||||
file_write(script_path, "#!/bin/sh\necho 'Hello from test script'");
|
||||
|
||||
// Make it executable
|
||||
let chmod_result = chmod_exec(script_path);
|
||||
print(` ${chmod_result}`);
|
||||
|
||||
// Clean up test files again
|
||||
delete(download_dir);
|
||||
print("Cleaned up test directory");
|
||||
|
||||
print("\nAll Download Tests completed successfully!");
|
||||
"Download Tests Success"
|
||||
"Download Tests Success"
|
124
examples/_archive/instructions_grpc.md
Normal file
124
examples/_archive/instructions_grpc.md
Normal file
@@ -0,0 +1,124 @@
|
||||
|
||||
|
||||
## ✅ Step-by-step: Connect Rust to containerd via gRPC
|
||||
|
||||
|
||||
CREATE A rhai script executing the following instructions below
|
||||
use all rhai script features we have + give priority to what functions we have defined in /src/docs/docs/sal in this repo
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 1. 🧱 Install Dependencies
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tonic = "0.11"
|
||||
prost = "0.12"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = "0.11"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 📁 Clone containerd's gRPC proto files
|
||||
|
||||
```bash
|
||||
git clone https://github.com/containerd/containerd.git
|
||||
cd containerd
|
||||
```
|
||||
|
||||
Containerd's API protos are in:
|
||||
```
|
||||
api/services/ # gRPC service definitions
|
||||
api/types/ # message types
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 📦 Set up `build.rs` to compile protos
|
||||
|
||||
In your Rust project root, create a `build.rs` file:
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
tonic_build::configure()
|
||||
.build_server(false)
|
||||
.compile(
|
||||
&[
|
||||
"containerd/api/services/images/v1/images.proto",
|
||||
"containerd/api/services/containers/v1/containers.proto",
|
||||
// Add more proto files as needed
|
||||
],
|
||||
&[
|
||||
"containerd/api",
|
||||
"containerd/api/types"
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
Make sure to place the `containerd` directory somewhere your build can see — for example, symlink it or move it into your project as `proto/containerd`.
|
||||
|
||||
---
|
||||
|
||||
### 4. 🧪 Example: Connect to containerd's image service
|
||||
|
||||
After `build.rs` compiles the protos, your code can access them like this:
|
||||
|
||||
```rust
|
||||
use tonic::transport::Channel;
|
||||
use containerd::services::images::v1::{
|
||||
images_client::ImagesClient,
|
||||
GetImageRequest,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Connect to containerd's gRPC socket (default path)
|
||||
let channel = Channel::from_static("http://[::]:50051") // placeholder
|
||||
.connect()
|
||||
.await?;
|
||||
|
||||
let mut client = ImagesClient::new(channel);
|
||||
|
||||
let response = client.get(GetImageRequest {
|
||||
name: "docker.io/library/ubuntu:latest".to_string(),
|
||||
}).await?;
|
||||
|
||||
println!("Image: {:?}", response.into_inner());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
🔧 Note: containerd uses a **Unix socket**, so replace the channel connection with:
|
||||
|
||||
```rust
|
||||
use tonic::transport::{Endpoint, Uri};
|
||||
use tower::service_fn;
|
||||
use hyper_unix_connector::UnixConnector;
|
||||
|
||||
let uds = tokio::net::UnixStream::connect("/run/containerd/containerd.sock").await?;
|
||||
let channel = Endpoint::try_from("http://[::]:50051")?
|
||||
.connect_with_connector(service_fn(move |_| async move {
|
||||
Ok::<_, std::io::Error>(uds)
|
||||
}))
|
||||
.await?;
|
||||
```
|
||||
|
||||
(We can wrap that part into a helper if you want.)
|
||||
|
||||
---
|
||||
|
||||
### 5. 🔁 Rebuild the project
|
||||
|
||||
Each time you add or change a `.proto`, rebuild to regenerate code:
|
||||
|
||||
```bash
|
||||
cargo clean && cargo build
|
||||
```
|
113
examples/_archive/package_management.rhai
Normal file
113
examples/_archive/package_management.rhai
Normal file
@@ -0,0 +1,113 @@
|
||||
// Example script demonstrating the mypackage management functions
|
||||
|
||||
// Set debug mode to true to see detailed output
|
||||
package_set_debug(true);
|
||||
|
||||
// Function to demonstrate mypackage management on Ubuntu
|
||||
fn demo_ubuntu() {
|
||||
print("Demonstrating mypackage management on Ubuntu...");
|
||||
|
||||
// Update mypackage lists
|
||||
print("Updating mypackage lists...");
|
||||
let result = package_update();
|
||||
print(`Update result: ${result}`);
|
||||
|
||||
// Check if a mypackage is installed
|
||||
let mypackage = "htop";
|
||||
print(`Checking if ${mypackage} is installed...`);
|
||||
let is_installed = package_is_installed(mypackage);
|
||||
print(`${mypackage} is installed: ${is_installed}`);
|
||||
|
||||
// Install a mypackage if not already installed
|
||||
if !is_installed {
|
||||
print(`Installing ${mypackage}...`);
|
||||
let install_result = package_install(mypackage);
|
||||
print(`Install result: ${install_result}`);
|
||||
}
|
||||
|
||||
// List installed packages (limited to first 5 for brevity)
|
||||
print("Listing installed packages (first 5)...");
|
||||
let packages = package_list();
|
||||
for i in 0..min(5, packages.len()) {
|
||||
print(` - ${packages[i]}`);
|
||||
}
|
||||
|
||||
// Search for packages
|
||||
let search_term = "editor";
|
||||
print(`Searching for packages with term '${search_term}'...`);
|
||||
let search_results = package_search(search_term);
|
||||
print(`Found ${search_results.len()} packages. First 5 results:`);
|
||||
for i in 0..min(5, search_results.len()) {
|
||||
print(` - ${search_results[i]}`);
|
||||
}
|
||||
|
||||
// Remove the mypackage if we installed it
|
||||
if !is_installed {
|
||||
print(`Removing ${mypackage}...`);
|
||||
let remove_result = package_remove(mypackage);
|
||||
print(`Remove result: ${remove_result}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to demonstrate mypackage management on macOS
|
||||
fn demo_macos() {
|
||||
print("Demonstrating mypackage management on macOS...");
|
||||
|
||||
// Update mypackage lists
|
||||
print("Updating mypackage lists...");
|
||||
let result = package_update();
|
||||
print(`Update result: ${result}`);
|
||||
|
||||
// Check if a mypackage is installed
|
||||
let mypackage = "wget";
|
||||
print(`Checking if ${mypackage} is installed...`);
|
||||
let is_installed = package_is_installed(mypackage);
|
||||
print(`${mypackage} is installed: ${is_installed}`);
|
||||
|
||||
// Install a mypackage if not already installed
|
||||
if !is_installed {
|
||||
print(`Installing ${mypackage}...`);
|
||||
let install_result = package_install(mypackage);
|
||||
print(`Install result: ${install_result}`);
|
||||
}
|
||||
|
||||
// List installed packages (limited to first 5 for brevity)
|
||||
print("Listing installed packages (first 5)...");
|
||||
let packages = package_list();
|
||||
for i in 0..min(5, packages.len()) {
|
||||
print(` - ${packages[i]}`);
|
||||
}
|
||||
|
||||
// Search for packages
|
||||
let search_term = "editor";
|
||||
print(`Searching for packages with term '${search_term}'...`);
|
||||
let search_results = package_search(search_term);
|
||||
print(`Found ${search_results.len()} packages. First 5 results:`);
|
||||
for i in 0..min(5, search_results.len()) {
|
||||
print(` - ${search_results[i]}`);
|
||||
}
|
||||
|
||||
// Remove the mypackage if we installed it
|
||||
if !is_installed {
|
||||
print(`Removing ${mypackage}...`);
|
||||
let remove_result = package_remove(mypackage);
|
||||
print(`Remove result: ${remove_result}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect platform and run the appropriate demo
|
||||
fn main() {
|
||||
// Create a PackHero instance to detect the platform
|
||||
let platform = package_platform();
|
||||
|
||||
if platform == "Ubuntu" {
|
||||
demo_ubuntu();
|
||||
} else if platform == "MacOS" {
|
||||
demo_macos();
|
||||
} else {
|
||||
print(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
main();
|
100
examples/_archive/package_test.rs
Normal file
100
examples/_archive/package_test.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
//! Example of using the package management module
|
||||
//!
|
||||
//! This example demonstrates how to use the package management module
|
||||
//! to install, remove, and manage packages on different platforms.
|
||||
|
||||
use sal::os::package::{PackHero, Platform};
|
||||
|
||||
fn main() {
|
||||
// Create a new PackHero instance
|
||||
let mut hero = PackHero::new();
|
||||
|
||||
// Enable debug output
|
||||
hero.set_debug(true);
|
||||
|
||||
// Detect the platform
|
||||
let platform = hero.platform();
|
||||
println!("Detected platform: {:?}", platform);
|
||||
|
||||
// Only proceed if we're on a supported platform
|
||||
if platform == Platform::Unknown {
|
||||
println!("Unsupported platform. This example only works on Ubuntu and macOS.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Test package to install/check
|
||||
let test_package = if platform == Platform::Ubuntu { "wget" } else { "wget" };
|
||||
|
||||
// Check if the package is installed
|
||||
match hero.is_installed(test_package) {
|
||||
Ok(is_installed) => {
|
||||
println!("Package {} is installed: {}", test_package, is_installed);
|
||||
|
||||
if is_installed {
|
||||
println!("Package {} is already installed", test_package);
|
||||
} else {
|
||||
println!("Package {} is not installed, attempting to install...", test_package);
|
||||
|
||||
// Try to install the package
|
||||
match hero.install(test_package) {
|
||||
Ok(_) => println!("Successfully installed package {}", test_package),
|
||||
Err(e) => println!("Failed to install package {}: {}", test_package, e),
|
||||
}
|
||||
|
||||
// Check if it was installed successfully
|
||||
match hero.is_installed(test_package) {
|
||||
Ok(is_installed_now) => {
|
||||
if is_installed_now {
|
||||
println!("Verified package {} was installed successfully", test_package);
|
||||
} else {
|
||||
println!("Package {} was not installed successfully", test_package);
|
||||
}
|
||||
},
|
||||
Err(e) => println!("Error checking if package is installed: {}", e),
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => println!("Error checking if package is installed: {}", e),
|
||||
}
|
||||
|
||||
// Search for packages
|
||||
let search_term = "wget";
|
||||
println!("Searching for packages with term '{}'...", search_term);
|
||||
match hero.search(search_term) {
|
||||
Ok(results) => {
|
||||
println!("Found {} packages matching '{}'", results.len(), search_term);
|
||||
for (i, package) in results.iter().enumerate().take(5) {
|
||||
println!(" {}. {}", i + 1, package);
|
||||
}
|
||||
if results.len() > 5 {
|
||||
println!(" ... and {} more", results.len() - 5);
|
||||
}
|
||||
},
|
||||
Err(e) => println!("Error searching for packages: {}", e),
|
||||
}
|
||||
|
||||
// List installed packages
|
||||
println!("Listing installed packages...");
|
||||
match hero.list_installed() {
|
||||
Ok(packages) => {
|
||||
println!("Found {} installed packages", packages.len());
|
||||
println!("First 5 installed packages:");
|
||||
for (i, package) in packages.iter().enumerate().take(5) {
|
||||
println!(" {}. {}", i + 1, package);
|
||||
}
|
||||
if packages.len() > 5 {
|
||||
println!(" ... and {} more", packages.len() - 5);
|
||||
}
|
||||
},
|
||||
Err(e) => println!("Error listing installed packages: {}", e),
|
||||
}
|
||||
|
||||
// Update package lists
|
||||
println!("Updating package lists...");
|
||||
match hero.update() {
|
||||
Ok(_) => println!("Successfully updated package lists"),
|
||||
Err(e) => println!("Error updating package lists: {}", e),
|
||||
}
|
||||
|
||||
println!("Package management example completed");
|
||||
}
|
121
examples/_archive/rfs_example.rhai
Normal file
121
examples/_archive/rfs_example.rhai
Normal file
@@ -0,0 +1,121 @@
|
||||
// RFS Example Script
|
||||
// This script demonstrates how to use the RFS wrapper in Rhai
|
||||
|
||||
// Mount a local directory
|
||||
fn mount_local_example() {
|
||||
print("Mounting a local directory...");
|
||||
|
||||
// Create a map for mount options
|
||||
let options = #{
|
||||
"readonly": "true"
|
||||
};
|
||||
|
||||
// Mount the directory
|
||||
let mount = rfs_mount("/source/path", "/target/path", "local", options);
|
||||
|
||||
print(`Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`);
|
||||
|
||||
// List all mounts
|
||||
let mounts = rfs_list_mounts();
|
||||
print(`Number of mounts: ${mounts.len()}`);
|
||||
|
||||
for mount in mounts {
|
||||
print(`Mount ID: ${mount.id}, Source: ${mount.source}, Target: ${mount.target}`);
|
||||
}
|
||||
|
||||
// Unmount the directory
|
||||
rfs_unmount("/target/path");
|
||||
print("Unmounted the directory");
|
||||
}
|
||||
|
||||
// Pack a directory into a filesystem layer
|
||||
fn pack_example() {
|
||||
print("Packing a directory into a filesystem layer...");
|
||||
|
||||
// Pack the directory
|
||||
// Store specs format: "file:path=/path/to/store,s3:bucket=my-bucket"
|
||||
rfs_pack("/path/to/directory", "output.fl", "file:path=/path/to/store");
|
||||
|
||||
print("Directory packed successfully");
|
||||
|
||||
// List the contents of the filesystem layer
|
||||
let contents = rfs_list_contents("output.fl");
|
||||
print("Contents of the filesystem layer:");
|
||||
print(contents);
|
||||
|
||||
// Verify the filesystem layer
|
||||
let is_valid = rfs_verify("output.fl");
|
||||
print(`Is the filesystem layer valid? ${is_valid}`);
|
||||
|
||||
// Unpack the filesystem layer
|
||||
rfs_unpack("output.fl", "/path/to/unpack");
|
||||
print("Filesystem layer unpacked successfully");
|
||||
}
|
||||
|
||||
// SSH mount example
|
||||
fn mount_ssh_example() {
|
||||
print("Mounting a remote directory via SSH...");
|
||||
|
||||
// Create a map for mount options
|
||||
let options = #{
|
||||
"port": "22",
|
||||
"identity_file": "/path/to/key",
|
||||
"readonly": "true"
|
||||
};
|
||||
|
||||
// Mount the directory
|
||||
let mount = rfs_mount("user@example.com:/remote/path", "/local/mount/point", "ssh", options);
|
||||
|
||||
print(`Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`);
|
||||
|
||||
// Get mount info
|
||||
let info = rfs_get_mount_info("/local/mount/point");
|
||||
print(`Mount info: ${info}`);
|
||||
|
||||
// Unmount the directory
|
||||
rfs_unmount("/local/mount/point");
|
||||
print("Unmounted the directory");
|
||||
}
|
||||
|
||||
// S3 mount example
|
||||
fn mount_s3_example() {
|
||||
print("Mounting an S3 bucket...");
|
||||
|
||||
// Create a map for mount options
|
||||
let options = #{
|
||||
"region": "us-east-1",
|
||||
"access_key": "your-access-key",
|
||||
"secret_key": "your-secret-key"
|
||||
};
|
||||
|
||||
// Mount the S3 bucket
|
||||
let mount = rfs_mount("s3://my-bucket", "/mnt/s3", "s3", options);
|
||||
|
||||
print(`Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`);
|
||||
|
||||
// Unmount the S3 bucket
|
||||
rfs_unmount("/mnt/s3");
|
||||
print("Unmounted the S3 bucket");
|
||||
}
|
||||
|
||||
// Unmount all example
|
||||
fn unmount_all_example() {
|
||||
print("Unmounting all filesystems...");
|
||||
|
||||
// Unmount all filesystems
|
||||
rfs_unmount_all();
|
||||
|
||||
print("All filesystems unmounted");
|
||||
}
|
||||
|
||||
// Run the examples
|
||||
// Note: These are commented out to prevent accidental execution
|
||||
// Uncomment the ones you want to run
|
||||
|
||||
// mount_local_example();
|
||||
// pack_example();
|
||||
// mount_ssh_example();
|
||||
// mount_s3_example();
|
||||
// unmount_all_example();
|
||||
|
||||
print("RFS example script completed");
|
36
examples/_archive/stdout_test.rhai
Normal file
36
examples/_archive/stdout_test.rhai
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
|
||||
// Create a bash script to set up the test environment
|
||||
let setup_script = `
|
||||
# Configure git to suppress the default branch name warning
|
||||
git config --global advice.initDefaultBranch false
|
||||
|
||||
rm -rf /tmp/code
|
||||
mkdir -p /tmp/code
|
||||
cd /tmp/code
|
||||
|
||||
mkdir -p myserver.com/myaccount/repogreen
|
||||
mkdir -p myserver.com/myaccount/repored
|
||||
|
||||
cd myserver.com/myaccount/repogreen
|
||||
git init
|
||||
echo 'Initial test file' > test.txt
|
||||
git add test.txt
|
||||
git config --local user.email 'test@example.com'
|
||||
git config --local user.name 'Test User'
|
||||
git commit -m 'Initial commit'
|
||||
|
||||
cd /tmp/code/myserver.com/myaccount/repored
|
||||
git init
|
||||
echo 'Initial test file' > test2.txt
|
||||
git add test2.txt
|
||||
git config --local user.email 'test@example.com'
|
||||
git config --local user.name 'Test User'
|
||||
git commit -m 'Initial commit'
|
||||
|
||||
# now we have 2 repos
|
||||
|
||||
`;
|
||||
|
||||
// Run the setup script
|
||||
let result = run(setup_script);
|
162
examples/_archive/text_tools.rhai
Normal file
162
examples/_archive/text_tools.rhai
Normal file
@@ -0,0 +1,162 @@
|
||||
// text_tools.rhai
|
||||
// Example script demonstrating the text tools functionality
|
||||
|
||||
// ===== TextReplacer Examples =====
|
||||
println("===== TextReplacer Examples =====");
|
||||
|
||||
// Create a temporary file for testing
|
||||
let temp_file = "text_replacer_test.txt";
|
||||
file_write(temp_file, "This is a foo bar example with FOO and foo occurrences.\nAnother line with foo and bar.");
|
||||
|
||||
// Example 1: Simple replacement
|
||||
println("\n--- Example 1: Simple replacement ---");
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo")
|
||||
.replacement("REPLACED")
|
||||
.build();
|
||||
|
||||
let result = replacer.replace("foo bar foo");
|
||||
println(`Result: ${result}`); // Should output: "REPLACED bar REPLACED"
|
||||
|
||||
// Example 2: Multiple replacements in one chain
|
||||
println("\n--- Example 2: Multiple replacements in one chain ---");
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo").replacement("AAA")
|
||||
.pattern("bar").replacement("BBB")
|
||||
.build();
|
||||
|
||||
let result = replacer.replace("foo bar foo baz");
|
||||
println(`Result: ${result}`); // Should output: "AAA BBB AAA baz"
|
||||
|
||||
// Example 3: Case-insensitive regex replacement
|
||||
println("\n--- Example 3: Case-insensitive regex replacement ---");
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo")
|
||||
.replacement("case-insensitive")
|
||||
.regex(true)
|
||||
.case_insensitive(true)
|
||||
.build();
|
||||
|
||||
let result = replacer.replace("FOO foo Foo fOo");
|
||||
println(`Result: ${result}`); // Should output: "case-insensitive case-insensitive case-insensitive case-insensitive"
|
||||
|
||||
// Example 4: File operations
|
||||
println("\n--- Example 4: File operations ---");
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo").replacement("EXAMPLE")
|
||||
.build();
|
||||
|
||||
// Replace and get result as string
|
||||
let file_result = replacer.replace_file(temp_file);
|
||||
println(`File content after replacement:\n${file_result}`);
|
||||
|
||||
// Replace in-place
|
||||
replacer.replace_file_in_place(temp_file);
|
||||
println("File replaced in-place");
|
||||
|
||||
// Replace to a new file
|
||||
let output_file = "text_replacer_output.txt";
|
||||
replacer.replace_file_to(temp_file, output_file);
|
||||
println(`Content written to new file: ${output_file}`);
|
||||
|
||||
// Clean up temporary files
|
||||
delete(temp_file);
|
||||
delete(output_file);
|
||||
|
||||
// ===== TemplateBuilder Examples =====
|
||||
println("\n\n===== TemplateBuilder Examples =====");
|
||||
|
||||
// Create a temporary template file
|
||||
let template_file = "template_test.txt";
|
||||
file_write(template_file, "Hello, {{ name }}! Welcome to {{ place }}.\n{% if show_greeting %}Glad to have you here!{% endif %}\nYour items:\n{% for item in items %} - {{ item }}{% if not loop.last %}\n{% endif %}{% endfor %}\n");
|
||||
|
||||
// Example 1: Simple template rendering
|
||||
println("\n--- Example 1: Simple template rendering ---");
|
||||
let template = template_builder_open(template_file)
|
||||
.add_var("name", "John")
|
||||
.add_var("place", "Rhai")
|
||||
.add_var("show_greeting", true)
|
||||
.add_var("items", ["apple", "banana", "cherry"]);
|
||||
|
||||
let result = template.render();
|
||||
println(`Rendered template:\n${result}`);
|
||||
|
||||
// Example 2: Using a map for variables
|
||||
println("\n--- Example 2: Using a map for variables ---");
|
||||
let vars = #{
|
||||
name: "Alice",
|
||||
place: "Template World"
|
||||
};
|
||||
|
||||
let template = template_builder_open(template_file)
|
||||
.add_vars(vars)
|
||||
.add_var("show_greeting", false)
|
||||
.add_var("items", ["laptop", "phone", "tablet"]);
|
||||
|
||||
let result = template.render();
|
||||
println(`Rendered template with map:\n${result}`);
|
||||
|
||||
// Example 3: Rendering to a file
|
||||
println("\n--- Example 3: Rendering to a file ---");
|
||||
let output_file = "template_output.txt";
|
||||
|
||||
let template = template_builder_open(template_file)
|
||||
.add_var("name", "Bob")
|
||||
.add_var("place", "File Output")
|
||||
.add_var("show_greeting", true)
|
||||
.add_var("items", ["document", "spreadsheet", "presentation"]);
|
||||
|
||||
template.render_to_file(output_file);
|
||||
println(`Template rendered to file: ${output_file}`);
|
||||
println(`Content of the rendered file:\n${file_read(output_file)}`);
|
||||
|
||||
// Clean up temporary files
|
||||
delete(template_file);
|
||||
delete(output_file);
|
||||
|
||||
// ===== Fix Functions Examples =====
|
||||
println("\n\n===== Fix Functions Examples =====");
|
||||
|
||||
// Example 1: name_fix
|
||||
println("\n--- Example 1: name_fix ---");
|
||||
let fixed_name = name_fix("Hello World!");
|
||||
println(`Original: "Hello World!"`);
|
||||
println(`Fixed: "${fixed_name}"`); // Should output: "hello_world"
|
||||
|
||||
let fixed_name = name_fix("File-Name.txt");
|
||||
println(`Original: "File-Name.txt"`);
|
||||
println(`Fixed: "${fixed_name}"`); // Should output: "file_name.txt"
|
||||
|
||||
let fixed_name = name_fix("Résumé.doc");
|
||||
println(`Original: "Résumé.doc"`);
|
||||
println(`Fixed: "${fixed_name}"`); // Should output: "rsum.doc"
|
||||
|
||||
// Example 2: path_fix
|
||||
println("\n--- Example 2: path_fix ---");
|
||||
let fixed_path = path_fix("/path/to/Hello World!");
|
||||
println(`Original: "/path/to/Hello World!"`);
|
||||
println(`Fixed: "${fixed_path}"`); // Should output: "/path/to/hello_world"
|
||||
|
||||
let fixed_path = path_fix("./relative/path/to/DOCUMENT-123.pdf");
|
||||
println(`Original: "./relative/path/to/DOCUMENT-123.pdf"`);
|
||||
println(`Fixed: "${fixed_path}"`); // Should output: "./relative/path/to/document_123.pdf"
|
||||
|
||||
// ===== Dedent Functions Examples =====
|
||||
println("\n\n===== Dedent Functions Examples =====");
|
||||
|
||||
// Example 1: dedent
|
||||
println("\n--- Example 1: dedent ---");
|
||||
let indented_text = " line 1\n line 2\n line 3";
|
||||
println(`Original:\n${indented_text}`);
|
||||
let dedented = dedent(indented_text);
|
||||
println(`Dedented:\n${dedented}`); // Should output: "line 1\nline 2\n line 3"
|
||||
|
||||
// Example 2: prefix
|
||||
println("\n--- Example 2: prefix ---");
|
||||
let text = "line 1\nline 2\nline 3";
|
||||
println(`Original:\n${text}`);
|
||||
let prefixed = prefix(text, " ");
|
||||
println(`Prefixed:\n${prefixed}`); // Should output: " line 1\n line 2\n line 3"
|
||||
|
||||
// Return success message
|
||||
"Text tools example completed successfully!"
|
102
examples/_archive/write_read.rhai
Normal file
102
examples/_archive/write_read.rhai
Normal file
@@ -0,0 +1,102 @@
|
||||
// write_read.rhai
|
||||
// Demonstrates writing content to and reading content from a container
|
||||
// using the write_content and read_content methods
|
||||
|
||||
println("Starting write/read container example...");
|
||||
|
||||
// Define image and container names
|
||||
let base_image = "ubuntu:22.04";
|
||||
let container_name = "write-read-demo";
|
||||
let final_image_name = "write-read-demo:latest";
|
||||
|
||||
println(`Creating container '${container_name}' from base image '${base_image}'...`);
|
||||
|
||||
// Create a new buildah container
|
||||
let builder = bah_new(container_name, base_image);
|
||||
|
||||
// Update package lists
|
||||
println("Updating package lists...");
|
||||
let update_result = builder.run("apt-get update -y");
|
||||
println(`Package update result: ${update_result.success ? "Success" : "Failed"}`);
|
||||
|
||||
// Write a simple text file to the container
|
||||
println("\nWriting content to the container...");
|
||||
let text_content = "This is a test file created using write_content.\nIt supports multiple lines.\n";
|
||||
let write_result = builder.write_content(text_content, "/test.txt");
|
||||
println(`Write result: ${write_result.success ? "Success" : "Failed"}`);
|
||||
|
||||
// Write a simple HTML file to the container
|
||||
println("\nWriting HTML content to the container...");
|
||||
let html_content = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Write Content Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 40px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
h1 {
|
||||
color: #0066cc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello from Buildah!</h1>
|
||||
<p>This HTML file was created using the write_content method.</p>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
let html_write_result = builder.write_content(html_content, "/var/www/html/index.html");
|
||||
println(`HTML write result: ${html_write_result.success ? "Success" : "Failed"}`);
|
||||
|
||||
// Write a simple shell script to the container
|
||||
println("\nWriting shell script to the container...");
|
||||
let script_content = `
|
||||
#!/bin/bash
|
||||
echo "This script was created using write_content"
|
||||
echo "Current directory: $(pwd)"
|
||||
echo "Files in current directory:"
|
||||
ls -la
|
||||
`;
|
||||
let script_write_result = builder.write_content(script_content, "/test.sh");
|
||||
println(`Script write result: ${script_write_result.success ? "Success" : "Failed"}`);
|
||||
|
||||
// Make the script executable
|
||||
builder.run("chmod +x /test.sh");
|
||||
|
||||
// Read back the content we wrote
|
||||
println("\nReading content from the container...");
|
||||
let read_text = builder.read_content("/test.txt");
|
||||
println("Text file content:");
|
||||
println(read_text);
|
||||
|
||||
let read_html = builder.read_content("/var/www/html/index.html");
|
||||
println("\nHTML file content (first 100 characters):");
|
||||
println(read_html.substr(0, 100) + "...");
|
||||
|
||||
let read_script = builder.read_content("/test.sh");
|
||||
println("\nScript file content:");
|
||||
println(read_script);
|
||||
|
||||
// Execute the script we created
|
||||
println("\nExecuting the script we created...");
|
||||
let script_result = builder.run("/test.sh");
|
||||
println("Script output:");
|
||||
println(script_result.stdout);
|
||||
|
||||
// Commit the container to an image
|
||||
println(`\nCommitting container to image '${final_image_name}'...`);
|
||||
let commit_result = builder.commit(final_image_name);
|
||||
println(`Commit result: ${commit_result.success ? "Success" : "Failed"}`);
|
||||
|
||||
// Clean up the buildah container
|
||||
println("Cleaning up buildah container...");
|
||||
builder.remove();
|
||||
|
||||
println("\nWrite/read example completed successfully!");
|
||||
|
||||
"Write/read example completed successfully!"
|
@@ -1,5 +1,3 @@
|
||||
// 05_directory_operations.rhai
|
||||
// Demonstrates directory operations using SAL, including the new chdir function
|
||||
|
||||
// Create a test directory structure
|
||||
let base_dir = "rhai_dir_test";
|
@@ -2,7 +2,7 @@
|
||||
// Demonstrates file system operations using SAL
|
||||
|
||||
// Create a test directory
|
||||
let test_dir = "rhai_test_dir";
|
||||
let test_dir = "/tmp/rhai_test_dir";
|
||||
println(`Creating directory: ${test_dir}`);
|
||||
let mkdir_result = mkdir(test_dir);
|
||||
println(`Directory creation result: ${mkdir_result}`);
|
@@ -1,115 +0,0 @@
|
||||
//! Example usage of the buildah module
|
||||
//!
|
||||
//! This file demonstrates how to use the buildah module to perform
|
||||
//! common container operations like creating containers, running commands,
|
||||
//! and managing images.
|
||||
|
||||
use sal::virt::buildah::{self, BuildahError};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Run a complete buildah workflow example
|
||||
pub fn run_buildah_example() -> Result<(), BuildahError> {
|
||||
println!("Starting buildah example workflow...");
|
||||
|
||||
// Step 1: Create a container from an image
|
||||
println!("\n=== Creating container from fedora:latest ===");
|
||||
let result = buildah::from("fedora:latest")?;
|
||||
let container_id = result.stdout.trim();
|
||||
println!("Created container: {}", container_id);
|
||||
|
||||
// Step 2: Run a command in the container
|
||||
println!("\n=== Installing nginx in container ===");
|
||||
// Use chroot isolation to avoid BPF issues
|
||||
let install_result = buildah::bah_run_with_isolation(container_id, "dnf install -y nginx", "chroot")?;
|
||||
println!("{:#?}", install_result);
|
||||
println!("Installation output: {}", install_result.stdout);
|
||||
|
||||
// Step 3: Copy a file into the container
|
||||
println!("\n=== Copying configuration file to container ===");
|
||||
buildah::bah_copy(container_id, "./example.conf", "/etc/example.conf").unwrap();
|
||||
|
||||
// Step 4: Configure container metadata
|
||||
println!("\n=== Configuring container metadata ===");
|
||||
let mut config_options = HashMap::new();
|
||||
config_options.insert("port".to_string(), "80".to_string());
|
||||
config_options.insert("label".to_string(), "maintainer=example@example.com".to_string());
|
||||
config_options.insert("entrypoint".to_string(), "/usr/sbin/nginx".to_string());
|
||||
buildah::bah_config(container_id, config_options)?;
|
||||
buildah::config(container_id, config_options)?;
|
||||
println!("Container configured");
|
||||
|
||||
// Step 5: Commit the container to create a new image
|
||||
println!("\n=== Committing container to create image ===");
|
||||
let image_name = "my-nginx:latest";
|
||||
buildah::image_commit(container_id, image_name, Some("docker"), true, true)?;
|
||||
println!("Created image: {}", image_name);
|
||||
|
||||
// Step 6: List images to verify our new image exists
|
||||
println!("\n=== Listing images ===");
|
||||
let images = buildah::images()?;
|
||||
println!("Found {} images:", images.len());
|
||||
for image in images {
|
||||
println!(" ID: {}", image.id);
|
||||
println!(" Names: {}", image.names.join(", "));
|
||||
println!(" Size: {}", image.size);
|
||||
println!(" Created: {}", image.created);
|
||||
println!();
|
||||
}
|
||||
|
||||
// // Step 7: Clean up (optional in a real workflow)
|
||||
println!("\n=== Cleaning up ===");
|
||||
buildah::image_remove(image_name).unwrap();
|
||||
|
||||
println!("\nBuildah example workflow completed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Demonstrate how to build an image from a Containerfile/Dockerfile
|
||||
pub fn build_image_example() -> Result<(), BuildahError> {
|
||||
println!("Building an image from a Containerfile...");
|
||||
|
||||
// Use the build function with tag, context directory, and isolation to avoid BPF issues
|
||||
let result = buildah::bah_build(Some("my-app:latest"), ".", "example_Dockerfile", Some("chroot"))?;
|
||||
|
||||
println!("Build output: {}", result.stdout);
|
||||
println!("Image built successfully!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Example of pulling and pushing images
|
||||
pub fn registry_operations_example() -> Result<(), BuildahError> {
|
||||
println!("Demonstrating registry operations...");
|
||||
|
||||
// Pull an image
|
||||
println!("\n=== Pulling an image ===");
|
||||
buildah::image_pull("docker.io/library/alpine:latest", true)?;
|
||||
println!("Image pulled successfully");
|
||||
|
||||
// Tag the image
|
||||
println!("\n=== Tagging the image ===");
|
||||
buildah::image_tag("alpine:latest", "my-alpine:v1.0")?;
|
||||
println!("Image tagged successfully");
|
||||
|
||||
// Push an image (this would typically go to a real registry)
|
||||
// println!("\n=== Pushing an image (example only) ===");
|
||||
// println!("In a real scenario, you would push to a registry with:");
|
||||
// println!("buildah::image_push(\"my-alpine:v1.0\", \"docker://registry.example.com/my-alpine:v1.0\", true)");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Main function to run all examples
|
||||
pub fn run_all_examples() -> Result<(), BuildahError> {
|
||||
println!("=== BUILDAH MODULE EXAMPLES ===\n");
|
||||
|
||||
run_buildah_example()?;
|
||||
build_image_example()?;
|
||||
registry_operations_example()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let _ = run_all_examples().unwrap();
|
||||
}
|
150
examples/containers/buildah.rhai
Normal file
150
examples/containers/buildah.rhai
Normal file
@@ -0,0 +1,150 @@
|
||||
// buildah.rhai
|
||||
// Demonstrates using buildah to create a custom image with golang and nginx,
|
||||
// then using nerdctl to run a container from that image
|
||||
|
||||
println("Starting buildah workflow to create a custom image...");
|
||||
|
||||
// Define image and container names
|
||||
let base_image = "ubuntu:22.04";
|
||||
let container_name = "golang-nginx-container";
|
||||
let final_image_name = "custom-golang-nginx:latest";
|
||||
|
||||
println(`Creating container '${container_name}' from base image '${base_image}'...`);
|
||||
|
||||
// Create a new buildah container using the builder pattern
|
||||
let builder = bah_new(container_name, base_image);
|
||||
|
||||
println("Enabling debug mode...");
|
||||
builder.debug_mode = true;
|
||||
|
||||
// Update package lists and install golang and nginx
|
||||
println("Installing packages (this may take a while)...");
|
||||
|
||||
// Update package lists
|
||||
let update_result = builder.run("apt-get update -y");
|
||||
|
||||
// Install required packages
|
||||
let install_result = builder.run("apt-get install -y golang nginx");
|
||||
|
||||
// Verify installations
|
||||
let go_version = builder.run("go version");
|
||||
println(`Go version: ${go_version.stdout}`);
|
||||
|
||||
let nginx_version = builder.run("nginx -v");
|
||||
println(`Nginx version: ${nginx_version.stderr}`); // nginx outputs version to stderr
|
||||
|
||||
// Create a simple Go web application
|
||||
println("Creating a simple Go web application...");
|
||||
|
||||
// Create a directory for the Go application
|
||||
builder.run("mkdir -p /app");
|
||||
|
||||
// Create a simple Go web server
|
||||
let go_app = `
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Hello from Go running in a custom container!")
|
||||
})
|
||||
|
||||
fmt.Println("Starting server on :8080")
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
||||
`;
|
||||
|
||||
// Write the Go application to a file using the write_content method
|
||||
builder.write_content(go_app, "/app/main.go");
|
||||
|
||||
// Compile the Go application
|
||||
builder.run("cd /app && go build -o server main.go");
|
||||
|
||||
// Configure nginx to proxy to the Go application
|
||||
let nginx_conf = `
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Write the nginx configuration using the write_content method
|
||||
let nginx_conf_result = builder.write_content(nginx_conf, "/etc/nginx/sites-available/default");
|
||||
|
||||
// Create a startup script
|
||||
let startup_script = `
|
||||
#!/bin/bash
|
||||
# Start the Go application in the background
|
||||
cd /app && ./server &
|
||||
# Start nginx in the foreground
|
||||
nginx -g "daemon off;"
|
||||
`;
|
||||
|
||||
// Write the startup script using the write_content method
|
||||
let startup_script_result = builder.write_content(startup_script, "/start.sh");
|
||||
builder.run("chmod +x /start.sh");
|
||||
|
||||
// Set the entrypoint to the startup script
|
||||
println("Setting entrypoint to /start.sh...");
|
||||
builder.set_entrypoint("/start.sh");
|
||||
|
||||
// Read back the startup script to verify it was written correctly
|
||||
let read_script = builder.read_content("/start.sh");
|
||||
println("Startup script content verification:");
|
||||
println(read_script);
|
||||
|
||||
// Commit the container to a new image
|
||||
println(`Committing container to image '${final_image_name}'...`);
|
||||
let commit_result = builder.commit(final_image_name);
|
||||
|
||||
// Clean up the buildah container
|
||||
println("Cleaning up buildah container...");
|
||||
builder.remove();
|
||||
|
||||
// Now use nerdctl to run a container from the new image
|
||||
println("\nStarting container from the new image using nerdctl...");
|
||||
|
||||
// Create a container using the builder pattern
|
||||
// Use localhost/ prefix to ensure nerdctl uses the local image
|
||||
let local_image_name = "localhost/" + final_image_name;
|
||||
println(`Using local image: ${local_image_name}`);
|
||||
|
||||
// Tag the image with the localhost prefix for nerdctl compatibility
|
||||
println(`Tagging image as ${local_image_name}...`);
|
||||
let tag_result = image_tag(final_image_name, local_image_name);
|
||||
|
||||
// Print a command to check if the image exists in buildah
|
||||
println("\nTo verify the image was created with buildah, run:");
|
||||
println("buildah images");
|
||||
|
||||
// Note: If nerdctl cannot find the image, you may need to push it to a registry
|
||||
// println("\nNote: If nerdctl cannot find the image, you may need to push it to a registry:");
|
||||
// println("buildah push localhost/custom-golang-nginx:latest docker://localhost:5000/custom-golang-nginx:latest");
|
||||
// println("nerdctl pull localhost:5000/custom-golang-nginx:latest");
|
||||
|
||||
let container = nerdctl_container_from_image("golang-nginx-demo", local_image_name)
|
||||
.with_detach(true)
|
||||
.with_port("8080:80") // Map port 80 in the container to 8080 on the host
|
||||
.with_restart_policy("unless-stopped")
|
||||
.build();
|
||||
|
||||
// Start the container
|
||||
let start_result = container.start();
|
||||
|
||||
println("\nWorkflow completed successfully!");
|
||||
println("The web server should be running at http://localhost:8080");
|
||||
println("You can check container logs with: nerdctl logs golang-nginx-demo");
|
||||
println("To stop the container: nerdctl stop golang-nginx-demo");
|
||||
println("To remove the container: nerdctl rm golang-nginx-demo");
|
||||
|
||||
"Buildah and nerdctl workflow completed successfully!"
|
39
examples/containers/buildah_debug.rhai
Normal file
39
examples/containers/buildah_debug.rhai
Normal file
@@ -0,0 +1,39 @@
|
||||
// buildah_debug.rhai
|
||||
// Demonstrates using the debug flag on the buildah Builder
|
||||
|
||||
println("Starting buildah debug example...");
|
||||
|
||||
// Define image and container names
|
||||
let base_image = "ubuntu:22.04";
|
||||
let container_name = "debug-test-container";
|
||||
|
||||
println(`Creating container '${container_name}' from base image '${base_image}'...`);
|
||||
|
||||
// Create a new buildah container using the builder pattern
|
||||
let builder = bah_new(container_name, base_image);
|
||||
|
||||
// Enable debug mode
|
||||
println("Enabling debug mode...");
|
||||
builder.debug_mode = true;
|
||||
|
||||
// Run a simple command to see debug output
|
||||
println("Running a command with debug enabled...");
|
||||
let result = builder.run("echo 'Hello from debug mode'");
|
||||
|
||||
// Disable debug mode
|
||||
println("Disabling debug mode...");
|
||||
builder.debug_mode = false;
|
||||
|
||||
// Run another command without debug
|
||||
println("Running a command with debug disabled...");
|
||||
let result2 = builder.run("echo 'Hello without debug'");
|
||||
|
||||
// Enable debug mode again
|
||||
println("Enabling debug mode again...");
|
||||
builder.debug_mode = true;
|
||||
|
||||
// Remove the container with debug enabled
|
||||
println("Removing the container with debug enabled...");
|
||||
builder.remove();
|
||||
|
||||
println("Debug example completed!");
|
44
examples/containers/buildah_run.rhai
Normal file
44
examples/containers/buildah_run.rhai
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
// Now use nerdctl to run a container from the new image
|
||||
println("\nStarting container from the new image using nerdctl...");
|
||||
|
||||
// Create a container using the builder pattern
|
||||
// Use localhost/ prefix to ensure nerdctl uses the local image
|
||||
let local_image_name = "localhost/custom-golang-nginx:latest";
|
||||
println(`Using local image: ${local_image_name}`);
|
||||
|
||||
// Import the image from buildah to nerdctl
|
||||
println("Importing image from buildah to nerdctl...");
|
||||
process_run("buildah", ["push", "custom-golang-nginx:latest", "docker-daemon:localhost/custom-golang-nginx:latest"]);
|
||||
|
||||
let tag_result = nerdctl_image_tag("custom-golang-nginx:latest", local_image_name);
|
||||
|
||||
// Tag the image with the localhost prefix for nerdctl compatibility
|
||||
// println(`Tagging image as ${local_image_name}...`);
|
||||
// let tag_result = bah_image_tag(final_image_name, local_image_name);
|
||||
|
||||
// Print a command to check if the image exists in buildah
|
||||
println("\nTo verify the image was created with buildah, run:");
|
||||
println("buildah images");
|
||||
|
||||
// Note: If nerdctl cannot find the image, you may need to push it to a registry
|
||||
// println("\nNote: If nerdctl cannot find the image, you may need to push it to a registry:");
|
||||
// println("buildah push localhost/custom-golang-nginx:latest docker://localhost:5000/custom-golang-nginx:latest");
|
||||
// println("nerdctl pull localhost:5000/custom-golang-nginx:latest");
|
||||
|
||||
let container = nerdctl_container_from_image("golang-nginx-demo", local_image_name)
|
||||
.with_detach(true)
|
||||
.with_port("8081:80") // Map port 80 in the container to 8080 on the host
|
||||
.with_restart_policy("unless-stopped")
|
||||
.build();
|
||||
|
||||
// Start the container
|
||||
let start_result = container.start();
|
||||
|
||||
println("\nWorkflow completed successfully!");
|
||||
println("The web server should be running at http://localhost:8081");
|
||||
println("You can check container logs with: nerdctl logs golang-nginx-demo");
|
||||
println("To stop the container: nerdctl stop golang-nginx-demo");
|
||||
println("To remove the container: nerdctl rm golang-nginx-demo");
|
||||
|
||||
"Buildah and nerdctl workflow completed successfully!"
|
86
examples/containers/nerdctl_webserver.rhai
Normal file
86
examples/containers/nerdctl_webserver.rhai
Normal file
@@ -0,0 +1,86 @@
|
||||
// 08__web_server.rhai
|
||||
// Demonstrates a complete workflow to set up a web server using
|
||||
// Note: This script requires to be installed and may need root privileges
|
||||
|
||||
println("Starting web server workflow...");
|
||||
|
||||
// Create and use a temporary directory for all files
|
||||
let work_dir = "/tmp/";
|
||||
mkdir(work_dir);
|
||||
chdir(work_dir);
|
||||
println(`Working in directory: ${work_dir}`);
|
||||
|
||||
|
||||
println("\n=== Creating custom nginx configuration ===");
|
||||
let config_content = `
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let config_file = `${work_dir}/custom-nginx.conf`;
|
||||
// Use file_write instead of run command
|
||||
file_write(config_file, config_content);
|
||||
println(`Created custom nginx configuration file at ${config_file}`);
|
||||
|
||||
// Step 3: Create a custom index.html file
|
||||
println("\n=== Creating custom index.html ===");
|
||||
let html_content = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 40px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
h1 {
|
||||
color: #0066cc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello from HeroScript !</h1>
|
||||
<p>This page is served by an Nginx container.</p>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
let html_file = `${work_dir}/index.html`;
|
||||
// Use file_write instead of run command
|
||||
file_write(html_file, html_content);
|
||||
println(`Created custom index.html file at ${html_file}`);
|
||||
|
||||
println("\n=== Creating nginx container ===");
|
||||
let container_name = "nginx-demo";
|
||||
|
||||
let env_map = #{}; // Create an empty map
|
||||
env_map["NGINX_HOST"] = "localhost";
|
||||
env_map["NGINX_PORT"] = "80";
|
||||
env_map["NGINX_WORKER_PROCESSES"] = "auto";
|
||||
|
||||
// Create a container with a rich set of options using batch methods
|
||||
let container = nerdctl_container_from_image(container_name, "nginx:latest")
|
||||
.reset()
|
||||
.with_detach(true)
|
||||
.with_ports(["8080:80"]) // Add multiple ports at once
|
||||
.with_volumes([`${work_dir}:/usr/share/nginx/html`, "/var/log:/var/log/nginx"]) // Mount our work dir
|
||||
.with_envs(env_map) // Add multiple environment variables at once
|
||||
.with_cpu_limit("1.0")
|
||||
.with_memory_limit("512m")
|
||||
.start();
|
||||
|
||||
|
||||
println("\n web server workflow completed successfully!");
|
||||
println("The web server is running at http://localhost:8080");
|
||||
|
||||
"Web server script completed successfully!"
|
28
examples/git/git_basic.rhai
Normal file
28
examples/git/git_basic.rhai
Normal file
@@ -0,0 +1,28 @@
|
||||
// Simplified Git Basic Operations Example
|
||||
|
||||
let git_tree = git_tree_new("/tmp/git"); // Using /tmp/git as base path
|
||||
|
||||
print("--- Git Basic Operations ---");
|
||||
// print(`Base path: ${git_tree.base_path()}`); // base_path() getter would need to be exposed from Rust
|
||||
|
||||
let all_repos = git_tree.list();
|
||||
print(`Listed ${all_repos.len()} repos.`);
|
||||
|
||||
// Find repos starting with "home" (adjust pattern if /tmp/git might contain other "home*" repos)
|
||||
let found_repos = git_tree.find("home*");
|
||||
print(`Found ${found_repos.len()} repos matching "home*".`);
|
||||
for r in found_repos {
|
||||
print(` - Found: ${r.path()}`);
|
||||
}
|
||||
|
||||
print("Getting/Cloning 'https://github.com/freeflowuniverse/home'...");
|
||||
let repo = git_tree.get("https://github.com/freeflowuniverse/home");
|
||||
print(`Repo path: ${repo.path()}`);
|
||||
print(`Has changes: ${repo.has_changes()}`);
|
||||
|
||||
print("Performing pull & reset...");
|
||||
repo.pull().reset();
|
||||
print("Pull and reset complete.");
|
||||
print(`Has changes after pull/reset: ${repo.has_changes()}`);
|
||||
|
||||
print("--- Example Finished ---");
|
76
examples/hero_vault/README.md
Normal file
76
examples/hero_vault/README.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# SAL Vault Examples
|
||||
|
||||
This directory contains examples demonstrating the SAL Vault functionality.
|
||||
|
||||
## Overview
|
||||
|
||||
SAL Vault provides secure key management and cryptographic operations including:
|
||||
|
||||
- Vault creation and management
|
||||
- KeySpace operations (encrypted key-value stores)
|
||||
- Symmetric key generation and operations
|
||||
- Asymmetric key operations (signing and verification)
|
||||
- Secure key derivation from passwords
|
||||
|
||||
## Current Status
|
||||
|
||||
⚠️ **Note**: The vault module is currently being updated to use Lee's implementation.
|
||||
The Rhai scripting integration is temporarily disabled while we adapt the examples
|
||||
to work with the new vault API.
|
||||
|
||||
## Available Operations
|
||||
|
||||
- **Vault Management**: Create and manage vault instances
|
||||
- **KeySpace Operations**: Open encrypted key-value stores within vaults
|
||||
- **Symmetric Encryption**: Generate keys and encrypt/decrypt data
|
||||
- **Asymmetric Operations**: Create keypairs, sign messages, verify signatures
|
||||
|
||||
## Example Files (Legacy - Sameh's Implementation)
|
||||
|
||||
⚠️ **These examples are currently archived and use the previous vault implementation**:
|
||||
|
||||
- `_archive/example.rhai` - Basic example demonstrating key management, signing, and encryption
|
||||
- `_archive/advanced_example.rhai` - Advanced example with error handling and complex operations
|
||||
- `_archive/key_persistence_example.rhai` - Demonstrates creating and saving a key space to disk
|
||||
- `_archive/load_existing_space.rhai` - Shows how to load a previously created key space
|
||||
- `_archive/contract_example.rhai` - Demonstrates smart contract interactions (Ethereum)
|
||||
- `_archive/agung_send_transaction.rhai` - Demonstrates Ethereum transactions on Agung network
|
||||
- `_archive/agung_contract_with_args.rhai` - Shows contract interactions with arguments
|
||||
|
||||
## Current Implementation (Lee's Vault)
|
||||
|
||||
The current vault implementation provides:
|
||||
|
||||
```rust
|
||||
// Create a new vault
|
||||
let vault = Vault::new(&path).await?;
|
||||
|
||||
// Open an encrypted keyspace
|
||||
let keyspace = vault.open_keyspace("my_space", "password").await?;
|
||||
|
||||
// Perform cryptographic operations
|
||||
// (API documentation coming soon)
|
||||
```
|
||||
|
||||
## Migration Status
|
||||
|
||||
- ✅ **Vault Core**: Lee's implementation is active
|
||||
- ✅ **Archive**: Sameh's implementation preserved in `vault/_archive/`
|
||||
- ⏳ **Rhai Integration**: Being developed for Lee's implementation
|
||||
- ⏳ **Examples**: Will be updated to use Lee's API
|
||||
- ❌ **Ethereum Features**: Not available in Lee's implementation
|
||||
|
||||
## Security
|
||||
|
||||
The vault uses:
|
||||
|
||||
- **ChaCha20Poly1305** for symmetric encryption
|
||||
- **Password-based key derivation** for keyspace encryption
|
||||
- **Secure key storage** with proper isolation
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Rhai Integration**: Implement Rhai bindings for Lee's vault
|
||||
2. **New Examples**: Create examples using Lee's simpler API
|
||||
3. **Documentation**: Complete API documentation for Lee's implementation
|
||||
4. **Migration Guide**: Provide guidance for users migrating from Sameh's implementation
|
233
examples/hero_vault/_archive/advanced_example.rhai
Normal file
233
examples/hero_vault/_archive/advanced_example.rhai
Normal file
@@ -0,0 +1,233 @@
|
||||
// Advanced Rhai script example for Hero Vault Cryptography Module
|
||||
// This script demonstrates conditional logic, error handling, and more complex operations
|
||||
|
||||
// Function to create a key space with error handling
|
||||
fn setup_key_space(name, password) {
|
||||
print("Attempting: Create key space: " + name);
|
||||
let result = create_key_space(name, password);
|
||||
|
||||
if result {
|
||||
print("✅ Create key space succeeded!");
|
||||
return true;
|
||||
} else {
|
||||
print("❌ Create key space failed!");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function to create and select a keypair
|
||||
fn setup_keypair(name, password) {
|
||||
print("Attempting: Create keypair: " + name);
|
||||
let result = create_keypair(name, password);
|
||||
|
||||
if result {
|
||||
print("✅ Create keypair succeeded!");
|
||||
|
||||
print("Attempting: Select keypair: " + name);
|
||||
let selected = select_keypair(name);
|
||||
|
||||
if selected {
|
||||
print("✅ Select keypair succeeded!");
|
||||
return true;
|
||||
} else {
|
||||
print("❌ Select keypair failed!");
|
||||
}
|
||||
} else {
|
||||
print("❌ Create keypair failed!");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function to sign multiple messages
|
||||
fn sign_messages(messages) {
|
||||
let signatures = [];
|
||||
|
||||
for message in messages {
|
||||
print("Signing message: " + message);
|
||||
print("Attempting: Sign message");
|
||||
let signature = sign(message);
|
||||
|
||||
if signature != "" {
|
||||
print("✅ Sign message succeeded!");
|
||||
signatures.push(#{
|
||||
message: message,
|
||||
signature: signature
|
||||
});
|
||||
} else {
|
||||
print("❌ Sign message failed!");
|
||||
}
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
// Function to verify signatures
|
||||
fn verify_signatures(signed_messages) {
|
||||
let results = [];
|
||||
|
||||
for item in signed_messages {
|
||||
let message = item.message;
|
||||
let signature = item.signature;
|
||||
|
||||
print("Verifying signature for: " + message);
|
||||
print("Attempting: Verify signature");
|
||||
let is_valid = verify(message, signature);
|
||||
|
||||
if is_valid {
|
||||
print("✅ Verify signature succeeded!");
|
||||
} else {
|
||||
print("❌ Verify signature failed!");
|
||||
}
|
||||
|
||||
results.push(#{
|
||||
message: message,
|
||||
valid: is_valid
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Function to encrypt multiple messages
|
||||
fn encrypt_messages(messages) {
|
||||
// Generate a symmetric key
|
||||
print("Attempting: Generate symmetric key");
|
||||
let key = generate_key();
|
||||
|
||||
if key == "" {
|
||||
print("❌ Generate symmetric key failed!");
|
||||
return [];
|
||||
}
|
||||
|
||||
print("✅ Generate symmetric key succeeded!");
|
||||
print("Using key: " + key);
|
||||
let encrypted_messages = [];
|
||||
|
||||
for message in messages {
|
||||
print("Encrypting message: " + message);
|
||||
print("Attempting: Encrypt message");
|
||||
let encrypted = encrypt(key, message);
|
||||
|
||||
if encrypted != "" {
|
||||
print("✅ Encrypt message succeeded!");
|
||||
encrypted_messages.push(#{
|
||||
original: message,
|
||||
encrypted: encrypted,
|
||||
key: key
|
||||
});
|
||||
} else {
|
||||
print("❌ Encrypt message failed!");
|
||||
}
|
||||
}
|
||||
|
||||
return encrypted_messages;
|
||||
}
|
||||
|
||||
// Function to decrypt messages
|
||||
fn decrypt_messages(encrypted_messages) {
|
||||
let decrypted_messages = [];
|
||||
|
||||
for item in encrypted_messages {
|
||||
let encrypted = item.encrypted;
|
||||
let key = item.key;
|
||||
let original = item.original;
|
||||
|
||||
print("Decrypting message...");
|
||||
print("Attempting: Decrypt message");
|
||||
let decrypted = decrypt(key, encrypted);
|
||||
|
||||
if decrypted != false {
|
||||
let success = decrypted == original;
|
||||
|
||||
decrypted_messages.push(#{
|
||||
decrypted: decrypted,
|
||||
original: original,
|
||||
success: success
|
||||
});
|
||||
|
||||
if success {
|
||||
print("Decryption matched original ✅");
|
||||
} else {
|
||||
print("Decryption did not match original ❌");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return decrypted_messages;
|
||||
}
|
||||
|
||||
// Main script execution
|
||||
print("=== Advanced Cryptography Script ===");
|
||||
|
||||
// Set up key space
|
||||
let space_name = "advanced_space";
|
||||
let password = "secure_password123";
|
||||
|
||||
if setup_key_space(space_name, password) {
|
||||
print("\n--- Key space setup complete ---\n");
|
||||
|
||||
// Set up keypair
|
||||
if setup_keypair("advanced_keypair", password) {
|
||||
print("\n--- Keypair setup complete ---\n");
|
||||
|
||||
// Define messages to sign
|
||||
let messages = [
|
||||
"This is the first message to sign",
|
||||
"Here's another message that needs signing",
|
||||
"And a third message for good measure"
|
||||
];
|
||||
|
||||
// Sign messages
|
||||
print("\n--- Signing Messages ---\n");
|
||||
let signed_messages = sign_messages(messages);
|
||||
|
||||
// Verify signatures
|
||||
print("\n--- Verifying Signatures ---\n");
|
||||
let verification_results = verify_signatures(signed_messages);
|
||||
|
||||
// Count successful verifications
|
||||
let successful_verifications = verification_results.filter(|r| r.valid).len();
|
||||
print("Successfully verified " + successful_verifications + " out of " + verification_results.len() + " signatures");
|
||||
|
||||
// Encrypt messages
|
||||
print("\n--- Encrypting Messages ---\n");
|
||||
let encrypted_messages = encrypt_messages(messages);
|
||||
|
||||
// Decrypt messages
|
||||
print("\n--- Decrypting Messages ---\n");
|
||||
let decryption_results = decrypt_messages(encrypted_messages);
|
||||
|
||||
// Count successful decryptions
|
||||
let successful_decryptions = decryption_results.filter(|r| r.success).len();
|
||||
print("Successfully decrypted " + successful_decryptions + " out of " + decryption_results.len() + " messages");
|
||||
|
||||
// Create Ethereum wallet
|
||||
print("\n--- Creating Ethereum Wallet ---\n");
|
||||
print("Attempting: Create Ethereum wallet");
|
||||
let wallet_created = create_ethereum_wallet();
|
||||
|
||||
if wallet_created {
|
||||
print("✅ Create Ethereum wallet succeeded!");
|
||||
|
||||
print("Attempting: Get Ethereum address");
|
||||
let address = get_ethereum_address();
|
||||
|
||||
if address != "" {
|
||||
print("✅ Get Ethereum address succeeded!");
|
||||
print("Ethereum wallet address: " + address);
|
||||
} else {
|
||||
print("❌ Get Ethereum address failed!");
|
||||
}
|
||||
} else {
|
||||
print("❌ Create Ethereum wallet failed!");
|
||||
}
|
||||
|
||||
print("\n=== Script execution completed successfully! ===");
|
||||
} else {
|
||||
print("Failed to set up keypair. Aborting script.");
|
||||
}
|
||||
} else {
|
||||
print("Failed to set up key space. Aborting script.");
|
||||
}
|
152
examples/hero_vault/_archive/agung_contract_with_args.rhai
Normal file
152
examples/hero_vault/_archive/agung_contract_with_args.rhai
Normal file
@@ -0,0 +1,152 @@
|
||||
// Example Rhai script for testing contract functions with arguments on Agung network
|
||||
// This script demonstrates how to use call_contract_read and call_contract_write with arguments
|
||||
|
||||
// Step 1: Set up wallet and network
|
||||
let space_name = "agung_contract_args_demo";
|
||||
let password = "secure_password123";
|
||||
let private_key = "51c194d20bcd25360a3aa94426b3b60f738007e42f22e1bc97821c65c353e6d2";
|
||||
let network_name = "agung";
|
||||
|
||||
print("=== Testing Contract Functions With Arguments on Agung Network ===\n");
|
||||
|
||||
// Create a key space
|
||||
print("Creating key space: " + space_name);
|
||||
if create_key_space(space_name, password) {
|
||||
print("✓ Key space created successfully");
|
||||
|
||||
// Create a keypair
|
||||
print("\nCreating keypair...");
|
||||
if create_keypair("contract_key", password) {
|
||||
print("✓ Created contract keypair");
|
||||
|
||||
// Create a wallet from the private key for the Agung network
|
||||
print("\nCreating wallet from private key for Agung network...");
|
||||
if create_wallet_from_private_key_for_network(private_key, network_name) {
|
||||
print("✓ Wallet created successfully");
|
||||
|
||||
// Get the wallet address
|
||||
let wallet_address = get_wallet_address_for_network(network_name);
|
||||
print("Wallet address: " + wallet_address);
|
||||
|
||||
// Check wallet balance
|
||||
print("\nChecking wallet balance...");
|
||||
let balance = get_balance(network_name, wallet_address);
|
||||
if balance != "" {
|
||||
print("Wallet balance: " + balance + " wei");
|
||||
|
||||
// Define a simple ERC-20 token contract ABI (partial)
|
||||
let token_abi = `[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [{"name": "", "type": "string"}],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [{"name": "", "type": "string"}],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [{"name": "", "type": "uint8"}],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [{"name": "_owner", "type": "address"}],
|
||||
"name": "balanceOf",
|
||||
"outputs": [{"name": "balance", "type": "uint256"}],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [{"name": "_to", "type": "address"}, {"name": "_value", "type": "uint256"}],
|
||||
"name": "transfer",
|
||||
"outputs": [{"name": "", "type": "bool"}],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
}
|
||||
]`;
|
||||
|
||||
// For this example, we'll use a test token contract on Agung
|
||||
let token_address = "0x7267B587E4416537060C6bF0B06f6Fd421106650";
|
||||
|
||||
print("\nLoading contract ABI...");
|
||||
let contract = load_contract_abi(network_name, token_address, token_abi);
|
||||
|
||||
if contract != "" {
|
||||
print("✓ Contract loaded successfully");
|
||||
|
||||
// First, let's try to read some data from the contract
|
||||
print("\nReading contract data...");
|
||||
|
||||
// Try to get token name (no arguments)
|
||||
let token_name = call_contract_read(contract, "name");
|
||||
print("Token name: " + token_name);
|
||||
|
||||
// Try to get token symbol (no arguments)
|
||||
let token_symbol = call_contract_read(contract, "symbol");
|
||||
print("Token symbol: " + token_symbol);
|
||||
|
||||
// Try to get token decimals (no arguments)
|
||||
let token_decimals = call_contract_read(contract, "decimals");
|
||||
print("Token decimals: " + token_decimals);
|
||||
|
||||
// Try to get token balance (with address argument)
|
||||
print("\nCalling balanceOf with address argument...");
|
||||
let balance = call_contract_read(contract, "balanceOf", [wallet_address]);
|
||||
print("Token balance: " + balance);
|
||||
|
||||
// Now, let's try to execute a write function with arguments
|
||||
print("\nExecuting contract write function with arguments...");
|
||||
|
||||
// Define a recipient address and amount for the transfer
|
||||
// Using a random valid address on the network
|
||||
let recipient = "0xEEdf3468E8F232A7a03D49b674bA44740C8BD8Be";
|
||||
let amount = 1000000; // Changed from string to number for uint256 compatibility
|
||||
|
||||
print("Attempting to transfer " + amount + " tokens to " + recipient);
|
||||
|
||||
// Call the transfer function with arguments
|
||||
let tx_hash = call_contract_write(contract, "transfer", [recipient, amount]);
|
||||
|
||||
if tx_hash != "" {
|
||||
print("✓ Transaction sent successfully");
|
||||
print("Transaction hash: " + tx_hash);
|
||||
print("You can view the transaction at: " + get_network_explorer_url(network_name) + "/tx/" + tx_hash);
|
||||
} else {
|
||||
print("✗ Failed to send transaction");
|
||||
print("This could be due to insufficient funds, contract issues, or other errors.");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to load contract");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to get wallet balance");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to create wallet from private key");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to create keypair");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to create key space");
|
||||
}
|
||||
|
||||
print("\nContract function with arguments test completed");
|
104
examples/hero_vault/_archive/agung_send_transaction.rhai
Normal file
104
examples/hero_vault/_archive/agung_send_transaction.rhai
Normal file
@@ -0,0 +1,104 @@
|
||||
// Script to create an Agung wallet from a private key and send tokens
|
||||
// This script demonstrates how to create a wallet from a private key and send tokens
|
||||
|
||||
// Define the private key and recipient address
|
||||
let private_key = "0x9ecfd58eca522b0e7c109bf945966ee208cd6d593b1dc3378aedfdc60b64f512";
|
||||
let recipient_address = "0xf400f9c3F7317e19523a5DB698Ce67e7a7E083e2";
|
||||
|
||||
print("=== Agung Wallet Transaction Demo ===");
|
||||
print(`From private key: ${private_key}`);
|
||||
print(`To address: ${recipient_address}`);
|
||||
|
||||
// First, create a key space and keypair (required for the wallet infrastructure)
|
||||
let space_name = "agung_transaction_demo";
|
||||
let password = "demo_password";
|
||||
|
||||
// Create a new key space
|
||||
if !create_key_space(space_name, password) {
|
||||
print("Failed to create key space");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a keypair
|
||||
if !create_keypair("demo_keypair", password) {
|
||||
print("Failed to create keypair");
|
||||
return;
|
||||
}
|
||||
|
||||
// Select the keypair
|
||||
if !select_keypair("demo_keypair") {
|
||||
print("Failed to select keypair");
|
||||
return;
|
||||
}
|
||||
|
||||
print("\nCreated and selected keypair successfully");
|
||||
|
||||
// Clear any existing Agung wallets to avoid conflicts
|
||||
if clear_wallets_for_network("agung") {
|
||||
print("Cleared existing Agung wallets");
|
||||
} else {
|
||||
print("Failed to clear existing Agung wallets");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a wallet from the private key directly
|
||||
print("\n=== Creating Wallet from Private Key ===");
|
||||
|
||||
// Create a wallet from the private key for the Agung network
|
||||
if create_wallet_from_private_key_for_network(private_key, "agung") {
|
||||
print("Successfully created wallet from private key for Agung network");
|
||||
|
||||
// Get the wallet address
|
||||
let wallet_address = get_wallet_address_for_network("agung");
|
||||
print(`Wallet address: ${wallet_address}`);
|
||||
|
||||
// Create a provider for the Agung network
|
||||
let provider_id = create_agung_provider();
|
||||
if provider_id != "" {
|
||||
print("Successfully created Agung provider");
|
||||
|
||||
// Check the wallet balance first
|
||||
let wallet_address = get_wallet_address_for_network("agung");
|
||||
let balance_wei = get_balance("agung", wallet_address);
|
||||
|
||||
if balance_wei == "" {
|
||||
print("Failed to get wallet balance");
|
||||
print("This could be due to network issues or other errors.");
|
||||
return;
|
||||
}
|
||||
|
||||
print(`Current wallet balance: ${balance_wei} wei`);
|
||||
|
||||
// Convert 1 AGNG to wei (1 AGNG = 10^18 wei)
|
||||
// Use string representation for large numbers
|
||||
let amount_wei_str = "1000000000000000000"; // 1 AGNG in wei as a string
|
||||
|
||||
// Check if we have enough balance
|
||||
if parse_int(balance_wei) < parse_int(amount_wei_str) {
|
||||
print(`Insufficient balance to send ${amount_wei_str} wei (1 AGNG)`);
|
||||
print(`Current balance: ${balance_wei} wei`);
|
||||
print("Please fund the wallet before attempting to send a transaction");
|
||||
return;
|
||||
}
|
||||
|
||||
print(`Attempting to send ${amount_wei_str} wei (1 AGNG) to ${recipient_address}`);
|
||||
|
||||
// Send the transaction using the blocking implementation
|
||||
let tx_hash = send_eth("agung", recipient_address, amount_wei_str);
|
||||
|
||||
if tx_hash != "" {
|
||||
print(`Transaction sent with hash: ${tx_hash}`);
|
||||
print(`You can view the transaction at: ${get_network_explorer_url("agung")}/tx/${tx_hash}`);
|
||||
} else {
|
||||
print("Transaction failed");
|
||||
print("This could be due to insufficient funds, network issues, or other errors.");
|
||||
print("Check the logs for more details.");
|
||||
}
|
||||
} else {
|
||||
print("Failed to create Agung provider");
|
||||
}
|
||||
} else {
|
||||
print("Failed to create wallet from private key");
|
||||
}
|
||||
|
||||
print("\nAgung transaction demo completed");
|
98
examples/hero_vault/_archive/contract_example.rhai
Normal file
98
examples/hero_vault/_archive/contract_example.rhai
Normal file
@@ -0,0 +1,98 @@
|
||||
// Example Rhai script for interacting with smart contracts using Hero Vault
|
||||
// This script demonstrates loading a contract ABI and interacting with a contract
|
||||
|
||||
// Step 1: Set up wallet and network
|
||||
let space_name = "contract_demo_space";
|
||||
let password = "secure_password123";
|
||||
|
||||
print("Creating key space: " + space_name);
|
||||
if create_key_space(space_name, password) {
|
||||
print("✓ Key space created successfully");
|
||||
|
||||
// Create a keypair
|
||||
print("\nCreating keypair...");
|
||||
if create_keypair("contract_key", password) {
|
||||
print("✓ Created contract keypair");
|
||||
}
|
||||
|
||||
// Step 2: Create an Ethereum wallet for Gnosis Chain
|
||||
print("\nCreating Ethereum wallet...");
|
||||
if create_ethereum_wallet() {
|
||||
print("✓ Ethereum wallet created");
|
||||
|
||||
let address = get_ethereum_address();
|
||||
print("Ethereum address: " + address);
|
||||
|
||||
// Step 3: Define a simple ERC-20 ABI (partial)
|
||||
let erc20_abi = `[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [{"name": "", "type": "string"}],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [{"name": "", "type": "string"}],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [{"name": "", "type": "uint8"}],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [{"name": "owner", "type": "address"}],
|
||||
"name": "balanceOf",
|
||||
"outputs": [{"name": "", "type": "uint256"}],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]`;
|
||||
|
||||
// Step 4: Load the contract ABI
|
||||
print("\nLoading contract ABI...");
|
||||
let contract = load_contract_abi("Gnosis", "0x4ECaBa5870353805a9F068101A40E0f32ed605C6", erc20_abi);
|
||||
if contract != "" {
|
||||
print("✓ Contract loaded successfully");
|
||||
|
||||
// Step 5: Call read-only functions
|
||||
print("\nCalling read-only functions...");
|
||||
|
||||
// Get token name
|
||||
let token_name = call_contract_read(contract, "name");
|
||||
print("Token name: " + token_name);
|
||||
|
||||
// Get token symbol
|
||||
let token_symbol = call_contract_read(contract, "symbol");
|
||||
print("Token symbol: " + token_symbol);
|
||||
|
||||
// Get token decimals
|
||||
let token_decimals = call_contract_read(contract, "decimals");
|
||||
print("Token decimals: " + token_decimals);
|
||||
|
||||
// For now, we're just demonstrating the basic structure
|
||||
} else {
|
||||
print("✗ Failed to load contract");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to create Ethereum wallet");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to create key space");
|
||||
}
|
||||
|
||||
print("\nContract example completed");
|
85
examples/hero_vault/_archive/example.rhai
Normal file
85
examples/hero_vault/_archive/example.rhai
Normal file
@@ -0,0 +1,85 @@
|
||||
// Example Rhai script for Hero Vault Cryptography Module
|
||||
// This script demonstrates key management, signing, and encryption
|
||||
|
||||
// Step 1: Create and manage a key space
|
||||
let space_name = "demo_space";
|
||||
let password = "secure_password123";
|
||||
|
||||
print("Creating key space: " + space_name);
|
||||
if create_key_space(space_name, password) {
|
||||
print("✓ Key space created successfully");
|
||||
|
||||
// Step 2: Create and use keypairs
|
||||
print("\nCreating keypairs...");
|
||||
if create_keypair("signing_key", password) {
|
||||
print("✓ Created signing keypair");
|
||||
}
|
||||
|
||||
if create_keypair("encryption_key", password) {
|
||||
print("✓ Created encryption keypair");
|
||||
}
|
||||
|
||||
// List all keypairs
|
||||
let keypairs = list_keypairs();
|
||||
print("Available keypairs: " + keypairs);
|
||||
|
||||
// Step 3: Sign a message
|
||||
print("\nPerforming signing operations...");
|
||||
if select_keypair("signing_key") {
|
||||
print("✓ Selected signing keypair");
|
||||
|
||||
let message = "This is a secure message that needs to be signed";
|
||||
print("Message: " + message);
|
||||
|
||||
let signature = sign(message);
|
||||
print("Signature: " + signature);
|
||||
|
||||
// Verify the signature
|
||||
let is_valid = verify(message, signature);
|
||||
if is_valid {
|
||||
print("Signature verification: ✓ Valid");
|
||||
} else {
|
||||
print("Signature verification: ✗ Invalid");
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Encrypt and decrypt data
|
||||
print("\nPerforming encryption operations...");
|
||||
|
||||
// Generate a symmetric key
|
||||
let sym_key = generate_key();
|
||||
print("Generated symmetric key: " + sym_key);
|
||||
|
||||
// Encrypt a message
|
||||
let secret = "This is a top secret message that must be encrypted";
|
||||
print("Original message: " + secret);
|
||||
|
||||
let encrypted_data = encrypt(sym_key, secret);
|
||||
print("Encrypted data: " + encrypted_data);
|
||||
|
||||
// Decrypt the message
|
||||
let decrypted_data = decrypt(sym_key, encrypted_data);
|
||||
print("Decrypted message: " + decrypted_data);
|
||||
|
||||
// Verify decryption was successful
|
||||
if decrypted_data == secret {
|
||||
print("✓ Encryption/decryption successful");
|
||||
} else {
|
||||
print("✗ Encryption/decryption failed");
|
||||
}
|
||||
|
||||
// Step 5: Create an Ethereum wallet
|
||||
print("\nCreating Ethereum wallet...");
|
||||
if select_keypair("encryption_key") {
|
||||
print("✓ Selected keypair for Ethereum wallet");
|
||||
|
||||
if create_ethereum_wallet() {
|
||||
print("✓ Ethereum wallet created");
|
||||
|
||||
let address = get_ethereum_address();
|
||||
print("Ethereum address: " + address);
|
||||
}
|
||||
}
|
||||
|
||||
print("\nScript execution completed successfully!");
|
||||
}
|
65
examples/hero_vault/_archive/key_persistence_example.rhai
Normal file
65
examples/hero_vault/_archive/key_persistence_example.rhai
Normal file
@@ -0,0 +1,65 @@
|
||||
// Example Rhai script demonstrating key space persistence for Hero Vault
|
||||
// This script shows how to create, save, and load key spaces
|
||||
|
||||
// Step 1: Create a key space
|
||||
let space_name = "persistent_space";
|
||||
let password = "secure_password123";
|
||||
|
||||
print("Creating key space: " + space_name);
|
||||
if create_key_space(space_name, password) {
|
||||
print("✓ Key space created successfully");
|
||||
|
||||
// Step 2: Create keypairs in this space
|
||||
print("\nCreating keypairs...");
|
||||
if create_keypair("persistent_key1", password) {
|
||||
print("✓ Created first keypair");
|
||||
}
|
||||
|
||||
if create_keypair("persistent_key2", password) {
|
||||
print("✓ Created second keypair");
|
||||
}
|
||||
|
||||
// List all keypairs
|
||||
let keypairs = list_keypairs();
|
||||
print("Available keypairs: " + keypairs);
|
||||
|
||||
// Step 3: Clear the session (simulate closing and reopening the CLI)
|
||||
print("\nClearing session (simulating restart)...");
|
||||
// Note: In a real script, you would exit here and run a new script
|
||||
// For demonstration purposes, we'll continue in the same script
|
||||
|
||||
// Step 4: Load the key space from disk
|
||||
print("\nLoading key space from disk...");
|
||||
if load_key_space(space_name, password) {
|
||||
print("✓ Key space loaded successfully");
|
||||
|
||||
// Verify the keypairs are still available
|
||||
let loaded_keypairs = list_keypairs();
|
||||
print("Keypairs after loading: " + loaded_keypairs);
|
||||
|
||||
// Step 5: Use a keypair from the loaded space
|
||||
print("\nSelecting and using a keypair...");
|
||||
if select_keypair("persistent_key1") {
|
||||
print("✓ Selected keypair");
|
||||
|
||||
let message = "This message was signed using a keypair from a loaded key space";
|
||||
let signature = sign(message);
|
||||
print("Message: " + message);
|
||||
print("Signature: " + signature);
|
||||
|
||||
// Verify the signature
|
||||
let is_valid = verify(message, signature);
|
||||
if is_valid {
|
||||
print("Signature verification: ✓ Valid");
|
||||
} else {
|
||||
print("Signature verification: ✗ Invalid");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to load key space");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to create key space");
|
||||
}
|
||||
|
||||
print("\nScript execution completed!");
|
65
examples/hero_vault/_archive/load_existing_space.rhai
Normal file
65
examples/hero_vault/_archive/load_existing_space.rhai
Normal file
@@ -0,0 +1,65 @@
|
||||
// Example Rhai script demonstrating loading an existing key space for Hero Vault
|
||||
// This script shows how to load a previously created key space and use its keypairs
|
||||
|
||||
// Define the key space name and password
|
||||
let space_name = "persistent_space";
|
||||
let password = "secure_password123";
|
||||
|
||||
print("Loading existing key space: " + space_name);
|
||||
|
||||
// Load the key space from disk
|
||||
if load_key_space(space_name, password) {
|
||||
print("✓ Key space loaded successfully");
|
||||
|
||||
// List available keypairs
|
||||
let keypairs = list_keypairs();
|
||||
print("Available keypairs: " + keypairs);
|
||||
|
||||
// Use both keypairs to sign different messages
|
||||
if select_keypair("persistent_key1") {
|
||||
print("\nUsing persistent_key1:");
|
||||
let message1 = "Message signed with the first keypair";
|
||||
let signature1 = sign(message1);
|
||||
print("Message: " + message1);
|
||||
print("Signature: " + signature1);
|
||||
|
||||
let is_valid1 = verify(message1, signature1);
|
||||
if is_valid1 {
|
||||
print("Verification: ✓ Valid");
|
||||
} else {
|
||||
print("Verification: ✗ Invalid");
|
||||
}
|
||||
}
|
||||
|
||||
if select_keypair("persistent_key2") {
|
||||
print("\nUsing persistent_key2:");
|
||||
let message2 = "Message signed with the second keypair";
|
||||
let signature2 = sign(message2);
|
||||
print("Message: " + message2);
|
||||
print("Signature: " + signature2);
|
||||
|
||||
let is_valid2 = verify(message2, signature2);
|
||||
if is_valid2 {
|
||||
print("Verification: ✓ Valid");
|
||||
} else {
|
||||
print("Verification: ✗ Invalid");
|
||||
}
|
||||
}
|
||||
|
||||
// Create an Ethereum wallet using one of the keypairs
|
||||
print("\nCreating Ethereum wallet from persistent keypair:");
|
||||
if select_keypair("persistent_key1") {
|
||||
if create_ethereum_wallet() {
|
||||
print("✓ Ethereum wallet created");
|
||||
|
||||
let address = get_ethereum_address();
|
||||
print("Ethereum address: " + address);
|
||||
} else {
|
||||
print("✗ Failed to create Ethereum wallet");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to load key space. Make sure you've run key_persistence_example.rhai first.");
|
||||
}
|
||||
|
||||
print("\nScript execution completed!");
|
72
examples/kubernetes/basic_operations.rhai
Normal file
72
examples/kubernetes/basic_operations.rhai
Normal file
@@ -0,0 +1,72 @@
|
||||
//! 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! ===");
|
134
examples/kubernetes/clusters/generic.rs
Normal file
134
examples/kubernetes/clusters/generic.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
//! Generic Application Deployment Example
|
||||
//!
|
||||
//! This example shows how to deploy any containerized application using the
|
||||
//! KubernetesManager convenience methods. This works for any Docker image.
|
||||
|
||||
use sal_kubernetes::KubernetesManager;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create Kubernetes manager
|
||||
let km = KubernetesManager::new("default").await?;
|
||||
|
||||
// Clean up any existing resources first
|
||||
println!("=== Cleaning up existing resources ===");
|
||||
let apps_to_clean = ["web-server", "node-app", "mongodb"];
|
||||
|
||||
for app in &apps_to_clean {
|
||||
match km.deployment_delete(app).await {
|
||||
Ok(_) => println!("✓ Deleted existing deployment: {}", app),
|
||||
Err(_) => println!("✓ No existing deployment to delete: {}", app),
|
||||
}
|
||||
|
||||
match km.service_delete(app).await {
|
||||
Ok(_) => println!("✓ Deleted existing service: {}", app),
|
||||
Err(_) => println!("✓ No existing service to delete: {}", app),
|
||||
}
|
||||
}
|
||||
|
||||
// Example 1: Simple web server deployment
|
||||
println!("\n=== Example 1: Simple Nginx Web Server ===");
|
||||
|
||||
km.deploy_application("web-server", "nginx:latest", 2, 80, None, None)
|
||||
.await?;
|
||||
println!("✅ Nginx web server deployed!");
|
||||
|
||||
// Example 2: Node.js application with labels
|
||||
println!("\n=== Example 2: Node.js Application ===");
|
||||
|
||||
let mut node_labels = HashMap::new();
|
||||
node_labels.insert("app".to_string(), "node-app".to_string());
|
||||
node_labels.insert("tier".to_string(), "backend".to_string());
|
||||
node_labels.insert("environment".to_string(), "production".to_string());
|
||||
|
||||
// Configure Node.js environment variables
|
||||
let mut node_env_vars = HashMap::new();
|
||||
node_env_vars.insert("NODE_ENV".to_string(), "production".to_string());
|
||||
node_env_vars.insert("PORT".to_string(), "3000".to_string());
|
||||
node_env_vars.insert("LOG_LEVEL".to_string(), "info".to_string());
|
||||
node_env_vars.insert("MAX_CONNECTIONS".to_string(), "1000".to_string());
|
||||
|
||||
km.deploy_application(
|
||||
"node-app", // name
|
||||
"node:18-alpine", // image
|
||||
3, // replicas - scale to 3 instances
|
||||
3000, // port
|
||||
Some(node_labels), // labels
|
||||
Some(node_env_vars), // environment variables
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("✅ Node.js application deployed!");
|
||||
|
||||
// Example 3: Database deployment (any database)
|
||||
println!("\n=== Example 3: MongoDB Database ===");
|
||||
|
||||
let mut mongo_labels = HashMap::new();
|
||||
mongo_labels.insert("app".to_string(), "mongodb".to_string());
|
||||
mongo_labels.insert("type".to_string(), "database".to_string());
|
||||
mongo_labels.insert("engine".to_string(), "mongodb".to_string());
|
||||
|
||||
// Configure MongoDB environment variables
|
||||
let mut mongo_env_vars = HashMap::new();
|
||||
mongo_env_vars.insert(
|
||||
"MONGO_INITDB_ROOT_USERNAME".to_string(),
|
||||
"admin".to_string(),
|
||||
);
|
||||
mongo_env_vars.insert(
|
||||
"MONGO_INITDB_ROOT_PASSWORD".to_string(),
|
||||
"mongopassword".to_string(),
|
||||
);
|
||||
mongo_env_vars.insert("MONGO_INITDB_DATABASE".to_string(), "myapp".to_string());
|
||||
|
||||
km.deploy_application(
|
||||
"mongodb", // name
|
||||
"mongo:6.0", // image
|
||||
1, // replicas - single instance for simplicity
|
||||
27017, // port
|
||||
Some(mongo_labels), // labels
|
||||
Some(mongo_env_vars), // environment variables
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("✅ MongoDB deployed!");
|
||||
|
||||
// Check status of all deployments
|
||||
println!("\n=== Checking Deployment Status ===");
|
||||
|
||||
let deployments = km.deployments_list().await?;
|
||||
|
||||
for deployment in &deployments {
|
||||
if let Some(name) = &deployment.metadata.name {
|
||||
let total_replicas = deployment
|
||||
.spec
|
||||
.as_ref()
|
||||
.and_then(|s| s.replicas)
|
||||
.unwrap_or(0);
|
||||
let ready_replicas = deployment
|
||||
.status
|
||||
.as_ref()
|
||||
.and_then(|s| s.ready_replicas)
|
||||
.unwrap_or(0);
|
||||
|
||||
println!(
|
||||
"{}: {}/{} replicas ready",
|
||||
name, ready_replicas, total_replicas
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n🎉 All deployments completed!");
|
||||
println!("\n💡 Key Points:");
|
||||
println!(" • Any Docker image can be deployed using this simple interface");
|
||||
println!(" • Use labels to organize and identify your applications");
|
||||
println!(
|
||||
" • The same method works for databases, web servers, APIs, and any containerized app"
|
||||
);
|
||||
println!(" • For advanced configuration, use the individual KubernetesManager methods");
|
||||
println!(
|
||||
" • Environment variables and resource limits can be added via direct Kubernetes API"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
79
examples/kubernetes/clusters/postgres.rhai
Normal file
79
examples/kubernetes/clusters/postgres.rhai
Normal file
@@ -0,0 +1,79 @@
|
||||
//! PostgreSQL Cluster Deployment Example (Rhai)
|
||||
//!
|
||||
//! This script shows how to deploy a PostgreSQL cluster using Rhai scripting
|
||||
//! with the KubernetesManager convenience methods.
|
||||
|
||||
print("=== PostgreSQL Cluster Deployment ===");
|
||||
|
||||
// Create Kubernetes manager for the database namespace
|
||||
print("Creating Kubernetes manager for 'database' namespace...");
|
||||
let km = kubernetes_manager_new("database");
|
||||
print("✓ Kubernetes manager created");
|
||||
|
||||
// Create the namespace if it doesn't exist
|
||||
print("Creating namespace 'database' if it doesn't exist...");
|
||||
try {
|
||||
create_namespace(km, "database");
|
||||
print("✓ Namespace 'database' created");
|
||||
} catch(e) {
|
||||
if e.to_string().contains("already exists") {
|
||||
print("✓ Namespace 'database' already exists");
|
||||
} else {
|
||||
print("⚠️ Warning: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up any existing resources first
|
||||
print("\nCleaning up any existing PostgreSQL resources...");
|
||||
try {
|
||||
delete_deployment(km, "postgres-cluster");
|
||||
print("✓ Deleted existing deployment");
|
||||
} catch(e) {
|
||||
print("✓ No existing deployment to delete");
|
||||
}
|
||||
|
||||
try {
|
||||
delete_service(km, "postgres-cluster");
|
||||
print("✓ Deleted existing service");
|
||||
} catch(e) {
|
||||
print("✓ No existing service to delete");
|
||||
}
|
||||
|
||||
// Create PostgreSQL cluster using the convenience method
|
||||
print("\nDeploying PostgreSQL cluster...");
|
||||
|
||||
try {
|
||||
// Deploy PostgreSQL using the convenience method
|
||||
let result = deploy_application(km, "postgres-cluster", "postgres:15", 2, 5432, #{
|
||||
"app": "postgres-cluster",
|
||||
"type": "database",
|
||||
"engine": "postgresql"
|
||||
}, #{
|
||||
"POSTGRES_DB": "myapp",
|
||||
"POSTGRES_USER": "postgres",
|
||||
"POSTGRES_PASSWORD": "secretpassword",
|
||||
"PGDATA": "/var/lib/postgresql/data/pgdata"
|
||||
});
|
||||
print("✓ " + result);
|
||||
|
||||
print("\n✅ PostgreSQL cluster deployed successfully!");
|
||||
|
||||
print("\n📋 Connection Information:");
|
||||
print(" Host: postgres-cluster.database.svc.cluster.local");
|
||||
print(" Port: 5432");
|
||||
print(" Database: postgres (default)");
|
||||
print(" Username: postgres (default)");
|
||||
|
||||
print("\n🔧 To connect from another pod:");
|
||||
print(" psql -h postgres-cluster.database.svc.cluster.local -U postgres");
|
||||
|
||||
print("\n💡 Next steps:");
|
||||
print(" • Set POSTGRES_PASSWORD environment variable");
|
||||
print(" • Configure persistent storage");
|
||||
print(" • Set up backup and monitoring");
|
||||
|
||||
} catch(e) {
|
||||
print("❌ Failed to deploy PostgreSQL cluster: " + e);
|
||||
}
|
||||
|
||||
print("\n=== Deployment Complete ===");
|
112
examples/kubernetes/clusters/postgres.rs
Normal file
112
examples/kubernetes/clusters/postgres.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
//! PostgreSQL Cluster Deployment Example
|
||||
//!
|
||||
//! This example shows how to deploy a PostgreSQL cluster using the
|
||||
//! KubernetesManager convenience methods.
|
||||
|
||||
use sal_kubernetes::KubernetesManager;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create Kubernetes manager for the database namespace
|
||||
let km = KubernetesManager::new("database").await?;
|
||||
|
||||
// Create the namespace if it doesn't exist
|
||||
println!("Creating namespace 'database' if it doesn't exist...");
|
||||
match km.namespace_create("database").await {
|
||||
Ok(_) => println!("✓ Namespace 'database' created"),
|
||||
Err(e) => {
|
||||
if e.to_string().contains("already exists") {
|
||||
println!("✓ Namespace 'database' already exists");
|
||||
} else {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up any existing resources first
|
||||
println!("Cleaning up any existing PostgreSQL resources...");
|
||||
match km.deployment_delete("postgres-cluster").await {
|
||||
Ok(_) => println!("✓ Deleted existing deployment"),
|
||||
Err(_) => println!("✓ No existing deployment to delete"),
|
||||
}
|
||||
|
||||
match km.service_delete("postgres-cluster").await {
|
||||
Ok(_) => println!("✓ Deleted existing service"),
|
||||
Err(_) => println!("✓ No existing service to delete"),
|
||||
}
|
||||
|
||||
// Configure PostgreSQL-specific labels
|
||||
let mut labels = HashMap::new();
|
||||
labels.insert("app".to_string(), "postgres-cluster".to_string());
|
||||
labels.insert("type".to_string(), "database".to_string());
|
||||
labels.insert("engine".to_string(), "postgresql".to_string());
|
||||
|
||||
// Configure PostgreSQL environment variables
|
||||
let mut env_vars = HashMap::new();
|
||||
env_vars.insert("POSTGRES_DB".to_string(), "myapp".to_string());
|
||||
env_vars.insert("POSTGRES_USER".to_string(), "postgres".to_string());
|
||||
env_vars.insert(
|
||||
"POSTGRES_PASSWORD".to_string(),
|
||||
"secretpassword".to_string(),
|
||||
);
|
||||
env_vars.insert(
|
||||
"PGDATA".to_string(),
|
||||
"/var/lib/postgresql/data/pgdata".to_string(),
|
||||
);
|
||||
|
||||
// Deploy the PostgreSQL cluster using the convenience method
|
||||
println!("Deploying PostgreSQL cluster...");
|
||||
km.deploy_application(
|
||||
"postgres-cluster", // name
|
||||
"postgres:15", // image
|
||||
2, // replicas (1 master + 1 replica)
|
||||
5432, // port
|
||||
Some(labels), // labels
|
||||
Some(env_vars), // environment variables
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("✅ PostgreSQL cluster deployed successfully!");
|
||||
|
||||
// Check deployment status
|
||||
let deployments = km.deployments_list().await?;
|
||||
let postgres_deployment = deployments
|
||||
.iter()
|
||||
.find(|d| d.metadata.name.as_ref() == Some(&"postgres-cluster".to_string()));
|
||||
|
||||
if let Some(deployment) = postgres_deployment {
|
||||
let total_replicas = deployment
|
||||
.spec
|
||||
.as_ref()
|
||||
.and_then(|s| s.replicas)
|
||||
.unwrap_or(0);
|
||||
let ready_replicas = deployment
|
||||
.status
|
||||
.as_ref()
|
||||
.and_then(|s| s.ready_replicas)
|
||||
.unwrap_or(0);
|
||||
|
||||
println!(
|
||||
"Deployment status: {}/{} replicas ready",
|
||||
ready_replicas, total_replicas
|
||||
);
|
||||
}
|
||||
|
||||
println!("\n📋 Connection Information:");
|
||||
println!(" Host: postgres-cluster.database.svc.cluster.local");
|
||||
println!(" Port: 5432");
|
||||
println!(" Database: postgres (default)");
|
||||
println!(" Username: postgres (default)");
|
||||
println!(" Password: Set POSTGRES_PASSWORD environment variable");
|
||||
|
||||
println!("\n🔧 To connect from another pod:");
|
||||
println!(" psql -h postgres-cluster.database.svc.cluster.local -U postgres");
|
||||
|
||||
println!("\n💡 Next steps:");
|
||||
println!(" • Set environment variables for database credentials");
|
||||
println!(" • Add persistent volume claims for data storage");
|
||||
println!(" • Configure backup and monitoring");
|
||||
|
||||
Ok(())
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user