1
0
Fork 0
mirror of https://codeberg.org/icewind/haze.git synced 2026-06-03 17:14:08 +02:00
haze/src/service.rs
2026-03-22 15:03:50 +01:00

445 lines
13 KiB
Rust

mod clam;
mod dav;
mod imaginary;
mod kaspersky;
mod ldap;
mod mail;
mod objectstore;
mod oc;
mod office;
mod onlyoffice;
mod push;
mod sftp;
// mod sharding;
mod redis;
mod sharded;
mod smb;
use crate::cloud::CloudOptions;
use crate::config::{HazeConfig, Preset, ProxyConfig};
pub use crate::service::clam::{Clam, ClamIcap, ClamIcapTls, ClamSocket};
use crate::service::dav::Dav;
use crate::service::imaginary::Imaginary;
use crate::service::kaspersky::{Kaspersky, KasperskyIcap};
pub use crate::service::ldap::{Ldap, LdapAdmin};
use crate::service::mail::Mail;
pub use crate::service::objectstore::ObjectStore;
use crate::service::oc::Oc;
pub use crate::service::office::Office;
pub use crate::service::onlyoffice::OnlyOffice;
pub use crate::service::push::NotifyPush;
use crate::service::redis::Redis;
use crate::service::sftp::Sftp;
use crate::service::sharded::{Sharding, ShardingMigrate, ShardingMigrateUnset, SingleShard};
use crate::service::smb::Smb;
use bollard::models::ContainerState;
use bollard::Docker;
use enum_dispatch::enum_dispatch;
use miette::{IntoDiagnostic, Report, Result, WrapErr};
use serde_json::Value;
use std::collections::HashMap;
use std::iter::empty;
use std::net::IpAddr;
use std::str::FromStr;
use std::time::Duration;
use strum::{Display, EnumIter, EnumMessage, EnumString, IntoStaticStr};
use tokio::time::{sleep, timeout};
#[async_trait::async_trait]
#[enum_dispatch(Service)]
pub trait ServiceTrait {
fn name(&self) -> &str;
fn env(&self) -> &[&str] {
&[]
}
async fn spawn(
&self,
_docker: &Docker,
_cloud_id: &str,
_network: &str,
_config: &HazeConfig,
_options: &CloudOptions,
) -> Result<Vec<String>> {
Ok(Vec::new())
}
async fn is_healthy(
&self,
_docker: &Docker,
_cloud_id: &str,
_options: &CloudOptions,
) -> Result<bool> {
Ok(true)
}
fn container_name(&self, _cloud_id: &str) -> Option<String> {
None
}
async fn start_message(
&self,
_docker: &Docker,
_cloud_id: &str,
_proxy: &ProxyConfig,
) -> Result<Option<String>> {
Ok(None)
}
fn apps(&self) -> &'static [&'static str] {
&[]
}
fn config(
&self,
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<HashMap<String, Value>> {
Ok(HashMap::default())
}
fn pre_setup(
&self,
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<Vec<String>>> {
Ok(Vec::new())
}
async fn post_setup(
&self,
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<String>> {
Ok(Vec::new())
}
async fn is_running(&self, docker: &Docker, cloud_id: &str) -> Result<bool> {
let Some(container) = self.container_name(cloud_id) else {
return Ok(true);
};
let info = docker
.inspect_container(&container, None)
.await
.into_diagnostic()?;
Ok(matches!(
info.state,
Some(ContainerState {
running: Some(true),
..
})
))
}
async fn wait_for_running(&self, docker: &Docker, cloud_id: &str) -> Result<()> {
timeout(Duration::from_secs(30), async {
while !self.is_running(docker, cloud_id).await? {
sleep(Duration::from_millis(100)).await
}
Ok(())
})
.await
.into_diagnostic()
.wrap_err("Timeout after 30 seconds")?
}
async fn get_ips(
&self,
docker: &Docker,
cloud_id: &str,
) -> Result<Box<dyn Iterator<Item = IpAddr>>> {
let Some(container) = self.container_name(cloud_id) else {
return Ok(Box::new(empty()));
};
docker
.start_container(&container, None)
.await
.into_diagnostic()?;
self.wait_for_running(docker, cloud_id).await?;
sleep(Duration::from_millis(100)).await;
let info = docker
.inspect_container(&container, None)
.await
.into_diagnostic()?;
if matches!(
info.state,
Some(ContainerState {
running: Some(true),
..
})
) {
let ips: Vec<_> = info
.network_settings
.unwrap()
.networks
.unwrap()
.values()
.filter_map(|network| network.ip_address.clone())
.filter_map(|address| IpAddr::from_str(&address).ok())
.collect();
Ok(Box::new(ips.into_iter()))
} else {
Err(Report::msg("service not started"))
}
}
fn proxy_port(&self) -> u16 {
80
}
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct RedisTls;
impl ServiceTrait for RedisTls {
fn name(&self) -> &str {
"redis-tls"
}
fn env(&self) -> &[&str] {
&["REDIS_TLS=1"]
}
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct FrankenPhp;
impl ServiceTrait for FrankenPhp {
fn name(&self) -> &str {
"franken-php"
}
fn env(&self) -> &[&str] {
&["FRANKENPHP=1"]
}
}
#[derive(
Copy, Clone, Debug, PartialEq, EnumString, EnumMessage, EnumIter, IntoStaticStr, Display,
)]
#[strum(serialize_all = "kebab-case")]
pub enum ServiceType {
/// S3 Primary storage and external storage
S3,
/// S3 Primary storage with TLS
S3s,
/// S3 multi-object store Primary storage and external storage
S3m,
/// S3 multi-bucket Primary storage and external storage
S3mb,
/// Azure Primary storage and external storage
Azure,
/// Ldap user backend
Ldap,
/// Ldap admin interface
LdapAdmin,
/// OnlyOffice
OnlyOffice,
/// Libre office online
Office,
/// notify_push
Push,
/// Smb external storage
Smb,
/// Database sharding
#[strum(serialize = "sharding", serialize = "sharded")]
Sharding,
/// Database sharding migration
#[strum(serialize = "sharding-migrate", serialize = "sharded-migrate")]
ShardingMigrate,
/// Database sharding migration, with the shards unset
#[strum(
serialize = "sharding-migrate-unset",
serialize = "sharded-migrate-unset"
)]
ShardingMigrateUnset,
/// Database sharding with a single shard
SingleShard,
/// WebDav external storage
Dav,
/// Sftp external storage
Sftp,
/// ownCloud instance for migration
Oc,
/// Imaginary for preview generation
Imaginary,
/// Kaspersky antivirus in http mode
Kaspersky,
/// Kaspersky antivirus in icap mode
KasperskyIcap,
/// Kaspersky antivirus in local binary
#[strum(serialize = "clamav", serialize = "clam")]
ClamAv,
/// Kaspersky antivirus in external socket mode
#[strum(serialize = "clamav-external", serialize = "clam-external")]
ClamAvExternal,
/// Kaspersky antivirus in local socket mode
#[strum(serialize = "clamav-socket", serialize = "clam-socket")]
ClamAvSocket,
/// Kaspersky antivirus in icap mode
#[strum(serialize = "clamav-icap", serialize = "clam-icap")]
ClamAvIcap,
/// Kaspersky antivirus in icap mode with TLS
#[strum(serialize = "clamav-icap-tls", serialize = "clam-icap-tls")]
ClamAvIcapTls,
/// Mail server
Mail,
/// External redis instance
Redis,
/// External redis instance with TLS
RedisTls,
/// Use FrankenPHP instead of PHP-FPM
FrankenPhp,
}
#[enum_dispatch]
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum Service {
ObjectStore(ObjectStore),
Ldap(Ldap),
LdapAdmin(LdapAdmin),
OnlyOffice(OnlyOffice),
Office(Office),
Push(NotifyPush),
Smb(Smb),
Dav(Dav),
Sharding(Sharding),
SingleShard(SingleShard),
ShardingMigrate(ShardingMigrate),
ShardingMigrateUnset(ShardingMigrateUnset),
Sftp(Sftp),
Kaspersky(Kaspersky),
KasperskyIcap(KasperskyIcap),
Clam(Clam),
ClamSocket(ClamSocket),
ClamIcap(ClamIcap),
ClamIcapTls(ClamIcapTls),
Oc(Oc),
Imaginary(Imaginary),
Mail(Mail),
Redis(Redis),
RedisTls(RedisTls),
FrankenPhp(FrankenPhp),
Preset(PresetService),
}
impl Service {
pub fn from_type(presets: &[Preset], ty: &str) -> Option<Vec<Self>> {
if let Ok(ty) = ServiceType::from_str(ty) {
match ty {
ServiceType::S3 => Some(vec![Service::ObjectStore(ObjectStore::S3)]),
ServiceType::S3s => Some(vec![Service::ObjectStore(ObjectStore::S3s)]),
ServiceType::S3m => Some(vec![Service::ObjectStore(ObjectStore::S3m)]),
ServiceType::S3mb => Some(vec![Service::ObjectStore(ObjectStore::S3mb)]),
ServiceType::Azure => Some(vec![Service::ObjectStore(ObjectStore::Azure)]),
ServiceType::Ldap => Some(vec![Service::Ldap(Ldap), Service::LdapAdmin(LdapAdmin)]),
ServiceType::LdapAdmin => {
Some(vec![Service::Ldap(Ldap), Service::LdapAdmin(LdapAdmin)])
}
ServiceType::OnlyOffice => Some(vec![Service::OnlyOffice(OnlyOffice)]),
ServiceType::Office => Some(vec![Service::Office(Office)]),
ServiceType::Push => Some(vec![Service::Push(NotifyPush)]),
ServiceType::Smb => Some(vec![Service::Smb(Smb)]),
ServiceType::Sharding => Some(vec![Service::Sharding(Sharding)]),
ServiceType::SingleShard => Some(vec![Service::SingleShard(SingleShard)]),
ServiceType::ShardingMigrate => {
Some(vec![Service::ShardingMigrate(ShardingMigrate)])
}
ServiceType::ShardingMigrateUnset => {
Some(vec![Service::ShardingMigrateUnset(ShardingMigrateUnset)])
}
ServiceType::Dav => Some(vec![Service::Dav(Dav)]),
ServiceType::Sftp => Some(vec![Service::Sftp(Sftp)]),
ServiceType::Oc => Some(vec![Service::Oc(Oc)]),
ServiceType::Imaginary => Some(vec![Service::Imaginary(Imaginary)]),
ServiceType::Kaspersky => Some(vec![Service::Kaspersky(Kaspersky)]),
ServiceType::KasperskyIcap => Some(vec![Service::KasperskyIcap(KasperskyIcap)]),
ServiceType::ClamAv => Some(vec![Service::Clam(Clam)]),
ServiceType::ClamAvExternal => Some(vec![Service::ClamSocket(ClamSocket)]),
ServiceType::ClamAvSocket => Some(vec![Service::ClamSocket(ClamSocket)]),
ServiceType::ClamAvIcap => Some(vec![Service::ClamIcap(ClamIcap)]),
ServiceType::ClamAvIcapTls => Some(vec![Service::ClamIcapTls(ClamIcapTls)]),
ServiceType::Mail => Some(vec![Service::Mail(Mail)]),
ServiceType::Redis => Some(vec![Service::Redis(Redis)]),
ServiceType::RedisTls => Some(vec![Service::RedisTls(RedisTls)]),
ServiceType::FrankenPhp => Some(vec![Service::FrankenPhp(FrankenPhp)]),
}
} else {
presets
.iter()
.find_map(|preset| (preset.name == ty).then(|| PresetService(preset.name.clone())))
.map(Service::Preset)
.map(|service| vec![service])
}
}
pub async fn wait_for_start(
&self,
docker: &Docker,
cloud_id: &str,
options: &CloudOptions,
) -> Result<()> {
timeout(Duration::from_secs(30), async {
while !self.is_healthy(docker, cloud_id, options).await? {
sleep(Duration::from_millis(100)).await
}
Ok(())
})
.await
.into_diagnostic()
.wrap_err("Timeout after 30 seconds")?
}
}
fn get_preset<'a>(presets: &'a [Preset], name: &str) -> Option<&'a Preset> {
presets.iter().find(|preset| preset.name == name)
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct PresetService(pub String);
#[async_trait::async_trait]
impl ServiceTrait for PresetService {
fn name(&self) -> &str {
self.0.as_str()
}
fn config(
&self,
_docker: &Docker,
_cloud_id: &str,
config: &HazeConfig,
) -> Result<HashMap<String, Value>> {
let preset =
get_preset(&config.preset, &self.0).ok_or_else(|| Report::msg("invalid preset"))?;
let config = preset
.config
.iter()
.map(|(k, v)| Ok((k.clone(), serde_json::to_value(v).into_diagnostic()?)))
.collect::<Result<HashMap<_, _>>>()?;
Ok(config)
}
async fn post_setup(
&self,
_docker: &Docker,
_cloud_id: &str,
config: &HazeConfig,
) -> Result<Vec<String>> {
let preset =
get_preset(&config.preset, &self.0).ok_or_else(|| Report::msg("invalid preset"))?;
let mut commands: Vec<_> = preset
.apps
.iter()
.map(|app| format!("occ app:enable {app} --force"))
.collect();
commands.extend_from_slice(&preset.commands);
Ok(commands)
}
}