mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-07-15 03:22:11 +02:00
Merge remote-tracking branch 'origin/main' into ragnorc/shaping-config-integration
# Conflicts: # crates/omnigraph-cluster/src/lib.rs # crates/omnigraph-cluster/src/serve.rs # crates/omnigraph-server/src/lib.rs # crates/omnigraph-server/src/settings.rs # docs/user/clusters/config.md
This commit is contained in:
commit
4f8c71fa23
75 changed files with 6557 additions and 6879 deletions
|
|
@ -1,11 +1,10 @@
|
|||
pub mod api;
|
||||
mod handlers;
|
||||
mod settings;
|
||||
pub use settings::{load_server_settings, classify_server_runtime_state, server_config_is_multi, ServerRuntimeState};
|
||||
pub use settings::{load_server_settings, classify_server_runtime_state, ServerRuntimeState};
|
||||
use settings::*;
|
||||
use handlers::*;
|
||||
pub mod auth;
|
||||
pub mod config;
|
||||
pub mod graph_id;
|
||||
pub mod identity;
|
||||
pub mod policy;
|
||||
|
|
@ -46,11 +45,6 @@ use axum::response::{IntoResponse, Response};
|
|||
use axum::routing::{delete, get, post};
|
||||
use axum::{Json, Router};
|
||||
use color_eyre::eyre::{Result, WrapErr, bail, eyre};
|
||||
pub use config::{
|
||||
AliasCommand, AliasConfig, CliDefaults, DEFAULT_CONFIG_FILE, OmnigraphConfig, PolicySettings,
|
||||
ProjectConfig, QueryDefaults, ReadOutputFormat, ServerDefaults, TableCellLayout, TargetConfig,
|
||||
graph_resource_id_for_selection, load_config,
|
||||
};
|
||||
use futures::stream;
|
||||
use omnigraph::db::{Omnigraph, ReadTarget};
|
||||
use omnigraph::error::{ManifestConflictDetails, ManifestErrorKind, OmniError};
|
||||
|
|
@ -122,6 +116,20 @@ fn hash_bearer_token(token: &str) -> BearerTokenHash {
|
|||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
/// The canonical served OpenAPI shape (RFC-011 cluster-only): the static
|
||||
/// `ApiDoc` with every protected path nested under `/graphs/{graph_id}/…`
|
||||
/// and `cluster_`-prefixed operation ids. `/healthz` and `/graphs` stay
|
||||
/// flat. This is the single source of nesting — both the runtime
|
||||
/// `server_openapi` handler and the committed `openapi.json` derive from
|
||||
/// it, so the published spec can never describe routes the server does
|
||||
/// not serve. The handler additionally strips security in open mode; the
|
||||
/// committed spec retains it.
|
||||
pub fn served_openapi() -> utoipa::openapi::OpenApi {
|
||||
let mut doc = ApiDoc::openapi();
|
||||
handlers::nest_paths_under_cluster_prefix(&mut doc);
|
||||
doc
|
||||
}
|
||||
|
||||
struct SecurityAddon;
|
||||
|
||||
impl utoipa::Modify for SecurityAddon {
|
||||
|
|
@ -143,11 +151,10 @@ const SERVER_SOURCE_VERSION: Option<&str> = option_env!("OMNIGRAPH_SOURCE_VERSIO
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServerConfig {
|
||||
/// Server topology + the graphs to open at startup. Single-mode
|
||||
/// invocations (`omnigraph-server <URI>` or `--target <name>`)
|
||||
/// produce `ServerConfigMode::Single`; multi-mode invocations
|
||||
/// (`--config omnigraph.yaml` with a non-empty `graphs:` map and
|
||||
/// no single-mode selector) produce `ServerConfigMode::Multi`.
|
||||
/// Server topology + the graphs to open at startup. RFC-011
|
||||
/// cluster-only: the server always boots from a cluster
|
||||
/// (`--cluster <dir | s3://…>`) and serves N graphs under cluster
|
||||
/// routes.
|
||||
pub mode: ServerConfigMode,
|
||||
pub bind: String,
|
||||
/// Operator opt-in for fully-unauthenticated dev mode (MR-723).
|
||||
|
|
@ -161,49 +168,33 @@ pub struct ServerConfig {
|
|||
pub allow_unauthenticated: bool,
|
||||
}
|
||||
|
||||
/// What `load_server_settings` produces after applying the four-rule
|
||||
/// mode inference matrix (MR-668 decision 2).
|
||||
/// What `load_server_settings` produces. RFC-011 cluster-only: the
|
||||
/// server always boots from a cluster's applied revision into a
|
||||
/// multi-graph deployment (N ≥ 1 graphs).
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ServerConfigMode {
|
||||
/// Legacy invocation — one graph at the given URI. Either:
|
||||
/// * `omnigraph-server <URI>` (CLI positional), or
|
||||
/// * `omnigraph-server --target <name> --config omnigraph.yaml`, or
|
||||
/// * `omnigraph-server --config omnigraph.yaml` with `server.graph`
|
||||
/// set to a named target.
|
||||
Single {
|
||||
uri: String,
|
||||
/// Cedar graph resource id for the single graph. A named selection
|
||||
/// uses the graph name; an anonymous URI uses the normalized URI to
|
||||
/// preserve legacy single-graph policy identity.
|
||||
graph_id: String,
|
||||
/// Top-level `policy.file` (single-graph Cedar policy).
|
||||
policy_file: Option<PathBuf>,
|
||||
/// Top-level stored-query registry, loaded and identity-checked
|
||||
/// at settings-build time; type-checked against the schema when
|
||||
/// the engine opens.
|
||||
queries: QueryRegistry,
|
||||
},
|
||||
/// Multi-graph invocation — `--config omnigraph.yaml` with a
|
||||
/// non-empty `graphs:` map and no single-mode selector.
|
||||
/// Cluster boot — `--cluster <dir | s3://…>` resolves the applied
|
||||
/// revision into per-graph startup configs plus an optional
|
||||
/// server-level policy.
|
||||
Multi {
|
||||
/// Per-graph startup configs, sorted by graph id (BTreeMap
|
||||
/// iteration order). The parallel-open loop iterates this.
|
||||
graphs: Vec<GraphStartupConfig>,
|
||||
/// Path to the config file the server was started from. Kept on
|
||||
/// the mode so future runtime mutation (deferred — see release
|
||||
/// notes) can locate the source of truth without re-parsing CLI
|
||||
/// args.
|
||||
/// The cluster boot source (config directory or storage root).
|
||||
/// Kept on the mode so future runtime mutation (deferred — see
|
||||
/// release notes) can locate the source of truth without
|
||||
/// re-parsing CLI args.
|
||||
config_path: PathBuf,
|
||||
/// `server.policy.file` (server-level Cedar policy for the
|
||||
/// management endpoints). Wired into `GET /graphs` authorization.
|
||||
/// Server-level Cedar policy for the management endpoints
|
||||
/// (`GET /graphs`). Wired into `GET /graphs` authorization.
|
||||
server_policy: Option<PolicySource>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Where a Cedar policy bundle comes from at startup. File-based for
|
||||
/// omnigraph.yaml deployments; inline (digest-verified catalog content)
|
||||
/// for cluster-mode boots, where the catalog may live on object storage
|
||||
/// and the server must not re-read mutable state after the snapshot.
|
||||
/// Where a Cedar policy bundle comes from at startup. Cluster-local files are
|
||||
/// used during config application; inline digest-verified catalog content is
|
||||
/// used for serving, where the catalog may live on object storage and the
|
||||
/// server must not re-read mutable state after the snapshot.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PolicySource {
|
||||
File(PathBuf),
|
||||
|
|
@ -227,36 +218,25 @@ pub struct GraphStartupConfig {
|
|||
pub queries: QueryRegistry,
|
||||
}
|
||||
|
||||
/// Runtime routing for the server. Single mode = legacy
|
||||
/// `omnigraph-server <URI>` invocation, one graph, flat HTTP routes.
|
||||
/// Multi mode = `--config omnigraph.yaml` with a non-empty `graphs:`
|
||||
/// map, N graphs, cluster routes (`/graphs/{graph_id}/...`). Mode is
|
||||
/// determined at startup by `load_server_settings`.
|
||||
/// Runtime routing for the server (RFC-011 cluster-only). Every
|
||||
/// deployment serves cluster routes (`/graphs/{graph_id}/...`) backed by
|
||||
/// a registry of N graphs (N ≥ 1). The single-graph convenience
|
||||
/// constructors build a one-graph registry keyed by `default`; the
|
||||
/// cluster boot path builds an N-graph registry. There is no longer a
|
||||
/// flat-route mode.
|
||||
///
|
||||
/// In single mode the handle lives here directly — there is no
|
||||
/// registry, no sentinel key, no walk-and-assert. In multi mode the
|
||||
/// registry carries N handles and the middleware dispatches on the
|
||||
/// URL's `{graph_id}` segment.
|
||||
/// `config_path` is the boot source (the cluster directory or storage
|
||||
/// root); preserved here so future runtime mutation (deferred) can find
|
||||
/// the source of truth without re-parsing CLI args. The server treats
|
||||
/// the source as operator-owned and never writes it.
|
||||
///
|
||||
/// Both modes share the same handler bodies — the routing middleware
|
||||
/// All handler bodies are mode-agnostic — the routing middleware
|
||||
/// (`resolve_graph_handle`) injects `Arc<GraphHandle>` as a request
|
||||
/// extension so handlers never see the routing discriminator.
|
||||
/// extension by looking up the `{graph_id}` URL segment in the registry.
|
||||
#[derive(Clone)]
|
||||
pub enum GraphRouting {
|
||||
/// Single-graph deployment: one handle, flat routes (`/snapshot`,
|
||||
/// `/read`, …). The `handle.uri` field carries the URI the engine
|
||||
/// was opened from. Backward compatible with v0.6.0 deployments.
|
||||
Single { handle: Arc<GraphHandle> },
|
||||
/// Multi-graph deployment: many handles, cluster routes
|
||||
/// (`/graphs/{graph_id}/...`). `config_path` is the `omnigraph.yaml`
|
||||
/// the server reads at startup; preserved here so future runtime
|
||||
/// mutation (deferred) can find the source of truth without
|
||||
/// re-parsing CLI args. The server treats the file as
|
||||
/// operator-owned and never writes it.
|
||||
Multi {
|
||||
registry: Arc<GraphRegistry>,
|
||||
config_path: Option<PathBuf>,
|
||||
},
|
||||
pub struct GraphRouting {
|
||||
pub registry: Arc<GraphRegistry>,
|
||||
pub config_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -272,12 +252,10 @@ pub struct AppState {
|
|||
/// see MR-668 decision Q6.
|
||||
workload: Arc<workload::WorkloadController>,
|
||||
bearer_tokens: Arc<[(BearerTokenHash, Arc<str>)]>,
|
||||
/// Server-level Cedar policy. Used by management endpoints (`POST
|
||||
/// /graphs`, `GET /graphs`) which act on the registry resource,
|
||||
/// not on a per-graph resource. Loaded from `server.policy.file`
|
||||
/// in `omnigraph.yaml`. `None` outside multi mode and when no
|
||||
/// server policy is configured. Per-graph policies live on each
|
||||
/// `GraphHandle.policy`.
|
||||
/// Server-level Cedar policy. Used by management endpoints (`GET
|
||||
/// /graphs`) which act on the registry resource, not on a per-graph
|
||||
/// resource. Loaded from the cluster-scoped policy binding when
|
||||
/// configured. Per-graph policies live on each `GraphHandle.policy`.
|
||||
server_policy: Option<Arc<PolicyEngine>>,
|
||||
}
|
||||
|
||||
|
|
@ -502,11 +480,13 @@ impl AppState {
|
|||
))
|
||||
}
|
||||
|
||||
/// Single-mode shared construction: wraps the bare engine + per-graph
|
||||
/// policy in a `GraphHandle` carried directly by `GraphRouting::Single`.
|
||||
/// Per-graph policy enforcement on the engine (MR-722) is re-applied
|
||||
/// via `Omnigraph::with_policy` so HTTP and engine layers can never
|
||||
/// diverge.
|
||||
/// Single-graph convenience construction (RFC-011 cluster-only):
|
||||
/// wraps the bare engine + per-graph policy in a `GraphHandle` keyed
|
||||
/// by `default`, then builds a one-graph registry so the deployment
|
||||
/// serves the same `/graphs/{graph_id}/...` cluster routes as any
|
||||
/// other. Per-graph policy enforcement on the engine (MR-722) is
|
||||
/// re-applied via `Omnigraph::with_policy` so HTTP and engine layers
|
||||
/// can never diverge.
|
||||
fn build_single_mode(
|
||||
uri: String,
|
||||
db: Omnigraph,
|
||||
|
|
@ -525,18 +505,13 @@ impl AppState {
|
|||
} else {
|
||||
db
|
||||
};
|
||||
// `GraphHandle.key` is required by the struct, but in single
|
||||
// mode it is never a registry key (there's no registry) and
|
||||
// never compared against user input (routes are flat, no
|
||||
// `{graph_id}` parameter). The label appears only in tracing
|
||||
// output from `resolve_graph_handle`. The literal below is a
|
||||
// log label, not a routing key — when the future cluster
|
||||
// catalog ships, single mode may carry the catalog-assigned
|
||||
// id here instead.
|
||||
// The convenience constructors address the single graph by the
|
||||
// reserved id `default` — both the registry key and the URL
|
||||
// segment (`/graphs/default/...`).
|
||||
let uri = normalize_root_uri(&uri).unwrap_or(uri);
|
||||
let key = GraphKey::cluster(
|
||||
GraphId::try_from("default").expect("'default' is a valid GraphId log label"),
|
||||
);
|
||||
let graph_id =
|
||||
GraphId::try_from("default").expect("'default' is a valid GraphId");
|
||||
let key = GraphKey::cluster(graph_id);
|
||||
let handle = Arc::new(GraphHandle {
|
||||
key,
|
||||
uri,
|
||||
|
|
@ -544,8 +519,15 @@ impl AppState {
|
|||
policy: policy_engine,
|
||||
queries,
|
||||
});
|
||||
let registry = Arc::new(
|
||||
GraphRegistry::from_handles(vec![handle])
|
||||
.expect("a single handle never collides on graph id"),
|
||||
);
|
||||
Self {
|
||||
routing: GraphRouting::Single { handle },
|
||||
routing: GraphRouting {
|
||||
registry,
|
||||
config_path: None,
|
||||
},
|
||||
workload,
|
||||
bearer_tokens,
|
||||
server_policy: None,
|
||||
|
|
@ -553,12 +535,11 @@ impl AppState {
|
|||
}
|
||||
|
||||
/// Multi-mode constructor — used by the startup loop. Operators
|
||||
/// reach this by invoking `omnigraph-server --config omnigraph.yaml`
|
||||
/// with a non-empty `graphs:` map.
|
||||
/// reach this by invoking `omnigraph-server --cluster <dir|s3://...>`.
|
||||
///
|
||||
/// Caller supplies the already-opened `GraphHandle`s and (optionally)
|
||||
/// the path to the source config file. `server_policy` is loaded
|
||||
/// from `server.policy.file` if configured.
|
||||
/// the path to the source cluster. `server_policy` is loaded from the
|
||||
/// cluster-scoped policy binding if configured.
|
||||
pub fn new_multi(
|
||||
handles: Vec<Arc<GraphHandle>>,
|
||||
bearer_tokens: Vec<(String, String)>,
|
||||
|
|
@ -569,7 +550,7 @@ impl AppState {
|
|||
let bearer_tokens = hash_bearer_tokens(bearer_tokens);
|
||||
let registry = Arc::new(GraphRegistry::from_handles(handles)?);
|
||||
Ok(Self {
|
||||
routing: GraphRouting::Multi {
|
||||
routing: GraphRouting {
|
||||
registry,
|
||||
config_path,
|
||||
},
|
||||
|
|
@ -581,9 +562,7 @@ impl AppState {
|
|||
|
||||
/// Runtime routing accessor. Handlers don't typically inspect this —
|
||||
/// they extract `Arc<GraphHandle>` via the routing middleware — but
|
||||
/// `build_app` matches on it to decide flat vs nested route
|
||||
/// mounting, and a handful of management endpoints (`GET /graphs`,
|
||||
/// the OpenAPI cluster rewrite) match on the discriminant.
|
||||
/// `server_graphs_list` reads the registry through it.
|
||||
pub fn routing(&self) -> &GraphRouting {
|
||||
&self.routing
|
||||
}
|
||||
|
|
@ -597,13 +576,9 @@ impl AppState {
|
|||
}
|
||||
// Any per-graph policy also requires auth — otherwise the
|
||||
// policy gate would receive unauthenticated requests. Reading
|
||||
// from `routing` is O(1) in both arms: single mode is a direct
|
||||
// `handle.policy.is_some()` check, multi mode reads the
|
||||
// cached `any_per_graph_policy` flag on the registry snapshot.
|
||||
match &self.routing {
|
||||
GraphRouting::Single { handle } => handle.policy.is_some(),
|
||||
GraphRouting::Multi { registry, .. } => registry.snapshot_ref().any_per_graph_policy,
|
||||
}
|
||||
// the cached `any_per_graph_policy` flag off the registry
|
||||
// snapshot is O(1).
|
||||
self.routing.registry.snapshot_ref().any_per_graph_policy
|
||||
}
|
||||
|
||||
fn authenticate_bearer_token(&self, provided_token: &str) -> Option<ResolvedActor> {
|
||||
|
|
@ -898,18 +873,6 @@ fn validate_and_attach(
|
|||
})
|
||||
}
|
||||
|
||||
/// Format every load error (parse / identity failure) into a multi-line
|
||||
/// boot-abort message.
|
||||
fn format_registry_load_errors(label: &str, errors: &[queries::LoadError]) -> String {
|
||||
let joined = errors
|
||||
.iter()
|
||||
.map(|e| e.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n ");
|
||||
format!("graph '{label}': stored-query registry failed to load:\n {joined}")
|
||||
}
|
||||
|
||||
|
||||
pub fn build_app(state: AppState) -> Router {
|
||||
// The per-graph protected routes, identical in single + multi mode.
|
||||
// Two middleware layers wrap them (outer first, inner last):
|
||||
|
|
@ -975,13 +938,9 @@ pub fn build_app(state: AppState) -> Router {
|
|||
// Management endpoints (`GET /graphs`) live alongside the per-graph
|
||||
// router. They go through bearer auth but NOT through
|
||||
// `resolve_graph_handle` — they operate on the registry directly.
|
||||
// The endpoint is mounted in both modes; in single mode the handler
|
||||
// returns 405 so clients see "resource exists, wrong context"
|
||||
// rather than 404 "no such resource."
|
||||
//
|
||||
// Runtime add/remove (`POST /graphs`, `DELETE /graphs/{id}`) is not
|
||||
// exposed in v0.6.0 — operators add graphs by editing
|
||||
// `omnigraph.yaml` and restarting.
|
||||
// exposed — operators run `cluster apply` and restart.
|
||||
let management = Router::new()
|
||||
.route("/graphs", get(server_graphs_list))
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
|
|
@ -989,15 +948,11 @@ pub fn build_app(state: AppState) -> Router {
|
|||
require_bearer_auth,
|
||||
));
|
||||
|
||||
// Mount the protected routes differently per mode:
|
||||
// * Single → flat routes (legacy: `/snapshot`, `/read`, etc.)
|
||||
// * Multi → nested under `/graphs/{graph_id}/...`
|
||||
let protected: Router<AppState> = match state.routing() {
|
||||
GraphRouting::Single { .. } => per_graph_protected.merge(management),
|
||||
GraphRouting::Multi { .. } => Router::new()
|
||||
.nest("/graphs/{graph_id}", per_graph_protected)
|
||||
.merge(management),
|
||||
};
|
||||
// RFC-011 cluster-only: per-graph routes always nest under
|
||||
// `/graphs/{graph_id}/...`; there are no flat single-graph routes.
|
||||
let protected: Router<AppState> = Router::new()
|
||||
.nest("/graphs/{graph_id}", per_graph_protected)
|
||||
.merge(management);
|
||||
|
||||
Router::new()
|
||||
.route("/healthz", get(server_health))
|
||||
|
|
@ -1018,7 +973,6 @@ pub async fn serve(config: ServerConfig) -> Result<()> {
|
|||
// policy OR any per-graph policy file. Mirrors the
|
||||
// `requires_bearer_auth` semantics on AppState.
|
||||
let has_policy_configured = match &config.mode {
|
||||
ServerConfigMode::Single { policy_file, .. } => policy_file.is_some(),
|
||||
ServerConfigMode::Multi {
|
||||
graphs,
|
||||
server_policy,
|
||||
|
|
@ -1039,36 +993,14 @@ pub async fn serve(config: ServerConfig) -> Result<()> {
|
|||
ServerRuntimeState::DefaultDeny => warn!(
|
||||
"bearer tokens are configured but no policy file is set — running in \
|
||||
default-deny mode (only `read` actions are permitted for authenticated \
|
||||
actors). Configure `policy.file` in omnigraph.yaml to enable Cedar rules."
|
||||
actors). Configure a graph or cluster policy bundle in the cluster config, \
|
||||
run `omnigraph cluster apply`, and restart to enable Cedar rules."
|
||||
),
|
||||
ServerRuntimeState::PolicyEnabled => {}
|
||||
}
|
||||
|
||||
let bind = config.bind.clone();
|
||||
let state = match config.mode {
|
||||
ServerConfigMode::Single {
|
||||
uri,
|
||||
graph_id,
|
||||
policy_file,
|
||||
queries,
|
||||
} => {
|
||||
let uri_for_log = uri.clone();
|
||||
info!(
|
||||
uri = %uri_for_log,
|
||||
graph_id = %graph_id,
|
||||
bind = %bind,
|
||||
mode = "single",
|
||||
"serving omnigraph"
|
||||
);
|
||||
AppState::open_single_with_queries_for_graph_id(
|
||||
uri,
|
||||
tokens,
|
||||
policy_file.as_ref(),
|
||||
queries,
|
||||
Some(graph_id),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
ServerConfigMode::Multi {
|
||||
graphs,
|
||||
config_path,
|
||||
|
|
@ -1076,7 +1008,7 @@ pub async fn serve(config: ServerConfig) -> Result<()> {
|
|||
} => {
|
||||
info!(
|
||||
bind = %bind,
|
||||
mode = "multi",
|
||||
mode = "cluster",
|
||||
graph_count = graphs.len(),
|
||||
config = %config_path.display(),
|
||||
"serving omnigraph"
|
||||
|
|
@ -1197,4 +1129,3 @@ async fn shutdown_signal() {
|
|||
}
|
||||
info!("shutdown signal received");
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue