Skip to main content

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}