diff --git a/CHANGELOG.md b/CHANGELOG.md index 557b232..ed38d46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Unreleased - Support having a `#!` inside a haze script to specify the interpreter to use +- Add webui for accessing the database. ## 2.4.1 diff --git a/src/cloud.rs b/src/cloud.rs index cbb8b5c..0928e33 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -4,9 +4,9 @@ use crate::exec::{ExitCode, exec, exec_io, exec_tty}; use crate::git::{create_worktree, find_app_repo, init_submodules, remove_worktree}; use crate::mapping::{Mapping, for_config}; use crate::php::PhpVersion; -use crate::service::Service; use crate::service::ServiceTrait; use crate::service::deduplicate_services; +use crate::service::{DbGate, Service}; use crate::sources::download_nc; use bollard::Docker; use bollard::config::NetworkCreateRequest; @@ -347,6 +347,8 @@ fn test_option_parse() { ); } +static DEFAULT_SERVICES: &[Service] = &[Service::DbGate(DbGate)]; + #[derive(Debug, Clone)] pub struct Cloud { pub id: String, @@ -539,6 +541,8 @@ impl Cloud { options .services .iter() + .filter(|service| !service.hidden()) + .chain(DEFAULT_SERVICES.into_iter()) .map(|service| service.spawn(docker, &id, &network, config, &options)), ) .await?; @@ -970,7 +974,10 @@ impl Cloud { } pub fn services(&self) -> impl Iterator { - self.options.services.iter() + self.options + .services + .iter() + .chain(DEFAULT_SERVICES.into_iter()) } pub fn db(&self) -> &Database { diff --git a/src/database.rs b/src/database.rs index 7f943bd..a38da00 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,13 +1,13 @@ use crate::config::HazeConfig; -use crate::exec::{exec, exec_tty, ExitCode}; +use crate::exec::{ExitCode, exec, exec_tty}; use crate::image::pull_image; +use bollard::Docker; use bollard::config::ContainerCreateBody; use bollard::models::{EndpointSettings, HostConfig, NetworkingConfig}; use bollard::query_parameters::CreateContainerOptions; -use bollard::Docker; use maplit::hashmap; use miette::{IntoDiagnostic, Report, Result, WrapErr}; -use std::io::{stdout, Stdout}; +use std::io::{Stdout, stdout}; use std::net::{IpAddr, Ipv4Addr}; use std::str::FromStr; use std::time::Duration; @@ -32,6 +32,39 @@ impl DatabaseFamily { pub fn name(&self) -> &'static str { self.into() } + + pub fn db_gate_driver(&self) -> &'static str { + match self { + DatabaseFamily::Mysql => "mysql@dbgate-plugin-mysql", + DatabaseFamily::MariaDB => "mariadb@dbgate-plugin-mysql", + DatabaseFamily::Postgres => "postgres@dbgate-plugin-postgres", + DatabaseFamily::Oracle => "oracle@dbgate-plugin-oracle", + DatabaseFamily::Sqlite => "sqlite@dbgate-plugin-sqlite", + } + } + + pub fn port(&self) -> u16 { + match self { + DatabaseFamily::Mysql | DatabaseFamily::MariaDB => 3306, + DatabaseFamily::Postgres => 5432, + DatabaseFamily::Oracle => 1521, + DatabaseFamily::Sqlite => 0, + } + } + + pub fn username(&self) -> &'static str { + match self { + DatabaseFamily::Oracle => "system", + _ => "haze", + } + } + + pub fn db(&self) -> &'static str { + match self { + DatabaseFamily::Oracle => "SYSTEM", + _ => "haze", + } + } } #[derive(Clone, Debug, Eq, PartialEq, Default)] diff --git a/src/help.rs b/src/help.rs index 566bff7..29c09ff 100644 --- a/src/help.rs +++ b/src/help.rs @@ -2,8 +2,8 @@ use crate::args::{HazeCommand, SubCommand}; use crate::database::DatabaseFamily; use crate::php::PhpVersion; use crate::service::ServiceType; -use owo_colors::colors::xterm::Gray; use owo_colors::OwoColorize; +use owo_colors::colors::xterm::Gray; use strum::{EnumMessage, EnumProperty, IntoEnumIterator}; pub fn help(command: Option<&dyn SubCommand>) { @@ -151,12 +151,14 @@ fn subcommand_help(command: &dyn SubCommand) { for service in ServiceType::iter() { let service: ServiceType = service; let service_str: &'static str = service.into(); - println!( - " {}{} {}", - service.blue(), - " ".repeat(max_service_len - service_str.len()), - service.get_documentation().unwrap_or_default(), - ); + if let Some(doc) = service.get_documentation() { + println!( + " {}{} {}", + service.blue(), + " ".repeat(max_service_len - service_str.len()), + doc, + ); + } } println!(); diff --git a/src/main.rs b/src/main.rs index 2c01ce4..d425a73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -129,7 +129,11 @@ async fn main() -> Result { HazeArgs::List { filter } => { let list = Cloud::list(&docker, filter, &config).await?; for cloud in list { - let mut services: Vec<_> = cloud.services().map(Service::name).collect(); + let mut services: Vec<_> = cloud + .services() + .filter(|service| !service.hidden()) + .map(Service::name) + .collect(); services.push(cloud.db().name()); let services = services.join(", "); let pin = if cloud.is_pinned(&docker).await.unwrap_or(false) { diff --git a/src/proxy.rs b/src/proxy.rs index 2223299..cc6f6ea 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -1,8 +1,8 @@ -use crate::service::{ServiceTrait, ServiceType}; use crate::Result; +use crate::service::{ServiceTrait, ServiceType}; use crate::{Cloud, HazeConfig}; -use axum::http::header::HOST; use axum::http::HeaderValue; +use axum::http::header::HOST; use axum::{ body::Body, extract::Request, @@ -10,13 +10,13 @@ use axum::{ }; use bollard::Docker; use futures_util::StreamExt; +use hyper::StatusCode; use hyper::body::Incoming; use hyper::server::conn::http1; use hyper::service::service_fn; -use hyper::StatusCode; use hyper_util::rt::TokioIo; use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor}; -use miette::{miette, IntoDiagnostic}; +use miette::{IntoDiagnostic, miette}; use std::collections::HashMap; use std::convert::Infallible; use std::fs::{create_dir_all, set_permissions}; @@ -237,10 +237,11 @@ async fn serve( } else { let listen: PathBuf = listen.into(); if let Some(parent) = listen.parent() - && !parent.exists() { - create_dir_all(parent).into_diagnostic()?; - set_permissions(parent, PermissionsExt::from_mode(0o755)).into_diagnostic()?; - } + && !parent.exists() + { + create_dir_all(parent).into_diagnostic()?; + set_permissions(parent, PermissionsExt::from_mode(0o755)).into_diagnostic()?; + } let _ = tokio::fs::remove_file(&listen).await; let listener = UnixListener::bind(&listen).unwrap(); @@ -337,7 +338,7 @@ async fn handler(state: AppState, mut req: Request) -> Result bool { + false + } + async fn spawn( &self, _docker: &Docker, @@ -340,6 +346,8 @@ pub enum ServiceType { Oidc, /// Configure Authentik as a SCIM server for Nextcloud Scim, + #[strum(serialize = "db", serialize = "db-gate")] + DbGate, } #[enum_dispatch] @@ -377,6 +385,7 @@ pub enum Service { AuthentikSaml(AuthentikSaml), AuthentikOidc(AuthentikOidc), AuthentikScim(AuthentikScim), + DbGate(DbGate), Preset(PresetService), } @@ -436,6 +445,7 @@ impl Service { Service::Authentik(Authentik), Service::AuthentikScim(AuthentikScim), ]), + ServiceType::DbGate => Some(vec![Service::DbGate(DbGate)]), } } else { presets diff --git a/src/service/dbgate.rs b/src/service/dbgate.rs new file mode 100644 index 0000000..050ec84 --- /dev/null +++ b/src/service/dbgate.rs @@ -0,0 +1,133 @@ +use std::io::Stdout; + +use crate::Result; +use crate::cloud::CloudOptions; +use crate::config::HazeConfig; +use crate::database::DatabaseFamily; +use crate::exec::exec; +use crate::image::pull_image; +use crate::mapping::Mapping; +use crate::service::ServiceTrait; +use bollard::Docker; +use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig}; +use bollard::query_parameters::CreateContainerOptions; +use maplit::hashmap; +use miette::IntoDiagnostic; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct DbGate; + +#[async_trait::async_trait] +impl ServiceTrait for DbGate { + fn name(&self) -> &str { + "db" + } + + fn hidden(&self) -> bool { + true + } + + async fn spawn( + &self, + docker: &Docker, + cloud_id: &str, + network: &str, + config: &HazeConfig, + options: &CloudOptions, + ) -> Result> { + let image = "dbgate/dbgate:7.3.1"; + pull_image(docker, image).await?; + let container_options = Some(CreateContainerOptions { + name: self.container_name(cloud_id), + ..CreateContainerOptions::default() + }); + + let db_family = options.db.family(); + + let mut env = vec![ + "CONNECTIONS=con1".into(), + "USAGE_ANALYTICS=false".into(), + "LOCAL_DBGATE_CLOUD=1".into(), + "LABEL_con1=haze".into(), + format!("ENGINE_con1={}", db_family.db_gate_driver()), + ]; + + if db_family == DatabaseFamily::Sqlite { + env.extend(["FILE_con1=/haze-data/haze.db".into()]); + } else { + env.extend([ + "SERVER_con1=db".into(), + format!("USER_con1={}", db_family.username()), + format!("PORT_con1={}", db_family.port()), + format!("DATABASE_con1={}", db_family.db()), + "PASSWORD_con1=haze".into(), + ]); + } + + if db_family == DatabaseFamily::Oracle { + env.push("SERVICE_NAME_con1=xe".into()); + } + + let mapping = Mapping::new( + crate::mapping::MappingSourceType::WorkDir, + "data", + "/haze-data", + ); + + let config = ContainerCreateBody { + image: Some(image.into()), + host_config: Some(HostConfig { + network_mode: Some(network.to_string()), + binds: Some(vec![ + mapping + .get_volume_arg(cloud_id, config, &config.sources_root) + .unwrap(), + ]), + ..Default::default() + }), + env: Some(env), + labels: Some(hashmap! { + "haze-type".into() => self.name().into(), + "haze-cloud-id".into() => cloud_id.into(), + }), + networking_config: Some(NetworkingConfig { + endpoints_config: Some(hashmap! { + network.into() => EndpointSettings { + aliases: Some(vec![self.name().to_string()]), + ..Default::default() + } + }), + }), + ..Default::default() + }; + let id = docker + .create_container(container_options, config) + .await + .into_diagnostic()? + .id; + docker.start_container(&id, None).await.into_diagnostic()?; + exec( + docker, + &id, + "root", + vec![ + "sed", + "-i", + "s/attr(t,`title`,`Upgrade to Premium`)/attr(t,`style`,`display:none`)/", + "public/build/bundle.js", + ], + Vec::::new(), + None::, + ) + .await?; + Ok(vec![id]) + } + + fn container_name(&self, cloud_id: &str) -> Option { + Some(format!("{}-db-gate", cloud_id)) + } + + fn proxy_port(&self) -> u16 { + 3000 + } +}