- Rust 100%
- Add tracing-appender dependency for rolling file logs - Create 'logs' directory inside workspace on startup - Configure tracing subscriber with both console and file layers - Log files rotate daily with format: spore-log-YYYY-MM-DD.log - File logs include file/line info and disable ANSI codes |
||
|---|---|---|
| spore-rs | ||
| README.md | ||
Spore
Spore is a self-updating runtime orchestrator written in Rust. It manages multiple long-running workloads -- both native processes and containers -- with automatic update detection, hot restarts, and a built-in self-update mechanism.
Architecture
Spore runs a single main process that supervises three kinds of subsystems:
- Process runners -- clone a git repository, execute a configured command, poll for upstream commits, and restart the subprocess when code changes are detected.
- Container runners -- pull a container image, start a named container, poll for image updates, and perform a stop-remove-restart cycle when a new image is available.
- Self-update -- monitors its own source repository for new commits, builds the updated binary, signals all runners to stop gracefully, then replaces itself and re-executes.
All subsystems communicate through Tokio mpsc channels. The main loop multiplexes messages from every runner via tokio::select!, logging lifecycle events (started, updated, stopped) and coordinating the shutdown sequence when a self-update is ready.
Component overview
| Module | File | Responsibility |
|---|---|---|
| Main loop | spore-rs/src/main.rs |
Parse config, spawn tasks, route messages, orchestrate shutdown |
| Process runner | spore-rs/src/runner.rs |
Git clone/pull, subprocess spawn, commit-based update detection |
| Container runner | spore-rs/src/container_runner.rs |
Image pull, container lifecycle, image-id-based update detection |
| Self-update | spore-rs/src/self_update.rs |
Repo polling, cargo build, binary swap, re-exec |
Building
Prerequisites: Rust toolchain (edition 2024), Cargo.
cd spore-rs
cargo build --release
The release binary is produced at spore-rs/target/release/spore-rs.
Configuration
Spore is configured entirely through a TOML file passed via -c / --config-file-path.
# Root workspace directory -- all runners and build artifacts live here
workspace_directory = "spore-workspace"
# Self-update settings
self_update_config = {
upstream_url = "https://git.example.com/org/spore.git",
release_branch = "main",
poll_interval_secs = 60,
}
# Process runners (git-backed subprocesses)
[[runners]]
name = "my-service"
upstream_url = "https://git.example.com/org/my-service.git"
upstream_branch = "main"
run_command = "bash start.sh"
poll_interval_secs = 30
kill_timeout_secs = 10
# Container runners
[[container_runners]]
name = "my-container"
image = "registry.example.com/my-container:latest"
container_engine = "docker" # or "podman"
poll_interval_secs = 60
stop_timeout_secs = 30
volumes = ["/host/path:/container/path"]
env = ["KEY=value"]
Configuration reference
Top-level
| Key | Type | Description |
|---|---|---|
workspace_directory |
string |
Root directory for all workspace data (build dirs, runner dirs, state files) |
self_update_config
| Key | Type | Description |
|---|---|---|
upstream_url |
string |
Git URL of the Spore source repository |
release_branch |
string |
Branch to track for updates |
poll_interval_secs |
integer |
How often to check for new commits |
runners (array of tables)
| Key | Type | Description |
|---|---|---|
name |
string |
Unique runner identifier (used for logging and directory naming) |
upstream_url |
string |
Git URL to clone/pull |
upstream_branch |
string |
Branch to check out |
run_command |
string |
Shell command executed inside the runner's execution directory |
poll_interval_secs |
integer |
How often to poll the remote for new commits |
kill_timeout_secs |
integer |
Seconds to wait after SIGTERM before giving up |
container_runners (array of tables)
| Key | Type | Description |
|---|---|---|
name |
string |
Unique runner identifier |
image |
string |
Container image reference (e.g. registry.example.com/img:tag) |
container_engine |
string |
Engine binary name (docker or podman) |
poll_interval_secs |
integer |
How often to pull and check for new images |
stop_timeout_secs |
integer |
Timeout passed to stop -t when stopping the container |
volumes |
array of strings |
Volume mounts in src:dst format (relative paths are resolved against cwd) |
env |
array of strings |
Environment variables in KEY=value format |
Workspace structure
Spore creates and manages the following directory layout inside workspace_directory:
spore-workspace/
├── build/
│ ├── spore/ # Self-update workspace (cloned spore repo)
│ │ └── .last_commit # Tracked commit SHA
│ └── <repo-name>/ # Process runner build directories (cloned repos)
│ └── .last_commit # Tracked commit SHA per runner
└── runners/
└── <runner-name>/ # Per-runner execution directory
└── .last_image_id # Container runner: tracked image ID
build/spore/-- the self-update task clones its source repo here. The built release binary lives atbuild/spore/spore-rs/target/release/spore-rs.build/<repo-name>/-- each process runner clones its upstream repo here. The repo name is derived from theupstream_url. A.last_commitfile tracks the last-known HEAD SHA.runners/<name>/-- each runner (process or container) gets an isolated execution directory. Process runners executerun_commandwith this as the working directory. Container runners store.last_image_idhere for update detection.
Running
# Build
cd spore-rs && cargo build --release
# Run with config
./spore-rs/target/release/spore-rs -c config.toml
# With custom log level
RUST_LOG=debug ./spore-rs/target/release/spore-rs -c config.toml
The config path can be absolute or relative to the current working directory. Spore resolves it to an absolute path and stores it in the SPORE_CONFIG_FILE_PATH environment variable so that the re-executed process after a self-update can find it.
Logging
Spore uses tracing with tracing-subscriber. Set the RUST_LOG environment variable to control verbosity:
RUST_LOG=info-- default, shows lifecycle eventsRUST_LOG=debug-- detailed operational logsRUST_LOG=warn-- warnings and errors onlyRUST_LOG=error-- errors only
Update detection
Process runners
On each poll interval, the runner executes git ls-remote against the configured upstream URL and branch, then compares the remote HEAD SHA to the locally stored .last_commit. When they differ:
- The runner sends SIGTERM to the running subprocess.
- It runs
git pullinside the build directory. - It records the new HEAD SHA in
.last_commit. - It spawns a fresh subprocess with
run_command. - It emits an
Updatedmessage.
If the subprocess exits on its own, the runner automatically restarts it -- unless the exit code is 130 (SIGINT), which triggers a clean shutdown.
Container runners
On each poll interval, the runner executes <engine> pull followed by <engine> inspect to get the current image ID, then compares it to the stored .last_image_id. When they differ:
- It stops the running container (
<engine> stop -t <timeout>). - It removes the container (
<engine> rm). - It starts a new container with the same configuration.
- It re-attaches the log follower.
- It records the new image ID and emits an
Updatedmessage.
If the container exits on its own, the runner performs the same stop-remove-restart cycle. Exit codes 137 (SIGKILL) and 130 (SIGINT) trigger a clean shutdown instead.
Self-update
The self-update task follows a similar commit-based detection pattern but with an additional coordination step:
- Detects a new commit via
git ls-remote. - Pulls the repo and runs
cargo build --release. - Sends
UpdateReadyto the main loop. - The main loop signals all runners (process and container) to shut down and waits for their
Stoppedmessages. - The main loop sends an
Updateacknowledgment back to the self-update task. - The self-update task copies the new binary over the running one and calls
exec()to replace the process.
This ensures that no runner is left running stale code after an update.
Dependencies
| Crate | Purpose |
|---|---|
clap |
CLI argument parsing |
tokio |
Async runtime, process spawning, I/O, channels |
serde + toml |
Configuration deserialization |
tracing + tracing-subscriber |
Structured logging with env-filter |