gonzalod/gonzalod.rs
1//! `gonzalod` — the gonzalo daemon binary. Serves a record + blob store over
2//! gRPC and HTTP/JSON. Configuration via environment variables:
3//!
4//! - `GONZALO_STORE` — substrate: `fs` (default) or `s3`
5//! - `GONZALO_ROOT` — fs store root directory (default `./gonzalo-data`)
6//! - `GONZALO_S3_BUCKET` — s3 bucket (required when `GONZALO_STORE=s3`)
7//! - `GONZALO_S3_ENDPOINT` — s3 endpoint for MinIO/Garage (optional)
8//! - `GONZALO_S3_REGION` — s3 region override (optional)
9//! - `GONZALO_ANCESTOR_CAP` — revisions kept in each record's `ancestors` list
10//! (default 32, at least 1). A smaller cap turns some sync fast-forwards
11//! into merges or conflicts; it never produces a wrong winner (gonzalo#203).
12//! - `GONZALO_HTTP_ADDR` — HTTP/JSON bind address (default `127.0.0.1:8080`)
13//! - `GONZALO_GRPC_ADDR` — gRPC bind address (default `127.0.0.1:50051`)
14//! - `GONZALO_MAX_BLOB_SIZE` — max bytes per blob over the transports (default 64 MiB).
15//! Note this also raises the **gRPC decode ceiling for every RPC**, records
16//! included: tonic's limit is per-server, not per-method (see `serve_grpc`).
17//! HTTP is unaffected — its blob limit is scoped to the blob sub-router (#194).
18//! - `GONZALO_AUTH_FILE` — TOML principals file for namespace-scoped auth
19//! - `GONZALO_TOKEN` — single admin token (used when no auth file is set)
20//!
21//! Blob endpoints (`/v1/blobs`) are served for the `fs` and `s3` substrates,
22//! which implement [`BlobStore`](gonzalo_core::BlobStore). Git is not a content-addressed blob store, so a
23//! git-backed deployment does not serve blobs (gonzalo#184).
24//!
25//! Credentials for s3 come from the standard `AWS_*` environment.
26
27use gonzalo_core::{BlobStore, Store};
28use gonzalo_server::{Auth, Service, StoreConfig, ancestor_cap_from_env, serve_grpc, serve_http};
29use gonzalo_store_fs::FsStore;
30use gonzalo_store_s3::S3Store;
31use std::sync::Arc;
32
33#[tokio::main]
34async fn main() -> Result<(), Box<dyn std::error::Error>> {
35 let http_addr = std::env::var("GONZALO_HTTP_ADDR").unwrap_or_else(|_| "127.0.0.1:8080".into());
36 let grpc_addr = std::env::var("GONZALO_GRPC_ADDR").unwrap_or_else(|_| "127.0.0.1:50051".into());
37
38 // GONZALO_AUTH_FILE (scoped principals) > GONZALO_TOKEN (single admin) > open.
39 let auth = Arc::new(Auth::from_env(
40 |k| std::env::var(k).ok(),
41 |path| std::fs::read_to_string(path).map_err(|e| e.to_string()),
42 )?);
43 let auth_on = !matches!(*auth, Auth::Disabled);
44
45 // Select the storage substrate from the environment. One store backs both
46 // the record store and the content-addressed blob store (each backend
47 // implements both traits).
48 let config = StoreConfig::from_env(|k| std::env::var(k).ok())?;
49 // Applied to the backing store, which folds ancestors inside its own OCC
50 // critical section (gonzalo#203).
51 let ancestor_cap = ancestor_cap_from_env(|k| std::env::var(k).ok())?;
52 let (store, blobs, graph_root): (
53 Arc<dyn Store>,
54 Arc<dyn BlobStore>,
55 Option<std::path::PathBuf>,
56 ) = match &config {
57 StoreConfig::Fs { root } => {
58 // Per-view SQLite graphs written by `gonzalo index` live under
59 // `<root>/graphs` and are queried directly.
60 let fs = Arc::new(FsStore::new(root).with_ancestor_cap(ancestor_cap)?);
61 let graphs = std::path::Path::new(root).join("graphs");
62 (fs.clone(), fs, Some(graphs))
63 }
64 StoreConfig::S3 {
65 bucket,
66 endpoint,
67 region,
68 } => {
69 // No local SQLite graph cache under S3: views assemble from the
70 // manifest + content-addressed slices (blobs) on demand.
71 let s3 = Arc::new(
72 S3Store::connect(bucket.clone(), endpoint.clone(), region.clone())
73 .await
74 .with_ancestor_cap(ancestor_cap)?,
75 );
76 (s3.clone(), s3, None)
77 }
78 };
79 let mut service = Service::new(store, blobs);
80 // Optional per-blob size ceiling (bytes). Defaults to the shared constant;
81 // a malformed value is a hard startup error rather than a silent fallback.
82 let max_blob_size = match std::env::var("GONZALO_MAX_BLOB_SIZE") {
83 Ok(v) if !v.is_empty() => v
84 .parse::<usize>()
85 .map_err(|e| format!("GONZALO_MAX_BLOB_SIZE must be a byte count: {e}"))?,
86 _ => gonzalo_proto::DEFAULT_MAX_BLOB_SIZE,
87 };
88 service = service.with_max_blob_size(max_blob_size);
89 if let Some(graph_root) = graph_root {
90 service = service.with_graph_root(graph_root);
91 }
92
93 let substrate = match &config {
94 StoreConfig::Fs { root } => format!("fs({root})"),
95 StoreConfig::S3 { bucket, .. } => format!("s3({bucket})"),
96 };
97 let http_listener = tokio::net::TcpListener::bind(&http_addr).await?;
98 let grpc_listener = tokio::net::TcpListener::bind(&grpc_addr).await?;
99 eprintln!(
100 "gonzalod: store {substrate}, ancestor cap {ancestor_cap}, HTTP on {http_addr}, gRPC on {grpc_addr}, auth {}",
101 if auth_on { "on" } else { "off" }
102 );
103
104 let http = tokio::spawn(serve_http(http_listener, service.clone(), auth.clone()));
105 let grpc = tokio::spawn(serve_grpc(grpc_listener, service, auth));
106
107 tokio::select! {
108 r = http => { r??; }
109 r = grpc => { r??; }
110 _ = tokio::signal::ctrl_c() => { eprintln!("gonzalod: shutting down"); }
111 }
112 Ok(())
113}