//! `twice-server` — a native HTTP control plane for `once`-deployed apps.
//!
//! Reads inventory from the Docker daemon, applies declarative infrastructure,
//! and drives start, stop, and image update operations through `once`.
mod ansi;
mod auth;
mod config;
mod docker;
mod error;
mod http;
mod jobs;
mod once_cli;
mod state;
use std::sync::Arc;
use anyhow::{Context as _, Result, bail};
use tokio::net::TcpListener;
use tower_http::trace::TraceLayer;
use tracing_subscriber::EnvFilter;
use crate::config::{ServerConfig, default_config_path};
use crate::docker::DockerInventory;
use crate::jobs::JobRegistry;
use crate::state::AppState;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_env("TWICE_LOG").unwrap_or_else(|_ignored| EnvFilter::new("info")),
)
.init();
let config_path = default_config_path()?;
let (config, created) = ServerConfig::load_or_create(&config_path)?;
if created {
tracing::warn!(
path = %config_path.display(),
api_key = %config.api_key.reveal(),
"generated a new API key; copy it into the twice UI now"
);
}
if !config.once_binary.is_file() {
bail!(
"once binary not found at {}; set TWICE_ONCE_BINARY or edit {}",
config.once_binary.display(),
config_path.display()
);
}
let docker =
DockerInventory::connect(&config.namespace).context("connecting to the Docker daemon")?;
let jobs = JobRegistry::new(config.once_binary.clone(), config.namespace.clone());
let bind = config.bind;
let state = AppState {
config: Arc::new(config),
docker: Arc::new(docker),
jobs,
};
let app = http::router(state).layer(TraceLayer::new_for_http());
let listener = TcpListener::bind(bind)
.await
.with_context(|| format!("binding {bind}"))?;
tracing::info!(%bind, "twice server listening");
axum::serve(listener, app).await.context("serving HTTP")?;
Ok(())
}