Archify Sync server, Dockerfile and Helm chart
Rust server (axum, yrs, sqlx/PostgreSQL): live shared models over the y-websocket protocol, versions, browser sign-in with PKCE, invites, single sign-on through OIDC, metrics. Docker image on distroless, compose file, and a Helm chart with one pod, optional bundled PostgreSQL and secrets from existing Secrets.
This commit is contained in:
commit
5c43b90ef8
67 files changed
+8362
No files matched your search
@@ -0,0 +1,176 @@
|
||||
//! Archify Sync server: live, shared Archify models.
|
||||
|
||||
mod api;
|
||||
mod auth;
|
||||
mod config;
|
||||
mod doc;
|
||||
mod error;
|
||||
mod hub;
|
||||
mod metrics;
|
||||
mod oidc;
|
||||
mod pages;
|
||||
mod state;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use archify_sync_store::Store;
|
||||
use axum::Router;
|
||||
use axum::extract::DefaultBodyLimit;
|
||||
use axum::routing::{delete, get, post};
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::state::AppState;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// The image has no shell or curl; `archify-sync-server healthcheck` is its probe.
|
||||
if std::env::args().nth(1).as_deref() == Some("healthcheck") {
|
||||
return healthcheck().await;
|
||||
}
|
||||
// reqwest (single sign-on) and sqlx share rustls with the ring provider.
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("info,tower_http=warn,sqlx=warn"));
|
||||
if std::env::var("ARCHIFY_SYNC_LOG_JSON").is_ok_and(|v| v == "true") {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(filter)
|
||||
.json()
|
||||
.init();
|
||||
} else {
|
||||
tracing_subscriber::fmt().with_env_filter(filter).init();
|
||||
}
|
||||
|
||||
let config = Config::from_env()?;
|
||||
let store = Store::connect(&config.database_url, config.database_max_connections).await?;
|
||||
store.migrate().await?;
|
||||
let listen = config.listen;
|
||||
let state = AppState::new(config, store).await?;
|
||||
|
||||
let background = tokio::spawn(housekeeping(state.clone()));
|
||||
if let Some(address) = state.config.metrics_listen {
|
||||
let listener = tokio::net::TcpListener::bind(address).await?;
|
||||
tracing::info!(listen = %address, "metrics listening");
|
||||
let app = Router::new()
|
||||
.route("/metrics", get(metrics::serve))
|
||||
.with_state(state.clone());
|
||||
tokio::spawn(async move { axum::serve(listener, app).await });
|
||||
}
|
||||
let listener = tokio::net::TcpListener::bind(listen).await?;
|
||||
tracing::info!(%listen, version = env!("CARGO_PKG_VERSION"), "Archify Sync listening");
|
||||
axum::serve(listener, router(state.clone()))
|
||||
.with_graceful_shutdown(shutdown())
|
||||
.await?;
|
||||
|
||||
background.abort();
|
||||
state.hub.snapshot_all().await;
|
||||
tracing::info!("stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn router(state: AppState) -> Router {
|
||||
let v1 = Router::new()
|
||||
.route("/info", get(api::info))
|
||||
.route("/setup", post(api::setup))
|
||||
.route("/auth/token", post(api::token))
|
||||
.route("/auth/logout", post(api::logout))
|
||||
.route("/me", get(api::me))
|
||||
.route("/spaces", get(api::list_spaces).post(api::create_space))
|
||||
.route("/spaces/{space}", get(api::get_space))
|
||||
.route("/spaces/{space}/members", get(api::members))
|
||||
.route(
|
||||
"/spaces/{space}/members/{account}",
|
||||
delete(api::remove_member),
|
||||
)
|
||||
.route("/spaces/{space}/invites", post(api::create_invite))
|
||||
.route("/invites/{token}/accept", post(api::accept_invite))
|
||||
.route(
|
||||
"/spaces/{space}/versions",
|
||||
get(api::versions).post(api::publish),
|
||||
)
|
||||
.route(
|
||||
"/spaces/{space}/versions/{version}/files",
|
||||
get(api::version_files),
|
||||
)
|
||||
.route(
|
||||
"/spaces/{space}/versions/{version}/restore",
|
||||
post(api::restore),
|
||||
)
|
||||
.route("/spaces/{space}/sync", get(api::sync));
|
||||
|
||||
Router::new()
|
||||
.nest("/v1", v1)
|
||||
.route("/healthz", get(api::healthz))
|
||||
.route("/readyz", get(api::readyz))
|
||||
.route("/setup", get(pages::setup_page).post(pages::setup_submit))
|
||||
.route(
|
||||
"/authorize",
|
||||
get(pages::authorize_page).post(pages::authorize_submit),
|
||||
)
|
||||
.route("/join/{token}", get(pages::join_page))
|
||||
.route("/oidc/start", get(pages::oidc_start))
|
||||
.route("/oidc/callback", get(pages::oidc_callback))
|
||||
.layer(DefaultBodyLimit::max(1024 * 1024))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
/// Idle snapshots every few seconds; expired sessions once an hour.
|
||||
async fn housekeeping(state: AppState) {
|
||||
let mut tick = tokio::time::interval(Duration::from_secs(5));
|
||||
let mut n: u64 = 0;
|
||||
loop {
|
||||
tick.tick().await;
|
||||
state.hub.snapshot_idle_rooms().await;
|
||||
n += 1;
|
||||
if n.is_multiple_of(720) {
|
||||
match state.store.delete_expired_refresh().await {
|
||||
Ok(removed) if removed > 0 => tracing::info!(removed, "expired sessions removed"),
|
||||
Ok(_) => {}
|
||||
Err(error) => tracing::warn!(%error, "removing expired sessions failed"),
|
||||
}
|
||||
let rooms = state.hub.open_rooms().await;
|
||||
tracing::debug!(rooms, "open rooms");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks the running server's `/healthz` on this machine; exits non-zero if it does not answer.
|
||||
async fn healthcheck() -> Result<()> {
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
let listen: std::net::SocketAddr = std::env::var("ARCHIFY_SYNC_LISTEN")
|
||||
.unwrap_or_else(|_| "0.0.0.0:8080".into())
|
||||
.parse()?;
|
||||
let target = std::net::SocketAddr::from(([127, 0, 0, 1], listen.port()));
|
||||
let reply = tokio::time::timeout(Duration::from_secs(3), async {
|
||||
let mut stream = tokio::net::TcpStream::connect(target).await?;
|
||||
stream
|
||||
.write_all(b"GET /healthz HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
||||
.await?;
|
||||
let mut reply = String::new();
|
||||
stream.read_to_string(&mut reply).await?;
|
||||
anyhow::Ok(reply)
|
||||
})
|
||||
.await??;
|
||||
anyhow::ensure!(reply.starts_with("HTTP/1.1 200"), "unhealthy");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown() {
|
||||
let ctrl_c = async {
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
};
|
||||
#[cfg(unix)]
|
||||
let term = async {
|
||||
if let Ok(mut s) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
{
|
||||
s.recv().await;
|
||||
}
|
||||
};
|
||||
#[cfg(not(unix))]
|
||||
let term = std::future::pending::<()>();
|
||||
tokio::select! { _ = ctrl_c => {}, _ = term => {} }
|
||||
tracing::info!("shutting down; storing snapshots");
|
||||
}
|
||||
Reference in new issue
Block a user