pub mod handlers;
pub mod http_api;
pub mod public_routes;
pub mod routes;
pub mod web_ui;
use crate::{
db::Db,
docker::DockerClient,
event_bus::EventBus,
ingress::{IngressController, TlsManager},
secrets::SecretsManager,
};
use shared::{DockerImageInfo, DockerNetworkInfo, DockerVolumeInfo};
use std::{
collections::HashMap,
path::PathBuf,
sync::{Arc, Mutex},
time::{Duration, Instant},
};
use tokio::sync::Mutex as AsyncMutex;
pub type OAuthStates = Arc<Mutex<HashMap<String, String>>>;
pub type ActiveDeploys = Arc<Mutex<HashMap<String, tokio::task::AbortHandle>>>;
pub type ActiveJobs = Arc<Mutex<HashMap<String, tokio::sync::watch::Sender<bool>>>>;
const DOCKER_CACHE_TTL: Duration = Duration::from_secs(300);
pub struct TtlCache<T> {
ttl: Duration,
slot: AsyncMutex<Option<(Instant, T)>>,
}
impl<T: Clone> TtlCache<T> {
fn new(ttl: Duration) -> Self {
Self {
ttl,
slot: AsyncMutex::new(None),
}
}
pub async fn get_or_refresh<F, Fut, E>(&self, refresh: F) -> Result<T, E>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<T, E>>,
{
let mut slot = self.slot.lock().await;
if let Some((at, v)) = slot.as_ref()
&& at.elapsed() < self.ttl
{
return Ok(v.clone());
}
let v = refresh().await?;
*slot = Some((Instant::now(), v.clone()));
Ok(v)
}
pub async fn invalidate(&self) {
*self.slot.lock().await = None;
}
}
pub struct DockerCache {
pub df: TtlCache<(Vec<DockerImageInfo>, Vec<DockerVolumeInfo>)>,
pub networks: TtlCache<Vec<DockerNetworkInfo>>,
}
impl DockerCache {
fn new() -> Self {
Self {
df: TtlCache::new(DOCKER_CACHE_TTL),
networks: TtlCache::new(DOCKER_CACHE_TTL),
}
}
}
#[derive(Clone)]
pub struct AppState {
pub db: Arc<Db>,
pub docker: Arc<DockerClient>,
pub ingress: Arc<IngressController>,
pub bus: Arc<EventBus>,
pub secrets: Arc<SecretsManager>,
pub tls: Arc<TlsManager>,
pub db_path: PathBuf,
pub backup_dir: PathBuf,
pub drain_secs: u64,
pub api: shared::ApiConfig,
pub started_at: std::time::Instant,
pub oauth_states: OAuthStates,
pub active_deploys: ActiveDeploys,
pub active_jobs: ActiveJobs,
pub deploy_queue: Arc<crate::deploy::queue::DeployQueue>,
pub docker_cache: Arc<DockerCache>,
pub registry_storage: Option<Arc<crate::registry::storage::RegistryStorage>>,
pub registry_internal_token: Option<Arc<str>>,
}
impl AppState {
#[allow(clippy::too_many_arguments)]
pub fn new(
db: Arc<Db>,
docker: Arc<DockerClient>,
ingress: Arc<IngressController>,
bus: Arc<EventBus>,
secrets: Arc<SecretsManager>,
tls: Arc<TlsManager>,
db_path: PathBuf,
backup_dir: PathBuf,
drain_secs: u64,
api: shared::ApiConfig,
registry_storage: Option<Arc<crate::registry::storage::RegistryStorage>>,
registry_internal_token: Option<Arc<str>>,
) -> Self {
Self {
db,
docker,
ingress,
bus,
secrets,
tls,
db_path,
backup_dir,
drain_secs,
api,
started_at: std::time::Instant::now(),
oauth_states: Arc::new(Mutex::new(HashMap::new())),
active_deploys: Arc::new(Mutex::new(HashMap::new())),
active_jobs: Arc::new(Mutex::new(HashMap::new())),
deploy_queue: crate::deploy::queue::DeployQueue::new(),
docker_cache: Arc::new(DockerCache::new()),
registry_storage,
registry_internal_token,
}
}
pub fn public_base_url(&self) -> String {
self.api.public_base_url(&outbound_ip())
}
}
fn outbound_ip() -> String {
use std::net::UdpSocket;
UdpSocket::bind("0.0.0.0:0")
.and_then(|s| {
s.connect("8.8.8.8:80")?;
s.local_addr()
})
.map(|addr| addr.ip().to_string())
.unwrap_or_else(|_| "localhost".to_string())
}