Skip to content
//! `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(())
}