1
0
Fork 0
mirror of https://codeberg.org/icewind/haze.git synced 2026-10-01 08:44:09 +02:00

add database webui

This commit is contained in:
Robin Appelman 2026-09-27 17:28:59 +02:00
commit 515fc60f49
8 changed files with 213 additions and 22 deletions

View file

@ -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

View file

@ -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<Item = &Service> {
self.options.services.iter()
self.options
.services
.iter()
.chain(DEFAULT_SERVICES.into_iter())
}
pub fn db(&self) -> &Database {

View file

@ -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)]

View file

@ -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!();

View file

@ -129,7 +129,11 @@ async fn main() -> Result<ExitCode> {
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) {

View file

@ -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<Incoming>) -> Result<Response
return Ok(hyper::Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(e.into())
.unwrap())
.unwrap());
}
};

View file

@ -1,6 +1,7 @@
mod authentik;
mod clam;
mod dav;
mod dbgate;
mod imaginary;
mod kaspersky;
mod ldap;
@ -22,6 +23,7 @@ use crate::config::{HazeConfig, Preset, ProxyConfig};
pub use crate::service::authentik::{Authentik, AuthentikOidc, AuthentikSaml, AuthentikScim};
pub use crate::service::clam::{Clam, ClamIcap, ClamIcapTls, ClamSocket};
use crate::service::dav::Dav;
pub use crate::service::dbgate::DbGate;
use crate::service::imaginary::Imaginary;
use crate::service::kaspersky::{Kaspersky, KasperskyIcap};
pub use crate::service::ldap::{Ldap, LdapAdmin};
@ -59,6 +61,10 @@ pub trait ServiceTrait {
&[]
}
fn hidden(&self) -> 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

133
src/service/dbgate.rs Normal file
View file

@ -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<Vec<String>> {
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::<String>::new(),
None::<Stdout>,
)
.await?;
Ok(vec![id])
}
fn container_name(&self, cloud_id: &str) -> Option<String> {
Some(format!("{}-db-gate", cloud_id))
}
fn proxy_port(&self) -> u16 {
3000
}
}