a container mesh bootloader / updater
Find a file
RolyPolyAgent 01896ad874 Add filesystem logging with daily rotation to spore workspace
- 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
2026-06-22 21:24:46 +00:00
spore-rs Add filesystem logging with daily rotation to spore workspace 2026-06-22 21:24:46 +00:00
README.md Rewrite README with full project documentation 2026-06-19 00:53:32 +00:00

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 at build/spore/spore-rs/target/release/spore-rs.
  • build/<repo-name>/ -- each process runner clones its upstream repo here. The repo name is derived from the upstream_url. A .last_commit file tracks the last-known HEAD SHA.
  • runners/<name>/ -- each runner (process or container) gets an isolated execution directory. Process runners execute run_command with this as the working directory. Container runners store .last_image_id here 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 events
  • RUST_LOG=debug -- detailed operational logs
  • RUST_LOG=warn -- warnings and errors only
  • RUST_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:

  1. The runner sends SIGTERM to the running subprocess.
  2. It runs git pull inside the build directory.
  3. It records the new HEAD SHA in .last_commit.
  4. It spawns a fresh subprocess with run_command.
  5. It emits an Updated message.

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:

  1. It stops the running container (<engine> stop -t <timeout>).
  2. It removes the container (<engine> rm).
  3. It starts a new container with the same configuration.
  4. It re-attaches the log follower.
  5. It records the new image ID and emits an Updated message.

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:

  1. Detects a new commit via git ls-remote.
  2. Pulls the repo and runs cargo build --release.
  3. Sends UpdateReady to the main loop.
  4. The main loop signals all runners (process and container) to shut down and waits for their Stopped messages.
  5. The main loop sends an Update acknowledgment back to the self-update task.
  6. 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