chore: initial scaffold — workspace, docs, blueprint
Sets up the PiCloud monorepo as a Cargo workspace organised around the
three-service architecture (manager / orchestrator / executor), each
backed by a *-core library crate so the same logic powers both the MVP
all-in-one `picloud` binary and the future split-process cluster mode.
* crates/shared, executor-core, orchestrator-core, manager-core
define the library surface and trait seams between the three
services (`ExecutorClient`, `ScriptResolver`, `ScriptRepository`).
* crates/picloud is the MVP entrypoint; serves /healthz on 8080
(override via PICLOUD_BIND).
* crates/picloud-{manager,orchestrator,executor} are skeleton
binaries that keep the crate boundaries honest until cluster
mode is built out in v1.3+.
* docs/git-workflow.md defines the trunk-based workflow:
short-lived branches, Conventional Commits, separate hotfix
flow with mandatory reproduction tests.
* CLAUDE.md captures the working rules for future Claude sessions.
Workspace passes `cargo fmt`, `cargo clippy -D warnings` (with
pedantic enabled), and `cargo test --workspace`. The all-in-one
binary responds on `/healthz` and `/`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
22
crates/manager-core/Cargo.toml
Normal file
22
crates/manager-core/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "picloud-manager-core"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
picloud-shared.workspace = true
|
||||
picloud-orchestrator-core.workspace = true
|
||||
|
||||
async-trait.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
chrono.workspace = true
|
||||
sqlx.workspace = true
|
||||
10
crates/manager-core/src/lib.rs
Normal file
10
crates/manager-core/src/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! Control plane: script storage, scheduling, configuration.
|
||||
//!
|
||||
//! Single-writer to Postgres. The orchestrator may *read* scripts from
|
||||
//! the same DB for now; once we add caching and per-node ingress, the
|
||||
//! manager will publish change events.
|
||||
|
||||
pub mod repo;
|
||||
pub mod scheduler;
|
||||
|
||||
pub use repo::{PostgresScriptRepository, ScriptRepository, ScriptRepositoryError};
|
||||
83
crates/manager-core/src/repo.rs
Normal file
83
crates/manager-core/src/repo.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use async_trait::async_trait;
|
||||
use picloud_orchestrator_core::{ResolverError, ScriptResolver};
|
||||
use picloud_shared::{Script, ScriptId};
|
||||
use sqlx::PgPool;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ScriptRepositoryError {
|
||||
#[error("database error: {0}")]
|
||||
Db(#[from] sqlx::Error),
|
||||
|
||||
#[error("not found: {0}")]
|
||||
NotFound(ScriptId),
|
||||
}
|
||||
|
||||
/// CRUD over the `scripts` table.
|
||||
#[async_trait]
|
||||
pub trait ScriptRepository: Send + Sync {
|
||||
async fn get(&self, id: ScriptId) -> Result<Option<Script>, ScriptRepositoryError>;
|
||||
async fn list(&self) -> Result<Vec<Script>, ScriptRepositoryError>;
|
||||
async fn create(&self, script: &Script) -> Result<(), ScriptRepositoryError>;
|
||||
async fn update(&self, script: &Script) -> Result<(), ScriptRepositoryError>;
|
||||
async fn delete(&self, id: ScriptId) -> Result<(), ScriptRepositoryError>;
|
||||
}
|
||||
|
||||
pub struct PostgresScriptRepository {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl PostgresScriptRepository {
|
||||
#[must_use]
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
// Real query bodies land alongside the first migration. Stubbing the trait
|
||||
// impl so the workspace compiles and the seam is visible.
|
||||
#[async_trait]
|
||||
impl ScriptRepository for PostgresScriptRepository {
|
||||
async fn get(&self, _id: ScriptId) -> Result<Option<Script>, ScriptRepositoryError> {
|
||||
let _ = &self.pool;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn list(&self) -> Result<Vec<Script>, ScriptRepositoryError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn create(&self, _script: &Script) -> Result<(), ScriptRepositoryError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update(&self, _script: &Script) -> Result<(), ScriptRepositoryError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, _id: ScriptId) -> Result<(), ScriptRepositoryError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapts a `ScriptRepository` into the `ScriptResolver` trait the
|
||||
/// orchestrator depends on, so we don't pull the manager into the
|
||||
/// orchestrator's dependency graph.
|
||||
pub struct RepoResolver<R: ScriptRepository> {
|
||||
repo: R,
|
||||
}
|
||||
|
||||
impl<R: ScriptRepository> RepoResolver<R> {
|
||||
pub fn new(repo: R) -> Self {
|
||||
Self { repo }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<R: ScriptRepository> ScriptResolver for RepoResolver<R> {
|
||||
async fn resolve(&self, id: ScriptId) -> Result<Option<Script>, ResolverError> {
|
||||
self.repo
|
||||
.get(id)
|
||||
.await
|
||||
.map_err(|e| ResolverError::Backend(e.to_string()))
|
||||
}
|
||||
}
|
||||
6
crates/manager-core/src/scheduler.rs
Normal file
6
crates/manager-core/src/scheduler.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
//! Cron / scheduled task dispatcher.
|
||||
//!
|
||||
//! Postgres `SELECT ... FOR UPDATE SKIP LOCKED` over a `schedules` table
|
||||
//! gives us strict singleton execution without external coordination.
|
||||
//!
|
||||
//! Skeleton only — wired up in v1.1+ when cron triggers ship.
|
||||
Reference in New Issue
Block a user