mirror of
https://codeberg.org/icewind/haze.git
synced 2026-06-03 17:14:08 +02:00
445 lines
13 KiB
Rust
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)
|
|
}
|
|
}
|