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/orchestrator-core/Cargo.toml
Normal file
22
crates/orchestrator-core/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "picloud-orchestrator-core"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
picloud-shared.workspace = true
|
||||
picloud-executor-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
|
||||
reqwest.workspace = true
|
||||
61
crates/orchestrator-core/src/client.rs
Normal file
61
crates/orchestrator-core/src/client.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use picloud_executor_core::{Engine, ExecError, ExecRequest, ExecResponse};
|
||||
|
||||
/// The seam between the orchestrator and the executor.
|
||||
///
|
||||
/// Single-node mode plugs in `LocalExecutorClient`, which calls
|
||||
/// `executor-core` in-process. Cluster mode plugs in `RemoteExecutorClient`,
|
||||
/// which forwards over HTTP to an executor node. Everything else in
|
||||
/// orchestrator-core depends only on this trait.
|
||||
#[async_trait]
|
||||
pub trait ExecutorClient: Send + Sync {
|
||||
async fn execute(&self, source: &str, req: ExecRequest) -> Result<ExecResponse, ExecError>;
|
||||
}
|
||||
|
||||
/// In-process executor — wraps `executor-core::Engine` directly.
|
||||
pub struct LocalExecutorClient {
|
||||
engine: Arc<Engine>,
|
||||
}
|
||||
|
||||
impl LocalExecutorClient {
|
||||
#[must_use]
|
||||
pub fn new(engine: Arc<Engine>) -> Self {
|
||||
Self { engine }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutorClient for LocalExecutorClient {
|
||||
async fn execute(&self, source: &str, req: ExecRequest) -> Result<ExecResponse, ExecError> {
|
||||
self.engine.execute(source, req).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Remote executor — forwards to a peer executor node over HTTP.
|
||||
///
|
||||
/// Skeleton only; fleshed out when cluster mode lands.
|
||||
pub struct RemoteExecutorClient {
|
||||
_client: reqwest::Client,
|
||||
_base_url: String,
|
||||
}
|
||||
|
||||
impl RemoteExecutorClient {
|
||||
#[must_use]
|
||||
pub fn new(base_url: impl Into<String>) -> Self {
|
||||
Self {
|
||||
_client: reqwest::Client::new(),
|
||||
_base_url: base_url.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutorClient for RemoteExecutorClient {
|
||||
async fn execute(&self, _source: &str, _req: ExecRequest) -> Result<ExecResponse, ExecError> {
|
||||
Err(ExecError::Runtime(
|
||||
"RemoteExecutorClient not implemented (cluster mode is v1.3+)".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
15
crates/orchestrator-core/src/lib.rs
Normal file
15
crates/orchestrator-core/src/lib.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
//! Per-node event ingress and dispatch.
|
||||
//!
|
||||
//! Owns the data-plane request path:
|
||||
//! inbound event → resolve script → call `ExecutorClient::execute`
|
||||
//!
|
||||
//! Does not import `executor-core` types in its public surface beyond the
|
||||
//! transport DTOs (`ExecRequest`/`ExecResponse`). The `ExecutorClient`
|
||||
//! trait is the seam that lets the orchestrator call executor logic
|
||||
//! in-process (single-node) or over HTTP (cluster).
|
||||
|
||||
pub mod client;
|
||||
pub mod resolver;
|
||||
|
||||
pub use client::{ExecutorClient, LocalExecutorClient, RemoteExecutorClient};
|
||||
pub use resolver::{ResolverError, ScriptResolver};
|
||||
17
crates/orchestrator-core/src/resolver.rs
Normal file
17
crates/orchestrator-core/src/resolver.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use async_trait::async_trait;
|
||||
use picloud_shared::{Script, ScriptId};
|
||||
|
||||
/// How the orchestrator looks up a script before dispatching to the
|
||||
/// executor. In MVP this is backed by a direct Postgres read inside the
|
||||
/// manager-core repository, exposed through this trait so orchestrator-core
|
||||
/// stays DB-agnostic.
|
||||
#[async_trait]
|
||||
pub trait ScriptResolver: Send + Sync {
|
||||
async fn resolve(&self, id: ScriptId) -> Result<Option<Script>, ResolverError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ResolverError {
|
||||
#[error("backend error: {0}")]
|
||||
Backend(String),
|
||||
}
|
||||
Reference in New Issue
Block a user