mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-21 02:28:07 +02:00
feat(server)!: cluster-only server — remove single-graph serving (RFC-011) (#250)
omnigraph-server boots only from --cluster; all HTTP is /graphs/<id>/…; flat single-graph routes and the omnigraph.yaml server boot are removed. GraphRouting/ServerConfigMode collapse to multi-only; openapi.json regenerated to the nested shape; ~100 server route tests migrated; parity/system_local boot from a converged cluster. Gate green (1410 tests).
This commit is contained in:
parent
b183db078f
commit
8b01c6e547
20 changed files with 988 additions and 1492 deletions
|
|
@ -37,9 +37,12 @@ struct Parity {
|
|||
|
||||
fn parity() -> Parity {
|
||||
let (temp, local, remote) = twin_graphs();
|
||||
let (local_cfg, server_cfg) = parity_configs(temp.path(), &local, &remote);
|
||||
let server = spawn_server_with_config_env(
|
||||
&server_cfg,
|
||||
// RFC-011 cluster-only: the remote arm is served from a converged
|
||||
// cluster directory (one graph, id `parity`), seeded with the same
|
||||
// fixture data as the local twin.
|
||||
let (local_cfg, cluster_dir) = parity_configs(temp.path(), &local, &remote);
|
||||
let server = spawn_server_with_cluster_env(
|
||||
&cluster_dir,
|
||||
&[(
|
||||
"OMNIGRAPH_SERVER_BEARER_TOKENS_JSON",
|
||||
r#"{"act-parity":"parity-tok"}"#,
|
||||
|
|
|
|||
|
|
@ -339,6 +339,63 @@ impl SystemGraph {
|
|||
}
|
||||
}
|
||||
|
||||
/// A converged cluster directory the server can boot from (`--cluster`),
|
||||
/// serving one graph seeded with the standard fixture. Holds the temp dir
|
||||
/// alive for the test's lifetime.
|
||||
pub struct ClusterFixture {
|
||||
_temp: TempDir,
|
||||
dir: PathBuf,
|
||||
}
|
||||
|
||||
impl ClusterFixture {
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.dir
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a converged cluster (RFC-011 cluster-only serving) with a single
|
||||
/// graph `graph_id`, seeded with the `test.jsonl` fixture so reads return
|
||||
/// data. When `policy_yaml` is `Some`, the bundle is bound to the graph
|
||||
/// scope. The server boots from the returned path via `--cluster`.
|
||||
pub fn converged_loaded_cluster(graph_id: &str, policy_yaml: Option<&str>) -> ClusterFixture {
|
||||
let temp = tempdir().unwrap();
|
||||
let dir = temp.path().to_path_buf();
|
||||
fs::copy(fixture("test.pg"), dir.join("graph.pg")).unwrap();
|
||||
|
||||
let policy_block = match policy_yaml {
|
||||
Some(source) => {
|
||||
fs::write(dir.join("graph.policy.yaml"), source).unwrap();
|
||||
format!(
|
||||
"policies:\n graph:\n file: ./graph.policy.yaml\n applies_to: [{graph_id}]\n"
|
||||
)
|
||||
}
|
||||
None => String::new(),
|
||||
};
|
||||
fs::write(
|
||||
dir.join("cluster.yaml"),
|
||||
format!(
|
||||
"version: 1\nmetadata:\n name: sys\nstate:\n backend: cluster\n lock: true\ngraphs:\n {graph_id}:\n schema: ./graph.pg\n{policy_block}"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
output_success(cli().arg("cluster").arg("import").arg("--config").arg(&dir));
|
||||
output_success(cli().arg("cluster").arg("apply").arg("--config").arg(&dir));
|
||||
|
||||
let served_root = dir.join("graphs").join(format!("{graph_id}.omni"));
|
||||
output_success(
|
||||
cli()
|
||||
.arg("load")
|
||||
.arg("--data")
|
||||
.arg(fixture("test.jsonl"))
|
||||
.arg("--mode")
|
||||
.arg("overwrite")
|
||||
.arg(&served_root),
|
||||
);
|
||||
|
||||
ClusterFixture { _temp: temp, dir }
|
||||
}
|
||||
|
||||
// ---- helpers moved from the monolithic tests/cli.rs ----
|
||||
#[allow(unused_imports)]
|
||||
use lance::Dataset;
|
||||
|
|
@ -788,29 +845,104 @@ rules:
|
|||
.to_string()
|
||||
}
|
||||
|
||||
/// Per-arm config files carrying the same policy. Both arms address the
|
||||
/// graph by positional URI, so the TOP-LEVEL policy.file applies on each
|
||||
/// side (single-graph semantics).
|
||||
pub fn parity_configs(root: &Path, _local_graph: &Path, remote_graph: &Path) -> (PathBuf, PathBuf) {
|
||||
/// The graph id the parity cluster serves the remote arm under. The
|
||||
/// remote arm addresses it with `--graph PARITY_GRAPH_ID` (RFC-011: the
|
||||
/// server is cluster-only, so a graph selector is required).
|
||||
pub const PARITY_GRAPH_ID: &str = "parity";
|
||||
|
||||
/// Build both arms' configuration (RFC-011 cluster-only server).
|
||||
///
|
||||
/// * Local arm: a `--config` file carrying the TOP-LEVEL `policy.file`
|
||||
/// (single-graph embedded semantics), used as-is by `run_both_with_config`.
|
||||
/// * Remote arm: a converged cluster directory whose single graph (id
|
||||
/// `parity`) carries the SAME Cedar bundle (bound to the graph scope).
|
||||
/// The cluster's derived graph root (`<dir>/graphs/parity.omni`) is
|
||||
/// seeded with the SAME fixture data as the local twin so the two arms
|
||||
/// compare like-for-like.
|
||||
///
|
||||
/// `local_graph` is overwritten with a byte-for-byte copy of the cluster's
|
||||
/// seeded served graph so identity-bearing values that are NOT scrubbed
|
||||
/// (e.g. `graph_commit_id`, edge `id`s in export) match across the arms —
|
||||
/// the served graph is the source of truth and the local twin mirrors it.
|
||||
///
|
||||
/// Returns `(local_config_path, cluster_dir)`. The caller spawns the
|
||||
/// server with `--cluster <cluster_dir>`.
|
||||
pub fn parity_configs(root: &Path, local_graph: &Path, _remote_graph: &Path) -> (PathBuf, PathBuf) {
|
||||
let policy = root.join("parity.policy.yaml");
|
||||
fs::write(&policy, parity_policy_yaml()).unwrap();
|
||||
|
||||
// Local arm config: top-level single-graph policy.
|
||||
let local_cfg = root.join("local.omnigraph.yaml");
|
||||
fs::write(
|
||||
&local_cfg,
|
||||
format!("policy:\n file: {}\n", policy.display()),
|
||||
)
|
||||
.unwrap();
|
||||
let server_cfg = root.join("server.omnigraph.yaml");
|
||||
|
||||
// Remote arm: a cluster directory the server boots from. One graph
|
||||
// (`parity`), schema = the shared fixture, policy bound to the graph.
|
||||
let cluster_dir = root.join("parity-cluster");
|
||||
fs::create_dir_all(&cluster_dir).unwrap();
|
||||
fs::copy(fixture("test.pg"), cluster_dir.join("parity.pg")).unwrap();
|
||||
fs::copy(&policy, cluster_dir.join("parity.policy.yaml")).unwrap();
|
||||
fs::write(
|
||||
&server_cfg,
|
||||
cluster_dir.join("cluster.yaml"),
|
||||
format!(
|
||||
"server:\n graph: parity\ngraphs:\n parity:\n uri: {}\n policy:\n file: {}\n",
|
||||
remote_graph.display(),
|
||||
policy.display()
|
||||
r#"version: 1
|
||||
metadata:
|
||||
name: parity
|
||||
state:
|
||||
backend: cluster
|
||||
lock: true
|
||||
graphs:
|
||||
{PARITY_GRAPH_ID}:
|
||||
schema: ./parity.pg
|
||||
policies:
|
||||
parity:
|
||||
file: ./parity.policy.yaml
|
||||
applies_to: [{PARITY_GRAPH_ID}]
|
||||
"#
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
(local_cfg, server_cfg)
|
||||
|
||||
// Converge the cluster (creates the empty graph at the derived root),
|
||||
// then seed it with the same fixture data the local twin holds.
|
||||
output_success(
|
||||
cli()
|
||||
.arg("cluster")
|
||||
.arg("import")
|
||||
.arg("--config")
|
||||
.arg(&cluster_dir),
|
||||
);
|
||||
output_success(
|
||||
cli()
|
||||
.arg("cluster")
|
||||
.arg("apply")
|
||||
.arg("--config")
|
||||
.arg(&cluster_dir),
|
||||
);
|
||||
let served_root = cluster_dir
|
||||
.join("graphs")
|
||||
.join(format!("{PARITY_GRAPH_ID}.omni"));
|
||||
output_success(
|
||||
cli()
|
||||
.arg("load")
|
||||
.arg("--data")
|
||||
.arg(fixture("test.jsonl"))
|
||||
.arg("--mode")
|
||||
.arg("overwrite")
|
||||
.arg(&served_root),
|
||||
);
|
||||
|
||||
// Mirror the seeded served graph into the local twin so both arms hold
|
||||
// identical ULIDs / commit ids (the served graph is authoritative).
|
||||
if local_graph.exists() {
|
||||
fs::remove_dir_all(local_graph).unwrap();
|
||||
}
|
||||
copy_dir(&served_root, local_graph);
|
||||
|
||||
(local_cfg, cluster_dir)
|
||||
}
|
||||
|
||||
/// Run one CLI invocation per arm with identical verb args: locally against
|
||||
|
|
@ -853,7 +985,11 @@ pub fn run_both_with_config(
|
|||
.env("OMNIGRAPH_BEARER_TOKEN", PARITY_TOKEN)
|
||||
.args(args)
|
||||
.arg("--server")
|
||||
.arg(server_url);
|
||||
.arg(server_url)
|
||||
// RFC-011: the parity server is cluster-only (multi-graph), so the
|
||||
// remote arm must name the graph it addresses.
|
||||
.arg("--graph")
|
||||
.arg(PARITY_GRAPH_ID);
|
||||
let remote_out = remote.output().unwrap();
|
||||
(local_out, remote_out)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2319,9 +2319,12 @@ fn cluster_server_boot_ignores_local_config_in_cwd() {
|
|||
/// 3), and `logout` revokes.
|
||||
#[test]
|
||||
fn local_cli_keyed_credentials_authenticate_url_matched_server() {
|
||||
let graph = SystemGraph::loaded();
|
||||
let server = spawn_server_with_env(
|
||||
graph.path(),
|
||||
// RFC-011 cluster-only: the server boots from a converged cluster
|
||||
// serving the fixture graph under id `local`; tokens-only boot is
|
||||
// default-deny, which still permits `read`.
|
||||
let cluster = converged_loaded_cluster("local", None);
|
||||
let server = spawn_server_with_cluster_env(
|
||||
cluster.path(),
|
||||
&[("OMNIGRAPH_SERVER_BEARER_TOKEN", "secret-tok")],
|
||||
);
|
||||
let operator_home = tempfile::tempdir().unwrap();
|
||||
|
|
@ -2344,6 +2347,8 @@ fn local_cli_keyed_credentials_authenticate_url_matched_server() {
|
|||
.arg("read")
|
||||
.arg("--server")
|
||||
.arg(&server.base_url)
|
||||
.arg("--graph")
|
||||
.arg("local")
|
||||
.arg("--query")
|
||||
.arg(fixture("test.gq"))
|
||||
.arg("get_person")
|
||||
|
|
@ -2432,26 +2437,40 @@ fn local_cli_keyed_credentials_authenticate_url_matched_server() {
|
|||
/// stored queries) end to end, with the keyed credential from PR 2.
|
||||
#[test]
|
||||
fn local_cli_operator_alias_and_server_flag_invoke_stored_query() {
|
||||
let graph = SystemGraph::loaded();
|
||||
graph.write_query(
|
||||
"stored-find-person.gq",
|
||||
// RFC-011 cluster-only: build a converged cluster serving graph `local`
|
||||
// with a stored query `find_person` and a per-graph policy granting the
|
||||
// operator invoke_query + read (invoke_query is policy-gated — anti-probing
|
||||
// 404 without the grant).
|
||||
let cluster = tempfile::tempdir().unwrap();
|
||||
fs::copy(fixture("test.pg"), cluster.path().join("local.pg")).unwrap();
|
||||
fs::write(
|
||||
cluster.path().join("find-person.gq"),
|
||||
"query find_person($name: String) { match { $p: Person { name: $name } } return { $p.name } }",
|
||||
);
|
||||
// invoke_query is policy-gated (anti-probing 404 without the grant),
|
||||
// so the server gets a per-graph bundle granting it to the operator.
|
||||
graph.write_file(
|
||||
"graph.policy.yaml",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
cluster.path().join("graph.policy.yaml"),
|
||||
"version: 1\ngroups:\n ops: [\"act-op\"]\nprotected_branches: [main]\nrules:\n - id: allow-invoke\n allow:\n actors: { group: ops }\n actions: [invoke_query]\n - id: allow-read\n allow:\n actors: { group: ops }\n actions: [read]\n branch_scope: any\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
cluster.path().join("cluster.yaml"),
|
||||
"version: 1\nmetadata:\n name: alias-sys\nstate:\n backend: cluster\n lock: true\ngraphs:\n local:\n schema: ./local.pg\n queries:\n find_person:\n file: ./find-person.gq\npolicies:\n graph:\n file: ./graph.policy.yaml\n applies_to: [local]\n",
|
||||
)
|
||||
.unwrap();
|
||||
output_success(cli().arg("cluster").arg("import").arg("--config").arg(cluster.path()));
|
||||
output_success(cli().arg("cluster").arg("apply").arg("--config").arg(cluster.path()));
|
||||
output_success(
|
||||
cli()
|
||||
.arg("load")
|
||||
.arg("--data")
|
||||
.arg(fixture("test.jsonl"))
|
||||
.arg("--mode")
|
||||
.arg("overwrite")
|
||||
.arg(cluster.path().join("graphs").join("local.omni")),
|
||||
);
|
||||
let config = graph.write_config(
|
||||
"omnigraph-server.yaml",
|
||||
&format!(
|
||||
"graphs:\n local:\n uri: {}\n policy:\n file: ./graph.policy.yaml\n queries:\n find_person:\n file: ./stored-find-person.gq\n",
|
||||
yaml_string(&graph.path().to_string_lossy())
|
||||
),
|
||||
);
|
||||
let server = spawn_server_with_config_env(
|
||||
&config,
|
||||
let server = spawn_server_with_cluster_env(
|
||||
cluster.path(),
|
||||
&[(
|
||||
"OMNIGRAPH_SERVER_BEARER_TOKENS_JSON",
|
||||
r#"{"act-op":"srv-tok"}"#,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue